use super::*;
use crate::model::*;
use crate::{CodeGraphBuildInput, CodeGraphExtractorConfig, CodeGraphIncrementalBuildInput};
use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Write;
use std::path::Path;
use tempfile::tempdir;
use ucm_core::{Block, BlockId, Content, Document, EdgeType, PortableDocument};

fn default_build_input(repo_root: &Path, commit_hash: &str) -> CodeGraphBuildInput {
    CodeGraphBuildInput {
        repository_path: repo_root.to_path_buf(),
        commit_hash: commit_hash.to_string(),
        config: CodeGraphExtractorConfig::default(),
    }
}

fn default_incremental_input(
    repo_root: &Path,
    state_file: &Path,
    commit_hash: &str,
) -> CodeGraphIncrementalBuildInput {
    CodeGraphIncrementalBuildInput {
        build: default_build_input(repo_root, commit_hash),
        state_file: state_file.to_path_buf(),
    }
}

fn diagnostic_signatures(diags: &[CodeGraphDiagnostic]) -> Vec<String> {
    let mut signatures = diags
        .iter()
        .map(|diag| {
            format!(
                "{:?}|{}|{}|{}|{}",
                diag.severity,
                diag.code,
                diag.path.clone().unwrap_or_default(),
                diag.logical_key.clone().unwrap_or_default(),
                diag.message,
            )
        })
        .collect::<Vec<_>>();
    signatures.sort();
    signatures
}

fn assert_builds_equivalent(full: &CodeGraphBuildResult, incremental: &CodeGraphBuildResult) {
    assert_eq!(full.status, incremental.status);
    assert_eq!(full.profile_version, incremental.profile_version);
    assert_eq!(full.stats, incremental.stats);
    assert_eq!(
        full.canonical_fingerprint,
        incremental.canonical_fingerprint
    );
    assert_eq!(
        canonical_codegraph_json(&full.document).unwrap(),
        canonical_codegraph_json(&incremental.document).unwrap()
    );
    assert_eq!(
        diagnostic_signatures(&full.diagnostics),
        diagnostic_signatures(&incremental.diagnostics)
    );
}

fn mutate_incremental_state_json(state_file: &Path, mutate: impl FnOnce(&mut serde_json::Value)) {
    let mut state: serde_json::Value =
        serde_json::from_str(&fs::read_to_string(state_file).unwrap()).unwrap();
    mutate(&mut state);
    fs::write(state_file, serde_json::to_string_pretty(&state).unwrap()).unwrap();
}

fn symbol_logical_keys(doc: &Document) -> Vec<String> {
    let mut out: Vec<_> = doc
        .blocks
        .values()
        .filter(|block| node_class(block).as_deref() == Some("symbol"))
        .filter_map(block_logical_key)
        .collect();
    out.sort();
    out
}

fn logical_key_to_block_id(doc: &Document) -> BTreeMap<String, BlockId> {
    doc.blocks
        .iter()
        .filter_map(|(id, block)| block_logical_key(block).map(|key| (key, *id)))
        .collect()
}

fn symbol_block_by_prefix<'a>(doc: &'a Document, prefix: &str) -> Option<&'a Block> {
    doc.blocks
        .values()
        .filter(|block| node_class(block).as_deref() == Some("symbol"))
        .filter_map(|block| block_logical_key(block).map(|key| (key, block)))
        .find(|(key, _)| *key == prefix)
        .map(|(_, block)| block)
        .or_else(|| {
            doc.blocks.values().find(|block| {
                node_class(block).as_deref() == Some("symbol")
                    && block_logical_key(block)
                        .map(|key| key.starts_with(prefix))
                        .unwrap_or(false)
            })
        })
}

fn symbol_exported(doc: &Document, prefix: &str) -> bool {
    symbol_block_by_prefix(doc, prefix)
        .and_then(|block| block.metadata.custom.get(META_EXPORTED))
        .and_then(|value| value.as_bool())
        .unwrap_or(false)
}

fn symbol_kind(doc: &Document, prefix: &str) -> Option<String> {
    symbol_block_by_prefix(doc, prefix)
        .and_then(|block| block.metadata.custom.get(META_SYMBOL_KIND))
        .and_then(|value| value.as_str())
        .map(|value| value.to_string())
}

fn symbol_summary(doc: &Document, prefix: &str) -> Option<String> {
    symbol_block_by_prefix(doc, prefix).and_then(|block| block.metadata.summary.clone())
}

fn symbol_content_string_field(doc: &Document, prefix: &str, field: &str) -> Option<String> {
    let block = symbol_block_by_prefix(doc, prefix)?;
    let Content::Json { value, .. } = &block.content else {
        return None;
    };
    value.get(field)?.as_str().map(|value| value.to_string())
}

fn symbol_content_json_field(
    doc: &Document,
    prefix: &str,
    field: &str,
) -> Option<serde_json::Value> {
    let block = symbol_block_by_prefix(doc, prefix)?;
    let Content::Json { value, .. } = &block.content else {
        return None;
    };
    value.get(field).cloned()
}

fn symbol_content_json_subfield(
    doc: &Document,
    prefix: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    symbol_content_json_field(doc, prefix, field)?
        .get(subfield)
        .cloned()
}

fn block_metadata_custom_field(block: &Block, field: &str) -> Option<serde_json::Value> {
    block.metadata.custom.get(field).cloned()
}

fn block_metadata_custom_subfield(
    block: &Block,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_field(block, field)?
        .get(subfield)
        .cloned()
}

fn repository_block(doc: &Document) -> Option<&Block> {
    doc.blocks
        .values()
        .find(|block| node_class(block).as_deref() == Some("repository"))
}

fn repository_content_json_field(doc: &Document, field: &str) -> Option<serde_json::Value> {
    let block = repository_block(doc)?;
    let Content::Json { value, .. } = &block.content else {
        return None;
    };
    value.get(field).cloned()
}

fn repository_content_json_subfield(
    doc: &Document,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    repository_content_json_field(doc, field)?
        .get(subfield)
        .cloned()
}

fn repository_metadata_custom_subfield(
    doc: &Document,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_subfield(repository_block(doc)?, field, subfield)
}

fn directory_block_by_key<'a>(doc: &'a Document, logical_key: &str) -> Option<&'a Block> {
    doc.blocks.values().find(|block| {
        node_class(block).as_deref() == Some("directory")
            && block_logical_key(block).as_deref() == Some(logical_key)
    })
}

fn directory_content_json_field(
    doc: &Document,
    logical_key: &str,
    field: &str,
) -> Option<serde_json::Value> {
    let block = directory_block_by_key(doc, logical_key)?;
    let Content::Json { value, .. } = &block.content else {
        return None;
    };
    value.get(field).cloned()
}

fn directory_content_json_subfield(
    doc: &Document,
    logical_key: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    directory_content_json_field(doc, logical_key, field)?
        .get(subfield)
        .cloned()
}

fn directory_metadata_custom_subfield(
    doc: &Document,
    logical_key: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_subfield(directory_block_by_key(doc, logical_key)?, field, subfield)
}

fn file_content_json_field(
    doc: &Document,
    logical_key: &str,
    field: &str,
) -> Option<serde_json::Value> {
    let block = file_block_by_key(doc, logical_key)?;
    let Content::Json { value, .. } = &block.content else {
        return None;
    };
    value.get(field).cloned()
}

fn file_content_json_subfield(
    doc: &Document,
    logical_key: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    file_content_json_field(doc, logical_key, field)?
        .get(subfield)
        .cloned()
}

fn file_metadata_custom_subfield(
    doc: &Document,
    logical_key: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_subfield(file_block_by_key(doc, logical_key)?, field, subfield)
}

fn symbol_metadata_custom_subfield(
    doc: &Document,
    prefix: &str,
    field: &str,
    subfield: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_subfield(symbol_block_by_prefix(doc, prefix)?, field, subfield)
}

fn symbol_metadata_custom_field(
    doc: &Document,
    prefix: &str,
    field: &str,
) -> Option<serde_json::Value> {
    block_metadata_custom_field(symbol_block_by_prefix(doc, prefix)?, field)
}

fn file_block_by_key<'a>(doc: &'a Document, logical_key: &str) -> Option<&'a Block> {
    doc.blocks.values().find(|block| {
        node_class(block).as_deref() == Some("file")
            && block_logical_key(block).as_deref() == Some(logical_key)
    })
}

fn file_summary(doc: &Document, logical_key: &str) -> Option<String> {
    file_block_by_key(doc, logical_key).and_then(|block| block.metadata.summary.clone())
}

fn block_logical_key_by_id(doc: &Document, block_id: BlockId) -> Option<String> {
    doc.blocks.get(&block_id).and_then(block_logical_key)
}

