#![allow(clippy::panic)]

//! Regression tests for manifest diff classification, builder diagnostics, and file-digest ordering invariants.

use super::support::*;

#[test]
fn manifest_diff_classifies_added_modified_deleted_in_path_order() {
    let old = vec![
        digest("repo/zeta.rs", 10, Some(10), "hash-z"),
        digest("repo/alpha.rs", 1, Some(1), "hash-a"),
        digest("repo/charlie.rs", 3, Some(3), "hash-c-old"),
    ];
    let new = vec![
        digest("repo/bravo.rs", 2, Some(2), "hash-b"),
        digest("repo/charlie.rs", 4, Some(4), "hash-c-new"),
        digest("repo/zeta.rs", 10, Some(10), "hash-z"),
    ];

    let manifest_diff = diff(&old, &new);

    assert_eq!(
        manifest_diff.added,
        vec![digest("repo/bravo.rs", 2, Some(2), "hash-b")]
    );
    assert_eq!(
        manifest_diff.modified,
        vec![digest("repo/charlie.rs", 4, Some(4), "hash-c-new")]
    );
    assert_eq!(
        manifest_diff.deleted,
        vec![digest("repo/alpha.rs", 1, Some(1), "hash-a")]
    );
}

#[cfg(unix)]
#[test]
fn changed_only_retains_previous_entry_when_digest_read_fails() -> FriggResult<()> {
    let root = temp_workspace_root("manifest-changed-only-read-failure-retains-entry");
    prepare_workspace(&root, &[("src/lib.rs", "pub fn original() {}\n")])?;

    let builder = ManifestBuilder::default();
    let previous = builder.build(&root)?;
    let file_path = root.join("src/lib.rs");
    let previous_entry = previous
        .iter()
        .find(|entry| entry.path == file_path)
        .cloned()
        .expect("previous manifest should contain the seeded file");

    fs::write(&file_path, "pub fn modified_with_a_longer_body() {}\n").map_err(FriggError::Io)?;
    set_file_mode(&file_path, 0o000)?;

    let output = builder.build_changed_only_with_diagnostics(&root, &previous)?;

    set_file_mode(&file_path, 0o644)?;

    assert!(
        output
            .diagnostics
            .iter()
            .any(|diagnostic| diagnostic.kind == ManifestDiagnosticKind::Read
                && diagnostic.path.as_deref() == Some(file_path.as_path())),
        "a Read diagnostic should be recorded for the unreadable file"
    );
    let retained = output
        .entries
        .iter()
        .find(|entry| entry.path == file_path)
        .expect("file with a transient read failure must be retained, not dropped");
    assert_eq!(
        retained, &previous_entry,
        "retained entry should reuse the previous digest verbatim"
    );
    assert!(
        diff(&previous, &output.entries).deleted.is_empty(),
        "a still-present file must not be classified as deleted after a read failure"
    );

    cleanup_workspace(&root);
    Ok(())
}

