//! Cursor persistence and recovery tests.
//!
//! Tests for persistent cursor tracking, crash recovery with cursor,
//! and verification of cursor state across restarts.

use super::*;
use stemedb_storage::key_codec;

/// Test: Cursor is persisted after ingestion and restored on restart.
///
/// After ingesting records, a new worker should resume from where the
/// previous one left off instead of replaying the entire WAL.
#[tokio::test]
async fn test_cursor_persists_across_restarts() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    // Write two assertions to the WAL
    let a1 = create_signed_assertion("Cursor_A", "property");
    let a2 = create_signed_assertion("Cursor_B", "property");
    journal.append(serialize_assertion(&a1).expect("ser")).expect("append");
    journal.append(serialize_assertion(&a2).expect("ser")).expect("append");

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

    // Worker 1: ingest both records
    {
        let mut worker =
            IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");
        while worker.step().await.expect("step") > 0 {}
    }

    // Worker 2: should have nothing to process (cursor was persisted)
    {
        let mut worker =
            IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");
        let bytes = worker.step().await.expect("step");
        assert_eq!(bytes, 0, "Second worker should find no new records");
    }

    // Verify both assertions are present via assertion count
    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, 2, "Both assertions should exist");
}

/// Test: Crash recovery with persistent cursor only processes new records.
///
/// Simulates: write 2 records -> ingest -> crash -> write 1 more -> recover.
/// The recovery should only process the new record, not replay the first two.
#[tokio::test]
async fn test_crash_recovery_resumes_from_cursor() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    // Phase 1: Write and ingest 2 records
    {
        let mut journal = Journal::open(&wal_dir).expect("open journal");
        let store = HybridStore::open(&db_dir).expect("open store");

        let a1 = create_signed_assertion("Phase1_A", "prop");
        let a2 = create_signed_assertion("Phase1_B", "prop");
        journal.append(serialize_assertion(&a1).expect("ser")).expect("append");
        journal.append(serialize_assertion(&a2).expect("ser")).expect("append");

        let journal = Arc::new(Mutex::new(journal));
        let store = Arc::new(store);
        let mut worker = IngestWorker::new(journal, store.clone()).await.expect("create worker");
        while worker.step().await.expect("step") > 0 {}

        store.flush().await.expect("flush");
        // Drop everything - simulates crash
    }

    // Phase 2: Write 1 more record to WAL, then recover
    {
        let mut journal = Journal::open(&wal_dir).expect("reopen journal");
        let a3 = create_signed_assertion("Phase2_C", "prop");
        journal.append(serialize_assertion(&a3).expect("ser")).expect("append");

        let store = HybridStore::open(&db_dir).expect("reopen store");
        let journal = Arc::new(Mutex::new(journal));
        let store = Arc::new(store);

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

        // Should only process the ONE new record from Phase 2
        let mut steps = 0;
        while worker.step().await.expect("step") > 0 {
            steps += 1;
        }
        assert_eq!(steps, 1, "Should only process the one new record, not replay all 3");

        // All 3 assertions should be 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, 3, "All 3 assertions should be present");
    }
}

/// Test: Partial crash recovery verifies cursor offset precisely.
///
/// 1. Writes 5 assertions to WAL
/// 2. Ingests only 2 (calls step() twice)
/// 3. Records cursor offset
/// 4. Drops worker (simulates crash)
/// 5. Creates new worker, verifies it resumes from exact cursor position
/// 6. Ingests remaining 3
/// 7. Verifies total 5 assertions in storage
#[tokio::test]
async fn test_cursor_partial_crash_recovery() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    // Write 5 assertions to the WAL
    let assertions: Vec<Assertion> =
        (0..5).map(|i| create_signed_assertion(&format!("Entity_{}", i), "property")).collect();

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

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

    // Create worker and ingest only 2 records
    let cursor_after_two = {
        let mut worker =
            IngestWorker::new(journal.clone(), store.clone()).await.expect("create worker");

        let bytes1 = worker.step().await.expect("step 1");
        assert!(bytes1 > 0, "First step should process data");

        let bytes2 = worker.step().await.expect("step 2");
        assert!(bytes2 > 0, "Second step should process data");

        // Worker dropped here - simulates crash
        worker.current_offset
    };

    // Verify exactly 2 assertions in store
    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, 2, "Should have exactly 2 assertions after partial ingestion");

    // Create new worker and verify it resumes from correct cursor
    {
        let mut worker =
            IngestWorker::new(journal.clone(), store.clone()).await.expect("create new worker");

        assert_eq!(
            worker.current_offset, cursor_after_two,
            "New worker should resume from cursor offset {} (got {})",
            cursor_after_two, worker.current_offset
        );

        // Ingest remaining 3 records
        let mut remaining_steps = 0;
        loop {
            let bytes = worker.step().await.expect("step");
            if bytes == 0 {
                break;
            }
            remaining_steps += 1;
        }

        assert_eq!(remaining_steps, 3, "Should have processed exactly 3 remaining records");
    }

    // Verify all 5 assertions are now in store
    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, 5, "All 5 assertions should be in store after full recovery");

    // Verify subject indexes for all 5 entities
    for i in 0..5 {
        let subject = format!("Entity_{}", i);
        let subj_key = key_codec::subjects_index_key(&subject);
        let entry = store.get(&subj_key).await.expect("get");
        assert!(entry.is_some(), "Entity_{} should have a subjects index entry", i);
    }
}

/// Test: Cursor value is stored correctly in the KV store.
#[tokio::test]
async fn test_cursor_stored_in_kv() {
    let dir = tempdir().expect("Failed to create temp dir");
    let wal_dir = dir.path().join("wal");
    let db_dir = dir.path().join("db");

    let mut journal = Journal::open(&wal_dir).expect("Failed to open journal");
    let store = HybridStore::open(&db_dir).expect("Failed to open store");

    let assertion = create_test_assertion();
    journal.append(serialize_assertion(&assertion).expect("ser")).expect("append");

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

    let cursor_key = key_codec::cursor_key();

    // Before ingestion: no cursor
    let cursor_before = store.get(&cursor_key).await.expect("get");
    assert!(cursor_before.is_none(), "No cursor should exist before ingestion");

    // After ingestion: cursor should be set
    let mut worker = IngestWorker::new(journal, store.clone()).await.expect("create worker");
    worker.step().await.expect("step");

    let cursor_after = store.get(&cursor_key).await.expect("get");
    assert!(cursor_after.is_some(), "Cursor should exist after ingestion");

    let offset_bytes = cursor_after.expect("cursor should be present");
    let offset = u64::from_le_bytes(offset_bytes.try_into().expect("cursor should be 8 bytes"));
    assert!(offset > HEADER_SIZE as u64, "Cursor should be beyond header: got {}", offset);
}