fn file_has_edge_to_symbol(
    doc: &Document,
    file_key: &str,
    edge_type: &str,
    relation: &str,
    symbol_prefix: &str,
) -> bool {
    let Some(block) = file_block_by_key(doc, file_key) else {
        return false;
    };

    block.edges.iter().any(|edge| {
        edge_type_name(&edge.edge_type) == edge_type
            && edge
                .metadata
                .custom
                .get("relation")
                .and_then(|value| value.as_str())
                == Some(relation)
            && block_logical_key_by_id(doc, edge.target)
                .map(|key| key.starts_with(symbol_prefix))
                .unwrap_or(false)
    })
}

fn symbol_has_edge_to_symbol(
    doc: &Document,
    source_prefix: &str,
    edge_type: &str,
    relation: &str,
    target_prefix: &str,
) -> bool {
    let Some(block) = symbol_block_by_prefix(doc, source_prefix) else {
        return false;
    };

    block.edges.iter().any(|edge| {
        edge_type_name(&edge.edge_type) == edge_type
            && edge
                .metadata
                .custom
                .get("relation")
                .and_then(|value| value.as_str())
                == Some(relation)
            && block_logical_key_by_id(doc, edge.target)
                .map(|key| key.starts_with(target_prefix))
                .unwrap_or(false)
    })
}

fn edge_type_name(edge_type: &EdgeType) -> String {
    match edge_type {
        EdgeType::References => "references".to_string(),
        EdgeType::Custom(name) => name.clone(),
        other => format!("{other:?}"),
    }
}

#[test]
fn test_validate_profile_detects_missing_markers() {
    let doc = Document::create();
    let result = validate_code_graph_profile(&doc);
    assert!(!result.valid);
    assert!(result
        .diagnostics
        .iter()
        .any(|d| d.code == "CG1001" || d.code == "CG1002"));
}

#[test]
fn test_canonical_fingerprint_stable_for_equivalent_docs() {
    let dir = tempdir().unwrap();
    let root = dir.path();
    fs::create_dir_all(root.join("src")).unwrap();
    fs::write(root.join("src/lib.rs"), "pub fn a() {}\n").unwrap();

    let input = CodeGraphBuildInput {
        repository_path: root.to_path_buf(),
        commit_hash: "abc123".to_string(),
        config: CodeGraphExtractorConfig::default(),
    };

    let first = build_code_graph(&input).unwrap();
    let second = build_code_graph(&input).unwrap();

    assert_eq!(first.canonical_fingerprint, second.canonical_fingerprint);
    assert_eq!(
        canonical_codegraph_json(&first.document).unwrap(),
        canonical_codegraph_json(&second.document).unwrap()
    );
}

#[test]
fn test_portable_document_roundtrip_preserves_fingerprint() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("pkg")).unwrap();
    fs::write(
        dir.path().join("pkg/main.py"),
        "from .util import helper\n\ndef run():\n    return helper()\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("pkg/util.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "def456".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    let portable = PortableDocument::from_document(&build.document);
    let json = serde_json::to_string_pretty(&portable).unwrap();
    let decoded: PortableDocument = serde_json::from_str(&json).unwrap();
    let roundtripped = decoded.to_document().unwrap();

    let fp1 = canonical_fingerprint(&build.document).unwrap();
    let fp2 = canonical_fingerprint(&roundtripped).unwrap();
    assert_eq!(fp1, fp2);
}

#[test]
fn test_unresolved_import_produces_diagnostic() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "use crate::missing::thing;\npub fn keep() {}\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ghi789".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(build
        .diagnostics
        .iter()
        .any(|d| d.code == "CG2006" && d.severity == CodeGraphSeverity::Warning));
}

#[test]
fn test_gitignore_rule_matches() {
    let rule = GitignoreRule::from_pattern("target/").unwrap();
    assert!(rule.regex.is_match("target"));
    assert!(rule.regex.is_match("target/debug/app"));
}

#[test]
fn test_import_resolution_ts_relative() {
    let mut known = BTreeSet::new();
    known.insert("src/main.ts".to_string());
    known.insert("src/util.ts".to_string());

    let resolved = resolve_ts_import("src/main.ts", "./util", &known);
    assert_eq!(
        resolved,
        ImportResolution::Resolved("src/util.ts".to_string())
    );
}

#[test]
fn test_rust_use_group_expansion() {
    let imports = expand_rust_use_declaration(
        "use crate::{block::{Block, BlockState}, edge::Edge, util::{self, helper as helper_fn}};",
    );

    assert_eq!(
        imports,
        vec![
            "crate::block::Block".to_string(),
            "crate::block::BlockState".to_string(),
            "crate::edge::Edge".to_string(),
            "crate::util".to_string(),
            "crate::util::helper".to_string(),
        ]
    );
}

#[test]
fn test_workspace_rust_imports_resolve_without_external_warnings() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("crates/demo/src")).unwrap();
    fs::write(
            dir.path().join("crates/demo/src/lib.rs"),
            "use anyhow::Result;\nuse crate::block::{helper, Thing};\npub fn run() -> Result<()> { helper(); let _ = Thing; Ok(()) }\n",
        )
        .unwrap();
    fs::write(
        dir.path().join("crates/demo/src/block.rs"),
        "pub struct Thing;\npub fn helper() {}\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "workspace-imports".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!build.diagnostics.iter().any(|d| d.code == "CG2006"));
    assert!(build.stats.reference_edges >= 1);
}

#[test]
fn test_rust_crate_root_symbol_imports_resolve_to_entry_file() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "pub struct Document;\npub type Result<T> = std::result::Result<T, ()>;\nmod inner;\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("src/inner.rs"),
        "use crate::Document;\nuse crate::Result;\npub fn run() {}\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "crate-root-symbols".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!build.diagnostics.iter().any(|d| d.code == "CG2006"));
    assert!(build.stats.reference_edges >= 1);
}

#[test]
fn test_python_relative_module_import_and_symbol_edges_are_captured() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("pkg")).unwrap();
    fs::write(
        dir.path().join("pkg/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("pkg/mod.py"),
        "from . import helper\nfrom .helper import helper as helper_fn\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "python-relative-imports".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!build.diagnostics.iter().any(|d| d.code == "CG2006"));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:pkg/mod.py",
        "imports_symbol",
        "imports_symbol",
        "symbol:pkg/helper.py::helper",
    ));
}

#[test]
fn test_python_all_marks_reexported_package_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("pkg")).unwrap();
    fs::write(
        dir.path().join("pkg/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("pkg/__init__.py"),
        "from .helper import helper\n__all__ = [\"helper\"]\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "python-all-reexports".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:pkg/__init__.py",
        "exports",
        "reexports",
        "symbol:pkg/helper.py::helper",
    ));
}

#[test]
fn test_rust_and_ts_reexports_point_to_target_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::write(dir.path().join("src/helper.rs"), "pub fn helper() {}\n").unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "pub mod helper;\npub use helper::helper;\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/helper.ts"),
        "export function helper() { return 1; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/mod.ts"),
        "export { helper } from './helper';\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "reexports".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:src/lib.rs",
        "exports",
        "reexports",
        "symbol:src/helper.rs::helper",
    ));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:web/mod.ts",
        "exports",
        "reexports",
        "symbol:web/helper.ts::helper",
    ));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:web/mod.ts",
        "imports_symbol",
        "imports_symbol",
        "symbol:web/helper.ts::helper",
    ));
}

#[test]
fn test_wildcard_imports_and_reexports_expand_to_exported_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("pkg")).unwrap();

    fs::write(
        dir.path().join("src/helper.rs"),
        "pub fn a() {}\npub fn b() {}\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "pub mod helper;\npub use helper::*;\n",
    )
    .unwrap();

    fs::write(
        dir.path().join("web/helper.ts"),
        "export function a() { return 1; }\nexport const b = 2;\n",
    )
    .unwrap();
    fs::write(dir.path().join("web/mod.ts"), "export * from './helper';\n").unwrap();

    fs::write(
        dir.path().join("pkg/helper.py"),
        "def a():\n    return 1\n\ndef _hidden():\n    return 2\n__all__ = [\"a\"]\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("pkg/__init__.py"),
        "from .helper import *\n__all__ = [\"a\"]\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "wildcard-semantics".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:src/lib.rs",
        "imports_symbol",
        "imports_symbol",
        "symbol:src/helper.rs::a",
    ));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:src/lib.rs",
        "exports",
        "reexports",
        "symbol:src/helper.rs::b",
    ));

    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:web/mod.ts",
        "imports_symbol",
        "imports_symbol",
        "symbol:web/helper.ts::a",
    ));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:web/mod.ts",
        "exports",
        "reexports",
        "symbol:web/helper.ts::b",
    ));

    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:pkg/__init__.py",
        "imports_symbol",
        "imports_symbol",
        "symbol:pkg/helper.py::a",
    ));
    assert!(file_has_edge_to_symbol(
        &build.document,
        "file:pkg/__init__.py",
        "exports",
        "reexports",
        "symbol:pkg/helper.py::a",
    ));
    assert!(!file_has_edge_to_symbol(
        &build.document,
        "file:pkg/__init__.py",
        "exports",
        "reexports",
        "symbol:pkg/helper.py::_hidden",
    ));
}

