use llmcc_core::context::CompileUnit;
use llmcc_core::interner::{InternPool, InternedStr};
use llmcc_core::ir::HirKind;
use llmcc_core::symbol::{Scope, ScopeStack};
use llmcc_rust::{build_llmcc_ir, collect_symbols, CollectionResult, CompileCtxt, LangRust};

struct Fixture<'tcx> {
    cc: &'tcx CompileCtxt<'tcx>,
    unit: CompileUnit<'tcx>,
    globals: &'tcx Scope<'tcx>,
    result: CollectionResult,
}

fn build_fixture(source: &str) -> Fixture<'static> {
    let sources = vec![source.as_bytes().to_vec()];
    let cc: &'static CompileCtxt<'static> =
        Box::leak(Box::new(CompileCtxt::from_sources::<LangRust>(&sources)));
    let unit = cc.compile_unit(0);
    build_llmcc_ir::<LangRust>(cc).unwrap();
    let globals = cc.create_globals();
    let result = collect_symbols(unit, globals);
    Fixture {
        cc,
        unit,
        globals,
        result,
    }
}

impl<'tcx> Fixture<'tcx> {
    fn interner(&self) -> &InternPool {
        self.unit.interner()
    }

    fn intern(&self, name: &str) -> InternedStr {
        self.interner().intern(name)
    }

    fn scope_stack(&self) -> ScopeStack<'tcx> {
        let mut stack = ScopeStack::new(&self.cc.arena, &self.cc.interner, &self.cc.symbol_map);
        stack.push(self.globals);
        stack
    }

    fn module_scope(&self, name: &str) -> &'tcx Scope<'tcx> {
        let stack = self.scope_stack();
        let key = self.intern(name);
        let symbol = stack.find_global_suffix(&[key]).unwrap();
        self.unit.opt_get_scope(symbol.owner()).unwrap()
    }
}

#[test]
fn inserts_symbols_for_local_and_global_resolution() {
    let source = r#"
        mod outer {
            pub fn inner(param: i32) {
                let local = param;
            }

            fn private_inner() {}
        }

        pub struct Foo {
            field: i32,
        }

        impl Foo {
            /// if Foo is public, we should export its methods too
            pub fn method(&self) {}

            fn private_method(&self) {}
        }

        struct Bar;
        impl Bar {
            /// if Bar is private, we should NOT export its methods
            fn bar_method(&self) {}
        }

        const MAX: i32 = 5;
    "#;

    let fixture = build_fixture(source);

    let outer_key = fixture.intern("outer");
    let inner_key = fixture.intern("inner");
    let private_inner_key = fixture.intern("private_inner");
    let foo_key = fixture.intern("Foo");
    let foo_method_key = fixture.intern("method");
    let foo_private_method_key = fixture.intern("private_method");
    let bar_key = fixture.intern("Bar");
    let bar_method_key = fixture.intern("bar_method");
    let max_key = fixture.intern("MAX");
    let param_key = fixture.intern("param");
    let local_key = fixture.intern("local");

    let scope_stack = fixture.scope_stack();

    assert!(fixture.globals.get_id(outer_key).is_some());
    assert!(fixture.globals.get_id(max_key).is_some());
    assert!(fixture.globals.get_id(foo_key).is_some());
    assert!(scope_stack
        .find_global_suffix(&[foo_method_key, foo_key])
        .is_some());
    assert!(scope_stack
        .find_global_suffix(&[foo_private_method_key, foo_key])
        .is_none());
    assert!(fixture.globals.get_id(bar_key).is_some());
    assert!(scope_stack
        .find_global_suffix(&[bar_method_key, bar_key])
        .is_none());
    assert!(fixture.globals.get_id(private_inner_key).is_none());

    let global_symbol = scope_stack
        .find_global_suffix(&[inner_key, outer_key])
        .unwrap();
    assert_eq!(global_symbol.fqn_name.borrow().as_str(), "outer::inner");

    let inner_desc = fixture
        .result
        .functions
        .iter()
        .find(|desc| desc.fqn == "outer::inner")
        .unwrap();

    let function_scope = fixture.unit.opt_get_scope(inner_desc.hir_id).unwrap();
    assert!(function_scope.get_id(param_key).is_some());

    let function_node = fixture.unit.hir_node(inner_desc.hir_id);
    let body_scope_id = function_node
        .children()
        .iter()
        .copied()
        .map(|child_id| fixture.unit.hir_node(child_id))
        .find(|child| child.kind() == HirKind::Scope)
        .map(|child| child.hir_id())
        .unwrap();
    let body_scope = fixture.unit.opt_get_scope(body_scope_id).unwrap();
    assert!(body_scope.get_id(local_key).is_some());

    let module_scope = fixture.module_scope("outer");
    assert!(module_scope.get_id(inner_key).is_some());
    assert!(module_scope.get_id(private_inner_key).is_some());

    assert!(fixture.globals.get_id(local_key).is_none());
}

