use oxc_ast::AstKind;
use oxc_semantic::{ScopeFlags, SymbolFlags};

use crate::util::{Expect, SemanticTester};

#[test]
fn test_only_program() {
    let tester = SemanticTester::js("let x = 1;");
    tester.has_root_symbol("x").in_scope(ScopeFlags::Top).test();

    let semantic = tester.build();
    let scopes = semantic.scoping();
    let root = semantic.scoping().root_scope_id();

    // ScopeTree contains a single root scope
    assert_eq!(scopes.scopes_len(), 1);
    assert!(!scopes.scopes_is_empty());

    // Root scope is associated with the Program
    let root_node_id = scopes.get_node_id(root);
    let root_node = semantic.nodes().get_node(root_node_id);
    assert!(matches!(root_node.kind(), AstKind::Program(_)));

    // ancestors
    assert_eq!(scopes.scope_ancestors(root).count(), 1);
    assert!(scopes.scope_parent_id(root).is_none());
}

#[test]
fn test_top_level_strict() {
    // Module with top-level "use strict"
    SemanticTester::js(
        r#"
    "use strict";
    function foo() {
        return 1
    }
    "#,
    )
    .has_root_symbol("foo")
    .in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
    // .expect(expect_strict)
    .test();

    // Module without top-level "use strict"
    SemanticTester::js(
        r"
    function foo() {
        return 1
    }
    ",
    )
    .has_root_symbol("foo")
    .in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
    .test();

    // Script with top-level "use strict"
    SemanticTester::js(
        r#"
    "use strict";
    function foo() {
        return 1
    }
    "#,
    )
    .with_script(true)
    .has_root_symbol("foo")
    .in_scope(ScopeFlags::Top | ScopeFlags::StrictMode)
    .test();

    // Script without top-level "use strict"
    SemanticTester::js(
        r"
    function foo() {
        return 1
    }
    ",
    )
    .with_script(true)
    .has_root_symbol("foo")
    .in_scope(ScopeFlags::Top)
    .not_in_scope(ScopeFlags::StrictMode)
    .test();
}

#[test]
fn test_function_level_strict() {
    let tester = SemanticTester::js(
        r#"
    function foo() {
        "use strict";
        let x = 1;
        return x
    }
    "#,
    )
    .with_script(true);

    tester
        .has_some_symbol("x")
        .in_scope(ScopeFlags::StrictMode | ScopeFlags::Function)
        .expect(|(semantic, symbol_id)| -> Result<(), &'static str> {
            let scope_id = semantic.symbol_scope(symbol_id);
            let Some(parent_scope_id) = semantic.scoping().scope_parent_id(scope_id) else {
                return Err("Expected x's scope to have a parent");
            };
            let parent_flags = semantic.scoping().scope_flags(parent_scope_id);
            if parent_flags.contains(ScopeFlags::Top) {
                Ok(())
            } else {
                Err("Expected x to be in a top-level function declaration.")
            }
        })
        .test();
    tester.has_some_symbol("foo").not_in_scope(ScopeFlags::StrictMode).test();
}

#[test]
fn test_switch_case() {
    SemanticTester::js(
        "
            const foo = 1;
            switch (foo) {
                case 1:
                    const foo = 2;
            }
        ",
    )
    .has_root_symbol("foo")
    .has_number_of_references(1)
    .test();
}

#[test]
fn test_function_parameters() {
    let tester = SemanticTester::js(
        "
            const foo = 2;
            const c = 0;
            function func(a = foo, b = c, c = 0) {
                const foo = 0;
            }
        ",
    );

    tester.has_root_symbol("foo").has_number_of_references(1).test();
    // b = c should reference the third parameter, so root symbol `c`` should have 0 reference
    tester.has_root_symbol("c").has_number_of_references(0).test();
}

#[test]
fn test_catch_clause_parameters() {
    SemanticTester::js(
        "
            const a = 0;
            try {
            } catch ({ [a]: b }) {
                const a = 1;
            }
        ",
    )
    .has_root_symbol("a")
    .has_number_of_references(1)
    .test();
}

#[test]
fn test_enums() {
    let test = SemanticTester::ts(
        "
        enum A {
            X,
            Y,
            Z
        }",
    );
    test.has_root_symbol("A").contains_flags(SymbolFlags::RegularEnum).test();
    test.has_some_symbol("X").contains_flags(SymbolFlags::EnumMember).test();

    let semantic = test.build();
    let program = semantic
        .nodes()
        .iter()
        .find(|node| matches!(node.kind(), AstKind::Program(_)))
        .expect("No program node found");
    assert_eq!(program.scope_id(), semantic.scoping().root_scope_id());

    let (enum_node, enum_decl) = semantic
        .nodes()
        .iter()
        .find_map(|node| {
            let e = node.kind().as_ts_enum_declaration()?;
            Some((node, e))
        })
        .expect("Expected TS test case to have an enum declaration for A.");

    assert_eq!(
        enum_node.scope_id(),
        program.scope_id(),
        "Expected `enum A` to be created in the top-level scope."
    );
    let enum_body_scope_id = enum_decl.body.scope_id.get().expect("Enum body has no scope id");
    assert_ne!(
        enum_node.scope_id(),
        enum_body_scope_id,
        "Enum body nodes should contain the scope ID they create, not the scope ID they're created in."
    );
    assert_eq!(enum_decl.body.members.len(), 3);
}

#[test]
fn var_hoisting() {
    SemanticTester::js(
        "
            try {} catch (e) {
                var e = 0;
            }
        ",
    )
    .has_root_symbol("e")
    // `e` was hoisted to the top scope so the symbol's scope is also the top scope
    .in_scope(ScopeFlags::Top)
    .test();
}

#[test]
fn test_ts_conditional_types() {
    SemanticTester::ts("type A<T> = T extends string ? T : false;")
        .has_some_symbol("T")
        .has_number_of_references(2)
        .test();

    // Conditional types create a new scope after check_type.
    SemanticTester::ts(
        "type S<A> = A extends (infer B extends number ? string : never) ? B : false;",
    )
    .has_some_symbol("B")
    .has_number_of_references(1)
    .test();

    // Inferred type parameter is only available within true branch
    SemanticTester::ts("type S<A> = A extends infer R ? never : R")
        .has_some_symbol("R")
        .has_number_of_references(0)
        .test();
}

#[test]
fn test_eval() {
    let direct_evals = ["eval('')", "function foo() { eval('') }"];
    for code in direct_evals {
        let tester = SemanticTester::js(code);
        let semantic = tester.build();
        assert!(semantic.scoping().root_scope_flags().contains_direct_eval());
    }

    let indirect_evals = [
        "eval?.('')",
        "(0, eval)('')",
        "const myEval = eval; myEval('')",
        "const obj = { eval }; obj.eval('x + y')",
    ];
    for code in indirect_evals {
        let tester = SemanticTester::js(code);
        let semantic = tester.build();
        assert!(!semantic.scoping().root_scope_flags().contains_direct_eval());
    }
}
