pub mod repository;
pub mod schema;

use std::path::Path;

use astmap_core::DbError;

pub use repository::SqliteRepository;

/// Convenience: open the default (SQLite) repository.
pub fn open(project_dir: &Path) -> Result<Box<SqliteRepository>, DbError> {
    Ok(Box::new(SqliteRepository::open(project_dir)?))
}

/// Open an existing repository (errors if DB doesn't exist).
pub fn open_existing(project_dir: &Path) -> Result<Box<SqliteRepository>, DbError> {
    Ok(Box::new(SqliteRepository::open_existing(project_dir)?))
}

#[cfg(test)]
mod tests {
    use super::*;
    use astmap_core::model::{DepLevel, DepType, ParseStatus, SymbolKind};
    use astmap_core::{ConfigStore, DiffStore, ExportStore, QueryStore, ScanStore, SymbolLookup};

    /// Create an in-memory DB populated with a realistic dataset.
    /// Layout:
    ///   file1 = "src/lib.rs"  → Scanner (struct, line 1-10), scan (method, line 5-10)
    ///   file2 = "src/db/mod.rs" → Database (struct, line 1-20), open (method, line 5-15, child of Database)
    ///   Dependencies: scan → open (calls), file1 → file2 (imports "crate::db")
    fn setup_populated_db() -> (SqliteRepository, i64, i64, i64, i64, i64) {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();

        let file1 = repo
            .upsert_file(
                scan_id,
                "src/lib.rs",
                "rust",
                1000,
                "h1",
                12345,
                ParseStatus::Ok,
            )
            .unwrap();
        let file2 = repo
            .upsert_file(
                scan_id,
                "src/db/mod.rs",
                "rust",
                500,
                "h2",
                12345,
                ParseStatus::Ok,
            )
            .unwrap();

        let scanner_id = repo
            .insert_symbol(
                file1,
                None,
                "Scanner",
                SymbolKind::Struct,
                Some("pub struct Scanner"),
                1,
                10,
                0,
                100,
            )
            .unwrap();
        let scan_method = repo
            .insert_symbol(
                file1,
                Some(scanner_id),
                "scan",
                SymbolKind::Method,
                Some("pub fn scan(&self)"),
                5,
                10,
                50,
                100,
            )
            .unwrap();

        let db_struct = repo
            .insert_symbol(
                file2,
                None,
                "Database",
                SymbolKind::Struct,
                Some("pub struct Database"),
                1,
                20,
                0,
                200,
            )
            .unwrap();
        let open_method = repo
            .insert_symbol(
                file2,
                Some(db_struct),
                "open",
                SymbolKind::Method,
                Some("pub fn open()"),
                5,
                15,
                50,
                150,
            )
            .unwrap();

        // scan calls open
        repo.insert_dependency(
            file1,
            Some(scan_method),
            Some(file2),
            Some(open_method),
            DepType::Calls,
            DepLevel::Symbol,
            None,
        )
        .unwrap();
        // file1 imports file2
        repo.insert_dependency(
            file1,
            None,
            Some(file2),
            None,
            DepType::Imports,
            DepLevel::File,
            Some("crate::db"),
        )
        .unwrap();

        repo.complete_scan(scan_id, 2, 4, 2).unwrap();

        // Return (repo, file1, file2, scanner_id, scan_method, open_method)
        (repo, file1, file2, scanner_id, scan_method, open_method)
    }

