//! End-to-end integration: open a repo storage, write symbols + edges, run
//! the production blast-radius / dead-code queries, and verify the round-trip.

use ckg_core::{Edge, EdgeKind, Kind, RepoId, Symbol};
use ckg_storage::{Storage, queries};
use cozo::DataValue;
use tempfile::tempdir;

fn s(id: &str, qname: &str) -> Symbol {
    Symbol {
        id: id.into(),
        qname: qname.into(),
        name: qname.split("::").last().unwrap_or(qname).into(),
        kind: Kind::Function,
        file: "x.rs".into(),
        line: 1,
        col: 0,
        is_public: true,
        doc: String::new(),
        hash: "h".into(),
    }
}

fn call(src: &str, dst: &str) -> Edge {
    Edge {
        kind: EdgeKind::Calls,
        src: src.into(),
        dst: dst.into(),
        confidence: 1.0,
    }
}

#[test]
fn blast_radius_round_trip() {
    let dir = tempdir().unwrap();
    let id = RepoId::try_new("e2eaaaaaaaaaaaaaaaaaaaaa").unwrap();
    let st = Storage::open_unverified(id, dir.path()).unwrap();

    // a → b → c chain plus a stray d → c
    st.put_symbols(&[s("a", "a"), s("b", "b"), s("c", "c"), s("d", "d")])
        .unwrap();
    st.put_edges(&[call("a", "b"), call("b", "c"), call("d", "c")])
        .unwrap();

    let (script, params) = queries::blast_radius("c", 3);
    let rows = st.run_with_immutable(&script, params).unwrap();
    let ids: Vec<String> = rows
        .rows
        .iter()
        .filter_map(|r| match r.first() {
            Some(DataValue::Str(s)) => Some(s.to_string()),
            _ => None,
        })
        .collect();
    // Ancestors of c: b (depth 1), a (depth 2), d (depth 1).
    assert!(ids.contains(&"b".into()));
    assert!(ids.contains(&"a".into()));
    assert!(ids.contains(&"d".into()));
    assert!(
        !ids.contains(&"c".into()),
        "self should not be in blast radius"
    );
}

#[test]
fn dead_code_round_trip() {
    let dir = tempdir().unwrap();
    let id = RepoId::try_new("e2ebbbbbbbbbbbbbbbbbbbbb").unwrap();
    let st = Storage::open_unverified(id, dir.path()).unwrap();
    // a calls b. c is dead.
    st.put_symbols(&[s("a", "a"), s("b", "b"), s("c", "c")])
        .unwrap();
    st.put_edges(&[call("a", "b")]).unwrap();

    let rows = st.run_immutable(queries::DEAD_CODE).unwrap();
    let ids: Vec<String> = rows
        .rows
        .iter()
        .filter_map(|r| match r.first() {
            Some(DataValue::Str(s)) => Some(s.to_string()),
            _ => None,
        })
        .collect();
    // a is dead (nothing calls it), c is dead. b is called by a → not dead.
    assert!(ids.contains(&"a".into()));
    assert!(ids.contains(&"c".into()));
    assert!(!ids.contains(&"b".into()));
}

#[test]
fn cross_file_resolution_round_trip() {
    let dir = tempdir().unwrap();
    let id = RepoId::try_new("e2eccccccccccccccccccccc").unwrap();
    let st = Storage::open_unverified(id, dir.path()).unwrap();
    // helper exists as a Symbol; another fn `caller` has a Calls edge with
    // dst="helper" (a bare name). resolve_cross_file_calls should rewrite it
    // to the helper's id.
    st.put_symbols(&[
        Symbol {
            id: "id-helper".into(),
            qname: "mod::helper".into(),
            name: "helper".into(),
            kind: Kind::Function,
            file: "b.rs".into(),
            line: 1,
            col: 0,
            is_public: true,
            doc: String::new(),
            hash: "h".into(),
        },
        s("id-caller", "mod::caller"),
    ])
    .unwrap();
    st.put_edges(&[Edge {
        kind: EdgeKind::Calls,
        src: "id-caller".into(),
        dst: "helper".into(),
        confidence: 0.5,
    }])
    .unwrap();

    let n = st.resolve_cross_file_calls().unwrap();
    assert_eq!(n, 1);
    let rows = st.run_immutable("?[src, dst] := *Calls{src, dst}").unwrap();
    let dst = match &rows.rows[0][1] {
        DataValue::Str(s) => s.to_string(),
        _ => panic!(),
    };
    assert_eq!(dst, "id-helper");
}

/// Auto-rebuild path: open a DB, stamp a fake old `schema_version`, drop
/// the handle, reopen → the binary's current version differs, so
/// `Storage::open` must wipe-and-recreate via the build-then-swap helper.
/// Verifies the data is gone (proving rebuild ran) and the rebuild leaves
/// no `.new` / `.old` debris behind.
#[test]
fn schema_version_drift_triggers_auto_rebuild() {
    let dir = tempdir().unwrap();
    let id = RepoId::try_new("0011223344556677889900aa").unwrap();

    // First open — fresh DB, current schema. Write a row so we can prove
    // the rebuild wiped it.
    {
        let st = Storage::open_unverified(id.clone(), dir.path()).unwrap();
        st.put_symbols(&[s("survivor", "crate::survivor")]).unwrap();
        let count = st.run_immutable("?[id] := *Symbol{id}").unwrap().rows.len();
        assert_eq!(count, 1, "first open should have 1 symbol");
    }

    // Force a version mismatch by stamping a stale value into Meta.
    {
        let st = Storage::open_unverified(id.clone(), dir.path()).unwrap();
        st.run_mutable_unchecked(
            "?[key, value] <- [[\"schema_version\", \"0\"]] :put Meta {key => value}",
        )
        .unwrap();
    }

    // Reopen — version "0" ≠ current → auto-rebuild fires. Symbol
    // relation must be empty (DDL recreated it from scratch).
    let st = Storage::open_unverified(id, dir.path()).unwrap();
    let count = st.run_immutable("?[id] := *Symbol{id}").unwrap().rows.len();
    assert_eq!(
        count, 0,
        "auto-rebuild should have wiped the Symbol relation"
    );

    // No leftover `.new` / `.old` debris from the swap.
    let workspace = dir.path().join("workspace_folders").join("rebuildtest0001");
    let parent = workspace.parent().unwrap();
    let leftovers: Vec<_> = std::fs::read_dir(parent)
        .unwrap()
        .filter_map(|e| e.ok())
        .filter(|e| {
            let n = e.file_name().to_string_lossy().to_string();
            n.ends_with(".new") || n.ends_with(".old") || n.contains("rebuild-bak")
        })
        .collect();
    assert!(
        leftovers.is_empty(),
        "rebuild swap left debris behind: {leftovers:?}"
    );
}
