use super::*;

#[test]
fn type_aware_method_dispatch() {
    let src = r#"
struct DbClient;
impl DbClient { fn fetch(&self) {} }

fn run(client: DbClient) {
    let q: DbClient = DbClient;
    q.fetch();
    client.fetch();
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let calls: Vec<&Edge> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Calls))
        .collect();
    // Both `q.fetch()` (let-binding) and `client.fetch()` (param) should
    // resolve to `DbClient::fetch` with conf 0.7.
    let typed: Vec<&&Edge> = calls
        .iter()
        .filter(|e| e.dst == "DbClient::fetch" && (e.confidence - 0.7).abs() < 0.001)
        .collect();
    assert_eq!(
        typed.len(),
        2,
        "got {:?}",
        calls
            .iter()
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn resolves_use_aliases_and_local_calls() {
    let src = r#"
use std::collections::HashMap;
use std::sync::{Arc, Mutex as M};

fn helper() {}

fn main_fn() {
    helper();
    let _m: HashMap<i32, i32> = HashMap::new();
    let _arc = Arc::new(0);
    let _m2 = M::new(0);
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let calls: Vec<&Edge> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Calls))
        .collect();
    // helper() should resolve to the local id with confidence 1.0
    let local = calls.iter().find(|e| e.dst.contains(":helper:")).unwrap();
    assert_eq!(local.confidence, 1.0);
    // HashMap::new — the called name is `new`; the receiver `HashMap` is an
    // alias. We resolve the bare leaf `new` against the symbol table; not
    // matching — but `HashMap` should appear via the field access path.
    // Verify the alias map worked by checking Imports are recorded.
    assert!(
        out.edges
            .iter()
            .any(|e| matches!(e.kind, EdgeKind::Imports) && e.dst.contains("HashMap"))
    );
    // Arc::new resolves to the alias path through the import.
    assert!(
        out.edges
            .iter()
            .any(|e| matches!(e.kind, EdgeKind::Imports) && e.dst.contains("Arc"))
    );
}

#[test]
fn extracts_axum_routes() {
    let src = r#"
fn build_app() {
    let r = Router::new()
        .route("/users", get(list_users))
        .route("/users", post(create_user));
}
"#;
    let out = extract("repo", Path::new("server.rs"), src).unwrap();
    let routes: Vec<&Symbol> = out
        .symbols
        .iter()
        .filter(|s| matches!(s.kind, Kind::Route))
        .collect();
    assert!(
        !routes.is_empty(),
        "no routes extracted: symbols={:?}",
        out.symbols
            .iter()
            .map(|s| (&s.name, s.kind))
            .collect::<Vec<_>>()
    );
    let names: Vec<&str> = routes.iter().map(|s| s.name.as_str()).collect();
    assert!(names.contains(&"GET /users"));
    assert!(names.contains(&"POST /users"));
}