#[test]
fn test_explicit_type_relationship_edges_resolve_across_supported_languages() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/base.ts"),
        "export class Base {}\nexport interface Face {}\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/app.ts"),
            "import { Base as ImportedBase, Face as ImportedFace } from './base';\nclass Child extends ImportedBase implements ImportedFace {}\n",
        )
        .unwrap();

    fs::write(dir.path().join("py/base.py"), "class Base:\n    pass\n").unwrap();
    fs::write(
        dir.path().join("py/app.py"),
        "from .base import Base as ImportedBase\nclass Child(ImportedBase):\n    pass\n",
    )
    .unwrap();

    fs::write(
        dir.path().join("src/base.rs"),
        "pub trait Face {}\npub struct Thing;\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "mod base;\nuse crate::base::Face;\nstruct Thing;\nimpl Face for Thing {}\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "type-relationships".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/app.ts::Child",
        "extends",
        "extends",
        "symbol:web/base.ts::Base",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/app.ts::Child",
        "implements",
        "implements",
        "symbol:web/base.ts::Face",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/app.py::Child",
        "extends",
        "extends",
        "symbol:py/base.py::Base",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::Thing#",
        "implements",
        "implements",
        "symbol:src/base.rs::Face",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::Thing#",
        "for_type",
        "for_type",
        "symbol:src/lib.rs::Thing",
    ));
}

#[test]
fn test_call_sites_resolve_to_same_file_and_imported_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("src/util.rs"),
        "pub fn greet() -> String { \"hi\".to_string() }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "mod util;\npub fn run() -> String { util::greet() }\n",
    )
    .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/main.py"),
        "from .helper import helper\ndef execute():\n    return helper()\n",
    )
    .unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/main.ts"),
        "import { util } from './util';\nexport function run() { return util(); }\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "call-sites".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
}

#[test]
fn test_ts_constructor_aliases_resolve_to_class_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(dir.path().join("web/thing.ts"), "export class Thing {}\n").unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { Thing } from './thing';\nconst Ctor = Thing;\nexport function make() { return new Ctor(); }\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("web/member.ts"),
            "import * as ns from './thing';\nconst First = ns.Thing;\nconst Second = First;\nexport function build() { return new Second(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ts-constructor-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::make",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/member.ts::build",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
}

#[test]
fn test_ts_namespace_and_module_alias_member_constructors_resolve_to_class_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(dir.path().join("web/thing.ts"), "export class Thing {}\n").unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as ns from './thing';\nexport function direct() { return new ns.Thing(); }\nconst alias = ns;\nexport function top() { return new alias.Thing(); }\nexport function local() { const first = ns; const second = first; return new second.Thing(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ts-namespace-member-constructors".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::direct",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::top",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::local",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
}

#[test]
fn test_ts_default_import_calls_and_constructors_resolve_to_default_export_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export default function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/thing.ts"),
        "export default class Thing {}\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import util from './util';\nimport { default as util_spec } from './util';\nimport Thing from './thing';\nconst first = util;\nconst second = first;\nconst third = util_spec;\nconst Alias = Thing;\nexport function run() { return second(); }\nexport function run_spec() { return third(); }\nexport function make() { return new Alias(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ts-default-import-calls-and-constructors".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_spec",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::make",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
}

#[test]
fn test_ts_default_import_shadowing_does_not_fall_back_and_anonymous_defaults_stay_unresolved() {
    let shadow_dir = tempdir().unwrap();
    fs::create_dir_all(shadow_dir.path().join("web")).unwrap();

    fs::write(
        shadow_dir.path().join("web/util.ts"),
        "export default function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            shadow_dir.path().join("web/main.ts"),
            "import util from './util';\nexport function missing_case() { const util = missing; return util(); }\nexport function expr_case() { const util = true ? missing : util; return util(); }\n",
        )
        .unwrap();

    let shadow_build = build_code_graph(&CodeGraphBuildInput {
        repository_path: shadow_dir.path().to_path_buf(),
        commit_hash: "ts-default-import-shadowing".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &shadow_build.document,
        "symbol:web/main.ts::missing_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &shadow_build.document,
        "symbol:web/main.ts::expr_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));

    let anon_dir = tempdir().unwrap();
    fs::create_dir_all(anon_dir.path().join("web")).unwrap();
    fs::write(
        anon_dir.path().join("web/util.ts"),
        "export default function() { return 42; }\n",
    )
    .unwrap();
    fs::write(
        anon_dir.path().join("web/main.ts"),
        "import util from './util';\nexport function run() { return util(); }\n",
    )
    .unwrap();

    let anon_build = build_code_graph(&CodeGraphBuildInput {
        repository_path: anon_dir.path().to_path_buf(),
        commit_hash: "ts-anonymous-default-import".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &anon_build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
}

#[test]
fn test_ts_reexported_calls_and_constructors_resolve_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export default function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/thing.ts"),
        "export default class Thing {}\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/index.ts"),
            "export { default as util } from './util';\nexport { util as util_alias } from './util';\nexport { default as Thing } from './thing';\nexport * from './util';\n",
        )
        .unwrap();
    fs::write(
        dir.path().join("web/index2.ts"),
        "export { util as util_chain } from './index';\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/ns_main.ts"),
            "import * as ns from './index';\nexport function run_alias_member() { return ns.util_alias(); }\n",
        )
        .unwrap();
    fs::write(
        dir.path().join("web/default_index.ts"),
        "export * from './util';\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { util, Thing } from './index';\nimport { util as util_wildcard } from './index';\nimport { util_chain } from './index2';\nimport util_default from './default_index';\nconst alias = util_chain;\nexport function run() { return util(); }\nexport function run_wildcard() { return util_wildcard(); }\nexport function run_chain() { return alias(); }\nexport function run_default_excluded() { return util_default(); }\nexport function make() { return new Thing(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ts-reexported-calls-and-constructors".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_wildcard",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_chain",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/ns_main.ts::run_alias_member",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_default_excluded",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::make",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/thing.ts::Thing",
    ));
}

#[test]
fn test_js_commonjs_require_calls_resolve_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
        dir.path().join("web/default_util.js"),
        "module.exports = function util() { return 42; };\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/named_util.js"),
        "exports.greet = function greet() { return 42; };\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/object_util.js"),
        "function greet() { return 42; }\nmodule.exports = { greet };\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/object_renamed_util.js"),
        "function greet() { return 42; }\nmodule.exports = { hi: greet };\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/object_inline_util.js"),
        "module.exports = { greet: function greet() { return 42; } };\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/object_class_util.js"),
        "class Thing {}\nmodule.exports = { Alias: Thing };\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.js"),
            "const util = require('./default_util');\nconst { greet } = require('./named_util');\nconst named = require('./named_util');\nconst { greet: object_greet } = require('./object_util');\nconst renamed = require('./object_renamed_util');\nconst inline_named = require('./object_inline_util');\nconst { hi } = require('./object_renamed_util');\nconst { greet: inline_greet } = require('./object_inline_util');\nconst { Alias } = require('./object_class_util');\nconst class_mod = require('./object_class_util');\nexport function run_default() { return util(); }\nexport function run_named() { return greet(); }\nexport function run_member() { return named.greet(); }\nexport function run_object_named() { return object_greet(); }\nexport function run_object_renamed() { return hi(); }\nexport function run_object_inline() { return inline_greet(); }\nexport function run_object_member_renamed() { return renamed.hi(); }\nexport function run_object_member_inline() { return inline_named.greet(); }\nexport function make_object_class() { return new Alias(); }\nexport function make_object_class_member() { return new class_mod.Alias(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "js-commonjs-require-calls".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_default",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/default_util.js::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_named",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/named_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_member",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/named_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_object_named",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_object_renamed",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_renamed_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_object_inline",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_inline_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_object_member_renamed",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_renamed_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::run_object_member_inline",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_inline_util.js::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::make_object_class",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_class_util.js::Thing",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.js::make_object_class_member",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/object_class_util.js::Thing",
    ));
}