#[cfg(unix)]
#[test]
fn changed_only_fails_when_new_file_digest_read_fails() -> FriggResult<()> {
    let root = temp_workspace_root("manifest-changed-only-new-read-failure-errors");
    prepare_workspace(&root, &[("src/lib.rs", "pub fn original() {}\n")])?;

    let builder = ManifestBuilder::default();
    let previous = builder.build(&root)?;
    let new_file_path = root.join("src/new.rs");
    fs::write(&new_file_path, "pub fn new_file() {}\n").map_err(FriggError::Io)?;
    set_file_mode(&new_file_path, 0o000)?;

    let err = builder
        .build_changed_only_with_diagnostics(&root, &previous)
        .expect_err("new files with unreadable content must fail changed-only manifest build");

    set_file_mode(&new_file_path, 0o644)?;

    assert!(
        matches!(err, FriggError::Io(_)),
        "expected unreadable new file to surface as an IO error, got {err:?}"
    );

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn changed_only_directory_hint_rehashes_child_with_matching_metadata() -> FriggResult<()> {
    let root = temp_workspace_root("manifest-changed-only-directory-hint");
    prepare_workspace(&root, &[("src/lib.rs", "pub fn original() {}\n")])?;

    let builder = ManifestBuilder::default();
    let original = builder.build(&root)?;
    let original_entry = original
        .iter()
        .find(|entry| entry.path == root.join("src/lib.rs"))
        .cloned()
        .expect("original manifest should contain src/lib.rs");

    fs::write(root.join("src/lib.rs"), "pub fn changed() {}\n").map_err(FriggError::Io)?;
    let current = builder.build(&root)?;
    let current_entry = current
        .iter()
        .find(|entry| entry.path == root.join("src/lib.rs"))
        .cloned()
        .expect("current manifest should contain src/lib.rs");
    let stale_metadata_entry = FileDigest {
        hash_blake3_hex: original_entry.hash_blake3_hex.clone(),
        ..current_entry.clone()
    };

    let unchanged_without_hint = builder.build_changed_only_with_hints_and_diagnostics(
        &root,
        &[stale_metadata_entry],
        &[],
    )?;
    assert_eq!(
        unchanged_without_hint
            .entries
            .first()
            .map(|entry| entry.hash_blake3_hex.as_str()),
        Some(original_entry.hash_blake3_hex.as_str()),
        "matching metadata without a dirty hint should reuse the previous digest"
    );

    let changed_with_directory_hint = builder.build_changed_only_with_hints_and_diagnostics(
        &root,
        &[unchanged_without_hint.entries[0].clone()],
        &[PathBuf::from("src")],
    )?;
    assert_eq!(
        changed_with_directory_hint
            .entries
            .first()
            .map(|entry| entry.hash_blake3_hex.as_str()),
        Some(current_entry.hash_blake3_hex.as_str()),
        "a dirty directory hint should force child files to be rehashed"
    );

    cleanup_workspace(&root);
    Ok(())
}

#[test]
fn navigation_symbol_target_rank_is_stable_and_precedence_ordered() {
    let symbol = SymbolDefinition {
        stable_id: "sym-user-001".to_owned(),
        language: SymbolLanguage::Rust,
        kind: SymbolKind::Struct,
        name: "User".to_owned(),
        path: PathBuf::from("src/lib.rs"),
        line: 1,
        span: SourceSpan {
            start_byte: 0,
            end_byte: 10,
            start_line: 1,
            start_column: 1,
            end_line: 1,
            end_column: 10,
        },
    };

    assert_eq!(
        navigation_symbol_target_rank(&symbol, "sym-user-001"),
        Some(0)
    );
    assert_eq!(navigation_symbol_target_rank(&symbol, "User"), Some(1));
    assert_eq!(navigation_symbol_target_rank(&symbol, "user"), Some(2));
    assert_eq!(navigation_symbol_target_rank(&symbol, "Account"), None);
}

#[test]
fn manifest_diff_detects_mtime_only_change_as_modified() {
    let old = vec![digest("repo/file.rs", 10, Some(100), "same-hash")];
    let new = vec![digest("repo/file.rs", 10, Some(200), "same-hash")];

    let manifest_diff = diff(&old, &new);

    assert!(manifest_diff.added.is_empty());
    assert_eq!(
        manifest_diff.modified,
        vec![digest("repo/file.rs", 10, Some(200), "same-hash")]
    );
    assert!(manifest_diff.deleted.is_empty());
}

#[test]
fn manifest_diff_is_empty_for_identical_records_with_different_input_order() {
    let old = vec![
        digest("repo/b.rs", 2, Some(2), "hash-b"),
        digest("repo/a.rs", 1, Some(1), "hash-a"),
        digest("repo/c.rs", 3, Some(3), "hash-c"),
    ];
    let new = vec![
        digest("repo/c.rs", 3, Some(3), "hash-c"),
        digest("repo/a.rs", 1, Some(1), "hash-a"),
        digest("repo/b.rs", 2, Some(2), "hash-b"),
    ];

    let manifest_diff = diff(&old, &new);

    assert!(manifest_diff.added.is_empty());
    assert!(manifest_diff.modified.is_empty());
    assert!(manifest_diff.deleted.is_empty());
}

#[test]
fn determinism_manifest_builder_repeated_runs_match_exactly() -> FriggResult<()> {
    let fixture_root = prepare_manifest_fixture_workspace("manifest-builder-determinism-runs")?;
    let builder = ManifestBuilder::default();

    let first = builder.build(&fixture_root)?;
    let second = builder.build(&fixture_root)?;
    let third = builder.build(&fixture_root)?;

    assert_eq!(first, second);
    assert_eq!(second, third);
    cleanup_workspace(&fixture_root);
    Ok(())
}

#[test]
fn determinism_manifest_builder_uses_fixture_only_expected_paths() -> FriggResult<()> {
    let fixture_root = prepare_manifest_fixture_workspace("manifest-builder-determinism-paths")?;
    let builder = ManifestBuilder::default();

    let manifest = builder.build(&fixture_root)?;
    let relative_paths = manifest_relative_paths(&manifest, &fixture_root)?;

    assert_eq!(
        relative_paths,
        vec![
            PathBuf::from("README.md"),
            PathBuf::from("src/lib.rs"),
            PathBuf::from("src/nested/data.txt"),
        ]
    );
    cleanup_workspace(&fixture_root);
    Ok(())
}

#[test]
fn manifest_builder_respects_gitignored_contract_artifacts() -> FriggResult<()> {
    let workspace_root = temp_workspace_root("manifest-builder-gitignored-contracts");
    prepare_workspace(
        &workspace_root,
        &[
            ("contracts/errors.md", "invalid_params\n"),
            ("src/main.rs", "fn main() {}\n"),
        ],
    )?;
    fs::write(workspace_root.join(".gitignore"), "contracts\n").map_err(FriggError::Io)?;

    let manifest = ManifestBuilder::default().build(&workspace_root)?;
    let relative_paths = manifest_relative_paths(&manifest, &workspace_root)?;

    assert!(
        !relative_paths.contains(&PathBuf::from("contracts/errors.md")),
        "manifest discovery should respect gitignored contract artifacts"
    );

    Ok(())
}

#[test]
fn manifest_builder_excludes_target_artifacts_without_gitignore() -> FriggResult<()> {
    let workspace_root = temp_workspace_root("manifest-builder-target-exclusion");
    prepare_workspace(
        &workspace_root,
        &[
            ("src/main.rs", "fn main() {}\n"),
            ("target/debug/app", "binary\n"),
        ],
    )?;

    let manifest = ManifestBuilder::default().build(&workspace_root)?;
    let relative_paths = manifest_relative_paths(&manifest, &workspace_root)?;

    assert!(
        !relative_paths
            .iter()
            .any(|path| path.starts_with(Path::new("target"))),
        "target artifacts must stay excluded from manifest discovery: {relative_paths:?}"
    );

    Ok(())
}

#[test]
fn manifest_builder_respects_root_ignore_file_for_auxiliary_trees() -> FriggResult<()> {
    let workspace_root = temp_workspace_root("manifest-builder-root-ignore");
    prepare_workspace(
        &workspace_root,
        &[
            ("src/main.rs", "fn main() {}\n"),
            ("auxiliary/embedded-repo/src/lib.rs", "pub fn leaked() {}\n"),
        ],
    )?;
    fs::write(workspace_root.join(".ignore"), "auxiliary/\n").map_err(FriggError::Io)?;

    let manifest = ManifestBuilder::default().build(&workspace_root)?;
    let relative_paths = manifest_relative_paths(&manifest, &workspace_root)?;

    assert!(
        !relative_paths
            .iter()
            .any(|path| path.starts_with(Path::new("auxiliary"))),
        "root ignore files must exclude auxiliary trees from manifest discovery: {relative_paths:?}"
    );

    Ok(())
}

#[test]
fn incremental_roundtrip_persist_load_and_diff() -> FriggResult<()> {
    let db_path = temp_db_path("incremental-roundtrip");
    let fixture_root = prepare_manifest_fixture_workspace("manifest-builder-roundtrip")?;
    let manifest_store = ManifestStore::new(&db_path);
    manifest_store.initialize()?;

    let repository_id = "repo-001";
    let snapshot_old = "snapshot-001";
    let snapshot_new = "snapshot-002";
    let builder = ManifestBuilder::default();
    let old_manifest = builder.build(&fixture_root)?;

    manifest_store.persist_snapshot_manifest(repository_id, snapshot_old, &old_manifest)?;
    let loaded_old = manifest_store.load_snapshot_manifest(snapshot_old)?;
    assert_eq!(loaded_old, old_manifest);

    let latest_before = manifest_store
        .load_latest_manifest_for_repository(repository_id)?
        .expect("expected latest repository manifest");
    assert_eq!(latest_before.snapshot_id, snapshot_old);
    assert_eq!(latest_before.entries, old_manifest);

    let new_manifest = mutate_manifest_for_incremental_roundtrip(&old_manifest, &fixture_root)?;
    manifest_store.persist_snapshot_manifest(repository_id, snapshot_new, &new_manifest)?;

    let loaded_new = manifest_store.load_snapshot_manifest(snapshot_new)?;
    assert_eq!(loaded_new, new_manifest);

    let latest_after = manifest_store
        .load_latest_manifest_for_repository(repository_id)?
        .expect("expected latest repository manifest after second snapshot");
    assert_eq!(latest_after.snapshot_id, snapshot_new);
    assert_eq!(latest_after.entries, new_manifest);

    let manifest_diff = diff(&latest_before.entries, &latest_after.entries);
    assert_eq!(manifest_diff.added.len(), 1);
    assert_eq!(manifest_diff.modified.len(), 1);
    assert_eq!(manifest_diff.deleted.len(), 1);
    assert_eq!(
        manifest_diff.added[0].path,
        fixture_root.join("src/incremental-new.rs")
    );
    assert_eq!(
        manifest_diff.modified[0].path,
        fixture_root.join("README.md")
    );
    assert_eq!(
        manifest_diff.deleted[0].path,
        fixture_root.join("src/nested/data.txt")
    );

    cleanup_db(&db_path);
    cleanup_workspace(&fixture_root);
    Ok(())
}

#[test]
fn index_materializes_authoritative_retrieval_projection_heads() -> FriggResult<()> {
    let db_path = temp_db_path("index-materializes-retrieval-projections");
    let workspace_root = temp_workspace_root("index-materializes-retrieval-projections");
    prepare_workspace(
        &workspace_root,
        &[
            (
                "Cargo.toml",
                "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
            ),
            ("src/main.rs", "fn main() {}\n"),
            ("tests/main_test.rs", "#[test]\nfn main_test() {}\n"),
        ],
    )?;

    let semantic_runtime = SemanticRuntimeConfig {
        enabled: false,
        provider: None,
        model: None,
        strict_mode: false,
    };
    let credentials = SemanticRuntimeCredentials {
        openai_api_key: None,
        gemini_api_key: None,
    };
    let summary = index_repository_with_runtime_config(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::Full,
        &semantic_runtime,
        &credentials,
    )?;

    let storage = Storage::new(&db_path);
    for family in [
        "path_witness",
        "test_subject",
        "entrypoint_surface",
        "subtree_coverage",
    ] {
        let head = storage
            .load_retrieval_projection_head_for_repository_snapshot_family(
                "repo-001",
                &summary.snapshot_id,
                family,
            )?
            .unwrap_or_else(|| panic!("expected retrieval projection head for family '{family}'"));
        assert_eq!(head.heuristic_version, 1);
        assert_eq!(head.input_modes, vec!["path".to_owned()]);
    }
    let path_relation_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_relation",
        )?
        .expect("expected path relation head");
    assert_eq!(path_relation_head.heuristic_version, 1);
    assert_eq!(path_relation_head.input_modes, vec!["path".to_owned()]);

    let path_surface_term_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_surface_term",
        )?
        .expect("expected path surface term head");
    assert_eq!(path_surface_term_head.heuristic_version, 1);
    assert_eq!(
        path_surface_term_head.input_modes,
        vec!["ast".to_owned(), "path".to_owned()]
    );

    let path_anchor_sketch_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_anchor_sketch",
        )?
        .expect("expected path anchor sketch head");
    assert_eq!(path_anchor_sketch_head.heuristic_version, 2);
    assert_eq!(
        path_anchor_sketch_head.input_modes,
        vec!["ast".to_owned(), "path".to_owned()]
    );
    assert!(
        !storage
            .load_path_witness_projections_for_repository_snapshot(
                "repo-001",
                &summary.snapshot_id
            )?
            .is_empty()
    );
    assert!(
        !storage
            .load_path_relation_projections_for_repository_snapshot(
                "repo-001",
                &summary.snapshot_id
            )?
            .is_empty()
    );
    assert!(
        !storage
            .load_path_surface_term_projections_for_repository_snapshot(
                "repo-001",
                &summary.snapshot_id,
            )?
            .is_empty()
    );

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn index_materializes_retrieval_projection_heads_with_scip_inputs() -> FriggResult<()> {
    let db_path = temp_db_path("index-materializes-retrieval-projections-scip");
    let workspace_root = temp_workspace_root("index-materializes-retrieval-projections-scip");
    prepare_workspace(
        &workspace_root,
        &[
            (
                "src/a.rs",
                "pub struct User;\nimpl User { pub fn helper() {} }\n",
            ),
            ("src/base.rs", "pub struct Entity;\n"),
            (
                ".frigg/scip/fixture.json",
                "{\n  \"documents\": [\n    {\n      \"relative_path\": \"src/a.rs\",\n      \"occurrences\": [\n        { \"symbol\": \"scip-rust pkg repo#User\", \"range\": [0, 11, 15], \"symbol_roles\": 1 }\n      ],\n      \"symbols\": [\n        {\n          \"symbol\": \"scip-rust pkg repo#User\",\n          \"display_name\": \"User\",\n          \"kind\": \"struct\",\n          \"relationships\": [\n            { \"symbol\": \"scip-rust pkg repo#Entity\", \"is_reference\": true },\n            { \"symbol\": \"scip-rust pkg repo#Entity\", \"is_implementation\": true }\n          ]\n        }\n      ]\n    },\n    {\n      \"relative_path\": \"src/base.rs\",\n      \"occurrences\": [\n        { \"symbol\": \"scip-rust pkg repo#Entity\", \"range\": [0, 11, 17], \"symbol_roles\": 1 }\n      ],\n      \"symbols\": [\n        { \"symbol\": \"scip-rust pkg repo#Entity\", \"display_name\": \"Entity\", \"kind\": \"struct\", \"relationships\": [] }\n      ]\n    }\n  ]\n}\n",
            ),
        ],
    )?;

    let semantic_runtime = SemanticRuntimeConfig {
        enabled: false,
        provider: None,
        model: None,
        strict_mode: false,
    };
    let credentials = SemanticRuntimeCredentials {
        openai_api_key: None,
        gemini_api_key: None,
    };
    let summary = index_repository_with_runtime_config(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::Full,
        &semantic_runtime,
        &credentials,
    )?;

    let storage = Storage::new(&db_path);
    let path_relation_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_relation",
        )?
        .expect("expected path relation head");
    assert_eq!(
        path_relation_head.input_modes,
        vec!["path".to_owned(), "scip".to_owned()]
    );

    let path_surface_term_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_surface_term",
        )?
        .expect("expected path surface term head");
    assert_eq!(
        path_surface_term_head.input_modes,
        vec!["ast".to_owned(), "path".to_owned(), "scip".to_owned()]
    );

    let path_anchor_sketch_head = storage
        .load_retrieval_projection_head_for_repository_snapshot_family(
            "repo-001",
            &summary.snapshot_id,
            "path_anchor_sketch",
        )?
        .expect("expected path anchor sketch head");
    assert_eq!(
        path_anchor_sketch_head.input_modes,
        vec!["ast".to_owned(), "path".to_owned(), "scip".to_owned()]
    );

    let path_relations = storage
        .load_path_relation_projections_for_repository_snapshot("repo-001", &summary.snapshot_id)?;
    assert!(
        path_relations
            .iter()
            .any(|relation| relation.evidence_source == "scip"),
        "expected at least one SCIP-backed relation row: {path_relations:?}"
    );

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn index_changed_only_repairs_missing_retrieval_projection_family_on_reused_snapshot()
-> FriggResult<()> {
    let db_path = temp_db_path("index-repairs-missing-retrieval-projection-family");
    let workspace_root = temp_workspace_root("index-repairs-missing-retrieval-projection-family");
    prepare_workspace(
        &workspace_root,
        &[
            (
                "Cargo.toml",
                "[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
            ),
            ("src/main.rs", "fn main() {}\n"),
            ("tests/main_test.rs", "#[test]\nfn main_test() {}\n"),
        ],
    )?;

    let semantic_runtime = SemanticRuntimeConfig {
        enabled: false,
        provider: None,
        model: None,
        strict_mode: false,
    };
    let credentials = SemanticRuntimeCredentials {
        openai_api_key: None,
        gemini_api_key: None,
    };
    let initial_summary = index_repository_with_runtime_config(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::Full,
        &semantic_runtime,
        &credentials,
    )?;

    let storage = Storage::new(&db_path);
    assert!(
        storage
            .missing_retrieval_projection_families_for_repository_snapshot(
                "repo-001",
                &initial_summary.snapshot_id,
            )?
            .is_empty()
    );
    assert!(
        !storage
            .load_path_relation_projections_for_repository_snapshot(
                "repo-001",
                &initial_summary.snapshot_id,
            )?
            .is_empty()
    );

    let conn = rusqlite::Connection::open(&db_path).map_err(|err| {
        FriggError::Internal(format!(
            "failed to open test db for retrieval projection family deletion: {err}"
        ))
    })?;
    conn.execute(
        "DELETE FROM retrieval_projection_head WHERE repository_id = ?1 AND snapshot_id = ?2 AND family = 'path_relation'",
        ("repo-001", initial_summary.snapshot_id.as_str()),
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to delete path_relation head for reused snapshot repair test: {err}"
        ))
    })?;
    conn.execute(
        "DELETE FROM path_relation_projection WHERE repository_id = ?1 AND snapshot_id = ?2",
        ("repo-001", initial_summary.snapshot_id.as_str()),
    )
    .map_err(|err| {
        FriggError::Internal(format!(
            "failed to delete path_relation rows for reused snapshot repair test: {err}"
        ))
    })?;

    assert_eq!(
        storage.missing_retrieval_projection_families_for_repository_snapshot(
            "repo-001",
            &initial_summary.snapshot_id,
        )?,
        vec!["path_relation".to_owned()]
    );
    assert!(
        storage
            .load_path_relation_projections_for_repository_snapshot(
                "repo-001",
                &initial_summary.snapshot_id,
            )?
            .is_empty()
    );

    let changed_summary = index_repository_with_runtime_config(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::ChangedOnly,
        &semantic_runtime,
        &credentials,
    )?;
    assert_eq!(changed_summary.snapshot_id, initial_summary.snapshot_id);
    assert_eq!(changed_summary.files_changed, 0);
    assert!(
        storage
            .missing_retrieval_projection_families_for_repository_snapshot(
                "repo-001",
                &changed_summary.snapshot_id,
            )?
            .is_empty()
    );
    assert!(
        !storage
            .load_path_relation_projections_for_repository_snapshot(
                "repo-001",
                &changed_summary.snapshot_id,
            )?
            .is_empty()
    );

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn incremental_roundtrip_changed_only_reports_zero_for_unchanged_workspace() -> FriggResult<()> {
    let db_path = temp_db_path("incremental-unchanged-db");
    let workspace_root = temp_workspace_root("incremental-unchanged-workspace");
    prepare_workspace(
        &workspace_root,
        &[("src/main.rs", "fn main() {}\n"), ("README.md", "hello\n")],
    )?;

    let full_summary =
        index_repository_without_semantic("repo-001", &workspace_root, &db_path, IndexMode::Full)?;
    let changed_summary = index_repository_without_semantic(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::ChangedOnly,
    )?;

    assert_eq!(full_summary.files_scanned, 2);
    assert_eq!(full_summary.files_changed, 2);
    assert_eq!(full_summary.files_deleted, 0);
    assert_eq!(full_summary.diagnostics.total_count(), 0);
    assert_eq!(changed_summary.files_scanned, 2);
    assert_eq!(changed_summary.files_changed, 0);
    assert_eq!(changed_summary.files_deleted, 0);
    assert_eq!(changed_summary.diagnostics.total_count(), 0);
    assert_eq!(changed_summary.snapshot_id, full_summary.snapshot_id);

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn incremental_roundtrip_changed_only_detects_modified_added_and_deleted_files() -> FriggResult<()>
{
    let db_path = temp_db_path("incremental-changed-db");
    let workspace_root = temp_workspace_root("incremental-changed-workspace");
    prepare_workspace(
        &workspace_root,
        &[("src/main.rs", "fn main() {}\n"), ("README.md", "hello\n")],
    )?;

    let full_summary =
        index_repository_without_semantic("repo-001", &workspace_root, &db_path, IndexMode::Full)?;

    fs::write(workspace_root.join("README.md"), "hello changed\n").map_err(FriggError::Io)?;
    fs::remove_file(workspace_root.join("src/main.rs")).map_err(FriggError::Io)?;
    fs::write(workspace_root.join("src/new.rs"), "pub fn added() {}\n").map_err(FriggError::Io)?;

    let changed_summary = index_repository_without_semantic(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::ChangedOnly,
    )?;

    assert_eq!(changed_summary.files_scanned, 2);
    assert_eq!(changed_summary.files_changed, 2);
    assert_eq!(changed_summary.files_deleted, 1);
    assert_eq!(
        changed_summary.changed_paths,
        vec!["README.md".to_owned(), "src/new.rs".to_owned()]
    );
    assert_eq!(
        changed_summary.deleted_paths,
        vec!["src/main.rs".to_owned()]
    );
    assert_eq!(changed_summary.diagnostics.total_count(), 0);
    assert_ne!(changed_summary.snapshot_id, full_summary.snapshot_id);

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}

#[test]
fn index_plan_changed_only_unchanged_workspace_reuses_existing_snapshot() -> FriggResult<()> {
    let db_path = temp_db_path("index-plan-unchanged-db");
    let workspace_root = temp_workspace_root("index-plan-unchanged-workspace");
    prepare_workspace(
        &workspace_root,
        &[("src/main.rs", "fn main() {}\n"), ("README.md", "hello\n")],
    )?;

    let full_summary =
        index_repository_without_semantic("repo-001", &workspace_root, &db_path, IndexMode::Full)?;
    let plan = build_index_plan_for_tests(
        "repo-001",
        &workspace_root,
        &db_path,
        IndexMode::ChangedOnly,
        &SemanticRuntimeConfig::default(),
        &[],
    )?;

    assert_eq!(
        plan.previous_snapshot_id.as_deref(),
        Some(full_summary.snapshot_id.as_str())
    );
    assert_eq!(plan.files_changed, 0);
    assert_eq!(plan.files_deleted, 0);
    assert!(matches!(
        &plan.snapshot_plan,
        super::super::ManifestSnapshotPlan::ReuseExisting { snapshot_id }
            if snapshot_id == &full_summary.snapshot_id
    ));
    assert_eq!(plan.semantic_refresh.mode, SemanticRefreshMode::Disabled);

    cleanup_workspace(&workspace_root);
    cleanup_db(&db_path);
    Ok(())
}