#[test]
fn tuple_struct_fields_and_iflet_bindings() {
    let src = r#"
pub struct Pair(i32, String);

struct Wrap;
impl Wrap { fn process(&self) {} }

fn run(opt: Option<Wrap>) {
    if let Some(w) = opt {
        w.process();
    }
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    // Tuple-struct fields → positional 0, 1.
    let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
    assert!(names.contains(&"0"), "tuple field 0 missing: {names:?}");
    assert!(names.contains(&"1"), "tuple field 1 missing");
    // if-let pattern binds `w: Some` → `w.process()` upgrades to typed
    // dispatch (confidence 0.7) with dst `Some::process`.
    let dispatch = out.edges.iter().find(|e| {
        matches!(e.kind, EdgeKind::Calls)
            && e.dst == "Some::process"
            && (e.confidence - 0.7).abs() < 0.001
    });
    assert!(
        dispatch.is_some(),
        "if-let dispatch missing: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn type_alias_chain_followed_in_dispatch() {
    let src = r#"
struct DbClient;
impl DbClient { fn fetch(&self) {} }

type Db = DbClient;
type MyDb = Db;

fn run(client: MyDb) {
    client.fetch();
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    // The dispatch should follow MyDb → Db → DbClient.
    let dispatched = out.edges.iter().find(|e| {
        matches!(e.kind, EdgeKind::Calls)
            && e.dst == "DbClient::fetch"
            && (e.confidence - 0.7).abs() < 0.001
    });
    assert!(
        dispatched.is_some(),
        "type-alias chain not followed: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn generic_bounds_and_where_clauses_emit_implements() {
    let src = r#"
fn f<T: Iterator + Clone>(x: T) {}

fn g<U>(x: U) where U: Send {}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let impls: Vec<(&str, &str, f32)> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Implements))
        .map(|e| (e.src.as_str(), e.dst.as_str(), e.confidence))
        .collect();
    // T: Iterator
    assert!(
        impls
            .iter()
            .any(|(s, d, c)| *s == "T" && *d == "Iterator" && (*c - 0.6).abs() < 0.001),
        "T: Iterator missing: {impls:?}"
    );
    // T: Clone
    assert!(impls.iter().any(|(s, d, _)| *s == "T" && *d == "Clone"));
    // where U: Send
    assert!(impls.iter().any(|(s, d, _)| *s == "U" && *d == "Send"));
}

/// CR-parse-H-2: regression for the `impl Trait for Type` Implements
/// edge that was dead code pre-`a16d182` (lived inside an
/// unreachable `if let Some(kind) = map_kind(k)` branch). Plus
/// CR-parse-H-1: generics are stripped from both src and dst so the
/// edge JOIN-able against `Symbol.qname`.
#[test]
fn impl_trait_for_type_emits_implements_edge() {
    let src = r#"
struct MyIter<T> { _phantom: std::marker::PhantomData<T> }
impl<T> Iterator for MyIter<T> {
    type Item = T;
    fn next(&mut self) -> Option<Self::Item> { None }
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let impls: Vec<(&str, &str, f32)> = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Implements))
        .map(|e| (e.src.as_str(), e.dst.as_str(), e.confidence))
        .collect();
    // The 0.9-confidence impl-block edge: src=MyIter (generics stripped),
    // dst=Iterator. Pre-fix the branch never fired so this assertion failed.
    assert!(
        impls.iter().any(|(s, d, c)| {
            *s == "MyIter" && *d == "Iterator" && (*c - 0.9).abs() < 0.001
        }),
        "expected impl_trait_for_type Implements with src=MyIter dst=Iterator @ 0.9, got: {impls:?}"
    );
}

#[test]
fn enum_variants_and_trait_assoc_emit_symbols() {
    let src = r#"
pub enum Color {
    Red,
    Green,
    Blue(u8, u8, u8),
}

pub trait Iterator {
    type Item;
    const ZERO: usize = 0;
    fn next(&mut self) -> Option<Self::Item>;
}
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
    // Enum variants
    for v in ["Red", "Green", "Blue"] {
        assert!(names.contains(&v), "variant {v} missing: {names:?}");
    }
    // Trait associated type and const
    assert!(names.contains(&"Item"), "associated type missing");
    assert!(names.contains(&"ZERO"), "associated const missing");
    let item_sym = out.symbols.iter().find(|s| s.name == "Item").unwrap();
    assert!(matches!(item_sym.kind, Kind::TypeAlias));
}

#[test]
fn glob_use_and_closure_calls() {
    let src = r#"
use std::collections::*;
use crate::util::{*, helper};

fn outer() {
    let _ = (0..10).map(|x| inner_call(x)).collect::<Vec<_>>();
}

fn inner_call(n: i32) -> i32 { n + 1 }
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    // Wildcard `*` import edges are recorded.
    let has_glob = out
        .edges
        .iter()
        .any(|e| matches!(e.kind, EdgeKind::Imports) && e.dst.contains('*'));
    assert!(has_glob, "glob `*` import not recorded");
    // Closure body's call to inner_call must be attributed to outer.
    let has_inner_call = out
        .edges
        .iter()
        .any(|e| matches!(e.kind, EdgeKind::Calls) && e.dst.contains(":inner_call:"));
    assert!(
        has_inner_call,
        "closure-internal call to inner_call not extracted: {:?}",
        out.edges
            .iter()
            .map(|e| (&e.dst, e.confidence))
            .collect::<Vec<_>>()
    );
}

#[test]
fn module_paths_match_cargo_conventions() {
    assert_eq!(path_to_qname_prefix(Path::new("src/lib.rs")), "crate");
    assert_eq!(path_to_qname_prefix(Path::new("src/main.rs")), "crate");
    assert_eq!(path_to_qname_prefix(Path::new("src/foo.rs")), "crate::foo");
    assert_eq!(
        path_to_qname_prefix(Path::new("src/foo/mod.rs")),
        "crate::foo"
    );
    assert_eq!(
        path_to_qname_prefix(Path::new("src/foo/bar.rs")),
        "crate::foo::bar"
    );
    // Workspace member: deepest `src` wins.
    assert_eq!(
        path_to_qname_prefix(Path::new("crates/cli/src/commands/x.rs")),
        "crate::commands::x"
    );
}

#[test]
fn extracts_basic_items() {
    let src = r#"
/// Doc for foo.
pub fn foo() { bar(); }
fn bar() {}
pub struct Point { x: i32 }
pub enum Color { Red, Green }
pub trait Greet { fn hi(&self); }
impl Greet for Point { fn hi(&self) { println!("hi"); } }
mod inner { pub fn nested() {} }
use std::collections::HashMap;
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();
    let names: Vec<&str> = out.symbols.iter().map(|s| s.name.as_str()).collect();
    for expected in ["foo", "bar", "Point", "Color", "Greet", "inner", "nested"] {
        assert!(names.contains(&expected), "missing {expected}: {names:?}");
    }
    let foo = out.symbols.iter().find(|s| s.name == "foo").unwrap();
    assert!(foo.doc.contains("Doc for foo"));
    assert!(out.edges.iter().any(|e| matches!(e.kind, EdgeKind::Calls)
        && e.dst.contains(":bar:")
        && e.confidence == 1.0));
    assert!(
        out.edges
            .iter()
            .any(|e| matches!(e.kind, EdgeKind::Imports) && e.dst.contains("HashMap"))
    );
}

/// Cover the four most recent Rust extractor features:
/// 1. `await` → distinct `Awaits` edge alongside `Calls`
/// 2. `mod foo;` (no body) → `@external_module` doc tag
/// 3. `#[test]` / `#[tokio::test]` → `@test` doc tag
/// 4. Macro-invocation call detection — calls inside `json!(…)` etc.
///    surface as `Calls` edges via the token-tree heuristic
#[test]
fn rust_recent_features() {
    let src = r#"
mod outer;

async fn helper() -> i32 { 42 }

async fn caller() {
    let _ = helper().await;
    let _ = json!({ "k": macro_helper() });
}

fn macro_helper() -> i32 { 0 }

#[test]
fn unit_test_one() { assert_eq!(2 + 2, 4); }

#[tokio::test]
async fn async_test_two() { assert!(true); }
"#;
    let out = extract("repo", Path::new("src/lib.rs"), src).unwrap();

    // 1. Awaits edge for `helper().await`. Resolver maps the bare
    //    `helper` to its file-local id — accept either form.
    let awaits = out
        .edges
        .iter()
        .filter(|e| matches!(e.kind, EdgeKind::Awaits))
        .count();
    assert!(awaits >= 1, "expected ≥1 Awaits edge, got {awaits}");

    // 2. `mod outer;` (no body) → @external_module on the Module.
    let outer = out
        .symbols
        .iter()
        .find(|s| s.name == "outer" && matches!(s.kind, Kind::Module))
        .expect("mod outer; symbol missing");
    assert!(
        outer.doc.contains("@external_module"),
        "@external_module tag missing: doc={:?}",
        outer.doc
    );

    // 3. Both `#[test]` and `#[tokio::test]` get `@test`.
    for tname in ["unit_test_one", "async_test_two"] {
        let t = out
            .symbols
            .iter()
            .find(|s| s.name == tname)
            .unwrap_or_else(|| panic!("{tname} missing"));
        assert!(
            t.doc.contains("@test"),
            "@test tag missing on {tname}: doc={:?}",
            t.doc
        );
    }

    // 4. Macro-invocation call detection: `json!({…macro_helper()…})`
    //    must surface a Calls edge to `macro_helper`.
    let macro_call = out.edges.iter().any(|e| {
        matches!(e.kind, EdgeKind::Calls)
            && (e.dst == "macro_helper" || e.dst.contains(":macro_helper:"))
    });
    assert!(
        macro_call,
        "Calls edge for macro-body `macro_helper()` missing. Got: {:?}",
        out.edges
            .iter()
            .filter(|e| matches!(e.kind, EdgeKind::Calls))
            .map(|e| &e.dst)
            .collect::<Vec<_>>()
    );
}