#[test]
fn test_python_package_reexported_calls_resolve_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("py/pkg")).unwrap();
    fs::create_dir_all(dir.path().join("py/pkg_wild")).unwrap();

    fs::write(
        dir.path().join("py/pkg/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/pkg/__init__.py"),
        "from .helper import helper\nfrom .helper import helper as alias\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/pkg_wild/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/pkg_wild/__init__.py"),
        "from .helper import *\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .pkg import helper\nfrom .pkg import helper as alias\nfrom .pkg_wild import helper as wild_helper\nfrom . import pkg\ndef run_pkg():\n    helper()\n    return alias()\ndef run_pkg_member_alias():\n    return pkg.alias()\ndef run_pkg_wild():\n    return wild_helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "python-package-reexported-calls".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_pkg",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/pkg/helper.py::helper",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_pkg_member_alias",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/pkg/helper.py::helper",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_pkg_wild",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/pkg_wild/helper.py::helper",
    ));
}

#[test]
fn test_python_wildcard_imports_resolve_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("py/pkg")).unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def greet():\n    return 1\ndef wave():\n    return 2\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/pkg/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/pkg/__init__.py"),
        "from .helper import helper\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .helper import *\nfrom .pkg import *\ndef run_module():\n    greet()\n    return wave()\ndef run_package():\n    return helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "python-wildcard-imports".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_module",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_module",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::wave",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run_package",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/pkg/helper.py::helper",
    ));
}

#[test]
fn test_python_wildcard_imports_respect_export_rules() {
    let underscore_dir = tempdir().unwrap();
    fs::create_dir_all(underscore_dir.path().join("py")).unwrap();

    fs::write(
        underscore_dir.path().join("py/helper.py"),
        "def public():\n    return 1\ndef _hidden():\n    return 2\n",
    )
    .unwrap();
    fs::write(
            underscore_dir.path().join("py/main.py"),
            "from .helper import *\ndef run_public():\n    return public()\ndef run_hidden():\n    return _hidden()\n",
        )
        .unwrap();

    let underscore_build = build_code_graph(&CodeGraphBuildInput {
        repository_path: underscore_dir.path().to_path_buf(),
        commit_hash: "python-wildcard-imports-underscore-rules".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &underscore_build.document,
        "symbol:py/main.py::run_public",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::public",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &underscore_build.document,
        "symbol:py/main.py::run_hidden",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::_hidden",
    ));

    let all_dir = tempdir().unwrap();
    fs::create_dir_all(all_dir.path().join("py")).unwrap();
    fs::write(
        all_dir.path().join("py/helper.py"),
        "__all__ = ['chosen']\n\ndef chosen():\n    return 1\n\ndef extra():\n    return 2\n",
    )
    .unwrap();
    fs::write(
            all_dir.path().join("py/main.py"),
            "from .helper import *\ndef run_chosen():\n    return chosen()\ndef run_extra():\n    return extra()\n",
        )
        .unwrap();

    let all_build = build_code_graph(&CodeGraphBuildInput {
        repository_path: all_dir.path().to_path_buf(),
        commit_hash: "python-wildcard-imports-all-rules".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &all_build.document,
        "symbol:py/main.py::run_chosen",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::chosen",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &all_build.document,
        "symbol:py/main.py::run_extra",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::extra",
    ));
}

#[test]
fn test_rust_import_aliases_and_nested_paths_resolve_to_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src/nested")).unwrap();

    fs::write(dir.path().join("src/util.rs"), "pub fn greet() {}\n").unwrap();
    fs::write(dir.path().join("src/nested/mod.rs"), "pub mod util;\n").unwrap();
    fs::write(dir.path().join("src/nested/util.rs"), "pub fn wave() {}\n").unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod util;\nmod nested;\nuse util::greet as hello;\npub fn run() { hello(); nested::util::wave(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-import-alias-and-nested-paths".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/nested/util.rs::wave",
    ));
}

#[test]
fn test_rust_pub_use_reexports_and_wildcards_resolve_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src/nested")).unwrap();

    fs::write(
        dir.path().join("src/util.rs"),
        "pub fn greet() {}\npub fn wave() {}\n",
    )
    .unwrap();
    fs::write(dir.path().join("src/nested/mod.rs"), "pub mod util;\n").unwrap();
    fs::write(dir.path().join("src/nested/util.rs"), "pub fn ping() {}\n").unwrap();
    fs::write(
        dir.path().join("src/barrel1.rs"),
        "pub use crate::util::greet;\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("src/barrel2.rs"),
        "pub use crate::barrel1::greet as hello;\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod util;\nmod nested;\nmod barrel1;\nmod barrel2;\npub use util::greet;\npub use util::wave as wave_alias;\npub use util::*;\npub use nested::util as util_mod;\npub use nested::{util as util_mod_two};\nuse barrel2::hello;\npub fn run() { greet(); wave(); wave_alias(); util_mod::ping(); util_mod_two::ping(); hello(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-pub-use-reexports-and-wildcards".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::wave",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/nested/util.rs::ping",
    ));
}

#[test]
fn test_rust_module_alias_paths_resolve_to_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src/nested")).unwrap();

    fs::write(dir.path().join("src/nested/mod.rs"), "pub mod util;\n").unwrap();
    fs::write(dir.path().join("src/nested/util.rs"), "pub fn wave() {}\n").unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod nested;\nuse nested::util as util_mod;\nuse nested::util::{self as util_mod_two};\nuse nested::{util as util_mod_three};\npub fn run() { util_mod::wave(); util_mod_two::wave(); util_mod_three::wave(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-module-alias-paths".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/nested/util.rs::wave",
    ));
}

#[test]
fn test_rust_function_local_aliases_resolve_to_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src/nested")).unwrap();

    fs::write(dir.path().join("src/util.rs"), "pub fn greet() {}\n").unwrap();
    fs::write(dir.path().join("src/nested/mod.rs"), "pub mod util;\n").unwrap();
    fs::write(dir.path().join("src/nested/util.rs"), "pub fn wave() {}\n").unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod util;\nmod nested;\nuse util::greet;\nuse nested::util as util_mod;\nuse nested::util::{wave as hello};\npub fn run() { let first = greet; let second = first; let wave_alias = util_mod::wave; let hello_alias = hello; second(); wave_alias(); hello_alias(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-function-local-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::greet",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/nested/util.rs::wave",
    ));
}

#[test]
fn test_rust_local_aliases_shadow_imported_names_and_stay_isolated() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();

    fs::write(dir.path().join("src/one.rs"), "pub fn one() {}\n").unwrap();
    fs::write(dir.path().join("src/two.rs"), "pub fn two() {}\n").unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod one;\nmod two;\nuse one::one;\nuse two::two;\npub fn run() { let one = two; one(); }\npub fn first() { let alias = one; alias(); }\npub fn second() { two(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-local-shadowing-and-isolation".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/two.rs::two",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::first",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/one.rs::one",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::second",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/two.rs::two",
    ));
}

#[test]
fn test_rust_unresolved_or_unsupported_local_shadowing_does_not_fall_back() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();

    fs::write(dir.path().join("src/one.rs"), "pub fn one() {}\n").unwrap();
    fs::write(
            dir.path().join("src/lib.rs"),
            "mod one;\nuse one::one;\npub fn missing_case() { let one = missing; one(); }\npub fn expr_case() { let one = if true { missing } else { one }; one(); }\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "rust-local-shadowing-no-fallback".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::missing_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/one.rs::one",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::expr_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/one.rs::one",
    ));
}

#[test]
fn test_alias_cycles_remain_unresolved_without_symbol_edges() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();

    fs::write(
        dir.path().join("web/main.ts"),
        "const a = b;\nconst b = a;\nexport function run() { return a(); }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/main.py"),
        "a = b\nb = a\ndef run():\n    return a()\n",
    )
    .unwrap();
    fs::write(dir.path().join("src/util.rs"), "pub fn greet() {}\n").unwrap();
    fs::write(
        dir.path().join("src/lib.rs"),
        "mod util;\nuse util::greet;\npub fn run() { let a = b; let b = a; a(); }\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "alias-cycles-unresolved".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/main.ts::a",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/main.py::a",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/lib.rs::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:src/util.rs::greet",
    ));
}

#[test]
fn test_interface_and_trait_inheritance_edges_are_emitted() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
        dir.path().join("src/base.rs"),
        "pub trait Parent {}\npub trait Child: Parent {}\n",
    )
    .unwrap();
    fs::write(dir.path().join("src/lib.rs"), "mod base;\n").unwrap();

    fs::write(
        dir.path().join("web/base.ts"),
        "export interface Parent {}\nexport interface Child extends Parent {}\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "interface-trait-inheritance".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/base.ts::Child",
        "extends",
        "extends",
        "symbol:web/base.ts::Parent",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:src/base.rs::Child",
        "extends",
        "extends",
        "symbol:src/base.rs::Parent",
    ));
}

