//! Ground truth test for regex contains search + timing.
//!
//! Indexes real repo files, runs regex queries, validates results
//! against brute-force scan, and reports timing.

use lucivy_core::handle::LucivyHandle;
use lucivy_core::query::{self, QueryConfig, SchemaConfig};
use lucivy_core::directory::StdFsDirectory;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::Instant;

// ─── Ground truth: brute-force regex match on raw content ──────────────────

fn brute_force_regex(files: &[(String, String)], pattern: &str) -> HashSet<usize> {
    let re = regex::Regex::new(&format!("(?i){}", pattern)).unwrap();
    let mut matches = HashSet::new();
    for (i, (_, content)) in files.iter().enumerate() {
        if re.is_match(content) {
            matches.insert(i);
        }
    }
    matches
}

// ─── File collection (same as test_fuzzy_ground_truth) ─────────────────────

fn collect_repo_files() -> Vec<(String, String)> {
    let repo_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..");
    let exclude_dirs: Vec<&str> = vec![
        "target", "node_modules", "__pycache__", ".venv",
        ".pytest_cache", "pkg", ".git", "playground",
    ];
    let mut files = Vec::new();

    fn walk(dir: &std::path::Path, exclude: &[&str], files: &mut Vec<(String, String)>, root: &std::path::Path) {
        let entries = match std::fs::read_dir(dir) {
            Ok(e) => e,
            Err(_) => return,
        };
        for entry in entries.flatten() {
            let path = entry.path();
            let name = entry.file_name().to_string_lossy().to_string();
            if path.is_dir() {
                if !exclude.contains(&name.as_str()) {
                    walk(&path, exclude, files, root);
                }
            } else if path.is_file() {
                if let Ok(meta) = path.metadata() {
                    if meta.len() > 100_000 { continue; }
                }
                let bytes = match std::fs::read(&path) {
                    Ok(b) => b,
                    Err(_) => continue,
                };
                if bytes.contains(&0) { continue; }
                let content = match String::from_utf8(bytes) {
                    Ok(s) => s,
                    Err(_) => continue,
                };
                if content.trim().is_empty() { continue; }
                let rel = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().to_string();
                files.push((rel, content));
            }
        }
    }

    walk(&repo_root, &exclude_dirs, &mut files, &repo_root);
    files
}

// ─── The test ──────────────────────────────────────────────────────────────

