//! Unit tests for symbol-graph relation traversal, heuristic hints, SCIP ingest validation, and precise navigation selection.

use protobuf::{EnumOrUnknown, Message};

use super::*;

fn symbol(symbol_id: &str, display_name: &str, kind: &str, path: &str, line: usize) -> SymbolNode {
    SymbolNode::new(symbol_id, "repo-001", display_name, kind, path, line)
}

#[test]
fn relation_traversal_registers_symbols_and_relations_deterministically() {
    let mut graph = SymbolGraph::default();
    graph.register_symbols([
        symbol("sym-class-user", "User", "class", "src/user.php", 3),
        symbol("sym-method-save", "save", "method", "src/user.php", 5),
        symbol(
            "sym-method-validate",
            "validate",
            "method",
            "src/user.php",
            9,
        ),
        symbol("sym-const-limit", "LIMIT", "constant", "src/user.php", 12),
    ]);

    assert_eq!(graph.symbol_count(), 4);

    assert!(
        graph
            .add_relation("sym-method-save", "sym-class-user", RelationKind::DefinedIn)
            .expect("defined_in insertion should succeed")
    );
    assert!(
        graph
            .add_relation(
                "sym-method-save",
                "sym-method-validate",
                RelationKind::Calls
            )
            .expect("calls insertion should succeed")
    );
    assert!(
        graph
            .add_relation("sym-method-save", "sym-const-limit", RelationKind::RefersTo)
            .expect("refers_to insertion should succeed")
    );

    // Duplicate edges with same relation are rejected deterministically.
    assert!(
        !graph
            .add_relation(
                "sym-method-save",
                "sym-method-validate",
                RelationKind::Calls
            )
            .expect("duplicate calls edge should be deduplicated")
    );

    assert_eq!(graph.relation_count(), 3);

    let outgoing = graph.outgoing_relations("sym-method-save");
    assert_eq!(
        outgoing,
        vec![
            SymbolRelation {
                from_symbol: "sym-method-save".to_string(),
                to_symbol: "sym-class-user".to_string(),
                relation: RelationKind::DefinedIn,
            },
            SymbolRelation {
                from_symbol: "sym-method-save".to_string(),
                to_symbol: "sym-const-limit".to_string(),
                relation: RelationKind::RefersTo,
            },
            SymbolRelation {
                from_symbol: "sym-method-save".to_string(),
                to_symbol: "sym-method-validate".to_string(),
                relation: RelationKind::Calls,
            },
        ]
    );

    let incoming = graph.incoming_relations("sym-method-validate");
    assert_eq!(
        incoming,
        vec![SymbolRelation {
            from_symbol: "sym-method-save".to_string(),
            to_symbol: "sym-method-validate".to_string(),
            relation: RelationKind::Calls,
        }]
    );

    let outgoing_neighbors = graph.outgoing_adjacency("sym-method-save");
    assert_eq!(
        outgoing_neighbors
            .iter()
            .map(|adjacent| (adjacent.relation, adjacent.symbol.symbol_id.clone()))
            .collect::<Vec<_>>(),
        vec![
            (RelationKind::DefinedIn, "sym-class-user".to_string()),
            (RelationKind::RefersTo, "sym-const-limit".to_string()),
            (RelationKind::Calls, "sym-method-validate".to_string()),
        ]
    );
}

#[test]
fn relation_traversal_requires_pre_registered_symbols() {
    let mut graph = SymbolGraph::default();
    graph.register_symbol(symbol(
        "sym-existing",
        "existing",
        "function",
        "src/lib.rs",
        1,
    ));

    let from_error = graph
        .add_relation("sym-missing", "sym-existing", RelationKind::RefersTo)
        .expect_err("missing source symbol should fail relation insertion");
    assert_eq!(
        from_error,
        SymbolGraphError::UnknownFromSymbol("sym-missing".to_string())
    );

    let to_error = graph
        .add_relation("sym-existing", "sym-also-missing", RelationKind::RefersTo)
        .expect_err("missing target symbol should fail relation insertion");
    assert_eq!(
        to_error,
        SymbolGraphError::UnknownToSymbol("sym-also-missing".to_string())
    );
}

