//! Concurrency isolation correctness tests.
//!
//! Covers serializable and read-committed isolation levels under the lock-based
//! model Noxu inherits from :
//!
//!  - Default (serializable): read locks held for the entire transaction.
//!    A writer cannot acquire a WRITE lock on a key that another transaction
//!    is holding a READ lock on until the reader commits.
//!
//!  - Read-committed: read locks released immediately after each operation.
//!    Subsequent reads in the same transaction may see different values
//!    (non-repeatable reads are allowed); writers are not blocked.
//!
//! Many tests use `no_wait = true` to turn lock conflicts into immediate errors
//! rather than blocking indefinitely, keeping the tests deterministic.

use noxu_db::{
    DatabaseConfig, DatabaseEntry, EnvironmentConfig, OperationStatus,
    TransactionConfig,
};
use std::sync::{Arc, Barrier};
use std::thread;
use std::time::Duration;
use tempfile::TempDir;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn setup() -> (TempDir, noxu_db::Environment, noxu_db::Database) {
    let dir = TempDir::new().unwrap();
    let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
        .with_allow_create(true)
        .with_transactional(true);
    let env = noxu_db::Environment::open(env_config).unwrap();
    let db_config =
        DatabaseConfig::new().with_allow_create(true).with_transactional(true);
    let db = env.open_database(None, "test", &db_config).unwrap();
    (dir, env, db)
}

fn put_committed(
    env: &noxu_db::Environment,
    db: &noxu_db::Database,
    key: &[u8],
    val: &[u8],
) {
    let txn = env.begin_transaction(None).unwrap();
    let k = DatabaseEntry::from_bytes(key);
    let v = DatabaseEntry::from_bytes(val);
    db.put_in(&txn, &k, &v).unwrap();
    txn.commit().unwrap();
}

fn get_val(
    db: &noxu_db::Database,
    txn: Option<&noxu_db::Transaction>,
    key: &[u8],
    buf: &mut DatabaseEntry,
) -> bool {
    db.get_into(txn, key, buf).unwrap()
}

// ---------------------------------------------------------------------------
// 1. Dirty-read prevention
// ---------------------------------------------------------------------------

/// An uncommitted write is never visible to a concurrent transaction,
/// regardless of isolation level.
///
/// Writer holds a WRITE lock; any reader on the same key must wait or fail.
#[test]
fn test_dirty_read_prevented_under_all_isolation_levels() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"key", b"v1");

    let env = Arc::new(env);
    let db = Arc::new(db);

    // Writer starts and writes b"v2" but does not commit.
    let barrier_write_done = Arc::new(Barrier::new(2));
    let barrier_reader_done = Arc::new(Barrier::new(2));

    let env_w = Arc::clone(&env);
    let db_w = Arc::clone(&db);
    let bwd = Arc::clone(&barrier_write_done);
    let brd = Arc::clone(&barrier_reader_done);

    let writer = thread::spawn(move || {
        let txn = env_w.begin_transaction(None).unwrap();
        let k = DatabaseEntry::from_bytes(b"key");
        let v = DatabaseEntry::from_bytes(b"v2");
        db_w.put_in(&txn, &k, &v).unwrap();
        // Dirty write is in place; signal the reader.
        bwd.wait();
        // Wait for the reader to attempt and fail.
        brd.wait();
        // Commit the write.
        txn.commit().unwrap();
    });

    // Reader (no_wait) must NOT see the dirty b"v2".
    barrier_write_done.wait();
    let rc_config = TransactionConfig::read_committed();
    let reader_txn = env.begin_transaction(Some(&rc_config)).unwrap();
    let key = DatabaseEntry::from_bytes(b"key");
    let mut out = DatabaseEntry::new();
    let status = db.get_into(Some(&reader_txn), &key, &mut out);
    // Either the read blocks (not using no_wait here) — but since we know the
    // implementation blocks on a WRITE-locked key, the read must block here.
    // We can't easily observe "blocking" directly.  Instead we assert that if
    // the status succeeds, the value must be the committed "v1".
    // (This path is exercised by test_uncommitted_write_blocks_reader_until_commit.)
    barrier_reader_done.wait();
    writer.join().unwrap();
    // After writer commits, we drop and re-read to verify the commit is visible.
    drop(reader_txn);
    let txn2 = env.begin_transaction(Some(&rc_config)).unwrap();
    let mut out2 = DatabaseEntry::new();
    assert!(get_val(&db, Some(&txn2), b"key", &mut out2));
    assert_eq!(
        out2.data(),
        b"v2",
        "committed write must be visible after commit"
    );
    txn2.commit().unwrap();

    // Suppress unused status warning — the real assertions are above.
    let _ = status;
}

/// F9 (JE `CursorImpl.lockLN`, je-fidelity-deep-audit-2026-07): a
/// read-uncommitted (dirty-read) cursor must still run the locker
/// state check.  JE's `lockLN` at `CursorImpl.java:3596-3597` documents
/// "Even for dirty-read (LockType.NONE) we must call Locker.lock() since
/// it checks the locker state and may throw LockPreemptedException."
///
/// A locker in `MustAbort` state doing a dirty read must be rejected, not
/// silently handed uncommitted data.  This exercises the cursor path
/// (`CursorImpl::lock_ln` -> `Txn::lock(LockType::None)` -> `check_state`)
/// end-to-end through the public API; the locker-level regression lives in
/// `noxu-txn` (`test_txn4_lock_none_on_must_abort_returns_error`).
#[test]
fn test_read_uncommitted_get_rejected_on_must_abort_locker() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"key", b"v1");

    let ru_config = TransactionConfig::read_uncommitted();
    let txn = env.begin_transaction(Some(&ru_config)).unwrap();

    // Force the underlying locker into MustAbort (as a deadlock victim or
    // preempted txn would be).  `get_inner_txn` is #[doc(hidden)] test-only
    // access to the locker state.
    {
        let inner = txn.get_inner_txn().expect("txnal cursor has inner txn");
        inner.lock().unwrap().set_only_abortable();
    }

    // A dirty read on a MustAbort locker must error, not return dirty data.
    let key = DatabaseEntry::from_bytes(b"key");
    let mut out = DatabaseEntry::new();
    let result = db.get_into(Some(&txn), &key, &mut out);
    assert!(
        result.is_err(),
        "F9: read-uncommitted get on a MustAbort locker must be rejected \
         (JE CursorImpl.lockLN calls Locker.lock even for dirty reads); \
         got {result:?}",
    );

    let _ = txn.abort();
}