#[test]
fn module_struct_visibility() {
    let source = r#"
        mod outer {
            pub struct Foo;
            impl Foo {
                pub fn create() {}
            }

            struct Bar;
            impl Bar {
                fn hidden() {}
            }
        }
    "#;

    let fixture = build_fixture(source);

    let foo_key = fixture.intern("Foo");
    let create_key = fixture.intern("create");
    let bar_key = fixture.intern("Bar");
    let hidden_key = fixture.intern("hidden");

    let scope_stack = fixture.scope_stack();

    assert!(fixture.globals.get_id(foo_key).is_some());
    assert!(scope_stack
        .find_global_suffix(&[create_key, foo_key])
        .is_some());
    assert!(fixture.globals.get_id(bar_key).is_none());
    assert!(scope_stack
        .find_global_suffix(&[hidden_key, bar_key])
        .is_none());

    let module_scope = fixture.module_scope("outer");
    assert!(module_scope.get_id(bar_key).is_some());
    assert!(module_scope.get_id(foo_key).is_some());
}

#[test]
fn module_enum_visibility() {
    let source = r#"
        mod outer {
            pub enum Visible {
                A,
            }

            enum Hidden {
                B,
            }
        }
    "#;

    let fixture = build_fixture(source);

    let outer_scope = fixture.module_scope("outer");
    let visible_key = fixture.intern("Visible");
    let hidden_key = fixture.intern("Hidden");
    let variant_a_key = fixture.intern("A");
    let variant_b_key = fixture.intern("B");

    let scope_stack = fixture.scope_stack();
    let outer_key = fixture.intern("outer");

    assert!(fixture.globals.get_id(visible_key).is_some());
    assert!(fixture.globals.get_id(hidden_key).is_none());
    assert!(scope_stack
        .find_global_suffix(&[visible_key, outer_key])
        .is_some());
    assert!(scope_stack
        .find_global_suffix(&[hidden_key, outer_key])
        .is_none());

    assert!(outer_scope.get_id(visible_key).is_some());
    assert!(outer_scope.get_id(hidden_key).is_some());

    let visible_desc = fixture
        .result
        .enums
        .iter()
        .find(|desc| desc.name == "Visible")
        .unwrap();
    let visible_scope = fixture.unit.opt_get_scope(visible_desc.hir_id).unwrap();
    assert!(visible_scope.get_id(variant_a_key).is_some());

    let hidden_desc = fixture
        .result
        .enums
        .iter()
        .find(|desc| desc.name == "Hidden")
        .unwrap();
    let hidden_scope = fixture.unit.opt_get_scope(hidden_desc.hir_id).unwrap();
    assert!(hidden_scope.get_id(variant_b_key).is_some());

    assert!(scope_stack
        .find_global_suffix(&[variant_a_key, visible_key, outer_key])
        .is_some());
    assert!(scope_stack
        .find_global_suffix(&[variant_b_key, hidden_key, outer_key])
        .is_none());
}

#[test]
fn enum_variant_symbols_are_registered() {
    let source = r#"
        pub enum Status {
            Ok,
            NotFound,
        }

        enum PrivateStatus {
            Hidden,
        }
    "#;

    let fixture = build_fixture(source);

    let status_key = fixture.intern("Status");
    let ok_key = fixture.intern("Ok");
    let not_found_key = fixture.intern("NotFound");
    let private_status_key = fixture.intern("PrivateStatus");
    let hidden_key = fixture.intern("Hidden");

    let scope_stack = fixture.scope_stack();

    assert!(fixture.globals.get_id(status_key).is_some());
    assert!(scope_stack
        .find_global_suffix(&[ok_key, status_key])
        .is_some());
    assert!(scope_stack
        .find_global_suffix(&[not_found_key, status_key])
        .is_some());

    assert!(fixture.globals.get_id(private_status_key).is_some());
    assert!(scope_stack
        .find_global_suffix(&[hidden_key, private_status_key])
        .is_none());

    let status_scope = fixture
        .unit
        .opt_get_scope(
            fixture
                .result
                .enums
                .iter()
                .find(|desc| desc.name == "Status")
                .unwrap()
                .hir_id,
        )
        .unwrap();
    assert!(status_scope.get_id(ok_key).is_some());
    assert!(status_scope.get_id(not_found_key).is_some());

    let private_scope = fixture
        .unit
        .opt_get_scope(
            fixture
                .result
                .enums
                .iter()
                .find(|desc| desc.name == "PrivateStatus")
                .unwrap()
                .hir_id,
        )
        .unwrap();
    assert!(private_scope.get_id(hidden_key).is_some());
}
