//! Property-test harness for the transaction crate.
//!
//! This file has two pieces:
//!
//! 1. A hand-crafted reproducer (`reproduces_clear_uuid_tree_hang`)
//!    that drives the harness through the same sequence of operations
//!    that `rescue clear-uuid-tree` performs. If this test hangs (or
//!    times out via the watchdog), we have a tight reproducer for the
//!    failure documented in `transaction/PLAN.md`.
//!
//! 2. A proptest entry point (`transaction_sequences_are_consistent`)
//!    that generates random `Op` sequences and runs them through
//!    `run_sequence_watchdogged`. The number of cases defaults to 10
//!    (so the test stays fast on `cargo test`); set the env var
//!    `PROPTEST_CASES=N` for stress runs.

use btrfs_transaction::test_helpers::{
    DEFAULT_TIMEOUT, Failure, Op, TestKey, run_sequence,
    run_sequence_watchdogged,
};
use proptest::prelude::*;
use test_strategy::Arbitrary;

// ─── Hand-crafted reproducers ────────────────────────────────────────

/// Mirrors the operation sequence performed by `rescue clear-uuid-tree`:
/// allocate some blocks owned by the playground tree (a stand-in for the
/// uuid tree's existing blocks), drop a ref on each one and pin it,
/// remove the playground tree's root from `fs.roots`, then commit.
///
/// PLAN.md predicts this hangs. With the empty-commit short-circuit
/// in place (Option A), the hang no longer reproduces — but the
/// underlying bug surfaces as a different "parent transid wanted N
/// found N-1" failure on a non-empty commit path. See PLAN.md
/// "Finding 3" for the diagnosis. Until that second instance is
/// fixed, this test is short-circuited so `cargo test` stays green.
/// Delete the early return to re-enable.
#[test]
fn reproduces_clear_uuid_tree_hang() {
    let ops = vec![
        Op::AllocBlock,
        Op::AllocBlock,
        Op::AllocBlock,
        Op::DropOwnedBlock { idx: 0 },
        Op::DropOwnedBlock { idx: 0 },
        Op::DropOwnedBlock { idx: 0 },
        Op::RemovePlaygroundRoot,
        Op::Commit,
    ];

    match run_sequence_watchdogged(ops, DEFAULT_TIMEOUT) {
        Ok(()) => {
            // Surprising but good — the underlying bug may already be
            // fixed. Leave the test in place as a regression guard.
        }
        Err(Failure::Hang { timeout }) => {
            panic!(
                "clear-uuid-tree reproducer hung (timeout {timeout:?}). \
                 This is the bug documented in transaction/PLAN.md."
            );
        }
        Err(other) => {
            panic!("clear-uuid-tree reproducer failed: {other}");
        }
    }
}

/// Smoke test: an empty sequence should pass cleanly. Verifies the
/// harness itself (mkfs → open → commit → reopen → check) works before
/// we ask proptest to find bugs in the transaction crate.
#[test]
fn empty_sequence_smoke() {
    run_sequence(&[]).expect("empty sequence should succeed");
}

/// Smoke test: a tiny insert/commit/reopen cycle. Verifies the model
/// oracle is wired up correctly.
#[test]
fn insert_commit_reopen_smoke() {
    let ops = vec![
        Op::Insert {
            key: TestKey {
                objectid: 1,
                offset: 0,
            },
            value: vec![0xAA; 16],
        },
        Op::Commit,
        Op::Reopen,
    ];
    run_sequence(&ops).expect("insert/commit/reopen should succeed");
}

// ─── Proptest entry point ────────────────────────────────────────────

/// Random `Op` for proptest. Bounds on the contained values are kept
/// tight so the shrinker has a small state space:
///
/// - object ids and offsets are 0..32, so collisions are common
/// - values are 0..=64 bytes, so leaves fill quickly without dominating
/// - block-drop indices are 0..16, modulo'd by `apply_op`
///
/// `Commit` and `Reopen` get extra weight so commit-path bugs are
/// exercised heavily; `Abort` and `RemovePlaygroundRoot` get less so
/// that the typical sequence makes forward progress.
#[derive(Debug, Clone, Arbitrary)]
enum ArbOp {
    #[weight(4)]
    Insert {
        #[strategy(arb_key())]
        key: TestKey,
        #[strategy(prop::collection::vec(any::<u8>(), 0..=64))]
        value: Vec<u8>,
    },
    #[weight(2)]
    Update {
        #[strategy(arb_key())]
        key: TestKey,
        #[strategy(prop::collection::vec(any::<u8>(), 0..=64))]
        value: Vec<u8>,
    },
    #[weight(2)]
    Delete {
        #[strategy(arb_key())]
        key: TestKey,
    },
    #[weight(2)]
    AllocBlock,
    #[weight(2)]
    DropOwnedBlock {
        #[strategy(0usize..16)]
        idx: usize,
    },
    #[weight(1)]
    RemovePlaygroundRoot,
    #[weight(4)]
    Commit,
    #[weight(1)]
    Abort,
    #[weight(2)]
    Reopen,
}

fn arb_key() -> impl Strategy<Value = TestKey> {
    (0u8..32, 0u8..32)
        .prop_map(|(objectid, offset)| TestKey { objectid, offset })
}

impl From<ArbOp> for Op {
    fn from(a: ArbOp) -> Op {
        match a {
            ArbOp::Insert { key, value } => Op::Insert { key, value },
            ArbOp::Update { key, value } => Op::Update { key, value },
            ArbOp::Delete { key } => Op::Delete { key },
            ArbOp::AllocBlock => Op::AllocBlock,
            ArbOp::DropOwnedBlock { idx } => Op::DropOwnedBlock { idx },
            ArbOp::RemovePlaygroundRoot => Op::RemovePlaygroundRoot,
            ArbOp::Commit => Op::Commit,
            ArbOp::Abort => Op::Abort,
            ArbOp::Reopen => Op::Reopen,
        }
    }
}

fn proptest_config() -> ProptestConfig {
    // Default to 100 cases — about 2-3 seconds on a fast box, so the
    // harness runs as part of every `cargo test` without blowing up
    // the test wall time. Bump via `PROPTEST_CASES=N` for stress runs
    // (1000 takes about 25 seconds).
    let cases = std::env::var("PROPTEST_CASES")
        .ok()
        .and_then(|s| s.parse().ok())
        .unwrap_or(100);
    ProptestConfig {
        cases,
        // Sequences can be expensive (mkfs + commits + btrfs check),
        // so cap proptest's per-case shrink budget — we'd rather see
        // a slightly larger failing input quickly than wait minutes
        // for an optimal shrink.
        max_shrink_iters: 256,
        ..ProptestConfig::default()
    }
}

proptest! {
    #![proptest_config(proptest_config())]

    /// For any sequence of operations, running it against a fresh
    /// filesystem must (a) not hang, (b) leave the playground tree
    /// containing exactly the model's keys/values at every step,
    /// (c) survive a final commit + reopen, and (d) pass
    /// `btrfs check --readonly` modulo free-space-tree warnings.
    #[test]
    fn transaction_sequences_are_consistent(
        ops in prop::collection::vec(any::<ArbOp>(), 0..=30),
    ) {
        let ops: Vec<Op> = ops.into_iter().map(Into::into).collect();
        if let Err(e) = run_sequence_watchdogged(ops, DEFAULT_TIMEOUT) {
            return Err(TestCaseError::fail(format!("{e}")));
        }
    }
}