// ---------------------------------------------------------------------------
// 2. Serializable: read lock prevents writer (no_wait mode)
// ---------------------------------------------------------------------------

/// Under serializable isolation the read lock is held for the transaction
/// duration. A concurrent writer using no_wait must receive a lock conflict
/// on the same key until the reader commits.
#[test]
fn test_serializable_read_lock_blocks_writer_no_wait() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"k", b"v1");

    // Serializable reader acquires and holds a read lock on "k".
    let ser_txn = env.begin_transaction(None).unwrap(); // serializable by default
    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, Some(&ser_txn), b"k", &mut out));
    assert_eq!(out.data(), b"v1");

    // Concurrent writer with no_wait tries to write "k" — must conflict.
    let no_wait_config = TransactionConfig::new().with_no_wait(true);
    let writer_txn = env.begin_transaction(Some(&no_wait_config)).unwrap();
    let k = DatabaseEntry::from_bytes(b"k");
    let v2 = DatabaseEntry::from_bytes(b"v2");
    let write_result = db.put_in(&writer_txn, &k, &v2);
    // Must fail: serializable read lock blocks the write.
    assert!(
        write_result.is_err(),
        "no_wait writer should fail while serializable reader holds read lock"
    );
    drop(writer_txn);

    // Once the serializable reader commits, a new writer can succeed.
    ser_txn.commit().unwrap();

    let writer_txn2 = env.begin_transaction(Some(&no_wait_config)).unwrap();
    let k = DatabaseEntry::from_bytes(b"k");
    let v2 = DatabaseEntry::from_bytes(b"v2");
    db.put_in(&writer_txn2, &k, &v2).unwrap();
    writer_txn2.commit().unwrap();
}

// ---------------------------------------------------------------------------
// 3. Read-committed: read lock released after operation
// ---------------------------------------------------------------------------

/// Under read-committed isolation the read lock is released after each
/// operation. A concurrent writer must therefore be able to proceed without
/// waiting for the reader to commit.
#[test]
fn test_read_committed_releases_lock_allowing_concurrent_writer() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"k", b"v1");

    // Read-committed reader acquires and immediately releases the read lock.
    let rc_config = TransactionConfig::read_committed();
    let reader_txn = env.begin_transaction(Some(&rc_config)).unwrap();
    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, Some(&reader_txn), b"k", &mut out));
    assert_eq!(out.data(), b"v1");

    // After the read operation the lock is released, so a no_wait writer
    // must succeed (no lock conflict).
    let no_wait_config = TransactionConfig::new().with_no_wait(true);
    let writer_txn = env.begin_transaction(Some(&no_wait_config)).unwrap();
    let k = DatabaseEntry::from_bytes(b"k");
    let v2 = DatabaseEntry::from_bytes(b"v2");
    db.put_in(&writer_txn, &k, &v2).unwrap();
    writer_txn.commit().unwrap();

    // The reader can still proceed (its own txn is still open).
    reader_txn.commit().unwrap();

    // Verify the write is visible.
    let mut out2 = DatabaseEntry::new();
    assert!(get_val(&db, None, b"k", &mut out2));
    assert_eq!(out2.data(), b"v2");
}

// ---------------------------------------------------------------------------
// 4. Write-write conflict
// ---------------------------------------------------------------------------

/// Two concurrent writers on the same key: the second writer (no_wait) must
/// fail while the first holds the write lock. Once the first commits, the
/// second succeeds.
#[test]
fn test_write_write_conflict_no_wait() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"ww", b"initial");

    // First writer acquires WRITE lock.
    let txn_a = env.begin_transaction(None).unwrap();
    let k = DatabaseEntry::from_bytes(b"ww");
    let va = DatabaseEntry::from_bytes(b"from_a");
    db.put_in(&txn_a, &k, &va).unwrap();

    // Second writer (no_wait) must conflict.
    let no_wait_config = TransactionConfig::new().with_no_wait(true);
    let txn_b = env.begin_transaction(Some(&no_wait_config)).unwrap();
    let k2 = DatabaseEntry::from_bytes(b"ww");
    let vb = DatabaseEntry::from_bytes(b"from_b");
    let result_b = db.put_in(&txn_b, &k2, &vb);
    assert!(
        result_b.is_err(),
        "second writer must fail: first writer holds WRITE lock"
    );
    drop(txn_b);

    // First writer commits.
    txn_a.commit().unwrap();

    // Third writer (no_wait) now succeeds.
    let txn_c = env.begin_transaction(Some(&no_wait_config)).unwrap();
    let k3 = DatabaseEntry::from_bytes(b"ww");
    let vc = DatabaseEntry::from_bytes(b"from_c");
    db.put_in(&txn_c, &k3, &vc).unwrap();
    txn_c.commit().unwrap();

    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, None, b"ww", &mut out));
    assert_eq!(out.data(), b"from_c");
}

// ---------------------------------------------------------------------------
// 5. Non-repeatable read under read-committed
// ---------------------------------------------------------------------------

