//! Index-level crash safety tests.
//!
//! Tests that the index is recoverable after crashes during various operations.

use luci_index::index::Index;
use luci_mapping::{FieldType,Mapping};
use serde_json::json;

fn test_dir(name: &str) -> std::path::PathBuf {
    let dir = std::env::temp_dir().join(format!(
        "luci_crash_test_{}_{name}",
        std::process::id()
    ));
    let _ = std::fs::remove_dir_all(&dir);
    dir
}

fn cleanup(path: &std::path::Path) {
    let _ = std::fs::remove_dir_all(path);
}

fn test_schema() -> Mapping {
    Mapping::builder()
        .field("title", FieldType::Text)
        .field("tag", FieldType::Keyword)
        .build()
}

/// Crash between add() and commit() — buffered docs should be lost but
/// committed state should be intact.
#[test]
fn crash_before_commit_preserves_last_committed() {
    let path = test_dir("pre_commit");

    // Phase 1: commit some docs
    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();
        index.add(json!({"title": "committed doc 1", "tag": "a"})).unwrap();
        index.add(json!({"title": "committed doc 2", "tag": "b"})).unwrap();
        index.commit().unwrap();

        // Add more but DON'T commit (simulating crash before commit)
        index.add(json!({"title": "uncommitted doc", "tag": "c"})).unwrap();
        // Drop without commit — simulates crash
    }

    // Phase 2: reopen and verify only committed docs are visible
    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 10).unwrap();
        assert_eq!(results.total_hits.value, 2, "only committed docs should survive");

        // Verify committed doc content is intact
        let r = index.search(json!({"term": {"tag": "a"}}), 10).unwrap();
        assert_eq!(r.total_hits.value, 1);
        let source = r.hits[0].source.as_ref().unwrap();
        assert_eq!(source["title"], "committed doc 1",
            "committed doc content should be intact after crash");

        // The uncommitted doc should NOT be found
        let results = index.search(json!({"term": {"tag": "c"}}), 10).unwrap();
        assert_eq!(results.total_hits.value, 0, "uncommitted doc should be lost");
    }

    cleanup(&path);
}

/// Multiple commit cycles — each commit should be independently recoverable.
#[test]
fn multiple_commits_independently_recoverable() {
    let path = test_dir("multi_commit");

    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();

        // Commit 1
        index.add(json!({"title": "batch one", "tag": "1"})).unwrap();
        index.commit().unwrap();

        // Commit 2
        index.add(json!({"title": "batch two", "tag": "2"})).unwrap();
        index.commit().unwrap();

        // Commit 3
        index.add(json!({"title": "batch three", "tag": "3"})).unwrap();
        index.commit().unwrap();

        // Uncommitted
        index.add(json!({"title": "batch four", "tag": "4"})).unwrap();
        // crash
    }

    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 10).unwrap();
        assert_eq!(results.total_hits.value, 3, "3 committed batches should survive");

        // Each batch should be searchable
        for tag in ["1", "2", "3"] {
            let r = index.search(json!({"term": {"tag": tag}}), 10).unwrap();
            assert_eq!(r.total_hits.value, 1, "batch {tag} should be present");
        }

        let r = index.search(json!({"term": {"tag": "4"}}), 10).unwrap();
        assert_eq!(r.total_hits.value, 0, "uncommitted batch 4 should be lost");
    }

    cleanup(&path);
}

/// Empty commit followed by crash — should not corrupt index.
#[test]
fn empty_commit_then_crash() {
    let path = test_dir("empty_commit");

    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();
        index.add(json!({"title": "real doc"})).unwrap();
        index.commit().unwrap();
        // Empty commit
        index.commit().unwrap();
        // crash
    }

    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 10).unwrap();
        assert_eq!(results.total_hits.value, 1);
        let source = results.hits[0].source.as_ref().unwrap();
        assert_eq!(source["title"], "real doc",
            "document content should survive empty commit + crash");
    }

    cleanup(&path);
}

/// Reopen and continue indexing after simulated crash.
#[test]
fn reopen_and_continue_after_crash() {
    let path = test_dir("reopen_continue");

    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();
        index.add(json!({"title": "first session"})).unwrap();
        index.commit().unwrap();
    }

    // "crash" happened, reopen and add more
    {
        let mut index = Index::open(&path).unwrap();
        index.add(json!({"title": "second session"})).unwrap();
        index.commit().unwrap();
    }

    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 10).unwrap();
        assert_eq!(results.total_hits.value, 2, "both sessions should be present");

        // Verify both sessions' content survived
        let r = index.search(json!({"match": {"title": "first"}}), 10).unwrap();
        assert_eq!(r.total_hits.value, 1);
        assert_eq!(r.hits[0].source.as_ref().unwrap()["title"], "first session");

        let r = index.search(json!({"match": {"title": "second"}}), 10).unwrap();
        assert_eq!(r.total_hits.value, 1);
        assert_eq!(r.hits[0].source.as_ref().unwrap()["title"], "second session");
    }

    cleanup(&path);
}

/// Stress test: many small commits, crash after random one.
#[test]
fn many_commits_crash_recovery() {
    let path = test_dir("many_commits");
    let commit_count = 50;

    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();
        for i in 0..commit_count {
            index.add(json!({"title": format!("doc {i}"), "tag": format!("{i}")})).unwrap();
            index.commit().unwrap();
        }
        // Add more but don't commit
        for i in commit_count..commit_count + 10 {
            index.add(json!({"title": format!("doc {i}"), "tag": format!("{i}")})).unwrap();
        }
    }

    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 100).unwrap();
        assert_eq!(results.total_hits.value, commit_count as u64,
            "exactly {commit_count} committed docs should survive");

        // Spot-check content at boundaries: first, middle, last committed doc
        for tag in ["0", "25", "49"] {
            let r = index.search(json!({"term": {"tag": tag}}), 10).unwrap();
            assert_eq!(r.total_hits.value, 1, "doc with tag={tag} should survive");
            let source = r.hits[0].source.as_ref().unwrap();
            assert_eq!(source["title"], format!("doc {tag}"),
                "doc content for tag={tag} should be intact");
        }

        // Uncommitted docs (tags 50-59) should be gone
        let r = index.search(json!({"term": {"tag": "55"}}), 10).unwrap();
        assert_eq!(r.total_hits.value, 0, "uncommitted doc tag=55 should be lost");
    }

    cleanup(&path);
}

/// Large document batch — verify nothing is silently dropped.
#[test]
fn large_batch_integrity() {
    let path = test_dir("large_batch");
    let batch_size = 5_000;

    {
        let mut index = Index::create_with_mapping(&path, test_schema()).unwrap();
        for i in 0..batch_size {
            index.add(json!({
                "title": format!("document number {i} in the large batch test"),
                "tag": format!("tag_{}", i % 100),
            })).unwrap();
        }
        index.commit().unwrap();
    }

    {
        let mut index = Index::open(&path).unwrap();
        let results = index.search(json!({"match_all": {}}), 0).unwrap();
        assert_eq!(results.total_hits.value, batch_size as u64,
            "all {batch_size} docs should be committed");

        // Spot-check a few specific tags
        let r = index.search(json!({"term": {"tag": "tag_0"}}), 100).unwrap();
        assert_eq!(r.total_hits.value, 50, "tag_0 should appear 50 times in 5000 docs");
    }

    cleanup(&path);
}
