use std::ops::ControlFlow;

use hir_def::db::DefDatabase;
use rustc_hash::{FxHashMap, FxHashSet};
use syntax::ToSmolStr;
use test_fixture::WithFixture;

use crate::{dyn_compatibility::dyn_compatibility_with_callback, test_db::TestDB};

use super::{
    DynCompatibilityViolation,
    MethodViolationCode::{self, *},
};

use DynCompatibilityViolationKind::*;

#[allow(clippy::upper_case_acronyms)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum DynCompatibilityViolationKind {
    SizedSelf,
    SelfReferential,
    Method(MethodViolationCode),
    AssocConst,
    GAT,
    HasNonCompatibleSuperTrait,
}

fn check_dyn_compatibility<'a>(
    #[rust_analyzer::rust_fixture] ra_fixture: &str,
    expected: impl IntoIterator<Item = (&'a str, Vec<DynCompatibilityViolationKind>)>,
) {
    let mut expected: FxHashMap<_, _> =
        expected.into_iter().map(|(id, osvs)| (id, FxHashSet::from_iter(osvs))).collect();
    let (db, file_ids) = TestDB::with_many_files(ra_fixture);
    for (trait_id, name) in file_ids.into_iter().flat_map(|file_id| {
        let module_id = db.module_for_file(file_id.file_id(&db));
        let def_map = module_id.def_map(&db);
        let scope = &def_map[module_id].scope;
        scope
            .declarations()
            .filter_map(|def| {
                if let hir_def::ModuleDefId::TraitId(trait_id) = def {
                    let name = db
                        .trait_signature(trait_id)
                        .name
                        .display_no_db(file_id.edition(&db))
                        .to_smolstr();
                    Some((trait_id, name))
                } else {
                    None
                }
            })
            .collect::<Vec<_>>()
    }) {
        let Some(expected) = expected.remove(name.as_str()) else {
            continue;
        };
        let mut osvs = FxHashSet::default();
        let db = &db;
        crate::attach_db(db, || {
            _ = dyn_compatibility_with_callback(db, trait_id, &mut |osv| {
                osvs.insert(match osv {
                    DynCompatibilityViolation::SizedSelf => SizedSelf,
                    DynCompatibilityViolation::SelfReferential => SelfReferential,
                    DynCompatibilityViolation::Method(_, mvc) => Method(mvc),
                    DynCompatibilityViolation::AssocConst(_) => AssocConst,
                    DynCompatibilityViolation::GAT(_) => GAT,
                    DynCompatibilityViolation::HasNonCompatibleSuperTrait(_) => {
                        HasNonCompatibleSuperTrait
                    }
                });
                ControlFlow::Continue(())
            });
        });
        assert_eq!(osvs, expected, "dyn-compatibility violations for `{name}` do not match;");
    }

    let remains: Vec<_> = expected.keys().collect();
    assert!(remains.is_empty(), "Following traits do not exist in the test fixture; {remains:?}");
}

#[test]
fn item_bounds_can_reference_self() {
    check_dyn_compatibility(
        r#"
//- minicore: eq
pub trait Foo {
    type X: PartialEq;
    type Y: PartialEq<Self::Y>;
    type Z: PartialEq<Self::Y>;
}
"#,
        [("Foo", vec![])],
    );
}

#[test]
fn associated_consts() {
    check_dyn_compatibility(
        r#"
trait Bar {
    const X: usize;
}
"#,
        [("Bar", vec![AssocConst])],
    );
}

#[test]
fn bounds_reference_self() {
    check_dyn_compatibility(
        r#"
//- minicore: eq
trait X {
    type U: PartialEq<Self>;
}
"#,
        [("X", vec![SelfReferential])],
    );
}

#[test]
fn by_value_self() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar {
    fn bar(self);
}

trait Baz {
    fn baz(self: Self);
}

trait Quux {
    // Legal because of the where clause:
    fn baz(self: Self) where Self : Sized;
}
"#,
        [("Bar", vec![]), ("Baz", vec![]), ("Quux", vec![])],
    );
}

#[test]
fn generic_methods() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar {
    fn bar<T>(&self, t: T);
}

trait Quux {
    fn bar<T>(&self, t: T)
        where Self : Sized;
}

trait Qax {
    fn bar<'a>(&self, t: &'a ());
}
"#,
        [("Bar", vec![Method(Generic)]), ("Quux", vec![]), ("Qax", vec![])],
    );
}

#[test]
fn mentions_self() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar {
    fn bar(&self, x: &Self);
}

trait Baz {
    fn baz(&self) -> Self;
}

