// Persistence tests: save/load roundtrip, SQLite, incremental builds,
// workspace siblings, checksums, corpus_lower lazy loading

#[test]
fn test_save_load_roundtrip_v1_1() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    // Create a simple Rust file
    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn hello() { world(); }\nfn world() { println!(\"hi\"); }\n",
    )
    .unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let index_path = project_path.join("idx");
    index.save(&index_path).unwrap();

    let loaded = AgentContextIndex::load(&index_path).unwrap();
    // load() prefers SQLite (v2.0.0) over blob (v1.4.0) when both exist
    assert!(
        loaded.manifest.version == "2.0.0" || loaded.manifest.version == "1.4.0",
        "expected v2.0.0 or v1.4.0, got {}",
        loaded.manifest.version,
    );
    assert_eq!(loaded.functions.len(), index.functions.len());
    // SQLite path skips corpus (FTS5 handles search); blob path has corpus
    if loaded.manifest.version == "2.0.0" {
        assert!(loaded.corpus.is_empty(), "SQLite load should skip corpus");
    } else {
        assert_eq!(loaded.corpus.len(), index.corpus.len());
    }
}

#[test]
fn test_load_prefers_sqlite_over_blob() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn alpha() { beta(); }\nfn beta() {}\n",
    )
    .unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let index_path = project_path.join("idx");
    index.save(&index_path).unwrap();

    // Phase 3: only context.db and manifest written (no blob)
    let db_path = index_path.with_extension("db");
    assert!(db_path.exists(), "context.db should exist after save");
    assert!(
        index_path.join("manifest.json").exists(),
        "manifest should exist"
    );
    assert!(
        !index_path.join("functions.lz4").exists(),
        "blob should NOT be written in Phase 3"
    );

    // load() prefers SQLite
    let loaded = AgentContextIndex::load(&index_path).unwrap();
    assert_eq!(loaded.manifest.version, "2.0.0");
    assert!(loaded.db_path.is_some());
    assert_eq!(loaded.functions.len(), index.functions.len());

    // Verify call graph queryable via on-demand SQLite lookup
    // (calls/called_by HashMaps are empty — queried on-demand)
    let has_call_data = (0..loaded.functions.len())
        .any(|i| !loaded.get_calls(i).is_empty() || !loaded.get_called_by(i).is_empty());
    assert!(
        has_call_data,
        "should have call graph data via SQLite query"
    );
}

#[test]
fn test_load_fails_without_sqlite_or_blob() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(project_path.join("src/lib.rs"), "fn gamma() {}\n").unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let index_path = project_path.join("idx");
    index.save(&index_path).unwrap();

    // Remove SQLite DB — no blob either (Phase 3 doesn't write blobs)
    let db_path = index_path.with_extension("db");
    std::fs::remove_file(&db_path).unwrap();

    // Should fail: no SQLite, no blob
    let result = AgentContextIndex::load(&index_path);
    assert!(result.is_err());
}

#[test]
fn test_incremental_build_unchanged() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn alpha() { }\nfn beta() { }\n",
    )
    .unwrap();

    let original = AgentContextIndex::build(project_path).unwrap();
    let incremental = AgentContextIndex::build_incremental(project_path, &original).unwrap();

    // Same number of functions (nothing changed)
    assert_eq!(incremental.functions.len(), original.functions.len());
}

#[test]
fn test_incremental_build_with_change() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(project_path.join("src/lib.rs"), "fn alpha() { }\n").unwrap();

    let original = AgentContextIndex::build(project_path).unwrap();
    assert_eq!(original.functions.len(), 1);

    // Modify the file to add a function
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn alpha() { }\nfn gamma() { }\n",
    )
    .unwrap();

    let incremental = AgentContextIndex::build_incremental(project_path, &original).unwrap();
    // Should now have 2 functions
    assert_eq!(incremental.functions.len(), 2);
}