    #[test]
    fn test_open_in_memory() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let (fc, _, _) = SymbolLookup::stats(&repo).unwrap();
        assert_eq!(fc, 0);
    }

    #[test]
    fn test_open_creates_directory() {
        let tmp = tempfile::tempdir().unwrap();
        let _repo = SqliteRepository::open(tmp.path()).unwrap();
        assert!(tmp.path().join(".astmap").join("index.db").exists());
    }

    #[test]
    fn test_open_existing_not_found() {
        let tmp = tempfile::tempdir().unwrap();
        let result = SqliteRepository::open_existing(tmp.path());
        assert!(result.is_err());
    }

    #[test]
    fn test_open_existing_success() {
        let tmp = tempfile::tempdir().unwrap();
        let _repo = SqliteRepository::open(tmp.path()).unwrap();
        let _repo2 = SqliteRepository::open_existing(tmp.path()).unwrap();
    }

    #[test]
    fn test_upsert_file_and_search() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let file_id = repo
            .upsert_file(
                scan_id,
                "src/main.rs",
                "rust",
                500,
                "hash1",
                1767225600,
                ParseStatus::Ok,
            )
            .unwrap();
        assert!(file_id > 0);
        let sym_id = repo
            .insert_symbol(
                file_id,
                None,
                "main",
                SymbolKind::Function,
                Some("fn main()"),
                1,
                5,
                0,
                50,
            )
            .unwrap();
        assert!(sym_id > 0);
        let results = repo.search_symbols("main", 10).unwrap();
        assert_eq!(results.len(), 1);
        assert_eq!(results[0].name, "main");
    }

    #[test]
    fn test_config_operations() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        assert_eq!(repo.config_get("llm.model").unwrap(), None);
        repo.config_set("llm.model", "claude-sonnet-4-5-20250514")
            .unwrap();
        assert_eq!(
            repo.config_get("llm.model").unwrap(),
            Some("claude-sonnet-4-5-20250514".to_string())
        );
    }

    #[test]
    fn test_config_list() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        repo.config_set("a.key", "val1").unwrap();
        repo.config_set("b.key", "val2").unwrap();
        let configs = repo.config_list().unwrap();
        assert_eq!(configs.len(), 2);
        assert_eq!(configs[0].key, "a.key");
        assert_eq!(configs[1].key, "b.key");
    }

    #[test]
    fn test_incremental_hash_check() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        assert_eq!(repo.get_file_hash("src/lib.rs").unwrap(), None);
        repo.upsert_file(
            scan_id,
            "src/lib.rs",
            "rust",
            200,
            "hash_v1",
            1767225600,
            ParseStatus::Ok,
        )
        .unwrap();
        assert_eq!(
            repo.get_file_hash("src/lib.rs").unwrap(),
            Some("hash_v1".to_string())
        );
    }

    #[test]
    fn test_impact_empty() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        assert!(SymbolLookup::impact_analysis(&repo, 1, 3)
            .unwrap()
            .is_empty());
    }

    #[test]
    fn test_find_path_empty() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        assert!(repo.find_path(1, 2, 5).unwrap().is_empty());
    }

    #[test]
    fn test_complete_scan() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let (fc, sc, dc) = SymbolLookup::stats(&repo).unwrap();
        assert_eq!(fc, 2);
        assert_eq!(sc, 4);
        assert_eq!(dc, 2);
    }

    #[test]
    fn test_get_all_file_paths() {
        let (repo, file1, file2, _, _, _) = setup_populated_db();
        let paths = repo.get_all_file_paths().unwrap();
        assert_eq!(paths.len(), 2);
        assert_eq!(paths.get("src/lib.rs"), Some(&file1));
        assert_eq!(paths.get("src/db/mod.rs"), Some(&file2));
    }

    #[test]
    fn test_search_symbols_by_name() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let results = SymbolLookup::search_symbols_by_name(&repo, "scan", 10).unwrap();
        assert!(!results.is_empty());
        assert!(results.iter().any(|r| r.name == "scan"));
    }

    #[test]
    fn test_get_symbol_by_id() {
        let (repo, _, _, scanner_id, _, _) = setup_populated_db();
        let sym = repo.get_symbol_by_id(scanner_id).unwrap();
        assert!(sym.is_some());
        let sym = sym.unwrap();
        assert_eq!(sym.name, "Scanner");
        assert_eq!(sym.kind, SymbolKind::Struct);

        // Non-existent ID
        assert!(repo.get_symbol_by_id(99999).unwrap().is_none());
    }

    #[test]
    fn test_get_children() {
        let (repo, _, _, scanner_id, _, _) = setup_populated_db();
        let children = SymbolLookup::get_children(&repo, scanner_id).unwrap();
        assert_eq!(children.len(), 1);
        assert_eq!(children[0].name, "scan");
        assert_eq!(children[0].kind, SymbolKind::Method);
    }

    #[test]
    fn test_delete_symbols_for_file() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        repo.delete_symbols_for_file(file1).unwrap();
        let symbols = SymbolLookup::get_symbols_for_file(&repo, file1).unwrap();
        assert!(symbols.is_empty());
    }

    #[test]
    fn test_delete_deps_for_file() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        repo.delete_deps_for_file(file1).unwrap();
        let deps = repo.get_deps_for_file(file1).unwrap();
        assert!(deps.is_empty());
    }

    #[test]
    fn test_symbols_in_line_range() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let syms = repo.symbols_in_line_range("src/lib.rs", 1, 3).unwrap();
        assert!(syms.iter().any(|s| s.name == "Scanner"));

        let syms = repo.symbols_in_line_range("src/lib.rs", 6, 8).unwrap();
        assert!(syms.iter().any(|s| s.name == "scan"));

        let syms = repo.symbols_in_line_range("src/lib.rs", 100, 200).unwrap();
        assert!(syms.is_empty());
    }

    #[test]
    fn test_get_file_id() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        assert_eq!(
            SymbolLookup::get_file_id(&repo, "src/lib.rs").unwrap(),
            Some(file1)
        );
        assert_eq!(
            SymbolLookup::get_file_id(&repo, "nonexistent.rs").unwrap(),
            None
        );
    }

    #[test]
    fn test_list_files_all() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let files = repo.list_files(None).unwrap();
        assert_eq!(files.len(), 2);
    }

    #[test]
    fn test_list_files_with_pattern() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let files = repo.list_files(Some("db")).unwrap();
        assert_eq!(files.len(), 1);
        assert_eq!(files[0].path, "src/db/mod.rs");
    }

    #[test]
    fn test_get_symbols_for_file() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        let syms = SymbolLookup::get_symbols_for_file(&repo, file1).unwrap();
        assert_eq!(syms.len(), 1);
        assert_eq!(syms[0].name, "Scanner");
    }

    #[test]
    fn test_get_deps_for_file() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        let deps = repo.get_deps_for_file(file1).unwrap();
        assert_eq!(deps.len(), 2);
        let types: Vec<DepType> = deps.iter().map(|d| d.dep_type).collect();
        assert!(types.contains(&DepType::Calls));
        assert!(types.contains(&DepType::Imports));
    }

    #[test]
    fn test_impact_analysis_with_data() {
        let (repo, _, _, _, _, open_method) = setup_populated_db();
        let impact = SymbolLookup::impact_analysis(&repo, open_method, 3).unwrap();
        assert!(!impact.is_empty());
        let scan_entry = impact.iter().find(|e| e.symbol_name == "scan").unwrap();
        assert_eq!(scan_entry.dep_type, DepType::Calls);
    }

    #[test]
    fn test_find_path_with_data() {
        let (repo, _, _, _, scan_method, open_method) = setup_populated_db();
        let path = repo.find_path(scan_method, open_method, 5).unwrap();
        assert!(!path.is_empty());
        let names: Vec<&str> = path.iter().map(|s| s.symbol_name.as_str()).collect();
        assert!(names.contains(&"scan"));
        assert!(names.contains(&"open"));
    }

    #[test]
    fn test_find_path_no_connection() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let f1 = repo
            .upsert_file(scan_id, "a.rs", "rust", 10, "h1", 1, ParseStatus::Ok)
            .unwrap();
        let f2 = repo
            .upsert_file(scan_id, "b.rs", "rust", 10, "h2", 1, ParseStatus::Ok)
            .unwrap();
        let s1 = repo
            .insert_symbol(f1, None, "A", SymbolKind::Struct, None, 1, 1, 0, 10)
            .unwrap();
        let s2 = repo
            .insert_symbol(f2, None, "B", SymbolKind::Struct, None, 1, 1, 0, 10)
            .unwrap();
        let path = repo.find_path(s1, s2, 5).unwrap();
        assert!(path.is_empty());
    }

    #[test]
    fn test_upsert_file_updates_on_conflict() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let id1 = repo
            .upsert_file(
                scan_id,
                "src/lib.rs",
                "rust",
                100,
                "old_hash",
                1,
                ParseStatus::Ok,
            )
            .unwrap();
        let id2 = repo
            .upsert_file(
                scan_id,
                "src/lib.rs",
                "rust",
                200,
                "new_hash",
                2,
                ParseStatus::Ok,
            )
            .unwrap();
        assert_eq!(id1, id2);
        assert_eq!(
            repo.get_file_hash("src/lib.rs").unwrap(),
            Some("new_hash".to_string())
        );
    }

    #[test]
    fn test_stats_with_data() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let (fc, sc, dc) = SymbolLookup::stats(&repo).unwrap();
        assert_eq!(fc, 2);
        assert_eq!(sc, 4);
        assert_eq!(dc, 2);
    }

    #[test]
    fn test_open_creates_db() {
        let tmp = tempfile::tempdir().unwrap();
        let db = open(tmp.path()).unwrap();
        let (fc, _, _) = SymbolLookup::stats(db.as_ref()).unwrap();
        assert_eq!(fc, 0);
    }

    #[test]
    fn test_open_existing_missing() {
        let tmp = tempfile::tempdir().unwrap();
        let result = open_existing(tmp.path());
        assert!(result.is_err());
    }

    #[test]
    fn test_db_error_from_rusqlite() {
        let rusqlite_err =
            rusqlite::Error::SqliteFailure(rusqlite::ffi::Error::new(1), Some("test".to_string()));
        let err = DbError::Storage(rusqlite_err.to_string());
        match err {
            DbError::Storage(msg) => assert!(msg.contains("test")),
            _ => panic!("expected Storage variant"),
        }
    }

    #[test]
    fn test_delete_file_cascades_symbols() {
        let (repo, file1, _, _, _, _) = setup_populated_db();
        // file1 has Scanner and scan symbols
        assert!(!SymbolLookup::get_symbols_for_file(&repo, file1)
            .unwrap()
            .is_empty());
        repo.delete_file(file1).unwrap();
        // After deleting file, symbols should be cascade-deleted
        assert!(SymbolLookup::get_symbols_for_file(&repo, file1)
            .unwrap()
            .is_empty());
        // file should no longer be findable
        assert!(SymbolLookup::get_file_id(&repo, "src/lib.rs")
            .unwrap()
            .is_none());
    }

    #[test]
    fn test_delete_file_nonexistent_id() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        // Deleting a non-existent file should not error
        repo.delete_file(99999).unwrap();
    }

    #[test]
    fn test_all_symbols_for_file() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let all = repo.all_symbols_for_file("src/lib.rs").unwrap();
        assert_eq!(all.len(), 2, "should include Scanner and scan");
        // Scanner is the parent (no parent_name)
        let scanner = all.iter().find(|s| s.name == "Scanner").unwrap();
        assert!(scanner.parent_name.is_none());
        // scan is a child of Scanner
        let scan = all.iter().find(|s| s.name == "scan").unwrap();
        assert_eq!(scan.parent_name, Some("Scanner".to_string()));
    }

    #[test]
    fn test_all_symbols_for_file_nonexistent() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let result = repo.all_symbols_for_file("nonexistent.rs").unwrap();
        assert!(result.is_empty());
    }

    #[test]
    fn test_all_symbols_for_file_with_nested_hierarchy() {
        let (repo, _, _file2, _, _, _) = setup_populated_db();
        let all = repo.all_symbols_for_file("src/db/mod.rs").unwrap();
        assert_eq!(all.len(), 2, "should include Database and open");
        let db = all.iter().find(|s| s.name == "Database").unwrap();
        assert!(db.parent_name.is_none());
        let open = all.iter().find(|s| s.name == "open").unwrap();
        assert_eq!(open.parent_name, Some("Database".to_string()));
        // Check that file_path is correct
        assert_eq!(open.file_path, "src/db/mod.rs");
    }

    #[test]
    fn test_insert_dependency_file_level_only() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let f1 = repo
            .upsert_file(scan_id, "a.rs", "rust", 10, "h1", 1, ParseStatus::Ok)
            .unwrap();
        let f2 = repo
            .upsert_file(scan_id, "b.rs", "rust", 10, "h2", 1, ParseStatus::Ok)
            .unwrap();
        // File-level dependency with no symbol references
        let dep_id = repo
            .insert_dependency(
                f1,
                None,
                Some(f2),
                None,
                DepType::Imports,
                DepLevel::File,
                Some("crate::b"),
            )
            .unwrap();
        assert!(dep_id > 0);
        let deps = repo.get_deps_for_file(f1).unwrap();
        assert_eq!(deps.len(), 1);
        assert_eq!(deps[0].dep_type, DepType::Imports);
        assert!(deps[0].source_symbol_id.is_none());
        assert!(deps[0].target_symbol_id.is_none());
        assert_eq!(deps[0].raw_import, Some("crate::b".to_string()));
    }

    #[test]
    fn test_config_set_overwrites_existing() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        repo.config_set("key", "value1").unwrap();
        assert_eq!(repo.config_get("key").unwrap(), Some("value1".to_string()));
        repo.config_set("key", "value2").unwrap();
        assert_eq!(repo.config_get("key").unwrap(), Some("value2".to_string()));
        // Should still be only one entry
        let configs = repo.config_list().unwrap();
        let matching: Vec<_> = configs.iter().filter(|c| c.key == "key").collect();
        assert_eq!(matching.len(), 1);
    }

    #[test]
    fn test_search_symbols_empty_results() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let f = repo
            .upsert_file(scan_id, "a.rs", "rust", 10, "h", 1, ParseStatus::Ok)
            .unwrap();
        repo.insert_symbol(f, None, "Foo", SymbolKind::Struct, None, 1, 1, 0, 10)
            .unwrap();
        // Search for something that doesn't match
        let results = SymbolLookup::search_symbols_by_name(&repo, "zzzzz", 10).unwrap();
        assert!(results.is_empty());
    }

    #[test]
    fn test_insert_symbol_with_null_signature() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let f = repo
            .upsert_file(scan_id, "a.rs", "rust", 10, "h", 1, ParseStatus::Ok)
            .unwrap();
        let sym_id = repo
            .insert_symbol(f, None, "Foo", SymbolKind::Struct, None, 1, 5, 0, 50)
            .unwrap();
        let sym = repo.get_symbol_by_id(sym_id).unwrap().unwrap();
        assert!(sym.signature.is_none());
        assert!(sym.summary.is_none());
    }

    #[test]
    fn test_list_files_no_pattern_match() {
        let (repo, _, _, _, _, _) = setup_populated_db();
        let files = repo.list_files(Some("nonexistent")).unwrap();
        assert!(files.is_empty());
    }

    #[test]
    fn test_get_children_no_children() {
        let repo = SqliteRepository::open_in_memory().unwrap();
        let scan_id = repo.begin_scan().unwrap();
        let f = repo
            .upsert_file(scan_id, "a.rs", "rust", 10, "h", 1, ParseStatus::Ok)
            .unwrap();
        let sym_id = repo
            .insert_symbol(f, None, "Foo", SymbolKind::Struct, None, 1, 5, 0, 50)
            .unwrap();
        let children = SymbolLookup::get_children(&repo, sym_id).unwrap();
        assert!(children.is_empty());
    }

    #[test]
    fn test_open_existing_after_open() {
        let tmp = tempfile::tempdir().unwrap();
        let _db = open(tmp.path()).unwrap();
        let db2 = open_existing(tmp.path()).unwrap();
        let (fc, _, _) = SymbolLookup::stats(db2.as_ref()).unwrap();
        assert_eq!(fc, 0);
    }
}