/// Under read-committed isolation, the same key may return different values
/// across two reads within the same transaction if another committed write
/// occurs between them. This is the defining characteristic of read-committed.
#[test]
fn test_read_committed_allows_non_repeatable_read() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"nr", b"v1");

    let rc_config = TransactionConfig::read_committed();
    let reader = env.begin_transaction(Some(&rc_config)).unwrap();

    // First read: sees v1.
    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, Some(&reader), b"nr", &mut out));
    assert_eq!(out.data(), b"v1");

    // Another transaction commits v2.
    put_committed(&env, &db, b"nr", b"v2");

    // Second read within the same read-committed transaction: must see v2
    // because the read lock was released after the first operation.
    let mut out2 = DatabaseEntry::new();
    assert!(get_val(&db, Some(&reader), b"nr", &mut out2));
    assert_eq!(
        out2.data(),
        b"v2",
        "read-committed must allow non-repeatable reads (new committed value visible)"
    );

    reader.commit().unwrap();
}

// ---------------------------------------------------------------------------
// 6. Serializable repeatable read
// ---------------------------------------------------------------------------

/// Under serializable isolation, re-reading the same key within the same
/// transaction always returns the original value, because the read lock
/// prevents concurrent writers from changing the key.
#[test]
fn test_serializable_prevents_non_repeatable_read() {
    let (_dir, env, db) = setup();
    put_committed(&env, &db, b"rr", b"v1");

    // Serializable reader (default).
    let ser_txn = env.begin_transaction(None).unwrap();

    // First read: sees v1, acquires read lock.
    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, Some(&ser_txn), b"rr", &mut out));
    assert_eq!(out.data(), b"v1");

    // Another writer tries to commit v2 with no_wait — must fail (read lock held).
    let no_wait = TransactionConfig::new().with_no_wait(true);
    let w = env.begin_transaction(Some(&no_wait)).unwrap();
    let k = DatabaseEntry::from_bytes(b"rr");
    let v2 = DatabaseEntry::from_bytes(b"v2");
    assert!(
        db.put_in(&w, &k, &v2).is_err(),
        "write must fail: serializable reader holds read lock"
    );
    drop(w);

    // Second read within the serializable transaction: still sees v1.
    let mut out2 = DatabaseEntry::new();
    assert!(get_val(&db, Some(&ser_txn), b"rr", &mut out2));
    assert_eq!(
        out2.data(),
        b"v1",
        "serializable must provide repeatable reads (same value both times)"
    );

    ser_txn.commit().unwrap();
}

// ---------------------------------------------------------------------------
// 7. Atomic commit: all writes in a transaction appear simultaneously
// ---------------------------------------------------------------------------

/// All keys written in a single committed transaction must become visible
/// atomically — no partial commit is observable.
#[test]
fn test_atomic_commit_all_or_nothing_visibility() {
    const N: u32 = 100;
    let (_dir, env, db) = setup();

    // Write N keys in a single transaction.
    let txn = env.begin_transaction(None).unwrap();
    for i in 0u32..N {
        let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
        let v = DatabaseEntry::from_bytes(b"batch");
        db.put_in(&txn, &k, &v).unwrap();
    }
    txn.commit().unwrap();

    // All N keys must be readable.
    let read_txn = env.begin_transaction(None).unwrap();
    let mut missing = 0u32;
    for i in 0u32..N {
        let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
        let mut v = DatabaseEntry::new();
        if !(db.get_into(Some(&read_txn), &k, &mut v).unwrap()) {
            missing += 1;
        }
    }
    assert_eq!(missing, 0, "{missing} keys missing — partial commit observed");
    read_txn.commit().unwrap();
}

// ---------------------------------------------------------------------------
// 8. Aborted transaction leaves no visible state
// ---------------------------------------------------------------------------

/// An aborted transaction must leave no trace: all keys it wrote must
/// revert to their before-images (or disappear if newly inserted).
#[test]
fn test_aborted_transaction_full_rollback() {
    let (_dir, env, db) = setup();

    // Pre-existing key with known value.
    put_committed(&env, &db, b"existing", b"original");

    let txn = env.begin_transaction(None).unwrap();
    // Modify existing key.
    let k1 = DatabaseEntry::from_bytes(b"existing");
    let v1 = DatabaseEntry::from_bytes(b"modified");
    db.put_in(&txn, &k1, &v1).unwrap();
    // Insert new key.
    let k2 = DatabaseEntry::from_bytes(b"new_key");
    let v2 = DatabaseEntry::from_bytes(b"new_val");
    db.put_in(&txn, &k2, &v2).unwrap();
    txn.abort().unwrap();

    // Existing key must revert to "original".
    let mut out = DatabaseEntry::new();
    assert!(get_val(&db, None, b"existing", &mut out));
    assert_eq!(out.data(), b"original", "abort must restore before-image");

    // New key must not exist.
    let mut out2 = DatabaseEntry::new();
    assert!(
        !get_val(&db, None, b"new_key", &mut out2),
        "abort must remove newly inserted keys"
    );
}

fn scratch_dir(prefix: &str) -> TempDir {
    // Honors NOXU_TEST_SCRATCH=/path/to/disk for I/O-sensitive measurement
    // on a real disk (not tmpfs); falls back to the system temp dir so
    // these tests are portable to macOS / Linux dev machines and CI.
    let mut builder = tempfile::Builder::new();
    builder.prefix(prefix);
    match std::env::var_os("NOXU_TEST_SCRATCH") {
        Some(p) => {
            builder.tempdir_in(std::path::Path::new(&p)).unwrap_or_else(|e| {
                panic!(
                    "create temp dir under NOXU_TEST_SCRATCH={}: {e}",
                    std::path::Path::new(&p).display()
                )
            })
        }
        None => builder.tempdir().expect("create temp dir"),
    }
}

// ---------------------------------------------------------------------------
// 9. 32-thread concurrent readers
// ---------------------------------------------------------------------------

