//! Large-scale integration tests.
//!
//! P0 CRITICAL tests that validate core durability guarantees.

use super::*;
use stemedb_storage::key_codec;
use tracing::info;

/// P0 CRITICAL: Crash recovery test that validates durability guarantees.
///
/// This test proves the fundamental durability claim from architecture.md:
/// "Write to WAL -> Crash -> Restart -> No data loss, no duplicate ingestion"
///
/// Test Scenario:
/// 1. Write N assertions to WAL
/// 2. Start IngestWorker, let it process some assertions (not all)
/// 3. Abruptly drop IngestWorker mid-stream (simulates crash)
/// 4. Verify cursor was persisted correctly
/// 5. Create NEW IngestWorker with same WAL + KV store
/// 6. Let it process remaining assertions
/// 7. Verify: all N assertions are in KV store (no data loss)
/// 8. Verify: no duplicates (count == N, not N + partial reprocessing)
/// 9. Verify: cursor position is correctly restored
/// 10. Verify: subject indexes are consistent
#[tokio::test]
async fn test_p0_crash_recovery_no_data_loss_no_duplicates() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    const TOTAL_ASSERTIONS: usize = 10;
    const PARTIAL_INGEST_COUNT: usize = 4;

    // PHASE 1: Write N assertions to WAL
    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let assertions: Vec<Assertion> = (0..TOTAL_ASSERTIONS)
        .map(|i| create_signed_assertion(&format!("CrashTest_Entity_{}", i), "has_property"))
        .collect();

    for assertion in &assertions {
        journal.append(serialize_assertion(assertion).expect("serialize")).expect("append to WAL");
    }

    let cursor_key = key_codec::cursor_key();

    // PHASE 2: Partial ingestion, then "crash"
    let cursor_before_crash = {
        let store = HybridStore::open(&db_dir).expect("Failed to open store");
        let journal = Arc::new(Mutex::new(journal));
        let store = Arc::new(store);

        let mut worker =
            IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");

        // Process only PARTIAL_INGEST_COUNT records
        for _ in 0..PARTIAL_INGEST_COUNT {
            let bytes = worker.step().await.expect("step");
            assert!(bytes > 0, "Should have processed data");
        }

        // Verify partial ingestion
        let count_key = key_codec::assertion_count_key();
        let count_bytes = store.get(&count_key).await.expect("get").expect("count exists");
        let count = u64::from_le_bytes(count_bytes.try_into().expect("8 bytes"));
        assert_eq!(
            count, PARTIAL_INGEST_COUNT as u64,
            "Should have exactly {} assertions before crash",
            PARTIAL_INGEST_COUNT
        );

        // Record cursor position before crash
        let cursor =
            store.get(&cursor_key).await.expect("get cursor").expect("cursor should exist");
        let offset = u64::from_le_bytes(cursor.try_into().expect("cursor should be 8 bytes"));

        info!(offset, "Cursor before crash");

        // Flush to ensure durability
        store.flush().await.expect("flush");

        // Worker and journal dropped here - SIMULATES CRASH
        offset
    };

    // PHASE 3: Recovery - reopen everything and verify cursor restoration
    {
        let journal = Journal::open(&wal_dir).expect("Failed to reopen journal");
        let store = HybridStore::open(&db_dir).expect("Failed to reopen store");

        let journal = Arc::new(Mutex::new(journal));
        let store = Arc::new(store);

        // Create new worker - should resume from cursor
        let mut worker =
            IngestWorker::new(journal, store.clone()).await.expect("create recovery worker");

        // CRITICAL VERIFICATION: Worker should resume from saved cursor
        assert_eq!(
            worker.current_offset, cursor_before_crash,
            "Recovery worker MUST resume from cursor offset {} (got {})",
            cursor_before_crash, worker.current_offset
        );

        info!(cursor = worker.current_offset, "Recovery worker restored cursor position");

        // PHASE 4: Process remaining assertions
        let mut steps = 0;
        loop {
            let bytes = worker.step().await.expect("step during recovery");
            if bytes == 0 {
                break;
            }
            steps += 1;
        }

        let remaining = TOTAL_ASSERTIONS - PARTIAL_INGEST_COUNT;
        assert_eq!(
            steps, remaining,
            "Should process exactly {} remaining records (got {})",
            remaining, steps
        );

        // PHASE 5: Verify NO DATA LOSS - all N assertions present
        let count_key = key_codec::assertion_count_key();
        let count_bytes = store.get(&count_key).await.expect("get").expect("count exists");
        let count = u64::from_le_bytes(count_bytes.try_into().expect("8 bytes"));
        assert_eq!(
            count, TOTAL_ASSERTIONS as u64,
            "All {} assertions must be present after recovery (no data loss, no duplicates)",
            TOTAL_ASSERTIONS
        );

        // PHASE 6: Verify subject indexes are consistent
        for i in 0..TOTAL_ASSERTIONS {
            let subject = format!("CrashTest_Entity_{}", i);
            let subj_key = key_codec::subjects_index_key(&subject);
            let entry = store.get(&subj_key).await.expect("get");
            assert!(entry.is_some(), "Subject {} should have an index entry", subject,);
        }

        // PHASE 7: Verify cursor is at end of WAL
        let cursor_final =
            store.get(&cursor_key).await.expect("get cursor").expect("cursor should exist");
        let final_offset =
            u64::from_le_bytes(cursor_final.try_into().expect("cursor should be 8 bytes"));

        info!(final_offset, assertions_count = count, "Crash recovery complete");

        // Cursor should be beyond the last record
        assert!(
            final_offset > cursor_before_crash,
            "Final cursor {} should be beyond pre-crash cursor {}",
            final_offset,
            cursor_before_crash
        );

        // One more step should return 0 (EOF)
        let eof_bytes = worker.step().await.expect("final step");
        assert_eq!(eof_bytes, 0, "Should be at EOF after processing all records");
    }
}