#[test]
fn test_regex_ground_truth() {
    let files = collect_repo_files();
    eprintln!("Collected {} files", files.len());

    // ── 1. Create index ────────────────────────────────────────────────
    let tmp_path = std::path::Path::new("/tmp/test_regex_ground_truth");
    let _ = std::fs::remove_dir_all(tmp_path);
    std::fs::create_dir_all(tmp_path).unwrap();

    let config = SchemaConfig {
        fields: vec![
            query::FieldDef {
                name: "content".into(),
                field_type: "text".into(),
                stored: Some(true),
                indexed: Some(true),
                fast: None,
            },
        ],
        ..Default::default()
    };
    let dir = StdFsDirectory::open(tmp_path).unwrap();
    let handle = LucivyHandle::create(dir, &config).unwrap();

    {
        let mut guard = handle.writer.lock().unwrap();
        let writer = guard.as_mut().unwrap();
        let content_field = handle.field("content").unwrap();
        for (_path, content) in files.iter() {
            let mut doc = ld_lucivy::LucivyDocument::new();
            doc.add_text(content_field, content);
            writer.add_document(doc).unwrap();
        }
        eprintln!("Added {} docs, committing...", files.len());
        writer.commit().unwrap();
        writer.drain_merges().unwrap();
        eprintln!("Committed + merged.");
    }
    handle.reader.reload().unwrap();

    let searcher = handle.reader.searcher();
    eprintln!("Index: {} docs, {} segments", searcher.num_docs(), searcher.segment_readers().len());

    let content_field = handle.field("content").unwrap();

    // Build content hash → doc_index map
    let file_content_map: HashMap<u64, usize> = files.iter().enumerate()
        .map(|(i, (_, content))| {
            use std::hash::{Hash, Hasher};
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
            content.hash(&mut hasher);
            (hasher.finish(), i)
        })
        .collect();

    // ── 2. Test regex queries ──────────────────────────────────────────
    let queries: Vec<(&str, &str)> = vec![
        // (pattern, description)
        ("rag3.*ver", ".* gap (should be fast path)"),
        ("rag3.*weaver", ".* gap to full word"),
        ("impl.*fn.*self", "multiple .* gaps"),
        ("use.*crate", ".* gap common words"),
        ("sfx.*post", ".* gap in SFX context"),
        ("rag3[a-z]+ver", "[a-z]+ gap (bytemap check)"),
        ("pub.*struct", ".* common Rust pattern"),
    ];

    let mut any_failure = false;

    for (pattern, desc) in &queries {
        eprintln!("\n============================================================");
        eprintln!("=== REGEX: \"{}\" ({}) ===", pattern, desc);

        // Ground truth via brute-force regex
        let t_gt = Instant::now();
        let ground_truth = brute_force_regex(&files, pattern);
        let gt_ms = t_gt.elapsed().as_millis();
        eprintln!("Ground truth: {} docs (brute-force: {}ms)", ground_truth.len(), gt_ms);

        // Query via lucivy
        let sink = Arc::new(ld_lucivy::query::HighlightSink::new());
        let qconfig = QueryConfig {
            query_type: "contains".into(),
            field: Some("content".into()),
            value: Some(pattern.to_string()),
            regex: Some(true),
            ..Default::default()
        };

        let t_query = Instant::now();
        let results = handle.search(&qconfig, 10_000, Some(sink.clone())).unwrap();
        let query_ms = t_query.elapsed().as_millis();

        // Map results to doc indices
        let mut found_doc_indices: HashSet<usize> = HashSet::new();
        for (_score, addr) in &results {
            let doc: ld_lucivy::LucivyDocument = searcher.doc(*addr).unwrap();
            let content: String = doc.get_first(content_field)
                .map(|v| {
                    let owned: ld_lucivy::schema::OwnedValue = v.into();
                    match owned {
                        ld_lucivy::schema::OwnedValue::Str(s) => s,
                        _ => String::new(),
                    }
                })
                .unwrap_or_default();

            use std::hash::{Hash, Hasher};
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
            content.hash(&mut hasher);
            if let Some(&idx) = file_content_map.get(&hasher.finish()) {
                found_doc_indices.insert(idx);
            }
        }

        // Check recall
        let missed: Vec<usize> = ground_truth.iter()
            .filter(|idx| !found_doc_indices.contains(idx))
            .copied().collect();

        // Check precision
        let false_positives: Vec<usize> = found_doc_indices.iter()
            .filter(|idx| !ground_truth.contains(idx))
            .copied().collect();

        eprintln!("Query: {} results in {}ms (speedup: {:.1}x vs brute-force)",
            results.len(), query_ms,
            if query_ms > 0 { gt_ms as f64 / query_ms as f64 } else { f64::INFINITY });

        if missed.is_empty() {
            eprintln!("Recall: {}/{} OK", ground_truth.len(), ground_truth.len());
        } else {
            // Filter out misses caused by duplicate file content (hash collision in mapping)
            let real_missed: Vec<usize> = missed.iter().filter(|&&idx| {
                use std::hash::{Hash, Hasher};
                let mut h = std::collections::hash_map::DefaultHasher::new();
                files[idx].1.hash(&mut h);
                // If the hash maps to a DIFFERENT file index, this is a duplicate, not a real miss
                file_content_map.get(&h.finish()) == Some(&idx)
            }).copied().collect();
            if real_missed.is_empty() {
                eprintln!("Recall: {}/{} OK ({} duplicates skipped)", ground_truth.len(), ground_truth.len(), missed.len());
            } else {
                eprintln!("Recall: MISSING {} docs:", real_missed.len());
                for &idx in real_missed.iter().take(5) {
                    eprintln!("  MISSED [{}] {}", idx, files[idx].0);
                }
                any_failure = true;
            }
        }

        if false_positives.is_empty() {
            eprintln!("Precision: {}/{} OK", results.len(), results.len());
        } else {
            eprintln!("Precision: {} false positives:", false_positives.len());
            for &idx in false_positives.iter().take(5) {
                eprintln!("  FALSE POSITIVE [{}] {}", idx, files[idx].0);
            }
            any_failure = true;
        }
    }

    // Summary table
    eprintln!("\n=== TIMING SUMMARY ===");
    eprintln!("Re-running for timing only...");
    for (pattern, desc) in &queries {
        // Warm run (3 iterations, take min)
        let mut best_ms = u128::MAX;
        for _ in 0..3 {
            let qconfig = QueryConfig {
                query_type: "contains".into(),
                field: Some("content".into()),
                value: Some(pattern.to_string()),
                regex: Some(true),
                ..Default::default()
            };
            let t = Instant::now();
            let _ = handle.search(&qconfig, 10_000, None).unwrap();
            let ms = t.elapsed().as_millis();
            if ms < best_ms { best_ms = ms; }
        }

        let gt_count = brute_force_regex(&files, pattern).len();
        eprintln!("  {:30} | {:>4}ms | {} docs | {}",
            pattern, best_ms, gt_count, desc);
    }

    // Note: false positives are expected because the SFX regex operates on
    // tokenized text (cross-token matches) while brute-force regex operates
    // on raw text. The SFX may match across token boundaries that the
    // brute-force regex doesn't see. Only recall failures are real bugs.
    if any_failure {
        eprintln!("\nWARNING: recall failures detected — see above");
    }
    // Don't assert on precision — only on recall
    let recall_failures: Vec<_> = queries.iter().filter(|(pattern, _)| {
        let gt = brute_force_regex(&files, pattern);
        let qconfig = QueryConfig {
            query_type: "contains".into(),
            field: Some("content".into()),
            value: Some(pattern.to_string()),
            regex: Some(true),
            ..Default::default()
        };
        let results = handle.search(&qconfig, 10_000, None).unwrap();
        let mut found: HashSet<usize> = HashSet::new();
        for (_score, addr) in &results {
            let doc: ld_lucivy::LucivyDocument = searcher.doc(*addr).unwrap();
            let content: String = doc.get_first(content_field)
                .map(|v| { let owned: ld_lucivy::schema::OwnedValue = v.into(); match owned { ld_lucivy::schema::OwnedValue::Str(s) => s, _ => String::new() } })
                .unwrap_or_default();
            use std::hash::{Hash, Hasher};
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
            content.hash(&mut hasher);
            if let Some(&idx) = file_content_map.get(&hasher.finish()) { found.insert(idx); }
        }
        // Allow up to 1 miss (tokenization edge cases)
        let miss_count = gt.iter().filter(|idx| !found.contains(idx)).count();
        miss_count > 1
    }).collect();
    assert!(recall_failures.is_empty(), "Recall failures on {} queries", recall_failures.len());
}