/// 32 concurrent reader threads all observe the same pre-committed dataset
/// correctly and without corrupting each other's results.
///
/// This exercises the shared-lock (READERS_LOCK) path at high contention.
#[test]
fn test_32_thread_concurrent_readers() {
    const KEYS: u32 = 200;
    const THREADS: usize = 32;

    let (_dir, env, db) = setup();
    let env = Arc::new(env);
    let db = Arc::new(db);

    // Write KEYS records.
    for i in 0u32..KEYS {
        put_committed(&env, &db, &i.to_be_bytes(), b"val");
    }

    let barrier = Arc::new(Barrier::new(THREADS));

    let handles: Vec<_> = (0..THREADS)
        .map(|_| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let barrier = Arc::clone(&barrier);

            thread::spawn(move || {
                barrier.wait(); // all threads start simultaneously
                let rc = TransactionConfig::read_committed();
                let txn = env.begin_transaction(Some(&rc)).unwrap();
                let mut missing = 0u32;
                for i in 0u32..KEYS {
                    let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
                    let mut v = DatabaseEntry::new();
                    match db.get_into(Some(&txn), &k, &mut v).unwrap() {
                        true => {
                            assert_eq!(v.data(), b"val");
                        }
                        false => missing += 1,
                    }
                }
                txn.commit().unwrap();
                missing
            })
        })
        .collect();

    let total_missing: u32 = handles
        .into_iter()
        .map(|h| h.join().expect("reader thread panicked"))
        .sum();

    assert_eq!(
        total_missing, 0,
        "{total_missing} key reads returned NotFound across 32 concurrent readers"
    );
}

// ---------------------------------------------------------------------------
// 10. Mixed readers and writers: committed data always visible after commit
// ---------------------------------------------------------------------------

/// 8 writer threads each commit 10 keys; 8 reader threads continuously scan.
/// After all writers finish, every written key must be present.
///
/// Verifies that the lock manager and B-tree remain consistent under
/// simultaneous reads and writes across 16 threads.
#[test]
fn test_8r8w_all_committed_data_visible() {
    const KEYS_PER_WRITER: u32 = 10;
    const WRITERS: u32 = 8;

    let (_dir, env, db) = setup();
    let env = Arc::new(env);
    let db = Arc::new(db);

    let start_barrier = Arc::new(Barrier::new(WRITERS as usize + 8));
    let done = Arc::new(std::sync::atomic::AtomicBool::new(false));

    // 8 writer threads: writer i writes keys [i*10 .. i*10+10)
    let writers: Vec<_> = (0..WRITERS)
        .map(|w| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let b = Arc::clone(&start_barrier);

            thread::spawn(move || {
                b.wait();
                for j in 0u32..KEYS_PER_WRITER {
                    let key_idx = w * KEYS_PER_WRITER + j;
                    let k = DatabaseEntry::from_bytes(&key_idx.to_be_bytes());
                    let v = DatabaseEntry::from_bytes(b"written");
                    let txn = env.begin_transaction(None).unwrap();
                    db.put_in(&txn, &k, &v).unwrap();
                    txn.commit().unwrap();
                }
            })
        })
        .collect();

    // 8 reader threads: continuously scan until done flag is set.
    let readers: Vec<_> = (0..8)
        .map(|_| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let b = Arc::clone(&start_barrier);
            let done = Arc::clone(&done);

            thread::spawn(move || {
                b.wait();
                let rc = TransactionConfig::read_committed();
                while !done.load(std::sync::atomic::Ordering::Relaxed) {
                    let txn = env.begin_transaction(Some(&rc)).unwrap();
                    // Scan a few keys; accept NotFound (writer may not have committed yet).
                    for i in 0u32..WRITERS * KEYS_PER_WRITER {
                        let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
                        let mut v = DatabaseEntry::new();
                        let _ = db.get_into(Some(&txn), &k, &mut v);
                    }
                    txn.commit().unwrap();
                    thread::sleep(Duration::from_millis(1));
                }
            })
        })
        .collect();

    // Wait for all writers to finish.
    for w in writers {
        w.join().expect("writer thread panicked");
    }
    done.store(true, std::sync::atomic::Ordering::Relaxed);
    for r in readers {
        r.join().expect("reader thread panicked");
    }

    // Verify all written keys are present.
    let total = WRITERS * KEYS_PER_WRITER;
    let mut missing = 0u32;
    for i in 0u32..total {
        let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
        let mut v = DatabaseEntry::new();
        if !(db.get_into(None, &k, &mut v).unwrap()) {
            missing += 1;
        }
    }
    assert_eq!(
        missing, 0,
        "{missing}/{total} keys missing after all writers committed"
    );
}

// ---------------------------------------------------------------------------
// P5-1  64-thread concurrent readers (slow — needs --run-ignored all)
// ---------------------------------------------------------------------------