#[test]
fn test_namespace_and_module_member_calls_resolve_to_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/main.ts"),
        "import * as utilns from './util';\nexport function run() { return utilns.util(); }\n",
    )
    .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/main.py"),
        "from . import helper as helper_mod\ndef execute():\n    return helper_mod.helper()\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "member-call-sites".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_top_level_aliases_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { util } from './util';\nconst alias = util;\nexport function run() { return alias(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/main.py"),
        "from .helper import helper\nalias = helper\ndef execute():\n    return alias()\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "top-level-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_top_level_member_aliases_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as utilns from './util';\nconst f = utilns.util;\nexport function run() { return f(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\nalias = helper_mod.helper\ndef execute():\n    return alias()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "top-level-member-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_top_level_module_aliases_resolve_member_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as ns from './util';\nconst alias = ns;\nexport function run() { return alias.util(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\nalias = helper_mod\ndef execute():\n    return alias.helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "top-level-module-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_unresolved_or_unsupported_module_alias_shadowing_does_not_fall_back() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as ns from './util';\nconst alias = ns;\nexport function missing_case() { const alias = missing; return alias.util(); }\nexport function expr_case() { const alias = true ? ns : ns; return alias.util(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\nalias = helper_mod\ndef missing_case():\n    alias = missing\n    return alias.helper()\ndef expr_case():\n    alias = helper_mod if True else helper_mod\n    return alias.helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "module-alias-shadowing-no-fallback".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::missing_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::expr_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::missing_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::expr_case",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_alias_chains_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { util } from './util';\nconst first = util;\nconst second = first;\nexport function run() { return second(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .helper import helper\nfirst = helper\nsecond = first\ndef execute():\n    return second()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "alias-chains".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_top_level_member_alias_chains_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as utilns from './util';\nconst first = utilns.util;\nconst second = first;\nexport function run() { return second(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\nfirst = helper_mod.helper\nsecond = first\ndef execute():\n    return second()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "top-level-member-alias-chains".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_mixed_scope_member_aliases_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as utilns from './util';\nconst top = utilns.util;\nexport function run() { const local = top; return local(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\ntop = helper_mod.helper\ndef execute():\n    local = top\n    return local()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "mixed-scope-member-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_local_aliases_shadow_top_level_aliases_of_same_name() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/one.ts"),
        "export function one() { return 1; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/two.ts"),
        "export function two() { return 2; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { one } from './one';\nimport { two } from './two';\nconst alias = one;\nexport function run() { const alias = two; return alias(); }\n",
        )
        .unwrap();

    fs::write(dir.path().join("py/one.py"), "def one():\n    return 1\n").unwrap();
    fs::write(dir.path().join("py/two.py"), "def two():\n    return 2\n").unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .one import one\nfrom .two import two\nalias = one\ndef execute():\n    alias = two\n    return alias()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "alias-shadowing".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/two.ts::two",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/two.py::two",
    ));
}

#[test]
fn test_unresolved_or_unsupported_local_shadowing_does_not_fall_back_to_top_level_aliases() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/one.ts"),
        "export function one() { return 1; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { one } from './one';\nconst alias = one;\nexport function run_missing() { const alias = missing; return alias(); }\nexport function run_expr() { const alias = true ? one : one; return alias(); }\n",
        )
        .unwrap();

    fs::write(dir.path().join("py/one.py"), "def one():\n    return 1\n").unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .one import one\nalias = one\ndef execute_missing():\n    alias = missing\n    return alias()\ndef execute_expr():\n    alias = 1 if True else one\n    return alias()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "shadowing-no-fallback".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_missing",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/one.ts::one",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run_expr",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/one.ts::one",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute_missing",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/one.py::one",
    ));
    assert!(!symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute_expr",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/one.py::one",
    ));
}

#[test]
fn test_local_aliases_are_isolated_to_their_own_enclosing_symbol() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/one.ts"),
        "export function one() { return 1; }\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("web/two.ts"),
        "export function two() { return 2; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { one } from './one';\nimport { two } from './two';\nexport function first() { const alias = one; return alias(); }\nexport function second() { return two(); }\n",
        )
        .unwrap();

    fs::write(dir.path().join("py/one.py"), "def one():\n    return 1\n").unwrap();
    fs::write(dir.path().join("py/two.py"), "def two():\n    return 2\n").unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .one import one\nfrom .two import two\ndef first():\n    alias = one\n    return alias()\ndef second():\n    return two()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "alias-scope-isolation".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::first",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/one.ts::one",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::second",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/two.ts::two",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::first",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/one.py::one",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::second",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/two.py::two",
    ));
}

#[test]
fn test_function_local_aliases_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { util } from './util';\nexport function run() { const localAlias = util; return localAlias(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
        dir.path().join("py/main.py"),
        "from .helper import helper\ndef execute():\n    alias = helper\n    return alias()\n",
    )
    .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_function_local_alias_chains_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import { util } from './util';\nexport function run() { const first = util; const second = first; return second(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from .helper import helper\ndef execute():\n    first = helper\n    second = first\n    return second()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-alias-chains".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_function_local_member_aliases_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as utilns from './util';\nexport function run() { const f = utilns.util; return f(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\ndef execute():\n    alias = helper_mod.helper\n    return alias()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-member-aliases".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_function_local_module_alias_chains_resolve_member_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as ns from './util';\nexport function run() { const first = ns; const second = first; return second.util(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\ndef execute():\n    first = helper_mod\n    second = first\n    return second.helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-module-alias-chains".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_function_local_module_aliases_are_isolated_to_their_own_enclosing_symbol() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as ns from './util';\nexport function first() { const alias = ns; return alias.util(); }\nexport function second() { return ns.util(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\ndef first():\n    alias = helper_mod\n    return alias.helper()\ndef second():\n    return helper_mod.helper()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-module-alias-isolation".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::first",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::second",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::first",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::second",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_function_local_member_alias_chains_resolve_call_sites_to_underlying_symbols() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();

    fs::write(
        dir.path().join("web/util.ts"),
        "export function util() { return 42; }\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("web/main.ts"),
            "import * as utilns from './util';\nexport function run() { const first = utilns.util; const second = first; return second(); }\n",
        )
        .unwrap();

    fs::write(
        dir.path().join("py/helper.py"),
        "def helper():\n    return 1\n",
    )
    .unwrap();
    fs::write(
            dir.path().join("py/main.py"),
            "from . import helper as helper_mod\ndef execute():\n    first = helper_mod.helper\n    second = first\n    return second()\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "function-local-member-alias-chains".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:web/main.ts::run",
        "uses_symbol",
        "uses_symbol",
        "symbol:web/util.ts::util",
    ));
    assert!(symbol_has_edge_to_symbol(
        &build.document,
        "symbol:py/main.py::execute",
        "uses_symbol",
        "uses_symbol",
        "symbol:py/helper.py::helper",
    ));
}

#[test]
fn test_nested_symbols_are_captured_and_nested_in_structure() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("pkg")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
            dir.path().join("src/lib.rs"),
            "pub struct Thing;\nimpl Thing { pub fn method(&self) {} }\npub fn top() { fn inner() {} inner(); }\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("pkg/mod.py"),
            "class Thing:\n    def method(self):\n        return 1\n\ndef top():\n    def inner():\n        return 2\n    return inner()\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("web/mod.ts"),
            "export class Thing {\n  method() { return 1; }\n}\nexport function top() {\n  function inner() { return 2; }\n  return inner();\n}\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "nested-symbols".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    let keys = symbol_logical_keys(&build.document);
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:src/lib.rs::Thing::method")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:src/lib.rs::top::inner")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:pkg/mod.py::Thing::method")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:pkg/mod.py::top::inner")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:web/mod.ts::Thing::method")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:web/mod.ts::top::inner")));

    let key_index = logical_key_to_block_id(&build.document);
    let rust_method_id = key_index
        .iter()
        .find(|(key, _)| key.starts_with("symbol:src/lib.rs::Thing::method"))
        .map(|(_, id)| *id)
        .unwrap();
    let rust_parent_id = build
        .document
        .structure
        .iter()
        .find(|(_, children)| children.contains(&rust_method_id))
        .map(|(id, _)| *id)
        .unwrap();

    let rust_parent_key = key_index
        .iter()
        .find(|(_, id)| **id == rust_parent_id)
        .map(|(key, _)| key.as_str())
        .unwrap();

    assert!(rust_parent_key.starts_with("symbol:src/lib.rs::Thing"));
}