#[test]
fn relation_traversal_register_symbol_upserts_existing_entry() {
    let mut graph = SymbolGraph::default();

    assert!(graph.register_symbol(symbol("sym-user", "User", "struct", "src/user.rs", 3,)));

    assert!(!graph.register_symbol(symbol(
        "sym-user",
        "UserRenamed",
        "struct",
        "src/user.rs",
        44,
    )));

    let symbol = graph
        .symbol("sym-user")
        .expect("registered symbol should be queryable");
    assert_eq!(symbol.display_name, "UserRenamed");
    assert_eq!(symbol.line, 44);
    assert_eq!(graph.symbol_count(), 1);
}

#[test]
fn heuristic_relation_hints_are_confidence_ranked_and_deterministic() {
    let mut graph = SymbolGraph::default();
    graph.register_symbols([
        symbol("sym-target", "User", "class", "src/user.php", 3),
        symbol("sym-calls", "save", "method", "src/service.php", 11),
        symbol("sym-contains", "Service", "class", "src/service.php", 1),
        symbol("sym-defined-in", "User", "class", "src/user.php", 3),
    ]);

    assert!(
        graph
            .add_relation("sym-calls", "sym-target", RelationKind::Calls)
            .expect("calls relation should be accepted")
    );
    assert!(
        graph
            .add_relation("sym-contains", "sym-target", RelationKind::Contains)
            .expect("contains relation should be accepted")
    );
    assert!(
        graph
            .add_relation("sym-defined-in", "sym-target", RelationKind::DefinedIn)
            .expect("defined_in relation should be accepted")
    );

    let first = graph.heuristic_relation_hints_for_target("sym-target");
    let second = graph.heuristic_relation_hints_for_target("sym-target");

    assert_eq!(first, second, "hint ordering should be deterministic");
    assert_eq!(
        first
            .iter()
            .map(|hint| (hint.source_symbol.symbol_id.clone(), hint.confidence))
            .collect::<Vec<_>>(),
        vec![
            ("sym-calls".to_string(), HeuristicConfidence::High),
            ("sym-contains".to_string(), HeuristicConfidence::Medium),
            ("sym-defined-in".to_string(), HeuristicConfidence::Low),
        ]
    );
}

#[test]
fn scip_ingest_maps_and_persists_normalized_records() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg a#User", "range": [1, 18, 22], "symbol_roles": 8 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#User",
                  "display_name": "User",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg a#Entity", "is_reference": true },
                    { "symbol": "scip-rust pkg a#Entity", "is_implementation": true }
                  ]
                }
              ]
            },
            {
              "relative_path": "src/base.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#Entity", "range": [0, 11, 17], "symbol_roles": 1 }
              ],
              "symbols": [
                { "symbol": "scip-rust pkg a#Entity", "display_name": "Entity", "kind": 7, "relationships": [] }
              ]
            }
          ]
        }"#;

    let summary = graph
        .ingest_scip_json("repo-001", "fixture:scip.json", payload)
        .expect("valid scip payload should ingest successfully");
    assert_eq!(summary.documents_ingested, 2);
    assert_eq!(summary.symbols_upserted, 2);
    assert_eq!(summary.occurrences_upserted, 3);
    assert_eq!(summary.relationships_upserted, 2);

    let counts = graph.precise_counts();
    assert_eq!(counts.symbols, 2);
    assert_eq!(counts.occurrences, 3);
    assert_eq!(counts.relationships, 2);

    let user_symbol = graph
        .precise_symbol("repo-001", "scip-rust pkg a#User")
        .expect("expected precise symbol");
    assert_eq!(user_symbol.display_name, "User");
    assert_eq!(user_symbol.kind, "struct");

    let user_occurrences = graph.precise_occurrences_for_symbol("repo-001", "scip-rust pkg a#User");
    assert_eq!(
        user_occurrences
            .iter()
            .map(|occurrence| {
                (
                    occurrence.path.clone(),
                    occurrence.range.start_line,
                    occurrence.range.start_column,
                    occurrence.symbol_roles,
                )
            })
            .collect::<Vec<_>>(),
        vec![
            ("src/a.rs".to_string(), 1, 8, 1),
            ("src/a.rs".to_string(), 2, 19, 8),
        ]
    );

    let user_references = graph.precise_references_for_symbol("repo-001", "scip-rust pkg a#User");
    assert_eq!(user_references.len(), 1);
    assert_eq!(user_references[0].range.start_line, 2);
    assert!(!user_references[0].is_definition());

    let relationships = graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg a#User");
    assert_eq!(
        relationships
            .iter()
            .map(|relationship| relationship.kind)
            .collect::<Vec<_>>(),
        vec![
            PreciseRelationshipKind::Reference,
            PreciseRelationshipKind::Implementation
        ]
    );
}