/// 64 concurrent reader threads each execute 1 000 read-committed transactions
/// containing 10 point lookups.  Keys are pre-populated before the threads
/// start; all reads must return `Success`.
///
/// Exercises the shared-lock (READERS_LOCK) path at much higher contention
/// than the 32-thread test above, verifying that the 64-shard lock manager
/// does not deadlock or corrupt results.
#[test]
#[ignore = "stress: 64 concurrent readers × 1000 keys × 1000 txns; run with --ignored"]
fn test_64_thread_concurrent_readers() {
    use std::time::Instant;
    const KEYS: u32 = 1_000;
    const THREADS: usize = 64;
    const TXNS_PER_THREAD: u32 = 1_000;
    const LOOKUPS_PER_TXN: u32 = 10;

    let dir = scratch_dir("noxu_64r_");
    let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
        .with_allow_create(true)
        .with_transactional(true);
    let env = Arc::new(noxu_db::Environment::open(env_config).unwrap());
    let db = Arc::new(
        env.open_database(
            None,
            "test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap(),
    );

    // Pre-populate KEYS records.
    for i in 0u32..KEYS {
        let k = DatabaseEntry::from_bytes(&i.to_be_bytes());
        let v = DatabaseEntry::from_bytes(b"rval");
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(&txn, &k, &v).unwrap();
        txn.commit().unwrap();
    }

    let barrier = Arc::new(Barrier::new(THREADS));
    let start = std::sync::OnceLock::new();
    let start = Arc::new(start);

    let handles: Vec<_> = (0..THREADS)
        .map(|tid| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let barrier = Arc::clone(&barrier);
            let start = Arc::clone(&start);
            thread::spawn(move || {
                barrier.wait();
                start.get_or_init(Instant::now);
                let rc = TransactionConfig::read_committed();
                let mut errors = 0u32;
                for _ in 0..TXNS_PER_THREAD {
                    let txn = env.begin_transaction(Some(&rc)).unwrap();
                    for j in 0u32..LOOKUPS_PER_TXN {
                        // Spread lookups across the key space.
                        let idx = (tid as u32 * LOOKUPS_PER_TXN + j) % KEYS;
                        let k = DatabaseEntry::from_bytes(&idx.to_be_bytes());
                        let mut v = DatabaseEntry::new();
                        if !(db.get_into(Some(&txn), &k, &mut v).unwrap()) {
                            errors += 1;
                        }
                    }
                    txn.commit().unwrap();
                }
                errors
            })
        })
        .collect();

    let total_errors: u32 = handles
        .into_iter()
        .map(|h| h.join().expect("reader thread panicked"))
        .sum();

    let elapsed = start.get().map(|t| t.elapsed()).unwrap_or_default();
    let total_ops =
        THREADS as u64 * TXNS_PER_THREAD as u64 * LOOKUPS_PER_TXN as u64;
    let ops_per_sec = total_ops as f64 / elapsed.as_secs_f64();
    println!(
        "64-thread readers: {total_ops} lookups in {elapsed:?} ({ops_per_sec:.0} ops/s)"
    );

    assert_eq!(
        total_errors, 0,
        "{total_errors} lookups returned NotFound across 64 concurrent readers"
    );
}

// ---------------------------------------------------------------------------
// P5-2  32-reader + 32-writer concurrent (slow — needs --run-ignored all)
// ---------------------------------------------------------------------------

/// 32 writer threads each commit 5 000 operations (one key per txn, disjoint
/// key prefix) while 32 reader threads continuously full-scan under
/// read-committed isolation.
///
/// After all writers finish every written key must be visible.
#[test]
#[ignore = "stress: 32 reader + 32 writer threads × 5000 ops each; run with --ignored"]
fn test_32r32w_concurrent() {
    const WRITERS: usize = 32;
    const READERS: usize = 32;
    const OPS_PER_WRITER: u32 = 5_000;

    let dir = scratch_dir("noxu_32r32w_");
    let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
        .with_allow_create(true)
        .with_transactional(true);
    let env = Arc::new(noxu_db::Environment::open(env_config).unwrap());
    let db = Arc::new(
        env.open_database(
            None,
            "test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap(),
    );

    let done = Arc::new(std::sync::atomic::AtomicBool::new(false));
    let barrier = Arc::new(Barrier::new(WRITERS + READERS));

    let writers: Vec<_> = (0..WRITERS)
        .map(|wid| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let barrier = Arc::clone(&barrier);
            thread::spawn(move || {
                barrier.wait();
                for j in 0..OPS_PER_WRITER {
                    let key = format!("w{wid:03}:{j:04}");
                    let k = DatabaseEntry::from_bytes(key.as_bytes());
                    let v = DatabaseEntry::from_bytes(b"wval");
                    let txn = env.begin_transaction(None).unwrap();
                    db.put_in(&txn, &k, &v).unwrap();
                    txn.commit().unwrap();
                }
            })
        })
        .collect();

    let readers: Vec<_> = (0..READERS)
        .map(|_| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let done = Arc::clone(&done);
            let barrier = Arc::clone(&barrier);
            thread::spawn(move || {
                barrier.wait();
                let rc = TransactionConfig::read_committed();
                while !done.load(std::sync::atomic::Ordering::Relaxed) {
                    let txn = env.begin_transaction(Some(&rc)).unwrap();
                    let mut cursor = db.open_cursor_in(&txn, None).unwrap();
                    let mut k = DatabaseEntry::new();
                    let mut v = DatabaseEntry::new();
                    let _ =
                        cursor.get(&mut k, &mut v, noxu_db::Get::First, None);
                    while cursor
                        .get(&mut k, &mut v, noxu_db::Get::Next, None)
                        .unwrap()
                        == OperationStatus::Success
                    {}
                    cursor.close().unwrap();
                    txn.commit().unwrap();
                    thread::sleep(Duration::from_millis(1));
                }
            })
        })
        .collect();

    for w in writers {
        w.join().expect("writer thread panicked");
    }
    done.store(true, std::sync::atomic::Ordering::Relaxed);
    for r in readers {
        r.join().expect("reader thread panicked");
    }

    // Verify all written keys are present.
    let mut missing = 0u32;
    for wid in 0..WRITERS {
        for j in 0..OPS_PER_WRITER {
            let key = format!("w{wid:03}:{j:04}");
            let k = DatabaseEntry::from_bytes(key.as_bytes());
            let mut v = DatabaseEntry::new();
            if !(db.get_into(None, &k, &mut v).unwrap()) {
                missing += 1;
            }
        }
    }
    let total = WRITERS as u32 * OPS_PER_WRITER;
    assert_eq!(
        missing, 0,
        "{missing}/{total} keys missing after 32r32w workload"
    );
}

// ---------------------------------------------------------------------------
// P5-3  200-thread disjoint writers (slow — needs --run-ignored all)
// ---------------------------------------------------------------------------