#[test]
fn test_ts_js_export_aliases_generators_and_function_like_members_are_captured() {
    let dir = tempdir().unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();

    fs::write(
            dir.path().join("src/mod.ts"),
            "function internal() { return 1; }\nexport { internal };\nexport function* gen() { yield 1; }\nexport const arrow = () => 1;\nclass Example {\n  handler = () => 1;\n}\nexport default Example;\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("src/mod.js"),
            "function internalJs() { return 1; }\nexport { internalJs };\nexport function* jsGen() { yield 1; }\nclass JsExample {\n  handler = () => 1;\n}\nexport default JsExample;\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "ts-js-coverage".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    let keys = symbol_logical_keys(&build.document);
    assert!(keys.iter().any(|k| k.starts_with("symbol:src/mod.ts::gen")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:src/mod.ts::Example::handler")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:src/mod.js::jsGen")));
    assert!(keys
        .iter()
        .any(|k| k.starts_with("symbol:src/mod.js::JsExample::handler")));

    assert!(symbol_exported(
        &build.document,
        "symbol:src/mod.ts::internal"
    ));
    assert!(symbol_exported(
        &build.document,
        "symbol:src/mod.ts::Example"
    ));
    assert!(symbol_exported(
        &build.document,
        "symbol:src/mod.js::internalJs"
    ));
    assert!(symbol_exported(
        &build.document,
        "symbol:src/mod.js::JsExample"
    ));

    assert_eq!(
        symbol_kind(&build.document, "symbol:src/mod.ts::arrow").as_deref(),
        Some("function")
    );
    assert_eq!(
        symbol_kind(&build.document, "symbol:src/mod.ts::Example::handler").as_deref(),
        Some("method")
    );
}

#[test]
fn test_file_descriptions_and_symbol_descriptions_are_extracted() {
    let dir = tempdir().unwrap();
    let repo_name = dir
        .path()
        .file_name()
        .map(|value| value.to_string_lossy().to_string())
        .unwrap();
    fs::create_dir_all(dir.path().join("src")).unwrap();
    fs::create_dir_all(dir.path().join("py")).unwrap();
    fs::create_dir_all(dir.path().join("web")).unwrap();

    fs::write(
            dir.path().join("src/lib.rs"),
            "//! Rust module summary\n\n/// Rust helper description.\npub async fn helper(value: i32) -> i32 { value }\n/// Rust thing description.\npub trait Thing: Send + Sync {}\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("py/mod.py"),
            "\"\"\"Python module summary.\"\"\"\nfrom typing import Protocol, runtime_checkable\n\ndef helper(value: int) -> int:\n    \"\"\"Python helper description.\"\"\"\n    return value\n\n@runtime_checkable\nclass Thing(Protocol):\n    \"\"\"Python thing description.\"\"\"\n    async def generate_answer(self, question: str) -> str:\n        \"\"\"Generate an answer.\"\"\"\n        return question\n\n    @staticmethod\n    def normalize(value: str) -> str:\n        return value\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("web/mod.ts"),
            "/** TS module summary */\n/** TS helper description. */\nexport async function helper(value: number, label: string): Promise<number> { return value; }\n/** TS thing description. */\nexport class Thing extends Base implements Named { public static make(value: number): Thing { return new Thing(); } }\n",
        )
        .unwrap();
    fs::write(
            dir.path().join("web/mod.js"),
            "/** JS helper description. */\nfunction* helper(value, label = 'x') { yield value; }\nmodule.exports = { helper };\n",
        )
        .unwrap();

    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "file-description-and-symbol-descriptions".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();

    assert_eq!(
        repository_content_json_subfield(&build.document, "coderef", "path"),
        Some(json!("."))
    );
    assert_eq!(
        repository_content_json_subfield(&build.document, "coderef", "display"),
        Some(json!(repo_name))
    );
    assert_eq!(
        repository_metadata_custom_subfield(&build.document, META_CODEREF, "path"),
        Some(json!("."))
    );
    assert_eq!(
        directory_content_json_subfield(&build.document, "directory:src", "coderef", "path"),
        Some(json!("src"))
    );
    assert_eq!(
        directory_content_json_subfield(&build.document, "directory:src", "coderef", "display"),
        Some(json!("src"))
    );
    assert_eq!(
        directory_content_json_field(&build.document, "directory:src", "path"),
        None
    );
    assert_eq!(
        directory_metadata_custom_subfield(&build.document, "directory:src", META_CODEREF, "path"),
        Some(json!("src"))
    );
    assert_eq!(
        file_summary(&build.document, "file:src/lib.rs").as_deref(),
        Some("Rust module summary")
    );
    assert_eq!(
        file_content_json_subfield(&build.document, "file:src/lib.rs", "coderef", "path"),
        Some(json!("src/lib.rs"))
    );
    assert_eq!(
        file_content_json_subfield(&build.document, "file:src/lib.rs", "coderef", "display"),
        Some(json!("src/lib.rs"))
    );
    assert_eq!(
        file_content_json_field(&build.document, "file:src/lib.rs", "path"),
        None
    );
    assert_eq!(
        file_metadata_custom_subfield(&build.document, "file:src/lib.rs", META_CODEREF, "display"),
        Some(json!("src/lib.rs"))
    );
    assert_eq!(
        file_summary(&build.document, "file:py/mod.py").as_deref(),
        Some("Python module summary.")
    );
    assert_eq!(
        file_content_json_subfield(&build.document, "file:py/mod.py", "coderef", "path"),
        Some(json!("py/mod.py"))
    );
    assert_eq!(
        file_summary(&build.document, "file:web/mod.ts").as_deref(),
        Some("TS module summary")
    );
    assert_eq!(
        file_content_json_subfield(&build.document, "file:web/mod.ts", "coderef", "path"),
        Some(json!("web/mod.ts"))
    );

    assert_eq!(
        symbol_summary(&build.document, "symbol:src/lib.rs::helper").as_deref(),
        Some("Rust helper description.")
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:src/lib.rs::helper", "description")
            .as_deref(),
        Some("Rust helper description.")
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:src/lib.rs::helper", "helper")
            .as_deref(),
        None
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:src/lib.rs::helper", "inputs"),
        Some(json!([{"name": "value", "type": "i32"}]))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:src/lib.rs::helper",
            "coderef",
            "path"
        ),
        Some(json!("src/lib.rs"))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:src/lib.rs::helper",
            "coderef",
            "display"
        ),
        Some(json!("src/lib.rs#L4"))
    );
    assert_eq!(
        symbol_metadata_custom_subfield(
            &build.document,
            "symbol:src/lib.rs::helper",
            META_CODEREF,
            "display"
        ),
        Some(json!("src/lib.rs#L4"))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:src/lib.rs::helper",
            "coderef",
            "start_line"
        ),
        Some(json!(4))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:src/lib.rs::helper",
            "coderef",
            "end_line"
        ),
        Some(json!(4))
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:src/lib.rs::helper", "path"),
        None
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:src/lib.rs::helper", "line_range"),
        None
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:src/lib.rs::helper", "span"),
        None
    );
    assert_eq!(
        symbol_metadata_custom_field(&build.document, "symbol:src/lib.rs::helper", "path"),
        None
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:src/lib.rs::helper", "output")
            .as_deref(),
        Some("i32")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:src/lib.rs::helper", "modifiers"),
        Some(json!({"async": true, "visibility": "public"}))
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:src/lib.rs::Thing").as_deref(),
        Some("Rust thing description.")
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:src/lib.rs::Thing", "type").as_deref(),
        Some("Send + Sync")
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:py/mod.py::helper").as_deref(),
        Some("Python helper description.")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:py/mod.py::helper", "inputs"),
        Some(json!([{"name": "value", "type": "int"}]))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:py/mod.py::helper",
            "coderef",
            "path"
        ),
        Some(json!("py/mod.py"))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:py/mod.py::helper",
            "coderef",
            "display"
        ),
        Some(json!("py/mod.py#L4-L6"))
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:py/mod.py::helper", "output")
            .as_deref(),
        Some("int")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:py/mod.py::helper", "modifiers"),
        None
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:py/mod.py::Thing").as_deref(),
        Some("Python thing description.")
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:py/mod.py::Thing", "type").as_deref(),
        Some("Protocol")
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:py/mod.py::Thing::generate_answer").as_deref(),
        Some("Generate an answer.")
    );
    assert_eq!(
        symbol_content_json_field(
            &build.document,
            "symbol:py/mod.py::Thing::generate_answer",
            "inputs"
        ),
        Some(json!([
            {"name": "self"},
            {"name": "question", "type": "str"}
        ]))
    );
    assert_eq!(
        symbol_content_string_field(
            &build.document,
            "symbol:py/mod.py::Thing::generate_answer",
            "output"
        )
        .as_deref(),
        Some("str")
    );
    assert_eq!(
        symbol_content_json_field(
            &build.document,
            "symbol:py/mod.py::Thing::generate_answer",
            "modifiers"
        ),
        Some(json!({"async": true}))
    );
    assert_eq!(
        symbol_content_json_field(
            &build.document,
            "symbol:py/mod.py::Thing::normalize",
            "modifiers"
        ),
        Some(json!({"static": true}))
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:web/mod.ts::helper").as_deref(),
        Some("TS helper description.")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:web/mod.ts::helper", "inputs"),
        Some(json!([
            {"name": "value", "type": "number"},
            {"name": "label", "type": "string"}
        ]))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:web/mod.ts::helper",
            "coderef",
            "path"
        ),
        Some(json!("web/mod.ts"))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:web/mod.ts::helper",
            "coderef",
            "display"
        ),
        Some(json!("web/mod.ts#L3"))
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:web/mod.ts::helper", "output")
            .as_deref(),
        Some("Promise<number>")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:web/mod.ts::helper", "modifiers"),
        Some(json!({"async": true}))
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:web/mod.ts::Thing").as_deref(),
        Some("TS thing description.")
    );
    assert_eq!(
        symbol_content_string_field(&build.document, "symbol:web/mod.ts::Thing", "type").as_deref(),
        Some("extends Base implements Named")
    );
    assert_eq!(
        symbol_content_json_field(
            &build.document,
            "symbol:web/mod.ts::Thing::make",
            "modifiers"
        ),
        Some(json!({"static": true, "visibility": "public"}))
    );
    assert_eq!(
        symbol_summary(&build.document, "symbol:web/mod.js::helper").as_deref(),
        Some("JS helper description.")
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:web/mod.js::helper", "inputs"),
        Some(json!([
            {"name": "value"},
            {"name": "label"}
        ]))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:web/mod.js::helper",
            "coderef",
            "path"
        ),
        Some(json!("web/mod.js"))
    );
    assert_eq!(
        symbol_content_json_subfield(
            &build.document,
            "symbol:web/mod.js::helper",
            "coderef",
            "display"
        ),
        Some(json!("web/mod.js#L2"))
    );
    assert_eq!(
        symbol_content_json_field(&build.document, "symbol:web/mod.js::helper", "modifiers"),
        Some(json!({"generator": true}))
    );
}