#[test]
fn scip_protobuf_ingest_maps_and_persists_normalized_records() {
    let mut graph = SymbolGraph::default();
    let mut index = ScipIndexProto::new();

    let mut user_doc = ScipDocumentProto::new();
    user_doc.relative_path = "src/a.rs".to_owned();
    let mut user_definition = ScipOccurrenceProto::new();
    user_definition.symbol = "scip-rust pkg a#User".to_owned();
    user_definition.range = vec![0, 7, 11];
    user_definition.symbol_roles = 1;
    user_doc.occurrences.push(user_definition);

    let mut user_reference = ScipOccurrenceProto::new();
    user_reference.symbol = "scip-rust pkg a#User".to_owned();
    user_reference.range = vec![1, 18, 22];
    user_reference.symbol_roles = 8;
    user_doc.occurrences.push(user_reference);

    let mut user_symbol = ScipSymbolInformationProto::new();
    user_symbol.symbol = "scip-rust pkg a#User".to_owned();
    user_symbol.display_name = "User".to_owned();
    user_symbol.kind = EnumOrUnknown::from_i32(7);

    let mut relationship_reference = ScipRelationshipProto::new();
    relationship_reference.symbol = "scip-rust pkg a#Entity".to_owned();
    relationship_reference.is_reference = true;
    user_symbol.relationships.push(relationship_reference);

    let mut relationship_implementation = ScipRelationshipProto::new();
    relationship_implementation.symbol = "scip-rust pkg a#Entity".to_owned();
    relationship_implementation.is_implementation = true;
    user_symbol.relationships.push(relationship_implementation);
    user_doc.symbols.push(user_symbol);

    let mut entity_doc = ScipDocumentProto::new();
    entity_doc.relative_path = "src/base.rs".to_owned();
    let mut entity_occurrence = ScipOccurrenceProto::new();
    entity_occurrence.symbol = "scip-rust pkg a#Entity".to_owned();
    entity_occurrence.range = vec![0, 11, 17];
    entity_occurrence.symbol_roles = 1;
    entity_doc.occurrences.push(entity_occurrence);

    let mut entity_symbol = ScipSymbolInformationProto::new();
    entity_symbol.symbol = "scip-rust pkg a#Entity".to_owned();
    entity_symbol.display_name = "Entity".to_owned();
    entity_symbol.kind = EnumOrUnknown::from_i32(7);
    entity_doc.symbols.push(entity_symbol);

    index.documents.push(user_doc);
    index.documents.push(entity_doc);

    let payload = index
        .write_to_bytes()
        .expect("protobuf fixture payload should serialize");

    let summary = graph
        .ingest_scip_protobuf("repo-001", "fixture:scip.scip", &payload)
        .expect("valid protobuf scip payload should ingest successfully");
    assert_eq!(summary.documents_ingested, 2);
    assert_eq!(summary.symbols_upserted, 2);
    assert_eq!(summary.occurrences_upserted, 3);
    assert_eq!(summary.relationships_upserted, 2);

    let counts = graph.precise_counts();
    assert_eq!(counts.symbols, 2);
    assert_eq!(counts.occurrences, 3);
    assert_eq!(counts.relationships, 2);

    let relationships = graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg a#User");
    assert_eq!(
        relationships
            .iter()
            .map(|relationship| relationship.kind)
            .collect::<Vec<_>>(),
        vec![
            PreciseRelationshipKind::Reference,
            PreciseRelationshipKind::Implementation
        ]
    );
}

#[test]
fn precise_navigation_symbol_selection_is_deterministic() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [],
              "symbols": [
                { "symbol": "scip-rust pkg a#User", "display_name": "User", "kind": "struct", "relationships": [] },
                { "symbol": "scip-rust pkg a#user_lower", "display_name": "user", "kind": "struct", "relationships": [] }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:precise-navigation.json", payload)
        .expect("fixture payload should ingest");

    let exact = graph
        .select_precise_symbol_for_navigation("repo-001", "scip-rust pkg a#User", "fallback")
        .expect("exact symbol query should resolve");
    assert_eq!(exact.symbol, "scip-rust pkg a#User");

    let case_insensitive = graph
        .select_precise_symbol_for_navigation("repo-001", "USER", "fallback")
        .expect("case-insensitive display-name query should resolve");
    assert_eq!(case_insensitive.symbol, "scip-rust pkg a#User");

    let fallback = graph
        .select_precise_symbol_for_navigation("repo-001", "missing", "user")
        .expect("fallback display-name query should resolve");
    assert_eq!(fallback.symbol, "scip-rust pkg a#user_lower");

    assert!(
        graph
            .select_precise_symbol_for_navigation("repo-001", "missing", "also-missing")
            .is_none(),
        "missing query and fallback should return None"
    );
}