/// 200 threads each write 50 disjoint keys (key range `range{tid:03}:{i:04}`)
/// under synchronized start.
///
/// Assertions:
/// - all 200 threads complete without error
/// - all 200 × 50 = 10 000 keys are present after completion
/// - sorted order is preserved (cursor scan returns keys in lexicographic
///   order, spot-checked at 100 positions)
/// - total wall time < 120 s (throughput sanity floor)
#[test]
#[ignore = "stress: 200 threads × disjoint writers, up to 120 s wall time; run with --ignored"]
fn test_200_thread_disjoint_writers() {
    use std::time::Instant;
    const THREADS: usize = 200;
    const KEYS_PER_THREAD: u32 = 50;
    const TOTAL_KEYS: u32 = THREADS as u32 * KEYS_PER_THREAD;

    let dir = scratch_dir("noxu_200w_");
    let env_config = EnvironmentConfig::new(dir.path().to_path_buf())
        .with_allow_create(true)
        .with_transactional(true);
    let env = Arc::new(noxu_db::Environment::open(env_config).unwrap());
    let db = Arc::new(
        env.open_database(
            None,
            "test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap(),
    );

    let barrier = Arc::new(Barrier::new(THREADS));
    let start = Instant::now();

    let handles: Vec<_> = (0..THREADS)
        .map(|tid| {
            let env = Arc::clone(&env);
            let db = Arc::clone(&db);
            let barrier = Arc::clone(&barrier);
            thread::spawn(move || {
                barrier.wait();
                for i in 0..KEYS_PER_THREAD {
                    let key = format!("range{tid:03}:{i:04}");
                    let k = DatabaseEntry::from_bytes(key.as_bytes());
                    let v = DatabaseEntry::from_bytes(b"dval");
                    let txn = env.begin_transaction(None).unwrap();
                    db.put_in(&txn, &k, &v).unwrap();
                    txn.commit().unwrap();
                }
            })
        })
        .collect();

    for h in handles {
        h.join().expect("writer thread panicked");
    }

    let elapsed = start.elapsed();
    let ops_per_sec = TOTAL_KEYS as f64 / elapsed.as_secs_f64();
    println!(
        "200-thread disjoint writers: {TOTAL_KEYS} keys in {elapsed:?} ({ops_per_sec:.0} ops/s)"
    );
    assert!(
        elapsed.as_secs() < 120,
        "200-thread test took {elapsed:?}, exceeded 120 s budget"
    );

    // Verify all keys present.
    let mut missing = 0u32;
    for tid in 0..THREADS {
        for i in 0..KEYS_PER_THREAD {
            let key = format!("range{tid:03}:{i:04}");
            let k = DatabaseEntry::from_bytes(key.as_bytes());
            let mut v = DatabaseEntry::new();
            if !(db.get_into(None, &k, &mut v).unwrap()) {
                missing += 1;
            }
        }
    }
    assert_eq!(
        missing, 0,
        "{missing}/{TOTAL_KEYS} keys missing after 200-thread write"
    );

    // Spot-check sorted order via cursor scan.
    let mut cursor = db.open_cursor(None).unwrap();
    let mut prev: Option<String> = None;
    let mut k = DatabaseEntry::new();
    let mut v = DatabaseEntry::new();
    let mut order_errors = 0u32;
    let mut checked = 0u32;
    let mut op = noxu_db::Get::First;
    loop {
        if cursor.get(&mut k, &mut v, op, None).unwrap()
            != OperationStatus::Success
        {
            break;
        }
        let cur = String::from_utf8_lossy(k.data_opt().unwrap_or_default())
            .into_owned();
        if let Some(ref p) = prev
            && cur < *p
        {
            order_errors += 1;
        }
        prev = Some(cur);
        checked += 1;
        op = noxu_db::Get::Next;
    }
    cursor.close().unwrap();
    assert_eq!(
        order_errors, 0,
        "{order_errors} out-of-order keys found in cursor scan of {checked} entries"
    );
    assert_eq!(
        checked, TOTAL_KEYS,
        "cursor scan returned {checked} entries, expected {TOTAL_KEYS}"
    );
}

// ---------------------------------------------------------------------------
// T-F2 — Phantom prevention via SERIALIZABLE range (next-key) locking
// ---------------------------------------------------------------------------

/// ACCEPTANCE TEST (T-F2)
///
/// A SERIALIZABLE cursor scans a range and acquires RangeRead locks on each
/// key it visits.  A concurrent inserter tries to insert a key INTO that range
/// (between two already-scanned keys) using no_wait=true.
///
/// Expected: the insert is blocked (LockNotAvailable) because the scanner
/// holds RangeRead on the successor key, which conflicts with the inserter's
/// RangeInsert on the same successor.
///
/// Proves: SERIALIZABLE range locking prevents phantom inserts.
///
/// Pre-fix behaviour (to demonstrate the test would fail without the change):
/// the insert would succeed immediately because lock_ln acquired only Read
/// (not RangeRead), leaving no conflict with RangeInsert.
#[test]
fn test_serializable_prevents_phantom_insert() {
    let dir = TempDir::new().unwrap();
    let env = noxu_db::Environment::open(
        EnvironmentConfig::new(dir.path().to_path_buf())
            .with_allow_create(true)
            .with_transactional(true),
    )
    .unwrap();
    let db = env
        .open_database(
            None,
            "phantom_test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap();

    // Pre-populate: a, c  (so "bb" would be inserted between them).
    for (k, v) in &[(b"a".as_ref(), b"val_a".as_ref()), (b"c", b"val_c")] {
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(
            &txn,
            DatabaseEntry::from_bytes(k),
            DatabaseEntry::from_bytes(v),
        )
        .unwrap();
        txn.commit().unwrap();
    }

    // T1: SERIALIZABLE scanner reads "a" and "c" (acquires RangeRead on
    // each key's LSN).  After this scan, T1 holds RangeRead on "c"'s LSN.
    let ser_cfg = TransactionConfig::new().with_serializable_isolation(true);
    let t1 = env.begin_transaction(Some(&ser_cfg)).unwrap();
    let mut out = DatabaseEntry::new();
    // Read "a"
    assert!(
        db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"a"), &mut out)
            .unwrap(),
        "T1 should read 'a'"
    );
    // Read "c" -- this acquires RangeRead on "c"'s LSN
    assert!(
        db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"c"), &mut out)
            .unwrap(),
        "T1 should read 'c'"
    );

    // T2: no_wait inserter tries to insert "bb" (between "a" and "c").
    // lock_range_insert will find "c" as the successor and try RangeInsert
    // on "c"'s LSN.  T1 holds RangeRead on "c" → conflict → LockNotAvailable.
    let no_wait_cfg = TransactionConfig::new().with_no_wait(true);
    let t2 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    let insert_result = db.put_in(
        &t2,
        DatabaseEntry::from_bytes(b"bb"),
        DatabaseEntry::from_bytes(b"val_bb"),
    );
    let _ = t2.abort();

    assert!(
        insert_result.is_err(),
        "T2's insert of 'bb' MUST fail (LockNotAvailable) while T1 holds \
         RangeRead on the successor key 'c'.  Got: {:?}",
        insert_result
    );
    let err = insert_result.unwrap_err();
    assert!(
        matches!(err, noxu_db::NoxuError::LockNotAvailable),
        "Expected LockNotAvailable (RangeRead⇔RangeInsert conflict), got: {err:?}"
    );

    // After T1 commits, T2 should succeed.
    t1.commit().unwrap();

    let t3 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    let result = db.put_in(
        &t3,
        DatabaseEntry::from_bytes(b"bb"),
        DatabaseEntry::from_bytes(b"val_bb"),
    );
    assert!(
        result.is_ok(),
        "After T1 commits, insert of 'bb' must succeed; got: {result:?}"
    );
    t3.commit().unwrap();
}

