//! Symbol reference graph for enriched PageRank (#144).
//!
//! Builds file-level edges from symbol definitions and references:
//! if file A references identifier "foo" and file B defines symbol "foo",
//! we add an edge A → B. This complements import-based edges.

use std::collections::HashMap;

use crate::pagerank::ImportGraph;

/// Index mapping symbol names to the files that define them.
pub struct SymbolIndex {
    /// symbol_name → list of defining file paths
    defs: HashMap<String, Vec<String>>,
}

impl SymbolIndex {
    /// Build a symbol index from per-file definitions.
    ///
    /// Input: `(file_path, vec_of_symbol_names)`.
    pub fn new(file_defs: &[(String, Vec<String>)]) -> Self {
        let mut defs: HashMap<String, Vec<String>> = HashMap::new();
        for (path, symbols) in file_defs {
            for sym in symbols {
                defs.entry(sym.clone()).or_default().push(path.clone());
            }
        }
        Self { defs }
    }

    /// Look up which files define a given symbol name.
    pub fn defining_files(&self, symbol: &str) -> &[String] {
        self.defs.get(symbol).map(|v| v.as_slice()).unwrap_or(&[])
    }

    /// Total number of unique symbols indexed.
    pub fn symbol_count(&self) -> usize {
        self.defs.len()
    }
}

/// Build a combined import + symbol reference graph.
///
/// Starts with the import-based graph, then adds edges from symbol references:
/// for each file's referenced identifiers, if that identifier is defined in
/// another file, add an edge from the referencing file to the defining file.
///
/// This produces a richer file-level graph that captures function calls,
/// type usage, and other cross-file symbol references beyond just imports.
pub fn build_symbol_reference_graph(
    import_graph: &mut ImportGraph,
    symbol_index: &SymbolIndex,
    file_refs: &[(String, Vec<String>)],
) {
    for (referencing_file, ref_names) in file_refs {
        for name in ref_names {
            let defining_files = symbol_index.defining_files(name);
            for def_file in defining_files {
                // Skip self-references
                if def_file != referencing_file {
                    import_graph.add_edge(referencing_file, def_file);
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn symbol_index_basic() {
        let file_defs = vec![
            ("src/auth.rs".to_string(), vec!["authenticate".to_string(), "Token".to_string()]),
            ("src/db.rs".to_string(), vec!["connect".to_string(), "Query".to_string()]),
        ];
        let index = SymbolIndex::new(&file_defs);

        assert_eq!(index.symbol_count(), 4);
        assert_eq!(index.defining_files("authenticate"), &["src/auth.rs"]);
        assert_eq!(index.defining_files("connect"), &["src/db.rs"]);
        assert!(index.defining_files("nonexistent").is_empty());
    }

    #[test]
    fn symbol_index_multiple_definitions() {
        // Same symbol defined in multiple files (e.g., "new" method)
        let file_defs = vec![
            ("src/auth.rs".to_string(), vec!["new".to_string()]),
            ("src/db.rs".to_string(), vec!["new".to_string()]),
        ];
        let index = SymbolIndex::new(&file_defs);
        assert_eq!(index.defining_files("new").len(), 2);
    }

    #[test]
    fn build_symbol_graph_adds_edges() {
        let mut graph = ImportGraph::new();
        graph.add_node("src/main.rs");
        graph.add_node("src/auth.rs");
        graph.add_node("src/db.rs");

        let file_defs = vec![
            ("src/auth.rs".to_string(), vec!["authenticate".to_string()]),
            ("src/db.rs".to_string(), vec!["connect".to_string()]),
        ];
        let index = SymbolIndex::new(&file_defs);

        // main.rs references both authenticate and connect
        let file_refs = vec![
            ("src/main.rs".to_string(), vec!["authenticate".to_string(), "connect".to_string()]),
        ];

        let initial_edges = graph.edge_count();
        build_symbol_reference_graph(&mut graph, &index, &file_refs);

        // Should have added 2 edges: main→auth, main→db
        assert_eq!(graph.edge_count(), initial_edges + 2);

        // auth and db should have higher PageRank than main
        let scores = graph.normalized_pagerank();
        assert!(scores["src/auth.rs"] > scores["src/main.rs"]);
        assert!(scores["src/db.rs"] > scores["src/main.rs"]);
    }

    #[test]
    fn no_self_reference_edges() {
        let mut graph = ImportGraph::new();
        graph.add_node("src/auth.rs");

        let file_defs = vec![
            ("src/auth.rs".to_string(), vec!["authenticate".to_string()]),
        ];
        let index = SymbolIndex::new(&file_defs);

        // auth.rs references its own symbol — should NOT create self-edge
        let file_refs = vec![
            ("src/auth.rs".to_string(), vec!["authenticate".to_string()]),
        ];

        build_symbol_reference_graph(&mut graph, &index, &file_refs);
        assert_eq!(graph.edge_count(), 0);
    }

    #[test]
    fn symbol_refs_complement_imports() {
        // Start with an import-based edge: main→auth
        let mut graph = ImportGraph::new();
        graph.add_edge("src/main.rs", "src/auth.rs");
        graph.add_node("src/db.rs");

        let file_defs = vec![
            ("src/db.rs".to_string(), vec!["connect".to_string()]),
        ];
        let index = SymbolIndex::new(&file_defs);

        // main.rs also references connect from db.rs (no import edge yet)
        let file_refs = vec![
            ("src/main.rs".to_string(), vec!["connect".to_string()]),
        ];

        build_symbol_reference_graph(&mut graph, &index, &file_refs);

        // Now main→auth (import) + main→db (symbol ref)
        assert_eq!(graph.edge_count(), 2);
    }
}