#[test]
fn precise_navigation_symbol_selection_matches_symbol_tail_when_display_name_is_missing() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [
            {
              "relative_path": "src/auth.ts",
              "occurrences": [
                {
                  "symbol": "scip-typescript npm app 1.0.0 src/auth.ts:requireServerUser.",
                  "range": [0, 6, 23],
                  "symbol_roles": 1
                }
              ],
              "symbols": [
                {
                  "symbol": "scip-typescript npm app 1.0.0 src/auth.ts:requireServerUser.",
                  "display_name": "",
                  "kind": "function",
                  "relationships": []
                }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:precise-navigation-tail.json", payload)
        .expect("fixture payload should ingest");

    let matched = graph
        .select_precise_symbol_for_navigation("repo-001", "requireServerUser", "fallback")
        .expect("symbol tail should resolve when display_name is missing");
    assert_eq!(
        matched.symbol,
        "scip-typescript npm app 1.0.0 src/auth.ts:requireServerUser."
    );
}

#[test]
fn precise_navigation_location_selection_prefers_containing_occurrence() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg a#Entity", "range": [0, 13, 19], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg a#User", "range": [1, 18, 22], "symbol_roles": 8 }
              ],
              "symbols": [
                { "symbol": "scip-rust pkg a#User", "display_name": "User", "kind": "struct", "relationships": [] },
                { "symbol": "scip-rust pkg a#Entity", "display_name": "Entity", "kind": "struct", "relationships": [] }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:precise-location.json", payload)
        .expect("fixture payload should ingest");

    let containing = graph
        .select_precise_symbol_for_location("repo-001", "src/a.rs", 1, Some(9))
        .expect("containing occurrence should resolve");
    assert_eq!(containing.symbol, "scip-rust pkg a#User");

    let later = graph
        .select_precise_symbol_for_location("repo-001", "src/a.rs", 1, Some(15))
        .expect("later containing occurrence should resolve");
    assert_eq!(later.symbol, "scip-rust pkg a#Entity");

    let reference = graph
        .select_precise_symbol_for_location("repo-001", "src/a.rs", 2, Some(20))
        .expect("reference occurrence should resolve");
    assert_eq!(reference.symbol, "scip-rust pkg a#User");
}

#[test]
fn scip_ingest_returns_typed_invalid_input_and_preserves_state() {
    let mut graph = SymbolGraph::default();
    let valid = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 }
              ],
              "symbols": [
                { "symbol": "scip-rust pkg a#User", "display_name": "User", "kind": "struct", "relationships": [] }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:valid.json", valid)
        .expect("valid ingest should succeed");
    let before = graph.precise_counts();

    let invalid = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7], "symbol_roles": 8 }
              ],
              "symbols": []
            }
          ]
        }"#;
    let error = graph
        .ingest_scip_json("repo-001", "fixture:invalid-range.json", invalid)
        .expect_err("invalid range payload should fail with typed invalid-input error");
    assert_eq!(
            error,
            ScipIngestError::InvalidInput {
                diagnostic: ScipInvalidInputDiagnostic {
                    artifact_label: "fixture:invalid-range.json".to_string(),
                    code: ScipInvalidInputCode::InvalidRange,
                    message: "occurrence range for symbol 'scip-rust pkg a#User' in 'src/a.rs' must have 3 or 4 numbers".to_string(),
                    line: None,
                    column: None,
                }
            }
        );

    let after = graph.precise_counts();
    assert_eq!(
        before, after,
        "failed ingest must not mutate precise graph state"
    );
    assert_eq!(
        graph
            .precise_occurrences_for_symbol("repo-001", "scip-rust pkg a#User")
            .len(),
        1
    );
}