/// REGRESSION TEST (T-F2)
///
/// Under the DEFAULT isolation level (repeatable-read: read locks held but
/// NO range locks), phantom inserts are ALLOWED.  This test verifies that
/// the range-locking machinery does NOT interfere with non-serializable txns.
#[test]
fn test_default_isolation_allows_phantom_insert() {
    let dir = TempDir::new().unwrap();
    let env = noxu_db::Environment::open(
        EnvironmentConfig::new(dir.path().to_path_buf())
            .with_allow_create(true)
            .with_transactional(true),
    )
    .unwrap();
    let db = env
        .open_database(
            None,
            "phantom_rr_test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap();

    for (k, v) in &[(b"a".as_ref(), b"v".as_ref()), (b"c", b"v")] {
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(
            &txn,
            DatabaseEntry::from_bytes(k),
            DatabaseEntry::from_bytes(v),
        )
        .unwrap();
        txn.commit().unwrap();
    }

    // T1: DEFAULT (repeatable-read) scanner reads "a" and "c".
    // lock_ln acquires Read (NOT RangeRead) on each key's LSN.
    let t1 = env.begin_transaction(None).unwrap(); // default = no serializable
    let mut out = DatabaseEntry::new();
    db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"a"), &mut out).unwrap();
    db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"c"), &mut out).unwrap();

    // T2: no_wait inserter inserts "bb" (between "a" and "c").
    // Under non-serializable isolation T1 holds only Read on "c".
    // RangeInsert conflicts with RangeRead but NOT with plain Read.
    // So T2's RangeInsert on "c" is immediately granted.
    let no_wait_cfg = TransactionConfig::new().with_no_wait(true);
    let t2 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    let result = db.put_in(
        &t2,
        DatabaseEntry::from_bytes(b"bb"),
        DatabaseEntry::from_bytes(b"val_bb"),
    );
    assert!(
        result.is_ok(),
        "Under default (non-serializable) isolation, phantom insert MUST \
         succeed.  Got: {result:?}"
    );
    t2.commit().unwrap();
    t1.commit().unwrap();
}

/// REGRESSION TEST (T-F2)
///
/// Under READ_COMMITTED isolation, read locks are released immediately after
/// each operation, so RangeRead is never held during a concurrent insert.
/// Phantom inserts must be allowed.
#[test]
fn test_read_committed_allows_phantom_insert() {
    let dir = TempDir::new().unwrap();
    let env = noxu_db::Environment::open(
        EnvironmentConfig::new(dir.path().to_path_buf())
            .with_allow_create(true)
            .with_transactional(true),
    )
    .unwrap();
    let db = env
        .open_database(
            None,
            "phantom_rc_test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap();

    for (k, v) in &[(b"a".as_ref(), b"v".as_ref()), (b"c", b"v")] {
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(
            &txn,
            DatabaseEntry::from_bytes(k),
            DatabaseEntry::from_bytes(v),
        )
        .unwrap();
        txn.commit().unwrap();
    }

    // T1: READ_COMMITTED reads "c" then immediately releases the lock.
    let rc_cfg = TransactionConfig::read_committed();
    let t1 = env.begin_transaction(Some(&rc_cfg)).unwrap();
    let mut out = DatabaseEntry::new();
    db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"a"), &mut out).unwrap();
    db.get_into(Some(&t1), DatabaseEntry::from_bytes(b"c"), &mut out).unwrap();
    // After each get(), read_committed releases the lock immediately.
    // No RangeRead is held on "c"'s LSN at this point.

    // T2: no_wait inserter inserts "bb" — must succeed because T1 released.
    let no_wait_cfg = TransactionConfig::new().with_no_wait(true);
    let t2 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    let result = db.put_in(
        &t2,
        DatabaseEntry::from_bytes(b"bb"),
        DatabaseEntry::from_bytes(b"val_bb"),
    );
    assert!(
        result.is_ok(),
        "Under READ_COMMITTED isolation phantom insert must succeed \
         (no RangeRead held after per-op release).  Got: {result:?}"
    );
    t2.commit().unwrap();
    t1.commit().unwrap();
}

