// Integration tests requiring filesystem, async, or SurrealDB

use leankg::db::get_elements_by_env;
use leankg::db::schema::init_db;
use leankg::doc::DocGenerator;
use leankg::graph::{GraphEngine, ImpactAnalyzer};
use leankg::indexer::{find_files_sync, index_file_sync, ParserManager};
use std::path::PathBuf;
use tempfile::TempDir;

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_empty_dir() {
    let tmp = TempDir::new().unwrap();
    let root = tmp.path().to_str().unwrap();
    let files = find_files_sync(root).unwrap();
    assert!(files.is_empty());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_discovers_go_files() {
    let tmp = tempfile::TempDir::new().unwrap();
    let root = tmp.path().to_str().unwrap();
    let go_file = tmp.path().join("main.go");
    std::fs::write(&go_file, "package main\nfunc main() {}").unwrap();
    let files = find_files_sync(root).unwrap();
    assert!(!files.is_empty());
    assert!(files.iter().any(|f| f.ends_with("main.go")));
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_excludes_node_modules() {
    let tmp = TempDir::new().unwrap();
    let node_dir = tmp.path().join("node_modules").join("pkg");
    std::fs::create_dir_all(&node_dir).unwrap();
    std::fs::write(node_dir.join("index.js"), "export {}").unwrap();
    let files = find_files_sync(tmp.path().to_str().unwrap()).unwrap();
    assert!(!files.iter().any(|f| f.contains("node_modules")));
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_excludes_nested_worktrees_from_project_root() {
    let tmp = TempDir::new().unwrap();
    std::fs::write(tmp.path().join("main.rs"), "fn main() {}").unwrap();
    let worktree_src = tmp.path().join("worktrees").join("feature").join("src");
    std::fs::create_dir_all(&worktree_src).unwrap();
    std::fs::write(worktree_src.join("duplicate.rs"), "fn duplicate() {}").unwrap();

    let files = find_files_sync(tmp.path().to_str().unwrap()).unwrap();
    assert!(files.iter().any(|f| f.ends_with("main.rs")));
    assert!(
        !files.iter().any(|f| f.contains("duplicate.rs")),
        "nested worktree files should not be indexed from the project root: {:?}",
        files
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_allows_explicit_worktree_root() {
    let tmp = TempDir::new().unwrap();
    let worktree_src = tmp.path().join("worktrees").join("feature").join("src");
    std::fs::create_dir_all(&worktree_src).unwrap();
    std::fs::write(worktree_src.join("feature.rs"), "fn feature() {}").unwrap();

    let worktree_root = tmp.path().join("worktrees").join("feature");
    let files = find_files_sync(worktree_root.to_str().unwrap()).unwrap();
    assert!(
        files.iter().any(|f| f.ends_with("feature.rs")),
        "explicit worktree roots should still be indexable: {:?}",
        files
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_in_nested_dirs() {
    let tmp = tempfile::TempDir::new().unwrap();
    let nested = tmp.path().join("a").join("b").join("c");
    std::fs::create_dir_all(&nested).unwrap();
    std::fs::write(nested.join("lib.py"), "def x(): pass").unwrap();
    let files = find_files_sync(tmp.path().to_str().unwrap()).unwrap();
    assert!(
        files.iter().any(|f| f.ends_with("lib.py")),
        "Should find lib.py in nested dirs, got: {:?}",
        files
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_init_db_creates_schema() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let _db = init_db(db_path.as_path()).unwrap();
    assert!(db_path.exists() || std::path::Path::new(db_path.parent().unwrap()).exists());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_init_db_repairs_legacy_code_elements_after_recorded_migration() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("legacy.db");
    let db_path_str = db_path.to_string_lossy().to_string();
    let legacy_db = cozo::new_cozo_sqlite(db_path_str).unwrap();

    legacy_db.run_script(
        r#":create code_elements {qualified_name: String, element_type: String, name: String, file_path: String, line_start: Int, line_end: Int, language: String, parent_qualified: String?, cluster_id: String?, cluster_label: String?, metadata: String}"#,
        Default::default(),
    ).unwrap();
    legacy_db.run_script(
        r#"?[qualified_name, element_type, name, file_path, line_start, line_end, language, parent_qualified, cluster_id, cluster_label, metadata] <- [["src/main.rs::main", "function", "main", "src/main.rs", 1, 3, "rust", null, null, null, "{}"]]
        :put code_elements {qualified_name, element_type, name, file_path, line_start, line_end, language, parent_qualified, cluster_id, cluster_label, metadata}"#,
        Default::default(),
    ).unwrap();
    legacy_db
        .run_script(
            r#":create relationships {source_qualified: String, target_qualified: String, rel_type: String, confidence: Float, metadata: String}"#,
            Default::default(),
        )
        .unwrap();
    legacy_db
        .run_script(
            r#":create migrations {id: String, applied_at: Int}"#,
            Default::default(),
        )
        .unwrap();
    legacy_db
        .run_script(
            r#"?[id, applied_at] <- [["006_safe_canonical_schema_repair", 1]]
        :put migrations {id, applied_at}"#,
            Default::default(),
        )
        .unwrap();
    drop(legacy_db);

    let repaired_db = init_db(db_path.as_path()).unwrap();
    let canonical_query = repaired_db
        .run_script(
            r#"?[qualified_name, env] := *code_elements[qualified_name, element_type, name, file_path, line_start, line_end, language, parent_qualified, cluster_id, cluster_label, metadata, env]"#,
            Default::default(),
        )
        .unwrap();
    assert_eq!(canonical_query.rows.len(), 1);
    assert_eq!(canonical_query.rows[0][1].as_str(), Some("local"));

    let graph = GraphEngine::new(repaired_db);
    let results = graph
        .search_by_name_typed("main", Some("function"), 10)
        .unwrap();
    assert_eq!(results.len(), 1);
    assert_eq!(results[0].qualified_name, "src/main.rs::main");
}

#[tokio::test(flavor = "multi_thread")]
async fn test_graph_queries_support_ontology_layer_code_elements_schema() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("ontology-layer.db");
    let db_path_str = db_path.to_string_lossy().to_string();
    let db = cozo::new_cozo_sqlite(db_path_str).unwrap();

    db.run_script(
        r#":create code_elements {qualified_name: String, element_type: String, name: String, file_path: String, line_start: Int, line_end: Int, language: String, parent_qualified: String?, cluster_id: String?, cluster_label: String?, metadata: String, env: String default 'local', ontology_layer: String default 'procedural'}"#,
        Default::default(),
    ).unwrap();
    db.run_script(
        r#"?[qualified_name, element_type, name, file_path, line_start, line_end, language, parent_qualified, cluster_id, cluster_label, metadata, env, ontology_layer] <-
        [["src/metrics/prometheus.go::registerPrometheus", "function", "registerPrometheus", "src/metrics/prometheus.go", 10, 20, "go", null, null, null, "{}", "local", "procedural"]]
        :put code_elements {qualified_name, element_type, name, file_path, line_start, line_end, language, parent_qualified, cluster_id, cluster_label, metadata, env, ontology_layer}"#,
        Default::default(),
    ).unwrap();
    db.run_script(
        r#":create relationships {source_qualified: String, target_qualified: String, rel_type: String, confidence: Float, metadata: String, env: String default 'local'}"#,
        Default::default(),
    ).unwrap();
    drop(db);

    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db.clone());

    assert!(graph.has_elements().unwrap());
    assert_eq!(graph.count_elements().unwrap(), 1);

    let search_results = graph
        .search_by_name_typed("prometheus", Some("function"), 10)
        .unwrap();
    assert_eq!(search_results.len(), 1);
    assert_eq!(search_results[0].name, "registerPrometheus");

    let env_results = get_elements_by_env(&db, "local", 10).unwrap();
    assert_eq!(env_results.len(), 1);
    assert_eq!(
        env_results[0].qualified_name,
        "src/metrics/prometheus.go::registerPrometheus"
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_graph_engine_all_elements_empty() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);
    let elements = graph.all_elements().unwrap();
    assert!(elements.is_empty());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_graph_engine_find_element_missing() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);
    let result = graph.find_element("nonexistent::foo").unwrap();
    assert!(result.is_none());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_impact_analyzer_empty_graph() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);
    let analyzer = ImpactAnalyzer::new(&graph);
    let result = analyzer.calculate_impact_radius("src/main.go", 3).unwrap();
    assert_eq!(result.start_file, "src/main.go");
    assert_eq!(result.max_depth, 3);
    assert!(result.affected_elements.is_empty());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_doc_generator_agents_md_empty() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);
    let doc_gen = DocGenerator::new(graph, PathBuf::from("./docs"));
    let content = doc_gen.generate_agents_md().unwrap();
    assert!(content.contains("# Agent Guidelines for LeanKG"));
    assert!(content.contains("## Project Overview"));
    assert!(content.contains("## Build Commands"));
    assert!(content.contains("## Code Structure Overview"));
}

#[tokio::test(flavor = "multi_thread")]
async fn test_doc_generator_claude_md_empty() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);
    let doc_gen = DocGenerator::new(graph, PathBuf::from("./docs"));
    let content = doc_gen.generate_claude_md().unwrap();
    assert!(content.contains("# CLAUDE.md"));
    assert!(content.contains("## Project Overview"));
    assert!(content.contains("## Architecture Decisions"));
    assert!(content.contains("## Context Statistics"));
}

#[tokio::test(flavor = "multi_thread")]
async fn test_doc_sync_for_file() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);

    let go_file = tmp.path().join("main.go");
    std::fs::write(
        &go_file,
        "package main\n\nfunc add(x int, y int) int { return x + y }",
    )
    .unwrap();

    let mut parser = ParserManager::new();
    if parser.init_parsers().is_err() {
        return;
    }
    let _count = index_file_sync(&graph, &mut parser, go_file.to_str().unwrap()).unwrap();

    let doc_gen = DocGenerator::new(graph, PathBuf::from("./docs"));
    let result = doc_gen
        .sync_docs_for_file(go_file.to_str().unwrap())
        .unwrap();
    assert_eq!(result.file_path, go_file.to_str().unwrap());
    assert!(result.elements_regenerated > 0);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_index_file_go() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);

    let go_file = tmp.path().join("main.go");
    std::fs::write(
        &go_file,
        "package main\n\nfunc add(x int, y int) int { return x + y }",
    )
    .unwrap();

    let mut parser = ParserManager::new();
    if parser.init_parsers().is_err() {
        return;
    }
    let count = index_file_sync(&graph, &mut parser, go_file.to_str().unwrap()).unwrap();
    assert!(count > 0);
}