#[test]
fn scip_ingest_rejects_document_paths_that_escape_repository_root() {
    let mut graph = SymbolGraph::default();
    let before = graph.precise_counts();

    for (label, relative_path) in [
        ("fixture:parent-escape.json", "../outside.rs"),
        ("fixture:nested-parent-escape.json", "src/../../outside.rs"),
        ("fixture:absolute-escape.json", "/etc/passwd"),
        ("fixture:backslash-escape.json", r"..\\outside.rs"),
    ] {
        let payload = format!(
            r#"{{
              "documents": [
                {{
                  "relative_path": "{relative_path}",
                  "occurrences": [
                    {{ "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 }}
                  ],
                  "symbols": [
                    {{ "symbol": "scip-rust pkg a#User", "display_name": "User", "kind": "struct", "relationships": [] }}
                  ]
                }}
              ]
            }}"#
        );
        let error = graph
            .ingest_scip_json("repo-001", label, payload.as_bytes())
            .expect_err("document path escaping the repository root should be rejected");
        assert!(
            matches!(
                error,
                ScipIngestError::InvalidInput { ref diagnostic }
                    if diagnostic.artifact_label == label
                        && diagnostic.code == ScipInvalidInputCode::InvalidDocumentPath
            ),
            "expected typed invalid-document-path error, got {error:?}"
        );
    }

    assert_eq!(
        before,
        graph.precise_counts(),
        "rejected document paths must not mutate precise graph state"
    );

    let normalized = br#"{
          "documents": [
            {
              "relative_path": "./src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 }
              ],
              "symbols": [
                { "symbol": "scip-rust pkg a#User", "display_name": "User", "kind": "struct", "relationships": [] }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:normalized.json", normalized)
        .expect("repository-relative document path should ingest");
    assert_eq!(
        graph
            .precise_occurrences_for_symbol("repo-001", "scip-rust pkg a#User")
            .len(),
        1
    );
}

#[test]
fn scip_ingest_rejects_payload_budget_overflow_with_typed_error() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [],
          "padding": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
        }"#;

    let error = graph
        .ingest_scip_json_with_budgets(
            "repo-001",
            "fixture:payload-budget.json",
            payload,
            ScipResourceBudgets {
                max_payload_bytes: 16,
                max_documents: usize::MAX,
                max_elapsed_ms: u64::MAX,
            },
        )
        .expect_err("oversized payload should fail with typed resource-budget error");
    assert_eq!(
        error,
        ScipIngestError::ResourceBudgetExceeded {
            diagnostic: ScipResourceBudgetDiagnostic {
                artifact_label: "fixture:payload-budget.json".to_string(),
                code: ScipResourceBudgetCode::PayloadBytes,
                message: "scip payload bytes exceed configured budget".to_string(),
                limit: 16,
                actual: u64::try_from(payload.len()).unwrap_or(u64::MAX),
            },
        }
    );
    assert_eq!(graph.precise_counts(), PreciseGraphCounts::default());
}

#[test]
fn scip_ingest_rejects_document_budget_overflow_with_typed_error() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": [
            { "relative_path": "src/a.rs", "occurrences": [], "symbols": [] },
            { "relative_path": "src/b.rs", "occurrences": [], "symbols": [] }
          ]
        }"#;

    let error = graph
        .ingest_scip_json_with_budgets(
            "repo-001",
            "fixture:document-budget.json",
            payload,
            ScipResourceBudgets {
                max_payload_bytes: usize::MAX,
                max_documents: 1,
                max_elapsed_ms: u64::MAX,
            },
        )
        .expect_err("document overflow should fail with typed resource-budget error");
    assert_eq!(
        error,
        ScipIngestError::ResourceBudgetExceeded {
            diagnostic: ScipResourceBudgetDiagnostic {
                artifact_label: "fixture:document-budget.json".to_string(),
                code: ScipResourceBudgetCode::Documents,
                message: "scip document count exceeds configured budget".to_string(),
                limit: 1,
                actual: 2,
            },
        }
    );
    assert_eq!(graph.precise_counts(), PreciseGraphCounts::default());
}

