use std::path::Path;
use std::collections::HashMap;
use std::sync::Mutex;
use anyhow::{Context, Result};

use crate::knowledge::types::{Concept, KnowledgeEntry};
use crate::types::FileEntity;

/// Kuzu knowledge graph for dependency analysis.
/// Uses the embedded kuzu graph database engine.
///
/// This is a self-referential struct: Connection borrows Database.
/// Fields are ordered so that `conn` is dropped before `_db`.
pub struct KuzuGraph {
    // IMPORTANT: conn must be declared BEFORE _db so it is dropped first.
    // Rust drops struct fields in declaration order.
    conn: Mutex<kuzu::Connection<'static>>,
    _db: Box<kuzu::Database>,
}

// Safety: Database is Send+Sync. Connection is protected by Mutex.
unsafe impl Send for KuzuGraph {}
unsafe impl Sync for KuzuGraph {}

impl KuzuGraph {
    pub fn new(path: &Path) -> Result<Self> {
        // Create parent directory only -- kuzu manages the database path itself.
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let db = match kuzu::Database::new(path, kuzu::SystemConfig::default()) {
            Ok(db) => db,
            Err(_) => {
                // Path may contain stale or incompatible database files.
                // Clean up database path and any related files (WAL, lock).
                if path.is_dir() {
                    let _ = std::fs::remove_dir_all(path);
                } else if path.exists() {
                    let _ = std::fs::remove_file(path);
                }
                // Also clean up sibling files like graph.wal, graph.lock
                if let Some(parent) = path.parent() {
                    if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                        for suffix in &[".wal", ".lock"] {
                            let sibling = parent.join(format!("{}{}", name, suffix));
                            if sibling.exists() {
                                let _ = std::fs::remove_file(&sibling);
                            }
                        }
                    }
                }
                kuzu::Database::new(path, kuzu::SystemConfig::default())
                    .context("Failed to create Kuzu database")?
            }
        };

        let db = Box::new(db);
        // Safety: db lives in a Box on the heap and won't move. conn is dropped
        // before _db (declaration order), so the reference remains valid.
        let db_ref: &'static kuzu::Database = unsafe { std::mem::transmute(&*db) };
        let conn = kuzu::Connection::new(db_ref)
            .context("Failed to create Kuzu connection")?;

        let graph = Self {
            conn: Mutex::new(conn),
            _db: db,
        };

        // Create schema
        graph.init_schema()?;