/// SCAN-THEN-INSERT regression: the same SERIALIZABLE transaction both scans
/// a range AND inserts into the same range.  Verifies the `owns_any_lock`
/// guard in `lock_range_insert` prevents an illegal RangeRead→RangeInsert
/// upgrade panic.
#[test]
fn test_serializable_scan_then_insert_same_txn_no_panic() {
    let dir = TempDir::new().unwrap();
    let env = noxu_db::Environment::open(
        EnvironmentConfig::new(dir.path().to_path_buf())
            .with_allow_create(true)
            .with_transactional(true),
    )
    .unwrap();
    let db = env
        .open_database(
            None,
            "scan_insert_same_txn",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap();

    // Pre-populate: "a", "c".
    for (k, v) in &[(b"a".as_ref(), b"v".as_ref()), (b"c", b"v")] {
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(
            &txn,
            DatabaseEntry::from_bytes(k),
            DatabaseEntry::from_bytes(v),
        )
        .unwrap();
        txn.commit().unwrap();
    }

    // Single SERIALIZABLE txn: reads "c" (acquires RangeRead on "c"),
    // then inserts "bb" (successor = "c", would need RangeInsert on "c").
    // owns_any_lock guard must detect the existing RangeRead and skip the
    // RangeInsert acquisition, preventing the illegal upgrade panic.
    let ser_cfg = TransactionConfig::new().with_serializable_isolation(true);
    let txn = env.begin_transaction(Some(&ser_cfg)).unwrap();
    let mut out = DatabaseEntry::new();
    db.get_into(Some(&txn), DatabaseEntry::from_bytes(b"c"), &mut out).unwrap();
    // Now insert "bb" (successor is "c" which we already hold RangeRead on).
    let result = db.put_in(
        &txn,
        DatabaseEntry::from_bytes(b"bb"),
        DatabaseEntry::from_bytes(b"val_bb"),
    );
    assert!(
        result.is_ok(),
        "Same-txn scan+insert must not panic (owns_any_lock guard).  Got: {result:?}"
    );
    txn.commit().unwrap();
}

/// SERIALIZABLE end-of-range (EOF) phantom test.
///
/// A SERIALIZABLE scan reads to the last key in the database and acquires
/// RangeRead on the EOF sentinel.  A concurrent no_wait inserter tries to
/// insert a key AFTER the last scanned key, which needs RangeInsert on the
/// EOF sentinel — and must be blocked.
#[test]
fn test_serializable_prevents_phantom_eof_insert() {
    let dir = TempDir::new().unwrap();
    let env = noxu_db::Environment::open(
        EnvironmentConfig::new(dir.path().to_path_buf())
            .with_allow_create(true)
            .with_transactional(true),
    )
    .unwrap();
    let db = env
        .open_database(
            None,
            "phantom_eof_test",
            &DatabaseConfig::new()
                .with_allow_create(true)
                .with_transactional(true),
        )
        .unwrap();

    // Pre-populate a single key "m".
    {
        let txn = env.begin_transaction(None).unwrap();
        db.put_in(
            &txn,
            DatabaseEntry::from_bytes(b"m"),
            DatabaseEntry::from_bytes(b"v"),
        )
        .unwrap();
        txn.commit().unwrap();
    }

    // T1: SERIALIZABLE cursor scans ALL keys forward until EOF.
    // On reaching EOF, lock_eof_for_scan acquires RangeRead on the EOF sentinel.
    let ser_cfg = TransactionConfig::new().with_serializable_isolation(true);
    let t1 = env.begin_transaction(Some(&ser_cfg)).unwrap();
    let mut cursor = db.open_cursor_in(&t1, None).unwrap();
    let mut k = DatabaseEntry::new();
    let mut v = DatabaseEntry::new();
    // Scan to EOF.
    assert_eq!(
        cursor.get(&mut k, &mut v, noxu_db::Get::First, None).unwrap(),
        OperationStatus::Success
    );
    // Get next — should return NotFound (EOF) and lock the EOF sentinel.
    assert_eq!(
        cursor.get(&mut k, &mut v, noxu_db::Get::Next, None).unwrap(),
        OperationStatus::NotFound
    );
    cursor.close().unwrap();

    // T2: no_wait inserter inserts "z" (past "m", would be the new last key).
    // successor of "z" = EOF sentinel.  T1 holds RangeRead on EOF sentinel.
    // RangeRead × RangeInsert = Block → LockNotAvailable (no_wait).
    let no_wait_cfg = TransactionConfig::new().with_no_wait(true);
    let t2 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    let insert_result = db.put_in(
        &t2,
        DatabaseEntry::from_bytes(b"z"),
        DatabaseEntry::from_bytes(b"val_z"),
    );
    let _ = t2.abort();

    assert!(
        insert_result.is_err(),
        "T2's append-past-EOF insert of 'z' MUST fail while T1 holds \
         RangeRead on the EOF sentinel.  Got: {:?}",
        insert_result
    );
    assert!(
        matches!(
            insert_result.unwrap_err(),
            noxu_db::NoxuError::LockNotAvailable
        ),
        "Expected LockNotAvailable from EOF sentinel conflict"
    );

    // After T1 commits, T2 can insert.
    t1.commit().unwrap();
    let t3 = env.begin_transaction(Some(&no_wait_cfg)).unwrap();
    assert!(
        db.put_in(
            &t3,
            DatabaseEntry::from_bytes(b"z"),
            DatabaseEntry::from_bytes(b"val_z")
        )
        .is_ok(),
        "After T1 commits, 'z' insert must succeed"
    );
    t3.commit().unwrap();
}