#[test]
fn scip_ingest_rejects_zero_elapsed_budget_with_typed_error() {
    let mut graph = SymbolGraph::default();
    let payload = br#"{
          "documents": []
        }"#;

    let error = graph
        .ingest_scip_json_with_budgets(
            "repo-001",
            "fixture:elapsed-budget.json",
            payload,
            ScipResourceBudgets {
                max_payload_bytes: usize::MAX,
                max_documents: usize::MAX,
                max_elapsed_ms: 0,
            },
        )
        .expect_err("zero elapsed budget should fail deterministically");
    assert_eq!(
        error,
        ScipIngestError::ResourceBudgetExceeded {
            diagnostic: ScipResourceBudgetDiagnostic {
                artifact_label: "fixture:elapsed-budget.json".to_string(),
                code: ScipResourceBudgetCode::ElapsedMs,
                message: "scip ingest elapsed time budget is zero".to_string(),
                limit: 0,
                actual: 0,
            },
        }
    );
    assert_eq!(graph.precise_counts(), PreciseGraphCounts::default());
}

#[test]
fn scip_ingest_replaces_file_level_precise_occurrences() {
    let mut graph = SymbolGraph::default();
    let first = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg a#User", "range": [1, 7, 11], "symbol_roles": 8 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#User",
                  "display_name": "User",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg a#Entity", "is_reference": true }
                  ]
                }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:first.json", first)
        .expect("first ingest should succeed");

    let second = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [2, 7, 11], "symbol_roles": 8 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#User",
                  "display_name": "User",
                  "kind": "struct",
                  "relationships": []
                }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:second.json", second)
        .expect("second ingest should succeed");

    let file_occurrences = graph.precise_occurrences_for_file("repo-001", "src/a.rs");
    assert_eq!(file_occurrences.len(), 1);
    assert_eq!(file_occurrences[0].range.start_line, 3);

    let relationships = graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg a#User");
    assert!(
        relationships.is_empty(),
        "file-level reingest should replace prior relationships"
    );
}

#[test]
fn scip_incremental_update_replaces_only_target_file_and_preserves_unaffected_data() {
    let mut graph = SymbolGraph::default();
    let initial = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#User",
                  "display_name": "User",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg a#Base", "is_reference": true }
                  ]
                }
              ]
            },
            {
              "relative_path": "src/b.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg b#Service", "range": [0, 7, 14], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg b#Service",
                  "display_name": "Service",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg a#Base", "is_reference": true }
                  ]
                }
              ]
            }
          ]
        }"#;

    graph
        .ingest_scip_json("repo-001", "fixture:initial.json", initial)
        .expect("initial payload should ingest");

    let before_b_occurrences = graph.precise_occurrences_for_file("repo-001", "src/b.rs");
    let before_b_relationships =
        graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg b#Service");
    assert_eq!(before_b_occurrences.len(), 1);
    assert_eq!(before_b_relationships.len(), 1);
    assert!(
        graph
            .precise_symbol("repo-001", "scip-rust pkg a#User")
            .is_some(),
        "expected initial symbol for src/a.rs"
    );

    let incremental = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#Account", "range": [2, 7, 14], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#Account",
                  "display_name": "Account",
                  "kind": "struct",
                  "relationships": []
                }
              ]
            }
          ]
        }"#;

    graph
        .ingest_scip_json("repo-001", "fixture:incremental.json", incremental)
        .expect("incremental payload should ingest");

    // Updated file replaced.
    assert!(
        graph
            .precise_symbol("repo-001", "scip-rust pkg a#User")
            .is_none(),
        "old symbol from updated file should be removed"
    );
    assert!(
        graph
            .precise_symbol("repo-001", "scip-rust pkg a#Account")
            .is_some(),
        "new symbol from updated file should be present"
    );
    let a_occurrences = graph.precise_occurrences_for_file("repo-001", "src/a.rs");
    assert_eq!(a_occurrences.len(), 1);
    assert_eq!(a_occurrences[0].symbol, "scip-rust pkg a#Account");
    assert!(
        graph
            .precise_relationships_from_symbol("repo-001", "scip-rust pkg a#Account")
            .is_empty(),
        "updated file relationships should reflect replacement payload"
    );

    // Unaffected file preserved exactly.
    assert_eq!(
        graph.precise_occurrences_for_file("repo-001", "src/b.rs"),
        before_b_occurrences
    );
    assert_eq!(
        graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg b#Service"),
        before_b_relationships
    );
}