#[test]
fn test_incremental_build_propagates_db_path() {
    // Regression: build_incremental used to reset db_path to None, so
    // deferred-source backfill was skipped and every query after an
    // incremental update returned empty source (Bug B of the source wipe).
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn alpha() { let a = 1; }\n",
    )
    .unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let index_path = project_path.join("idx");
    index.save(&index_path).unwrap();

    let loaded = AgentContextIndex::load(&index_path).unwrap();
    let db_path = loaded.db_path.clone().expect("SQLite load sets db_path");

    let updated = AgentContextIndex::build_incremental(project_path, &loaded).unwrap();
    assert_eq!(
        updated.db_path.as_deref(),
        Some(db_path.as_path()),
        "incremental index must keep the db_path it loaded from"
    );

    // Deferred source must remain retrievable through the incremental index
    let (file_path, start_line) = {
        let entry = &updated.functions[0];
        (entry.file_path.clone(), entry.start_line)
    };
    let src = updated.load_source_for(&file_path, start_line);
    assert!(
        src.contains("alpha"),
        "expected source via DB backfill, got {src:?}"
    );
}

#[test]
fn test_incremental_save_preserves_source_for_reused_entries() {
    // Regression (source wipe, Bug A): lightweight SQLite loads defer source
    // (entries carry ""). build_incremental clones those entries for
    // checksum-reused files, and saving used to rewrite the full DB with
    // source='' for every reused row — repeated incremental saves converged
    // the index to all-empty source, breaking --include-source,
    // pmat_query_code, and pmat_get_function.
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(project_path.join("src/a.rs"), "fn alpha() { let a = 1; }\n").unwrap();
    std::fs::write(project_path.join("src/b.rs"), "fn beta() { let b = 2; }\n").unwrap();
    std::fs::write(project_path.join("src/c.rs"), "fn gamma() { let c = 3; }\n").unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let index_path = project_path.join("idx");
    index.save(&index_path).unwrap();

    // Lightweight SQLite load: source is deferred (empty) for every entry
    let loaded = AgentContextIndex::load(&index_path).unwrap();
    assert!(loaded.functions.iter().all(|f| f.source.is_empty()));

    // Change one of three files; a.rs and b.rs entries are checksum-reused
    std::fs::write(
        project_path.join("src/c.rs"),
        "fn gamma() { let c = 30; }\nfn delta() { let d = 4; }\n",
    )
    .unwrap();

    let mut updated = AgentContextIndex::build_incremental(project_path, &loaded).unwrap();
    assert_eq!(updated.functions.len(), 4);

    // The save path backfills deferred source before rewriting the DB
    updated.load_all_source();
    assert!(
        updated.functions.iter().all(|f| !f.source.is_empty()),
        "backfill must restore source for reused entries"
    );
    updated.save(&index_path).unwrap();

    // Reload raw rows: EVERY row must still have non-empty source
    let conn = super::sqlite_backend::open_db(&index_path.with_extension("db")).unwrap();
    let rows = super::sqlite_backend::load_functions(&conn).unwrap();
    assert_eq!(rows.len(), 4);
    for row in &rows {
        assert!(
            !row.source.is_empty(),
            "row '{}' persisted with empty source after incremental save",
            row.function_name
        );
    }
}

#[test]
fn test_parse_workspace_siblings() {
    let toml = r#"siblings = ["../aprender", "../trueno", "../realizar"]"#;
    let result = parse_workspace_siblings(toml);
    assert_eq!(result, vec!["../aprender", "../trueno", "../realizar"]);
}

#[test]
fn test_parse_workspace_siblings_empty() {
    let toml = "# no siblings configured\n";
    let result = parse_workspace_siblings(toml);
    assert!(result.is_empty());
}

#[test]
fn test_parse_workspace_siblings_single() {
    let toml = r#"siblings = ["../trueno"]"#;
    let result = parse_workspace_siblings(toml);
    assert_eq!(result, vec!["../trueno"]);
}

#[test]
fn test_parse_workspace_siblings_with_spaces() {
    let toml = r#"siblings  =  [ "../a" , "../b" ]"#;
    let result = parse_workspace_siblings(toml);
    assert_eq!(result, vec!["../a", "../b"]);
}