#[test]
fn test_performance_smoke_medium_fixture() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();

    for i in 0..300usize {
        let mut file = fs::File::create(src.join(format!("m{}.rs", i))).unwrap();
        writeln!(file, "pub fn f{}() {{}}", i).unwrap();
        if i > 0 {
            writeln!(file, "use crate::m{}::f{};", i - 1, i - 1).unwrap();
        }
    }

    let start = std::time::Instant::now();
    let build = build_code_graph(&CodeGraphBuildInput {
        repository_path: dir.path().to_path_buf(),
        commit_hash: "perf-smoke".to_string(),
        config: CodeGraphExtractorConfig::default(),
    })
    .unwrap();
    let elapsed = start.elapsed();

    assert!(build.stats.file_nodes >= 300);
    assert!(elapsed.as_secs_f64() < 3.0, "elapsed: {elapsed:?}");
}

#[test]
fn test_incremental_build_reuses_unchanged_files() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();
    fs::write(src.join("util.rs"), "pub fn util() -> i32 { 1 }\n").unwrap();
    fs::write(
        src.join("lib.rs"),
        "mod util;\npub fn add(a:i32,b:i32)->i32{util::util()+a+b}\n",
    )
    .unwrap();

    let state_file = dir.path().join("codegraph-state.json");
    let input = default_incremental_input(dir.path(), &state_file, "incremental-smoke");
    let full = build_code_graph(&default_build_input(dir.path(), "incremental-smoke")).unwrap();

    let first = build_code_graph_incremental(&input).unwrap();
    let first_stats = first.incremental.clone().unwrap();
    assert_builds_equivalent(&full, &first);
    assert_eq!(first_stats.scanned_files, 2);
    assert_eq!(first_stats.state_entries, 0);
    assert_eq!(first_stats.direct_invalidated_files, 2);
    assert_eq!(first_stats.surface_changed_files, 0);
    assert_eq!(first_stats.rebuilt_files, 2);
    assert_eq!(first_stats.reused_files, 0);
    assert_eq!(
        first_stats.full_rebuild_reason.as_deref(),
        Some("missing_state")
    );
    assert!(state_file.exists());

    let second = build_code_graph_incremental(&input).unwrap();
    let second_stats = second.incremental.clone().unwrap();
    assert_builds_equivalent(&full, &second);
    assert_eq!(second_stats.scanned_files, 2);
    assert_eq!(second_stats.state_entries, 2);
    assert_eq!(second_stats.direct_invalidated_files, 0);
    assert_eq!(second_stats.surface_changed_files, 0);
    assert_eq!(second_stats.rebuilt_files, 0);
    assert_eq!(second_stats.reused_files, 2);
    assert_eq!(second_stats.full_rebuild_reason, None);
    assert_eq!(
        first.canonical_fingerprint, second.canonical_fingerprint,
        "unchanged incremental rebuild should preserve the graph fingerprint"
    );
}

#[test]
fn test_incremental_build_invalidates_dependents_and_deletions() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();
    fs::write(src.join("helper.rs"), "pub fn helper() -> i32 { 2 }\n").unwrap();
    fs::write(src.join("util.rs"), "pub fn util() -> i32 { 1 }\n").unwrap();
    fs::write(
        src.join("lib.rs"),
        "mod helper;\nmod util;\npub fn add(a:i32,b:i32)->i32{helper::helper()+util::util()+a+b}\n",
    )
    .unwrap();

    let state_file = dir.path().join("codegraph-state.json");
    let input = default_incremental_input(dir.path(), &state_file, "incremental-invalidations");

    let baseline = build_code_graph_incremental(&input).unwrap();
    assert_eq!(baseline.stats.file_nodes, 3);

    fs::write(src.join("util.rs"), "pub fn util() -> i32 { 7 }\n").unwrap();
    let changed = build_code_graph_incremental(&input).unwrap();
    let changed_stats = changed.incremental.clone().unwrap();
    assert_eq!(changed_stats.scanned_files, 3);
    assert_eq!(changed_stats.state_entries, 3);
    assert_eq!(changed_stats.direct_invalidated_files, 1);
    assert_eq!(changed_stats.surface_changed_files, 0);
    assert_eq!(changed_stats.changed_files, 1);
    assert_eq!(changed_stats.rebuilt_files, 1);
    assert_eq!(changed_stats.reused_files, 2);
    assert_eq!(changed_stats.invalidated_files, 1);

    fs::remove_file(src.join("helper.rs")).unwrap();
    fs::write(
        src.join("lib.rs"),
        "mod util;\npub fn add(a:i32,b:i32)->i32{util::util()+a+b}\n",
    )
    .unwrap();
    let deleted = build_code_graph_incremental(&input).unwrap();
    let deleted_stats = deleted.incremental.clone().unwrap();
    assert_eq!(deleted_stats.scanned_files, 2);
    assert_eq!(deleted_stats.state_entries, 3);
    assert_eq!(deleted_stats.direct_invalidated_files, 2);
    assert_eq!(deleted_stats.surface_changed_files, 2);
    assert_eq!(deleted_stats.deleted_files, 1);
    assert_eq!(deleted_stats.changed_files, 1);
    assert_eq!(deleted_stats.rebuilt_files, 1);
    assert_eq!(deleted_stats.reused_files, 1);
    assert_eq!(deleted_stats.invalidated_files, 2);
    assert_eq!(deleted.stats.file_nodes, 2);
}