        Ok(graph)
    }

    /// Acquire the connection lock, converting poison errors to anyhow::Error.
    fn lock_conn(&self) -> Result<std::sync::MutexGuard<'_, kuzu::Connection<'static>>> {
        self.conn
            .lock()
            .map_err(|e| anyhow::anyhow!("Kuzu connection lock poisoned: {}", e))
    }

    fn init_schema(&self) -> Result<()> {
        let conn = self.lock_conn()?;

        // Create node table for files
        let _ = conn.query(
            "CREATE NODE TABLE IF NOT EXISTS File(\
                path STRING, \
                language STRING, \
                functions STRING, \
                types STRING, \
                imports STRING, \
                exports STRING, \
                PRIMARY KEY(path))"
        );

        // Create relationship tables
        let _ = conn.query(
            "CREATE REL TABLE IF NOT EXISTS IMPORTS(\
                FROM File TO File)"
        );

        let _ = conn.query(
            "CREATE REL TABLE IF NOT EXISTS DEPENDS_ON(\
                FROM File TO File)"
        );

        // Knowledge base node tables
        let _ = conn.query(
            "CREATE NODE TABLE IF NOT EXISTS Knowledge(\
                id STRING, \
                source STRING, \
                collection STRING, \
                title STRING, \
                content STRING, \
                metadata STRING, \
                relevance_score DOUBLE, \
                retrieved_at INT64, \
                PRIMARY KEY(id))"
        );

        let _ = conn.query(
            "CREATE NODE TABLE IF NOT EXISTS Concept(\
                id STRING, \
                name STRING, \
                description STRING, \
                category STRING, \
                source_entries STRING, \
                PRIMARY KEY(id))"
        );

        // Knowledge base relationship tables
        let _ = conn.query(
            "CREATE REL TABLE IF NOT EXISTS DEFINES_REQUIREMENT(\
                FROM Knowledge TO Concept)"
        );

        let _ = conn.query(
            "CREATE REL TABLE IF NOT EXISTS RELATES_TO(\
                FROM Concept TO File)"
        );

        let _ = conn.query(
            "CREATE REL TABLE IF NOT EXISTS INFORMS(\
                FROM Knowledge TO File)"
        );

        Ok(())
    }

    /// Add or update a file entity in the graph.
    pub async fn add_file_to_graph(&self, file: &FileEntity) -> Result<()> {
        let functions_json = serde_json::to_string(&file.functions).unwrap_or_default();
        let types_json = serde_json::to_string(&file.types).unwrap_or_default();
        let imports_json = serde_json::to_string(&file.imports).unwrap_or_default();
        let exports_json = serde_json::to_string(&file.exports).unwrap_or_default();

        let conn = self.lock_conn()?;

        // Use MERGE to upsert the node
        let query = format!(
            "MERGE (f:File {{path: '{}'}}) \
             SET f.language = '{}', \
                 f.functions = '{}', \
                 f.types = '{}', \
                 f.imports = '{}', \
                 f.exports = '{}'",
            escape_cypher(&file.path),
            escape_cypher(&file.language),
            escape_cypher(&functions_json),
            escape_cypher(&types_json),
            escape_cypher(&imports_json),
            escape_cypher(&exports_json),
        );
        let _ = conn.query(&query);

        // Add import edges
        for import in &file.imports {
            // Ensure the target node exists
            let merge_target = format!(
                "MERGE (f:File {{path: '{}'}})",
                escape_cypher(&import.source)
            );
            let _ = conn.query(&merge_target);

            // Create the import relationship
            let rel_query = format!(
                "MATCH (a:File {{path: '{}'}}), (b:File {{path: '{}'}}) \
                 MERGE (a)-[:IMPORTS]->(b)",
                escape_cypher(&file.path),
                escape_cypher(&import.source),
            );
            let _ = conn.query(&rel_query);
        }

        Ok(())
    }

    /// Execute a Cypher query against the graph.
    /// Also supports legacy keyword queries ("DEFINES", "CONTAINS", "DEPENDS_ON")
    /// that the old in-memory implementation dispatched.
    pub async fn execute_query(&self, query: &str) -> Result<Vec<HashMap<String, String>>> {
        // Handle legacy keyword-based queries by scanning File nodes.
        if query == "DEFINES" || query == "CONTAINS" || query == "DEPENDS_ON" || query == "Imports" {
            return self.legacy_keyword_query(query);
        }

        let conn = self.lock_conn()?;
        let mut result = match conn.query(query) {
            Ok(r) => r,
            Err(_) => return Ok(Vec::new()),
        };

        let column_names = result.get_column_names();
        let mut rows = Vec::new();

        for row_values in &mut result {
            let mut row = HashMap::new();
            for (i, val) in row_values.iter().enumerate() {
                let col_name = column_names.get(i)
                    .cloned()
                    .unwrap_or_else(|| format!("col_{}", i));
                row.insert(col_name, kuzu_value_to_string(val));
            }
            rows.push(row);
        }

        Ok(rows)
    }

    /// Handle legacy keyword queries by scanning all File nodes and parsing
    /// their JSON-serialized fields.
    fn legacy_keyword_query(&self, keyword: &str) -> Result<Vec<HashMap<String, String>>> {
        let conn = self.lock_conn()?;
        let mut result = match conn.query(
            "MATCH (f:File) RETURN f.path, f.functions, f.types, f.imports"
        ) {
            Ok(r) => r,
            Err(_) => return Ok(Vec::new()),
        };

        let mut rows = Vec::new();
        for row_values in &mut result {
            let path = kuzu_value_to_string(&row_values[0]);
            let functions_json = kuzu_value_to_string(&row_values[1]);
            let types_json = kuzu_value_to_string(&row_values[2]);
            let imports_json = kuzu_value_to_string(&row_values[3]);

            match keyword {
                "DEFINES" => {
                    if let Ok(types) = serde_json::from_str::<Vec<crate::types::TypeDef>>(&types_json) {
                        for t in types {
                            let mut row = HashMap::new();
                            row.insert("file_path".to_string(), path.clone());
                            row.insert("type_name".to_string(), t.name);
                            row.insert("kind".to_string(), t.kind);
                            rows.push(row);
                        }
                    }
                }
                "CONTAINS" => {
                    if let Ok(funcs) = serde_json::from_str::<Vec<crate::types::FunctionDef>>(&functions_json) {
                        for f in funcs {
                            let mut row = HashMap::new();
                            row.insert("file_path".to_string(), path.clone());
                            row.insert("function_name".to_string(), f.name);
                            row.insert("signature".to_string(), f.signature);
                            rows.push(row);
                        }
                    }
                }
                "DEPENDS_ON" | "Imports" => {
                    if let Ok(imports) = serde_json::from_str::<Vec<crate::types::ImportDef>>(&imports_json) {
                        for imp in imports {
                            let mut row = HashMap::new();
                            row.insert("from_path".to_string(), path.clone());
                            row.insert("to_path".to_string(), imp.source);
                            rows.push(row);
                        }
                    }
                }
                _ => {}
            }
        }
        Ok(rows)
    }

    /// Find files that depend on a given file (i.e., files that import it).
    pub async fn get_dependents(&self, file_path: &str) -> Result<Vec<String>> {
        let query = format!(
            "MATCH (a:File)-[:IMPORTS]->(b:File {{path: '{}'}}) RETURN a.path",
            escape_cypher(file_path)
        );
        let results = self.execute_query(&query).await?;
        Ok(results.into_iter()
            .filter_map(|row| row.get("a.path").cloned())
            .collect())
    }

    /// Find files that a given file depends on (i.e., files it imports).
    pub async fn get_dependencies(&self, file_path: &str) -> Result<Vec<String>> {
        let query = format!(
            "MATCH (a:File {{path: '{}'}})-[:IMPORTS]->(b:File) RETURN b.path",
            escape_cypher(file_path)
        );
        let results = self.execute_query(&query).await?;
        Ok(results.into_iter()
            .filter_map(|row| row.get("b.path").cloned())
            .collect())
    }

    /// Get a file entity by path.
    pub async fn get_file(&self, file_path: &str) -> Result<Option<FileEntity>> {
        let conn = self.lock_conn()?;
        let query = format!(
            "MATCH (f:File {{path: '{}'}}) RETURN f.path, f.language, f.functions, f.types, f.imports, f.exports",
            escape_cypher(file_path)
        );

        let mut result = match conn.query(&query) {
            Ok(r) => r,
            Err(_) => return Ok(None),
        };

        if let Some(row) = result.next() {
            let path = kuzu_value_to_string(&row[0]);
            let language = kuzu_value_to_string(&row[1]);
            let functions_json = kuzu_value_to_string(&row[2]);
            let types_json = kuzu_value_to_string(&row[3]);
            let imports_json = kuzu_value_to_string(&row[4]);
            let exports_json = kuzu_value_to_string(&row[5]);

            let functions = serde_json::from_str(&functions_json).unwrap_or_default();
            let types = serde_json::from_str(&types_json).unwrap_or_default();
            let imports = serde_json::from_str(&imports_json).unwrap_or_default();
            let exports = serde_json::from_str(&exports_json).unwrap_or_default();

            Ok(Some(FileEntity {
                path,
                language,
                functions,
                types,
                imports,
                exports,
            }))
        } else {
            Ok(None)
        }
    }

    // -----------------------------------------------------------------------
    // Knowledge Base graph methods
    // -----------------------------------------------------------------------

    /// Add or update a Knowledge node in the graph.
    pub async fn add_knowledge_node(&self, entry: &KnowledgeEntry) -> Result<()> {
        let metadata_json = serde_json::to_string(&entry.metadata).unwrap_or_default();
        let conn = self.lock_conn()?;

        let query = format!(
            "MERGE (k:Knowledge {{id: '{}'}}) \
             SET k.source = '{}', \
                 k.collection = '{}', \
                 k.title = '{}', \
                 k.content = '{}', \
                 k.metadata = '{}', \
                 k.relevance_score = {}, \
                 k.retrieved_at = {}",
            escape_cypher(&entry.id),
            escape_cypher(&entry.source),
            escape_cypher(&entry.collection),
            escape_cypher(&entry.title),
            escape_cypher(&entry.content),
            escape_cypher(&metadata_json),
            entry.relevance_score,
            entry.retrieved_at,
        );
        let _ = conn.query(&query);
        Ok(())
    }

    /// Add or update a Concept node in the graph.
    pub async fn add_concept_node(&self, concept: &Concept) -> Result<()> {
        let source_entries_json = serde_json::to_string(&concept.source_entries).unwrap_or_default();
        let conn = self.lock_conn()?;

        let query = format!(
            "MERGE (c:Concept {{id: '{}'}}) \
             SET c.name = '{}', \
                 c.description = '{}', \
                 c.category = '{}', \
                 c.source_entries = '{}'",
            escape_cypher(&concept.id),
            escape_cypher(&concept.name),
            escape_cypher(&concept.description),
            escape_cypher(&concept.category.to_string()),
            escape_cypher(&source_entries_json),
        );
        let _ = conn.query(&query);
        Ok(())
    }

    /// Create a DEFINES_REQUIREMENT edge from Knowledge to Concept.
    pub async fn add_knowledge_concept_edge(
        &self,
        knowledge_id: &str,
        concept_id: &str,
    ) -> Result<()> {
        let conn = self.lock_conn()?;
        let query = format!(
            "MATCH (k:Knowledge {{id: '{}'}}), (c:Concept {{id: '{}'}}) \
             MERGE (k)-[:DEFINES_REQUIREMENT]->(c)",
            escape_cypher(knowledge_id),
            escape_cypher(concept_id),
        );
        let _ = conn.query(&query);
        Ok(())
    }

    /// Create a RELATES_TO edge from Concept to File.
    pub async fn add_concept_file_edge(
        &self,
        concept_id: &str,
        file_path: &str,
    ) -> Result<()> {
        let conn = self.lock_conn()?;
        let query = format!(
            "MATCH (c:Concept {{id: '{}'}}), (f:File {{path: '{}'}}) \
             MERGE (c)-[:RELATES_TO]->(f)",
            escape_cypher(concept_id),
            escape_cypher(file_path),
        );
        let _ = conn.query(&query);
        Ok(())
    }

    /// Create an INFORMS edge directly from Knowledge to File.
    pub async fn add_knowledge_file_edge(
        &self,
        knowledge_id: &str,
        file_path: &str,
    ) -> Result<()> {
        let conn = self.lock_conn()?;
        let query = format!(
            "MATCH (k:Knowledge {{id: '{}'}}), (f:File {{path: '{}'}}) \
             MERGE (k)-[:INFORMS]->(f)",
            escape_cypher(knowledge_id),
            escape_cypher(file_path),
        );
        let _ = conn.query(&query);
        Ok(())
    }

    /// Find File nodes whose path, functions, or types contain the concept name.
    pub async fn find_files_matching_concept(&self, concept_name: &str) -> Result<Vec<String>> {
        let conn = self.lock_conn()?;
        let escaped = escape_cypher(concept_name);

        let query = format!(
            "MATCH (f:File) WHERE f.path CONTAINS '{}' \
             OR f.functions CONTAINS '{}' \
             OR f.types CONTAINS '{}' \
             RETURN f.path",
            escaped, escaped, escaped,
        );

        let mut result = match conn.query(&query) {
            Ok(r) => r,
            Err(_) => return Ok(Vec::new()),
        };

        let mut paths = Vec::new();
        for row in &mut result {
            paths.push(kuzu_value_to_string(&row[0]));
        }
        Ok(paths)
    }

    /// Get Concept nodes related to a specific file via RELATES_TO edges.
    pub async fn get_concepts_for_file(&self, file_path: &str) -> Result<Vec<HashMap<String, String>>> {
        let query = format!(
            "MATCH (c:Concept)-[:RELATES_TO]->(f:File {{path: '{}'}}) \
             RETURN c.id, c.name, c.description, c.category",
            escape_cypher(file_path)
        );
        self.execute_query(&query).await
    }

    /// Get Knowledge nodes, optionally filtered by source.
    pub async fn get_knowledge_entries(
        &self,
        source_filter: Option<&str>,
    ) -> Result<Vec<HashMap<String, String>>> {
        let query = if let Some(source) = source_filter {
            format!(
                "MATCH (k:Knowledge) WHERE k.source = '{}' \
                 RETURN k.id, k.source, k.collection, k.title",
                escape_cypher(source)
            )
        } else {
            "MATCH (k:Knowledge) RETURN k.id, k.source, k.collection, k.title".to_string()
        };
        self.execute_query(&query).await
    }

    /// Count the total number of Concept nodes.
    pub async fn get_concept_count(&self) -> Result<usize> {
        let conn = self.lock_conn()?;
        let mut result = match conn.query("MATCH (c:Concept) RETURN count(c)") {
            Ok(r) => r,
            Err(_) => return Ok(0),
        };

        if let Some(row) = result.next() {
            let count_str = kuzu_value_to_string(&row[0]);
            Ok(count_str.parse::<usize>().unwrap_or(0))
        } else {
            Ok(0)
        }
    }

    /// Remove a file and its edges from the graph.
    pub async fn remove_file(&self, file_path: &str) -> Result<()> {
        let conn = self.lock_conn()?;

        // Delete all relationships first, then the node
        let del_rels = format!(
            "MATCH (f:File {{path: '{}'}})-[r]-() DELETE r",
            escape_cypher(file_path)
        );
        let _ = conn.query(&del_rels);

        let del_node = format!(
            "MATCH (f:File {{path: '{}'}}) DELETE f",
            escape_cypher(file_path)
        );
        let _ = conn.query(&del_node);

        Ok(())
    }
}