trait Quux {
    fn quux(&self, s: &Self) -> Self where Self : Sized;
}
"#,
        [
            ("Bar", vec![Method(ReferencesSelfInput)]),
            ("Baz", vec![Method(ReferencesSelfOutput)]),
            ("Quux", vec![]),
        ],
    );
}

#[test]
fn no_static() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Foo {
    fn foo() {}
}
"#,
        [("Foo", vec![Method(StaticMethod)])],
    );
}

#[test]
fn sized_self() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar: Sized {
    fn bar<T>(&self, t: T);
}
"#,
        [("Bar", vec![SizedSelf])],
    );

    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar
    where Self : Sized
{
    fn bar<T>(&self, t: T);
}
"#,
        [("Bar", vec![SizedSelf])],
    );
}

#[test]
fn supertrait_gat() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait GatTrait {
    type Gat<T>;
}

trait SuperTrait<T>: GatTrait {}
"#,
        [("GatTrait", vec![GAT]), ("SuperTrait", vec![HasNonCompatibleSuperTrait])],
    );
}

#[test]
fn supertrait_mentions_self() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Bar<T> {
    fn bar(&self, x: &T);
}

trait Baz : Bar<Self> {
}
"#,
        // FIXME: We should also report `SizedSelf` here
        [("Bar", vec![]), ("Baz", vec![SelfReferential])],
    );
}

#[test]
fn rustc_issue_19538() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Foo {
    fn foo<T>(&self, val: T);
}

trait Bar: Foo {}
"#,
        [("Foo", vec![Method(Generic)]), ("Bar", vec![HasNonCompatibleSuperTrait])],
    );
}

#[test]
fn rustc_issue_22040() {
    check_dyn_compatibility(
        r#"
//- minicore: fmt, eq, dispatch_from_dyn
use core::fmt::Debug;

trait Expr: Debug + PartialEq {
    fn print_element_count(&self);
}
"#,
        [("Expr", vec![SelfReferential])],
    );
}

#[test]
fn rustc_issue_102762() {
    check_dyn_compatibility(
        r#"
//- minicore: future, send, sync, dispatch_from_dyn, deref
use core::pin::Pin;

struct Box<T: ?Sized> {}
impl<T: ?Sized> core::ops::Deref for Box<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        loop {}
    }
}
impl<T: ?Sized + Unsize<U>, U: ?Sized> DispatchFromDyn<Box<U>> for Box<T> {}

struct Vec<T> {}

pub trait Fetcher: Send + Sync {
    fn get<'a>(self: &'a Box<Self>) -> Pin<Box<dyn Future<Output = Vec<u8>> + 'a>>
    where
        Self: Sync,
    {
        loop {}
    }
}
"#,
        [("Fetcher", vec![Method(UndispatchableReceiver)])],
    );
}

#[test]
fn rustc_issue_102933() {
    check_dyn_compatibility(
        r#"
//- minicore: future, dispatch_from_dyn, deref
use core::future::Future;

struct Box<T: ?Sized> {}
impl<T: ?Sized> core::ops::Deref for Box<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        loop {}
    }
}
impl<T: ?Sized + Unsize<U>, U: ?Sized> DispatchFromDyn<Box<U>> for Box<T> {}

pub trait Service {
    type Response;
    type Future: Future<Output = Self::Response>;
}

pub trait A1: Service<Response = i32> {}

pub trait A2: Service<Future = Box<dyn Future<Output = i32>>> + A1 {
    fn foo(&self) {}
}

pub trait B1: Service<Future = Box<dyn Future<Output = i32>>> {}

pub trait B2: Service<Response = i32> + B1 {
    fn foo(&self) {}
}
        "#,
        [("A2", vec![]), ("B2", vec![])],
    );
}

#[test]
fn rustc_issue_106247() {
    check_dyn_compatibility(
        r#"
//- minicore: sync, dispatch_from_dyn
pub trait Trait {
    fn method(&self) where Self: Sync;
}
"#,
        [("Trait", vec![])],
    );
}

#[test]
fn std_error_is_dyn_compatible() {
    check_dyn_compatibility(
        r#"
//- minicore: fmt, dispatch_from_dyn
trait Erased<'a>: 'a {}

pub struct Request<'a>(dyn Erased<'a> + 'a);

pub trait Error: core::fmt::Debug + core::fmt::Display {
    fn provide<'a>(&'a self, request: &mut Request<'a>);
}
"#,
        [("Error", vec![])],
    );
}

#[test]
fn lifetime_gat_is_dyn_incompatible() {
    check_dyn_compatibility(
        r#"
//- minicore: dispatch_from_dyn
trait Foo {
    type Bar<'a>;
}
"#,
        [("Foo", vec![DynCompatibilityViolationKind::GAT])],
    );
}