#[tokio::test(flavor = "multi_thread")]
async fn test_find_files_discovers_java_files() {
    let tmp = tempfile::TempDir::new().unwrap();
    let java_dir = tmp.path().join("com").join("example");
    std::fs::create_dir_all(&java_dir).unwrap();
    std::fs::write(
        java_dir.join("Main.java"),
        "public class Main { public static void main(String[] args) {} }",
    )
    .unwrap();
    let files = find_files_sync(tmp.path().to_str().unwrap()).unwrap();
    assert!(
        !files.is_empty(),
        "Should find some files, got: {:?}",
        files
    );
    assert!(
        files.iter().any(|f| f.ends_with("Main.java")),
        "Should find Main.java, got: {:?}",
        files
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_index_file_java() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg.db");
    let db = init_db(db_path.as_path()).unwrap();
    let graph = GraphEngine::new(db);

    let java_file = tmp.path().join("UserService.java");
    std::fs::write(
        &java_file,
        "import com.example.model.User;\npublic class UserService {\n    public User createUser(String name) {\n        return new User(name);\n    }\n}",
    )
    .unwrap();

    let mut parser = ParserManager::new();
    if parser.init_parsers().is_err() {
        return;
    }
    let count = index_file_sync(&graph, &mut parser, java_file.to_str().unwrap()).unwrap();
    assert!(count > 0, "Should index Java elements, got {}", count);

    let elements = graph.all_elements().unwrap();
    let java_classes: Vec<_> = elements
        .iter()
        .filter(|e| e.element_type == "class" && e.language == "java")
        .collect();
    assert!(!java_classes.is_empty(), "Should find Java class");
    assert_eq!(java_classes[0].name, "UserService");
}

#[tokio::test(flavor = "multi_thread")]
async fn test_get_relationships_with_real_db() {
    // Use the real .leankg database from current dir
    let db_path = std::path::Path::new(".leankg");
    if !db_path.exists() {
        println!("Skipping - no .leankg database in current dir");
        return;
    }

    let db = init_db(db_path).expect("failed to init db");
    let graph = GraphEngine::new(db);

    // Test with path that exists in DB (from graph.json we know ./src/api/auth.rs has imports)
    let result = graph.get_relationships("./src/api/auth.rs");
    match result {
        Ok(rels) => {
            println!(
                "get_relationships('./src/api/auth.rs') returned {} results",
                rels.len()
            );
            for rel in rels.iter().take(5) {
                println!(
                    "  {} -> {} ({})",
                    rel.source_qualified, rel.target_qualified, rel.rel_type
                );
            }
            // We expect at least one relationship based on graph.json
            assert!(
                !rels.is_empty(),
                "Should find relationships for ./src/api/auth.rs"
            );
        }
        Err(e) => {
            panic!("get_relationships failed: {}", e);
        }
    }

    // Test without ./ prefix
    let result2 = graph.get_relationships("src/api/auth.rs");
    match result2 {
        Ok(rels) => {
            println!(
                "get_relationships('src/api/auth.rs') returned {} results",
                rels.len()
            );
            assert!(
                !rels.is_empty(),
                "Should find relationships without prefix too"
            );
        }
        Err(e) => {
            panic!("get_relationships without prefix failed: {}", e);
        }
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn test_get_dependencies_with_real_db() {
    let db_path = std::path::Path::new(".leankg");
    if !db_path.exists() {
        println!("Skipping - no .leankg database");
        return;
    }

    let db = init_db(db_path).expect("failed to init db");
    let graph = GraphEngine::new(db.clone());

    // get_dependencies returns CodeElements for imported items
    // Since most imports are external (std::, crate::), we might get empty results
    // But the important thing is the QUERY works (path normalization is correct)
    let dep_result = graph.get_dependencies("./src/api/auth.rs");
    match dep_result {
        Ok(deps) => {
            println!("get_dependencies returned {} CodeElements", deps.len());
        }
        Err(e) => {
            panic!("get_dependencies failed: {}", e);
        }
    }

    // Verify the raw relationship query works (this is the core fix)
    let normalized = "./src/api/auth.rs"
        .strip_prefix("./")
        .unwrap_or("./src/api/auth.rs");
    let escaped = normalized.replace('\\', "\\\\").replace('"', "\\\"");
    let query = format!(
        r#"?[target_qualified] := *relationships[source_qualified, target_qualified, rel_type, confidence, metadata, _], (source_qualified = "{}" or source_qualified = "./{}"), rel_type = "imports""#,
        escaped, escaped
    );

    let result = db
        .run_script(&query, std::collections::BTreeMap::new())
        .unwrap();
    assert!(
        result.rows.len() > 0,
        "Should find import relationships with path normalization"
    );
    println!(
        "Confirmed: path normalization works - found {} import relationships",
        result.rows.len()
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_get_call_graph_with_real_db() {
    let db_path = std::path::Path::new(".leankg");
    if !db_path.exists() {
        println!("Skipping - no .leankg database");
        return;
    }

    let db = init_db(db_path).expect("failed to init db");
    let graph = GraphEngine::new(db);

    // Find a function that has calls
    let call_graph_result = graph.get_call_graph_bounded("./src/api/auth.rs", 1, 10);
    match call_graph_result {
        Ok(calls) => {
            println!(
                "get_call_graph('./src/api/auth.rs', depth=1) returned {} calls",
                calls.len()
            );
            for (src, tgt, depth) in calls.iter().take(5) {
                println!("  {} -> {} (depth {})", src, tgt, depth);
            }
        }
        Err(e) => {
            println!("get_call_graph failed: {}", e);
        }
    }
}

#[tokio::test(flavor = "multi_thread")]
async fn test_persistent_cache_hit_after_insert() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg_cache_test.db");
    let db = init_db(&db_path).unwrap();
    let graph = GraphEngine::with_persistence(db);

    use leankg::db::models::{CodeElement, Relationship};

    let elem_b = CodeElement {
        qualified_name: "src/b.rs::mod_b".to_string(),
        element_type: "module".to_string(),
        name: "mod_b".to_string(),
        file_path: "src/b.rs".to_string(),
        line_start: 1,
        line_end: 10,
        language: "rust".to_string(),
        ..Default::default()
    };
    graph.insert_element(&elem_b).unwrap();

    let rel = Relationship {
        id: None,
        source_qualified: "src/a.rs".to_string(),
        target_qualified: "src/b.rs::mod_b".to_string(),
        rel_type: "imports".to_string(),
        confidence: 1.0,
        metadata: serde_json::json!({}),
        ..Default::default()
    };
    graph.insert_relationship(&rel).unwrap();

    let deps_first = graph.get_dependencies("src/a.rs").unwrap();
    assert!(
        !deps_first.is_empty(),
        "First call should return results from DB"
    );

    let deps_second = graph.get_dependencies("src/a.rs").unwrap();
    assert!(
        !deps_second.is_empty(),
        "Second call (cache hit) should return results"
    );
    assert_eq!(
        deps_first.len(),
        deps_second.len(),
        "Cache hit should return same count"
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn test_persistent_cache_hit_on_second_call() {
    let tmp = TempDir::new().unwrap();
    let db_path = tmp.path().join("leankg_cache_survive_test.db");

    let db = init_db(&db_path).unwrap();
    let graph = GraphEngine::with_persistence(db);
    use leankg::db::models::{CodeElement, Relationship};

    let elem_y = CodeElement {
        qualified_name: "src/y.rs::mod_y".to_string(),
        element_type: "module".to_string(),
        name: "mod_y".to_string(),
        file_path: "src/y.rs".to_string(),
        line_start: 1,
        line_end: 5,
        language: "rust".to_string(),
        ..Default::default()
    };
    graph.insert_element(&elem_y).unwrap();

    let rel = Relationship {
        id: None,
        source_qualified: "src/x.rs".to_string(),
        target_qualified: "src/y.rs::mod_y".to_string(),
        rel_type: "imports".to_string(),
        confidence: 1.0,
        metadata: serde_json::json!({}),
        ..Default::default()
    };
    graph.insert_relationship(&rel).unwrap();

    let deps_first = graph.get_dependencies("src/x.rs").unwrap();
    assert!(!deps_first.is_empty(), "First call should return results");

    let deps_second = graph.get_dependencies("src/x.rs").unwrap();
    assert!(
        !deps_second.is_empty(),
        "Second call should return results (L1 cache hit)"
    );
    assert_eq!(deps_first.len(), deps_second.len(), "Same results expected");
}