/// Escape special characters for Cypher string literals.
///
/// Handles single quotes, backslashes, newlines, carriage returns, tabs,
/// and null bytes to prevent Cypher injection in string values.
fn escape_cypher(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('\'', "\\'")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
        .replace('\0', "")
}

/// Convert a kuzu Value to a display string.
fn kuzu_value_to_string(val: &kuzu::Value) -> String {
    match val {
        kuzu::Value::String(s) => s.clone(),
        kuzu::Value::Int64(n) => n.to_string(),
        kuzu::Value::Int32(n) => n.to_string(),
        kuzu::Value::Double(f) => f.to_string(),
        kuzu::Value::Float(f) => f.to_string(),
        kuzu::Value::Bool(b) => b.to_string(),
        kuzu::Value::Null(_) => String::new(),
        other => format!("{}", other),
    }
}

// ===========================================================================
// Tests
// ===========================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::knowledge::types::{Concept, ConceptCategory, KnowledgeEntry};
    use tempfile::TempDir;

    fn make_kuzu() -> (KuzuGraph, TempDir) {
        let dir = TempDir::new().expect("temp dir");
        let kuzu = KuzuGraph::new(dir.path()).expect("kuzu");
        (kuzu, dir)
    }

    #[tokio::test]
    async fn test_add_knowledge_node() {
        let (kuzu, _dir) = make_kuzu();
        let entry = KnowledgeEntry {
            id: "kb-test-1".into(),
            source: "ChromaDB".into(),
            collection: "design-docs".into(),
            title: "Auth Design".into(),
            content: "Use JWT tokens for auth".into(),
            metadata: HashMap::new(),
            relevance_score: 0.85,
            retrieved_at: 1707300600,
        };

        kuzu.add_knowledge_node(&entry).await.unwrap();

        let entries = kuzu.get_knowledge_entries(None).await.unwrap();
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].get("k.id").unwrap(), "kb-test-1");
        assert_eq!(entries[0].get("k.source").unwrap(), "ChromaDB");
    }

    #[tokio::test]
    async fn test_add_concept_node() {
        let (kuzu, _dir) = make_kuzu();
        let concept = Concept {
            id: "concept-1".into(),
            name: "JWT Authentication".into(),
            description: "Must use JWT for auth".into(),
            category: ConceptCategory::Requirement,
            source_entries: vec!["kb-1".into()],
        };

        kuzu.add_concept_node(&concept).await.unwrap();

        let count = kuzu.get_concept_count().await.unwrap();
        assert_eq!(count, 1);
    }

    #[tokio::test]
    async fn test_knowledge_concept_edge() {
        let (kuzu, _dir) = make_kuzu();

        // Create Knowledge node
        let entry = KnowledgeEntry {
            id: "kb-edge-1".into(),
            source: "test".into(),
            collection: "col".into(),
            title: "Title".into(),
            content: "Content".into(),
            metadata: HashMap::new(),
            relevance_score: 0.9,
            retrieved_at: 0,
        };
        kuzu.add_knowledge_node(&entry).await.unwrap();

        // Create Concept node
        let concept = Concept {
            id: "concept-edge-1".into(),
            name: "Test Concept".into(),
            description: "A test concept".into(),
            category: ConceptCategory::Requirement,
            source_entries: vec!["kb-edge-1".into()],
        };
        kuzu.add_concept_node(&concept).await.unwrap();

        // Create edge
        kuzu.add_knowledge_concept_edge("kb-edge-1", "concept-edge-1")
            .await
            .unwrap();

        // Verify via query
        let results = kuzu
            .execute_query(
                "MATCH (k:Knowledge)-[:DEFINES_REQUIREMENT]->(c:Concept) RETURN k.id, c.id",
            )
            .await
            .unwrap();
        assert_eq!(results.len(), 1);
    }

    #[tokio::test]
    async fn test_concept_file_edge() {
        let (kuzu, _dir) = make_kuzu();

        // Create File node
        let file = FileEntity {
            path: "src/auth.rs".into(),
            language: "rust".into(),
            functions: vec![],
            types: vec![],
            imports: vec![],
            exports: vec![],
        };
        kuzu.add_file_to_graph(&file).await.unwrap();

        // Create Concept node
        let concept = Concept {
            id: "concept-file-1".into(),
            name: "auth".into(),
            description: "Authentication concept".into(),
            category: ConceptCategory::Domain,
            source_entries: vec![],
        };
        kuzu.add_concept_node(&concept).await.unwrap();

        // Create edge
        kuzu.add_concept_file_edge("concept-file-1", "src/auth.rs")
            .await
            .unwrap();

        // Verify
        let concepts = kuzu.get_concepts_for_file("src/auth.rs").await.unwrap();
        assert_eq!(concepts.len(), 1);
        assert_eq!(concepts[0].get("c.name").unwrap(), "auth");
    }

    #[tokio::test]
    async fn test_find_files_matching_concept() {
        let (kuzu, _dir) = make_kuzu();

        let file = FileEntity {
            path: "src/authentication/jwt.rs".into(),
            language: "rust".into(),
            functions: vec![],
            types: vec![],
            imports: vec![],
            exports: vec![],
        };
        kuzu.add_file_to_graph(&file).await.unwrap();

        let matches = kuzu.find_files_matching_concept("authentication").await.unwrap();
        assert_eq!(matches.len(), 1);
        assert_eq!(matches[0], "src/authentication/jwt.rs");
    }

    #[tokio::test]
    async fn test_get_knowledge_entries_with_filter() {
        let (kuzu, _dir) = make_kuzu();

        let e1 = KnowledgeEntry {
            id: "k1".into(),
            source: "ChromaDB".into(),
            collection: "col".into(),
            title: "T1".into(),
            content: "C1".into(),
            metadata: HashMap::new(),
            relevance_score: 0.5,
            retrieved_at: 0,
        };
        let e2 = KnowledgeEntry {
            id: "k2".into(),
            source: "Pinecone".into(),
            collection: "col".into(),
            title: "T2".into(),
            content: "C2".into(),
            metadata: HashMap::new(),
            relevance_score: 0.6,
            retrieved_at: 0,
        };

        kuzu.add_knowledge_node(&e1).await.unwrap();
        kuzu.add_knowledge_node(&e2).await.unwrap();

        // All entries
        let all = kuzu.get_knowledge_entries(None).await.unwrap();
        assert_eq!(all.len(), 2);

        // Filtered
        let filtered = kuzu.get_knowledge_entries(Some("ChromaDB")).await.unwrap();
        assert_eq!(filtered.len(), 1);
        assert_eq!(filtered[0].get("k.source").unwrap(), "ChromaDB");
    }

    #[tokio::test]
    async fn test_get_concept_count_empty() {
        let (kuzu, _dir) = make_kuzu();
        let count = kuzu.get_concept_count().await.unwrap();
        assert_eq!(count, 0);
    }

    #[tokio::test]
    async fn test_knowledge_file_edge() {
        let (kuzu, _dir) = make_kuzu();

        // Create Knowledge node
        let entry = KnowledgeEntry {
            id: "kb-informs-1".into(),
            source: "test".into(),
            collection: "col".into(),
            title: "Auth Design".into(),
            content: "JWT auth patterns".into(),
            metadata: HashMap::new(),
            relevance_score: 0.9,
            retrieved_at: 0,
        };
        kuzu.add_knowledge_node(&entry).await.unwrap();

        // Create File node
        let file = FileEntity {
            path: "src/auth.rs".into(),
            language: "rust".into(),
            functions: vec![],
            types: vec![],
            imports: vec![],
            exports: vec![],
        };
        kuzu.add_file_to_graph(&file).await.unwrap();

        // Create INFORMS edge
        kuzu.add_knowledge_file_edge("kb-informs-1", "src/auth.rs")
            .await
            .unwrap();

        // Verify via query
        let results = kuzu
            .execute_query(
                "MATCH (k:Knowledge)-[:INFORMS]->(f:File) RETURN k.id, f.path",
            )
            .await
            .unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].get("k.id").unwrap(), "kb-informs-1");
        assert_eq!(results[0].get("f.path").unwrap(), "src/auth.rs");
    }
}