#[test]
fn test_discover_siblings_no_config() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let result = AgentContextIndex::discover_sibling_indexes(temp_dir.path());
    assert!(result.is_empty());
}

#[test]
fn test_file_checksums_populated() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(project_path.join("src/lib.rs"), "fn test_func() { }\n").unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    assert!(!index.manifest.file_checksums.is_empty());
    assert!(index.manifest.file_checksums.contains_key("src/lib.rs"));
}

#[test]
fn test_discover_siblings_with_config() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    // Create .pmat/workspace.toml
    std::fs::create_dir_all(project_path.join(".pmat")).unwrap();
    std::fs::write(
        project_path.join(".pmat/workspace.toml"),
        r#"siblings = ["../sibling_a"]"#,
    )
    .unwrap();

    // We can't easily create a real sibling in tempdir, so just verify
    // the function reads the config correctly without panicking
    let result = AgentContextIndex::discover_sibling_indexes(project_path);
    // Sibling doesn't exist, so no results
    assert!(result.is_empty());
}

#[test]
fn test_discover_siblings_with_real_sibling() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let workspace = temp_dir.path();

    // Create project A
    let project_a = workspace.join("project_a");
    std::fs::create_dir_all(project_a.join(".pmat")).unwrap();

    // Create project B with an index
    let project_b = workspace.join("project_b");
    std::fs::create_dir_all(project_b.join("src")).unwrap();
    std::fs::write(project_b.join("src/lib.rs"), "fn sibling_func() {}\n").unwrap();
    let b_index = AgentContextIndex::build(&project_b).unwrap();
    let b_idx_path = project_b.join(".pmat/context.idx");
    std::fs::create_dir_all(b_idx_path.parent().unwrap()).unwrap();
    b_index.save(&b_idx_path).unwrap();

    // Configure A to point to B
    std::fs::write(
        project_a.join(".pmat/workspace.toml"),
        r#"siblings = ["../project_b"]"#,
    )
    .unwrap();

    let siblings = AgentContextIndex::discover_sibling_indexes(&project_a);
    assert_eq!(siblings.len(), 1);
    assert_eq!(siblings[0].1, "project_b");
}

#[test]
fn test_save_and_load_preserves_calls() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn caller() { callee(); }\nfn callee() { println!(\"hi\"); }\n",
    )
    .unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let idx_path = project_path.join("idx");
    index.save(&idx_path).unwrap();

    let loaded = AgentContextIndex::load(&idx_path).unwrap();
    // Call graph queryable via on-demand SQLite (in-memory maps empty on SQLite load)
    // Verify by checking actual call relationships
    let original_calls: Vec<String> = index.get_calls(0).iter().map(|s| s.to_string()).collect();
    let loaded_calls: Vec<String> = loaded.get_calls(0).iter().map(|s| s.to_string()).collect();
    assert_eq!(
        loaded_calls.len(),
        original_calls.len(),
        "call graph should be preserved"
    );
}

#[test]
fn test_load_invalid_path() {
    let result = AgentContextIndex::load(Path::new("/nonexistent/path"));
    assert!(result.is_err());
}

#[test]
fn test_save_load_roundtrip_corpus_lower_lazy() {
    let temp_dir = tempfile::TempDir::new().unwrap();
    let project_path = temp_dir.path();

    std::fs::create_dir_all(project_path.join("src")).unwrap();
    std::fs::write(
        project_path.join("src/lib.rs"),
        "fn hello_world() { }\nfn goodbye_world() { }\n",
    )
    .unwrap();

    let index = AgentContextIndex::build(project_path).unwrap();
    let idx_path = project_path.join("idx");
    index.save(&idx_path).unwrap();

    let loaded = AgentContextIndex::load(&idx_path).unwrap();
    // corpus_lower should be lazily computed on load
    assert_eq!(loaded.corpus_lower.len(), loaded.corpus.len());
    for (orig, lower) in loaded.corpus.iter().zip(loaded.corpus_lower.iter()) {
        assert_eq!(lower, &orig.to_lowercase());
    }
}
