use super::*;
use ckg_core::EdgeKind;

#[test]
fn extracts_basic_js() {
    let src = r#"
        export function helper() { return 1; }
        function caller() { return helper(); }
        export class Service {
            ping() { return helper(); }
        }
        const arrow = () => caller();
    "#;
    let out = extract("repo", Path::new("src/api.js"), src, "javascript").unwrap();
    let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
    for expected in ["helper", "caller", "Service", "ping", "arrow"] {
        assert!(names.contains(&expected), "missing {expected}: {names:?}");
    }
    // file-local call resolution at conf 1.0 — the id ends with
    // `helper:function` after qname.
    assert!(
        out.edges.iter().any(|e| matches!(e.kind, EdgeKind::Calls)
            && e.dst.ends_with("helper:function")
            && (e.confidence - 1.0).abs() < 1e-3),
        "no file-local helper call at conf 1.0. Got: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn extracts_imports_and_aliases() {
    let src = r#"
        import { foo, bar as baz } from './lib';
        import Default from './default';
        import * as ns from './ns';

        function use_them() {
            foo();
            baz();
            Default();
            ns.thing();
        }
    "#;
    let out = extract("repo", Path::new("src/app.js"), src, "javascript").unwrap();

    // Imports edge from app to ./lib / ./default / ./ns
    let imp: Vec<_> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Imports))
        .map(|e| e.dst.as_str())
        .collect();
    assert!(imp.contains(&"./lib"), "missing import: ./lib in {imp:?}");
    assert!(imp.contains(&"./default"));
    assert!(imp.contains(&"./ns"));

    // Alias resolution: `baz` should land at `./lib.bar` at conf 0.9
    let aliased = out.edges.iter().any(|e| {
        matches!(e.kind, EdgeKind::Calls)
            && e.dst == "./lib.bar"
            && (e.confidence - 0.9).abs() < 1e-3
    });
    assert!(
        aliased,
        "expected `baz()` → `./lib.bar` at 0.9. Got: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn extracts_typescript_specifics() {
    let src = r#"
        interface Greeter { greet(): string; }
        type Name = string;
        enum Color { Red, Green, Blue }
        class Hello implements Greeter {
            greet(): string { return "hi"; }
        }
    "#;
    let out = extract("repo", Path::new("src/types.ts"), src, "typescript").unwrap();
    let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
    for expected in ["Greeter", "Name", "Color", "Hello", "greet"] {
        assert!(names.contains(&expected), "missing {expected}: {names:?}");
    }
    let kinds: Vec<Kind> = out.symbols.iter().map(|s| s.kind).collect();
    assert!(kinds.contains(&Kind::Interface), "no Interface kind");
    assert!(kinds.contains(&Kind::TypeAlias), "no TypeAlias kind");
    assert!(kinds.contains(&Kind::Enum), "no Enum kind");
}

#[test]
fn extracts_class_inheritance() {
    let src = r#"
        class Base {}
        class Child extends Base {
            method() {}
        }
    "#;
    let out = extract("repo", Path::new("src/cls.ts"), src, "typescript").unwrap();
    let extends = out
        .edges
        .iter()
        .find(|e| matches!(e.kind, EdgeKind::Extends) && e.dst == "Base");
    assert!(extends.is_some(), "missing Extends: Child → Base");
}

/// `await` produces a distinct Awaits edge alongside the Calls edge.
#[test]
fn await_emits_awaits_edge() {
    let src = r#"
        async function helper() { return 1; }
        async function caller() {
            const x = await helper();
            return x;
        }
    "#;
    let out = extract("repo", Path::new("src/a.js"), src, "javascript").unwrap();
    let awaits = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Awaits))
        .count();
    assert!(awaits >= 1, "expected ≥1 Awaits edge, got {awaits}");
}

/// `const x: Foo = new Foo()` then `x.bar()` resolves to `Foo.bar` at conf 0.7.
#[test]
fn type_aware_dispatch_via_new_or_annotation() {
    let src = r#"
        class Foo {
            bar() { return 1; }
        }
        function annotated(): void {
            const x: Foo = new Foo();
            x.bar();
        }
        function inferred(): void {
            const y = new Foo();
            y.bar();
        }
    "#;
    let out = extract("repo", Path::new("src/td.ts"), src, "typescript").unwrap();
    let typed = out.edges.iter().filter(|e| {
        matches!(e.kind, EdgeKind::Calls) && e.dst == "Foo.bar" && (e.confidence - 0.7).abs() < 1e-3
    });
    assert!(
        typed.count() >= 2,
        "expected ≥2 Foo.bar edges at 0.7 (annotated + inferred)"
    );
}