#[test]
fn test_incremental_build_matches_full_build_after_dependency_affecting_change() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();
    fs::write(src.join("helper.rs"), "pub fn helper() -> i32 { 2 }\n").unwrap();
    fs::write(
        src.join("util.rs"),
        "pub fn util() -> i32 { helper::helper() }\n",
    )
    .unwrap();
    fs::write(
        src.join("lib.rs"),
        "mod helper;\nmod util;\npub fn add(a:i32,b:i32)->i32{util::util()+a+b}\n",
    )
    .unwrap();

    let state_file = dir.path().join("codegraph-state.json");
    let input = default_incremental_input(dir.path(), &state_file, "incremental-equivalence");
    build_code_graph_incremental(&input).unwrap();

    fs::write(
        src.join("util.rs"),
        "pub fn util() -> i32 { helper::helper() }\npub fn util_twice() -> i32 { util() * 2 }\n",
    )
    .unwrap();

    let full =
        build_code_graph(&default_build_input(dir.path(), "incremental-equivalence")).unwrap();
    let incremental = build_code_graph_incremental(&input).unwrap();
    let incremental_stats = incremental.incremental.clone().unwrap();

    assert_builds_equivalent(&full, &incremental);
    assert_eq!(incremental_stats.scanned_files, 3);
    assert_eq!(incremental_stats.state_entries, 3);
    assert_eq!(incremental_stats.direct_invalidated_files, 1);
    assert_eq!(incremental_stats.surface_changed_files, 1);
    assert_eq!(incremental_stats.changed_files, 1);
    assert_eq!(incremental_stats.rebuilt_files, 2);
    assert_eq!(incremental_stats.reused_files, 1);
}

#[test]
fn test_incremental_build_falls_back_for_invalid_state_contents() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();
    fs::write(src.join("lib.rs"), "pub fn add(a:i32,b:i32)->i32{a+b}\n").unwrap();

    let state_file = dir.path().join("codegraph-state.json");
    let input = default_incremental_input(dir.path(), &state_file, "incremental-invalid-state");
    build_code_graph_incremental(&input).unwrap();

    fs::write(&state_file, "{ definitely-not-valid-json ").unwrap();

    let full = build_code_graph(&default_build_input(
        dir.path(),
        "incremental-invalid-state",
    ))
    .unwrap();
    let rebuilt = build_code_graph_incremental(&input).unwrap();
    let stats = rebuilt.incremental.clone().unwrap();

    assert_eq!(full.stats, rebuilt.stats);
    assert_eq!(full.canonical_fingerprint, rebuilt.canonical_fingerprint);
    assert_eq!(
        canonical_codegraph_json(&full.document).unwrap(),
        canonical_codegraph_json(&rebuilt.document).unwrap()
    );
    assert_eq!(stats.full_rebuild_reason.as_deref(), Some("invalid_state"));
    assert_eq!(stats.surface_changed_files, 0);
    assert_eq!(stats.rebuilt_files, 1);
    assert_eq!(stats.reused_files, 0);
    assert!(diagnostic_signatures(&rebuilt.diagnostics)
        .iter()
        .any(|diag| diag.contains("CG2009") && diag.contains("incremental state invalid")));
}

#[test]
fn test_incremental_build_falls_back_for_incompatible_state_metadata() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();
    fs::write(src.join("lib.rs"), "pub fn add(a:i32,b:i32)->i32{a+b}\n").unwrap();

    let state_file = dir.path().join("codegraph-state.json");
    let input = default_incremental_input(dir.path(), &state_file, "incremental-incompatibilities");
    build_code_graph_incremental(&input).unwrap();

    type StateMutation = fn(&mut serde_json::Value);

    let scenarios: [(&str, StateMutation); 3] = [
        ("extractor_version_changed", |state| {
            state["extractor_version"] = json!("ucp-codegraph-extractor.v-next");
        }),
        ("repository_changed", |state| {
            state["repository_path"] = json!("/some/other/repository");
        }),
        ("config_changed", |state| {
            state["config"]["include_hidden"] = json!(true);
        }),
    ];

    for (reason, mutate) in scenarios {
        build_code_graph_incremental(&input).unwrap();
        mutate_incremental_state_json(&state_file, mutate);

        let full = build_code_graph(&default_build_input(
            dir.path(),
            "incremental-incompatibilities",
        ))
        .unwrap();
        let rebuilt = build_code_graph_incremental(&input).unwrap();
        let stats = rebuilt.incremental.clone().unwrap();

        assert_builds_equivalent(&full, &rebuilt);
        assert_eq!(stats.full_rebuild_reason.as_deref(), Some(reason));
        assert_eq!(stats.surface_changed_files, 0);
        assert_eq!(stats.rebuilt_files, 1);
        assert_eq!(stats.reused_files, 0);
    }
}

#[test]
fn test_incremental_build_performance_harness_large_fixture() {
    let dir = tempdir().unwrap();
    let src = dir.path().join("src");
    fs::create_dir_all(&src).unwrap();

    for i in 0..400usize {
        let mut file = fs::File::create(src.join(format!("m{}.rs", i))).unwrap();
        writeln!(file, "pub fn f{}() -> usize {{ {} }}", i, i).unwrap();
        if i > 0 {
            writeln!(file, "use crate::m{}::f{};", i - 1, i - 1).unwrap();
            writeln!(
                file,
                "pub fn chain{}() -> usize {{ f{}() + {} }}",
                i,
                i - 1,
                i
            )
            .unwrap();
        }
    }

    let state_file = dir.path().join("codegraph-state.json");
    let full_input = default_build_input(dir.path(), "incremental-perf");
    let incremental_input = default_incremental_input(dir.path(), &state_file, "incremental-perf");

    let full_start = std::time::Instant::now();
    let full = build_code_graph(&full_input).unwrap();
    let full_elapsed = full_start.elapsed();

    let seed_start = std::time::Instant::now();
    let seed = build_code_graph_incremental(&incremental_input).unwrap();
    let seed_elapsed = seed_start.elapsed();

    let no_change_start = std::time::Instant::now();
    let no_change = build_code_graph_incremental(&incremental_input).unwrap();
    let no_change_elapsed = no_change_start.elapsed();

    fs::write(
        src.join("m399.rs"),
        "pub fn f399() -> usize { 400 }\nuse crate::m398::f398;\npub fn chain399() -> usize { f398() + 399 }\n",
    )
    .unwrap();
    let body_change_start = std::time::Instant::now();
    let body_changed = build_code_graph_incremental(&incremental_input).unwrap();
    let body_change_elapsed = body_change_start.elapsed();

    fs::write(
        src.join("m398.rs"),
        "pub fn f398() -> usize { 398 }\nuse crate::m397::f397;\npub fn chain398() -> usize { f397() + 398 }\npub fn added_api398() -> usize { chain398() }\n",
    )
    .unwrap();
    let api_change_start = std::time::Instant::now();
    let api_changed = build_code_graph_incremental(&incremental_input).unwrap();
    let api_change_elapsed = api_change_start.elapsed();

    let final_full = build_code_graph(&full_input).unwrap();

    eprintln!(
        "full={full_elapsed:?} seed_incremental={seed_elapsed:?} no_change_incremental={no_change_elapsed:?} body_change_incremental={body_change_elapsed:?} api_change_incremental={api_change_elapsed:?}"
    );

    assert_builds_equivalent(&full, &seed);
    let seed_stats = seed.incremental.clone().unwrap();
    assert_eq!(seed_stats.scanned_files, 400);
    assert_eq!(seed_stats.rebuilt_files, 400);
    assert_eq!(seed_stats.reused_files, 0);

    let no_change_stats = no_change.incremental.clone().unwrap();
    assert_eq!(no_change_stats.scanned_files, 400);
    assert_eq!(no_change_stats.state_entries, 400);
    assert_eq!(no_change_stats.direct_invalidated_files, 0);
    assert_eq!(no_change_stats.rebuilt_files, 0);
    assert_eq!(no_change_stats.reused_files, 400);

    let body_changed_stats = body_changed.incremental.clone().unwrap();
    assert_eq!(body_changed_stats.scanned_files, 400);
    assert_eq!(body_changed_stats.state_entries, 400);
    assert_eq!(body_changed_stats.direct_invalidated_files, 1);
    assert_eq!(body_changed_stats.surface_changed_files, 0);
    assert_eq!(body_changed_stats.changed_files, 1);
    assert_eq!(body_changed_stats.rebuilt_files, 1);
    assert_eq!(body_changed_stats.reused_files, 399);
    assert_eq!(body_changed_stats.invalidated_files, 1);

    let api_changed_stats = api_changed.incremental.clone().unwrap();
    assert_builds_equivalent(&final_full, &api_changed);
    assert_eq!(api_changed_stats.scanned_files, 400);
    assert_eq!(api_changed_stats.state_entries, 400);
    assert_eq!(api_changed_stats.direct_invalidated_files, 1);
    assert_eq!(api_changed_stats.surface_changed_files, 1);
    assert_eq!(api_changed_stats.changed_files, 1);
    assert_eq!(api_changed_stats.rebuilt_files, 2);
    assert_eq!(api_changed_stats.reused_files, 398);
    assert_eq!(api_changed_stats.invalidated_files, 2);
}