#[test]
fn scip_incremental_update_is_deterministic_across_repeated_reingest() {
    let mut graph = SymbolGraph::default();
    let seed = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#User", "range": [0, 7, 11], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#User",
                  "display_name": "User",
                  "kind": "struct",
                  "relationships": []
                }
              ]
            },
            {
              "relative_path": "src/b.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg b#Service", "range": [0, 7, 14], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg b#Service",
                  "display_name": "Service",
                  "kind": "struct",
                  "relationships": []
                }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:seed.json", seed)
        .expect("seed ingest should succeed");

    let incremental = br#"{
          "documents": [
            {
              "relative_path": "src/a.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg a#Account", "range": [2, 7, 14], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg a#Account", "range": [3, 10, 17], "symbol_roles": 8 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg a#Account",
                  "display_name": "Account",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg b#Service", "is_reference": true }
                  ]
                }
              ]
            }
          ]
        }"#;
    graph
        .ingest_scip_json("repo-001", "fixture:inc-1.json", incremental)
        .expect("first incremental ingest should succeed");

    let counts_after_first = graph.precise_counts();
    let file_a_after_first = graph.precise_occurrences_for_file("repo-001", "src/a.rs");
    let refs_after_first =
        graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg a#Account");

    graph
        .ingest_scip_json("repo-001", "fixture:inc-2.json", incremental)
        .expect("second incremental ingest should succeed");

    assert_eq!(graph.precise_counts(), counts_after_first);
    assert_eq!(
        graph.precise_occurrences_for_file("repo-001", "src/a.rs"),
        file_a_after_first
    );
    assert_eq!(
        graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg a#Account"),
        refs_after_first
    );
    assert!(
        graph
            .precise_symbol("repo-001", "scip-rust pkg a#User")
            .is_none(),
        "stale symbols must not reappear across repeated incremental updates"
    );
}

#[test]
fn scip_overlay_ingest_preserves_overlapping_same_file_precise_data() {
    let mut graph = SymbolGraph::default();
    let canary = br#"{
          "documents": [
            {
              "relative_path": "src/lib.rs",
              "occurrences": [
                { "symbol": "scip-rust pkg repo#Service", "range": [0, 10, 17], "symbol_roles": 1 },
                { "symbol": "scip-rust pkg repo#Impl", "range": [1, 11, 15], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "scip-rust pkg repo#Service",
                  "display_name": "Service",
                  "kind": "trait",
                  "relationships": []
                },
                {
                  "symbol": "scip-rust pkg repo#Impl",
                  "display_name": "Impl",
                  "kind": "struct",
                  "relationships": [
                    { "symbol": "scip-rust pkg repo#Service", "is_implementation": true }
                  ]
                }
              ]
            }
          ]
        }"#;
    graph
        .overlay_scip_json_with_budgets(
            "repo-001",
            "fixture:canary.json",
            canary,
            ScipResourceBudgets::default(),
        )
        .expect("canary overlay should ingest");

    let main = br#"{
          "documents": [
            {
              "relative_path": "src/lib.rs",
              "occurrences": [
                { "symbol": "rust-analyzer cargo repo 0.1.0 svc/Service#", "range": [0, 10, 17], "symbol_roles": 1 }
              ],
              "symbols": [
                {
                  "symbol": "rust-analyzer cargo repo 0.1.0 svc/Service#",
                  "display_name": "Service",
                  "kind": "trait",
                  "relationships": []
                }
              ]
            }
          ]
        }"#;
    graph
        .overlay_scip_json_with_budgets(
            "repo-001",
            "fixture:main.json",
            main,
            ScipResourceBudgets::default(),
        )
        .expect("main overlay should ingest");

    let matched = graph.matching_precise_symbols_for_navigation("repo-001", "Service", "Service");
    assert_eq!(matched.len(), 2);
    assert_eq!(
        matched[0].symbol,
        "rust-analyzer cargo repo 0.1.0 svc/Service#"
    );
    assert_eq!(matched[1].symbol, "scip-rust pkg repo#Service");
    assert!(
        graph
            .precise_symbol("repo-001", "scip-rust pkg repo#Service")
            .is_some(),
        "overlay ingest should preserve earlier same-file symbol namespace"
    );
    assert_eq!(
        graph.precise_relationships_from_symbol("repo-001", "scip-rust pkg repo#Impl"),
        vec![PreciseRelationshipRecord {
            repository_id: "repo-001".to_owned(),
            from_symbol: "scip-rust pkg repo#Impl".to_owned(),
            to_symbol: "scip-rust pkg repo#Service".to_owned(),
            kind: PreciseRelationshipKind::Implementation,
        }]
    );
}