/// `this.foo()` inside `class Bar { method() { ... } }` resolves to
/// `Bar.foo` at conf 0.7.
#[test]
fn this_type_resolves_inside_class() {
    let src = r#"
        class Bar {
            foo() { return 1; }
            run() {
                this.foo();
            }
        }
    "#;
    let out = extract("repo", Path::new("src/this.ts"), src, "typescript").unwrap();
    let typed = out.edges.iter().any(|e| {
        matches!(e.kind, EdgeKind::Calls) && e.dst == "Bar.foo" && (e.confidence - 0.7).abs() < 1e-3
    });
    assert!(typed, "this.foo() should resolve to Bar.foo at 0.7");
}

/// Jest `describe('…', () => { it('…', () => {…}) })` synthesizes
/// Symbols tagged `@test` so dead-code skips them.
#[test]
fn jest_describe_it_tagged_test() {
    let src = r#"
        describe('outer', () => {
            it('inner case', () => {
                console.log('hi');
            });
            it('another', () => { foo(); });
        });
    "#;
    let out = extract("repo", Path::new("src/x.test.js"), src, "javascript").unwrap();
    let test_syms: Vec<_> = out
        .symbols
        .iter()
        .filter(|s| s.doc.contains("@test"))
        .collect();
    assert!(
        test_syms.len() >= 3,
        "expected ≥3 @test-tagged symbols (1 describe + 2 it). Got {}: {:?}",
        test_syms.len(),
        test_syms
            .iter()
            .map(|s| (&s.name, &s.doc))
            .collect::<Vec<_>>()
    );
}

/// `class C implements I { f() {} }` emits a back-edge `I.f` → `C.f`-id
/// at conf 0.6 so dispatched calls reach the impl.
#[test]
fn implements_clause_emits_backedges() {
    let src = r#"
        interface I { f(): void; }
        class C implements I {
            f() {}
        }
    "#;
    let out = extract("repo", Path::new("src/i.ts"), src, "typescript").unwrap();
    let backedge = out.edges.iter().any(|e| {
        matches!(e.kind, EdgeKind::Calls) && e.src == "I.f" && (e.confidence - 0.6).abs() < 1e-3
    });
    assert!(backedge, "missing interface-impl back-edge I.f → C.f");
}

/// `class X { constructor() {} }` emits a Constructor-kind Symbol.
#[test]
fn constructor_kind() {
    let src = r#"
        class X {
            constructor() {}
            run() {}
        }
    "#;
    let out = extract("repo", Path::new("src/x.ts"), src, "typescript").unwrap();
    let ctor = out.symbols.iter().find(|s| s.name == "constructor");
    assert!(ctor.is_some(), "no constructor symbol");
    assert_eq!(ctor.unwrap().kind, Kind::Constructor);
}

/// `const x = require('./lib')` emits Imports + alias.
#[test]
fn commonjs_require() {
    let src = r#"
        const fs = require('fs');
        const lib = require('./lib');
        function use_them() {
            fs.readFileSync('a');
            lib.helper();
        }
    "#;
    let out = extract("repo", Path::new("src/cjs.js"), src, "javascript").unwrap();
    let imports: Vec<&str> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Imports))
        .map(|e| e.dst.as_str())
        .collect();
    assert!(imports.contains(&"fs"), "missing import: fs in {imports:?}");
    assert!(imports.contains(&"./lib"));

    // Alias resolution: `lib.helper()` → `./lib.helper` at conf 0.9
    let aliased = out.edges.iter().any(|e| {
        matches!(e.kind, EdgeKind::Calls)
            && e.dst == "./lib.helper"
            && (e.confidence - 0.9).abs() < 1e-3
    });
    assert!(aliased, "expected lib.helper() → ./lib.helper at 0.9");
}

/// JSX `<Comp />` produces a Calls edge to Comp.
#[test]
fn jsx_component_call() {
    let src = r#"
        function Comp() { return null; }
        function App() {
            return <Comp x={1} />;
        }
    "#;
    let out = extract("repo", Path::new("src/app.tsx"), src, "tsx").unwrap();
    let jsx_call = out
        .edges
        .iter()
        .any(|e| matches!(e.kind, EdgeKind::Calls) && e.dst.ends_with("Comp:function"));
    assert!(
        jsx_call,
        "expected JSX <Comp/> to produce a Calls edge resolved to Comp. Got: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

/// TS class decorator → `@<name>` doc tag on the class.
#[test]
fn decorator_role_tag() {
    let src = r#"
        @Component
        class MyComp {
            run() {}
        }
    "#;
    let out = extract("repo", Path::new("src/c.ts"), src, "typescript").unwrap();
    let cls = out
        .symbols
        .iter()
        .find(|s| s.name == "MyComp")
        .expect("MyComp missing");
    assert!(
        cls.doc.contains("@Component"),
        "expected @Component in doc, got: {:?}",
        cls.doc
    );
}
