//! Unit + smoke tests for the unified-userspace VM. Run with `cargo test -p quay-vm`.

use crate::opcode::{Op, MAX_STACK_DEPTH};
use crate::vm::{evaluate, EvalResult, Inputs, RuntimeError, TxContext};

/// Every enum variant the VM defines. The list is the source of truth
/// for byte-mapping round-trip / instr_len / stack_delta tests.
///
/// **Compile-enforced**: when a new variant is added, the exhaustive
/// match in `every_op_byte_round_trips` forces this list to grow too.
const EVERY_OP: &[Op] = &[
    Op::LoadQuote,
    Op::LoadQuoteU,
    Op::LoadQuoteU64,
    Op::LoadQuoteI64,
    Op::LoadQuotesTimestampSec,
    Op::LoadQuotesTimestampNanos,
    Op::LoadSize,
    Op::LoadSide,
    Op::LoadConst,
    Op::LoadNowSlot,
    Op::LoadNowUnixSec,
    Op::LoadInvBase,
    Op::LoadInvQuote,
    Op::LoadBaseDecimals,
    Op::LoadQuoteDecimals,
    Op::LoadLastTradeSlot,
    Op::LoadIxDepth,
    Op::LoadTxFlags,
    Op::EntrypointIs,
    Op::IsSignedBy,
    Op::LoadI64,
    Op::LoadU64,
    Op::StoreI64,
    Op::StoreU64,
    Op::Dup,
    Op::Swap,
    Op::Drop,
    Op::Pick,
    Op::Add,
    Op::Sub,
    Op::Mul,
    Op::Div,
    Op::Muldiv,
    Op::Neg,
    Op::Abs,
    Op::Sqrt,
    Op::DivCeil,
    Op::Mod,
    Op::Pow,
    Op::MulShr,
    Op::MulQ48,
    Op::DivQ48,
    Op::MuladdQ48,
    Op::And,
    Op::Or,
    Op::Xor,
    Op::Not,
    Op::Shl,
    Op::Shr,
    Op::Min,
    Op::Max,
    Op::Clamp,
    Op::TierLookup,
    Op::LerpLookup,
    Op::TierLookupDyn,
    Op::TierLookup2,
    Op::CmpLt,
    Op::CmpGt,
    Op::CmpLe,
    Op::CmpGe,
    Op::CmpEq,
    Op::Select,
    Op::IfLt,
    Op::IfGt,
    Op::IfLe,
    Op::IfGe,
    Op::IfEq,
    Op::Jmp,
    Op::Halt,
    Op::Reject,
    Op::AssertNonzero,
];

/// Pin every Op variant → byte → Op round-trip. Catches:
///   - new variant added to the enum without a `from_byte_const` arm
///     (would be silently un-decodable on-chain → UnknownOpcode at first
///     use, hard to debug)
///   - `EVERY_OP` falling out of sync with the enum
///
/// The exhaustive match in the body fails to compile if a new variant
/// is added, forcing `EVERY_OP` to grow in lockstep.
#[test]
fn every_op_byte_round_trips() {
    // Force compile-error when a new variant is added without updating
    // EVERY_OP. The match is exhaustive over `Op`.
    fn variant_seen(o: Op) -> bool {
        match o {
            Op::LoadQuote
            | Op::LoadQuoteU
            | Op::LoadQuoteU64
            | Op::LoadQuoteI64
            | Op::LoadQuotesTimestampSec
            | Op::LoadQuotesTimestampNanos
            | Op::LoadSize
            | Op::LoadSide
            | Op::LoadConst
            | Op::LoadNowSlot
            | Op::LoadNowUnixSec
            | Op::LoadInvBase
            | Op::LoadInvQuote
            | Op::LoadBaseDecimals
            | Op::LoadQuoteDecimals
            | Op::LoadLastTradeSlot
            | Op::LoadIxDepth
            | Op::LoadTxFlags
            | Op::EntrypointIs
            | Op::IsSignedBy
            | Op::LoadI64
            | Op::LoadU64
            | Op::StoreI64
            | Op::StoreU64
            | Op::Dup
            | Op::Swap
            | Op::Drop
            | Op::Pick
            | Op::Add
            | Op::Sub
            | Op::Mul
            | Op::Div
            | Op::Muldiv
            | Op::Neg
            | Op::Abs
            | Op::Sqrt
            | Op::DivCeil
            | Op::Mod
            | Op::Pow
            | Op::MulShr
            | Op::MulQ48
            | Op::DivQ48
            | Op::MuladdQ48
            | Op::And
            | Op::Or
            | Op::Xor
            | Op::Not
            | Op::Shl
            | Op::Shr
            | Op::Min
            | Op::Max
            | Op::Clamp
            | Op::TierLookup
            | Op::LerpLookup
            | Op::TierLookupDyn
            | Op::TierLookup2
            | Op::CmpLt
            | Op::CmpGt
            | Op::CmpLe
            | Op::CmpGe
            | Op::CmpEq
            | Op::Select
            | Op::IfLt
            | Op::IfGt
            | Op::IfLe
            | Op::IfGe
            | Op::IfEq
            | Op::Jmp
            | Op::Halt
            | Op::Reject
            | Op::AssertNonzero => true,
        }
    }
    for &o in EVERY_OP {
        assert!(variant_seen(o), "stale EVERY_OP entry: {o:?}");
        let byte = o as u8;
        let decoded = Op::from_byte_const(byte).unwrap_or_else(|| {
            panic!("Op::{o:?} (byte 0x{byte:02X}) missing from from_byte_const")
        });
        assert_eq!(decoded, o, "Op::{o:?} round-trip mismatch");
        // Stack delta and instr_len are total functions over Op (compile-
        // enforced exhaustive matches). Touch them so any future variant
        // that forgets an arm fails here, not somewhere downstream.
        let _ = o.instr_len();
        let _ = o.stack_delta();
    }
}

/// Byte → opcode collision check: no two Op variants share a byte.
#[test]
fn op_bytes_are_unique() {
    let mut seen = [false; 256];
    for &o in EVERY_OP {
        let b = o as u8;
        assert!(!seen[b as usize], "byte 0x{b:02X} maps to two variants");
        seen[b as usize] = true;
    }
}

/// The bytes formerly assigned to retired opcodes must NOT decode. If
/// they did, a malicious curve could ship bytecode that runs under
/// arm-stale semantics from a prior version.
#[test]
fn retired_opcode_bytes_are_unmapped() {
    // 0x2B / 0x2C: `EmitFeeAmount` / `EmitFeeSide` (retired 2026-05-15).
    // Curves don't declare fees anymore; the protocol derives the fee from
    // `amount_in` (input-derived).
    //
    // 0x4B: `HaltQ48` (retired 2026-05-30). The Q48 settlement path was
    // added speculatively to fix a curve-calibration drift; later disassembly
    // proved the drift was calibration-bound, not precision-bound, so the
    // opcode never had a real user.
    for retired in [0x2Bu8, 0x2C, 0x4B] {
        assert!(
            Op::from_byte_const(retired).is_none(),
            "retired byte 0x{retired:02X} re-introduced — would break a prior-version invariant",
        );
    }
}

/// `mutates_userspace` must be a strict subset of the writers actually
/// implemented by the dispatcher. If it ever returns false for an op
/// that writes userspace, `is_stateless` would mis-report a stateful
/// curve as cacheable — aggregators would route stale quotes.
#[test]
fn mutates_userspace_matches_dispatcher() {
    for &o in EVERY_OP {
        let claims_mutates = o.mutates_userspace();
        let is_store = matches!(o, Op::StoreI64 | Op::StoreU64);
        assert_eq!(
            claims_mutates, is_store,
            "Op::{o:?}: mutates_userspace={claims_mutates} but dispatcher writes={is_store}. \
             If you added a new writing opcode, update `mutates_userspace` AND this test \
             in the same commit, then audit `is_stateless` callers.",
        );
    }
}

fn make_inputs<'a>(userspace: &'a mut [u8], quotes: &'a [i32]) -> Inputs<'a> {
    Inputs {
        quotes,
        userspace,
        size: 0,
        side: 0,
        current_slot: 0,
        inventory_base: 0,
        inventory_quote: 0,
        current_unix_sec: 0,
        base_decimals: 0,
        quote_decimals: 0,
        quotes_timestamp_ns: 0,
        last_update_slot: 0,
        tx: TxContext::DIRECT,
    }
}

fn run_simple(bytecode: &[u8]) -> Result<EvalResult, RuntimeError> {
    let mut us = vec![0u8; 1024];
    let inputs = make_inputs(&mut us, &[]);
    evaluate(bytecode, inputs)
}

#[test]
fn load_const_halt() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(42i64.to_le_bytes());
    code.push(Op::Halt as u8);
    let r = run_simple(&code).unwrap();
    assert_eq!(r.amount_out(), 42);
    assert_eq!(r, EvalResult(42));
}

#[test]
fn add_works() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(10i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(5i64.to_le_bytes());
    code.push(Op::Add as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 15);
}

#[test]
fn pick_0_equivalent_to_dup() {
    // Pick 0 should copy top-of-stack — same as Dup.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(42i64.to_le_bytes());
    code.push(Op::Pick as u8);
    code.push(0); // depth-from-top = 0
    code.push(Op::Add as u8); // 42 + 42 = 84
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 84);
}

#[test]
fn pick_copies_deeper_stack_value() {
    // Build stack [10, 20, 30], then Pick 2 (bottom value), then Add.
    // After Pick 2: stack = [10, 20, 30, 10]; Add: [10, 20, 40]; Halt → 40.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(10i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(20i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(30i64.to_le_bytes());
    code.push(Op::Pick as u8);
    code.push(2); // copy the 3rd-from-top (value 10)
    code.push(Op::Add as u8); // 30 + 10 = 40
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 40);
}

#[test]
fn pick_beyond_stack_depth_underflows() {
    // Pick 3 with only 2 values on the stack → StackUnderflow.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(10i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(20i64.to_le_bytes());
    code.push(Op::Pick as u8);
    code.push(3); // out of range — only 2 items on stack
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::StackUnderflow));
}

#[test]
fn unknown_opcode() {
    let code = [0xCC, Op::Halt as u8];
    assert_eq!(run_simple(&code), Err(RuntimeError::UnknownOpcode(0xCC)));
}

#[test]
fn truncated_immediate() {
    let code = [Op::LoadConst as u8, 0x00, 0x00];
    assert_eq!(run_simple(&code), Err(RuntimeError::TruncatedImmediate));
}

#[test]
fn halt_empty_stack() {
    let code = [Op::Halt as u8];
    assert_eq!(run_simple(&code), Err(RuntimeError::StackUnderflow));
}

#[test]
fn halt_not_reached() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    assert_eq!(run_simple(&code), Err(RuntimeError::HaltNotReached));
}

#[test]
fn forward_jump_works() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    code.extend(9i16.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(99i64.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 1);
}

/// `Jmp imm=0` is a fall-through, NOT a backward loop. The step counter
/// still ticks, so even an artificial `Jmp 0` chain is bounded by
/// MAX_STEPS. Verify it doesn't fault on the cheapest legal forward jump.
#[test]
fn jump_zero_imm_falls_through() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(7i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    code.extend(0i16.to_le_bytes()); // jump to next instruction
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 7);
}

/// Forward jump landing at `bytecode_len - 1` is legal. If that byte is
/// `Halt`, the curve halts cleanly. Pins the `target >= bytecode_len`
/// half of the bounds gate vs the strict `>` we'd accidentally accept.
#[test]
fn jump_to_last_byte_is_legal_when_halt() {
    // [LoadConst 42, Jmp +0_skip_to_halt, ..., Halt]
    let mut code = vec![Op::LoadConst as u8];
    code.extend(42i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    // imm chosen so the target == bytecode.len() - 1 (the Halt byte).
    // After Jmp instr (3 bytes), `after = pc + 3`. We want target = last
    // byte. We'll layout: LoadConst(9) + Jmp(3) + filler(N) + Halt(1).
    // total = 9 + 3 + N + 1 = 13 + N. Target should = 12 + N. After-Jmp
    // pc = 12. imm = N. Pick N = 5.
    code.extend(5i16.to_le_bytes());
    code.extend([0u8; 5]); // unreachable filler (would be UnknownOpcode if run)
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 42);
}

/// Jump exactly to bytecode end is rejected (target >= bytecode_len).
/// Distinct from `jump_past_end_rejected` which tests target > end.
#[test]
fn jump_to_one_past_end_rejected() {
    // [LoadConst 1, Jmp +imm, Halt] — choose imm so target == bytecode.len().
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    // After Jmp at pc=9, after = 12, total len = 13 (Halt at 12). For
    // target = 13 (one past end) we need imm = 1.
    code.extend(1i16.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::InvalidJumpTarget));
}

/// Jump targeted *into* a multi-byte immediate's bytes lands on whatever
/// the immediate happens to encode. The dispatcher must treat that byte
/// as a regular opcode (decode via `from_byte`, dispatch or
/// `UnknownOpcode`) — no special-case alignment check is needed. This
/// confirms the "mid-instruction landing is safe" invariant.
#[test]
fn jump_into_immediate_decodes_byte_as_opcode() {
    // [Jmp +imm targeting byte 5, LoadConst <8 imm bytes containing Halt=0x23>, Halt]
    //
    // We hand-craft the LoadConst's i64 immediate so byte at offset
    // pc=5 (the 3rd byte of the imm) is 0x23 (Halt). When Jmp lands
    // there, the dispatcher tries to execute Halt on whatever the stack
    // holds. Stack is empty → StackUnderflow.
    let mut code = vec![Op::Jmp as u8];
    code.extend(2i16.to_le_bytes()); // after Jmp instr (pc=3) + imm=2 → target=5
    code.push(Op::LoadConst as u8); // pc=3 (post-Jmp), but we jumped past it
    code.extend(0x0000_0023_0000_0000u64.to_le_bytes()); // byte at imm offset 3 = Halt
    code.push(Op::Halt as u8);
    // At target=5, byte = 0x00 (the 6th byte of the encoded sequence). Let me
    // recount: index 0=Jmp, 1..3=imm, 3=LoadConst, 4..12=LoadConst's imm, 12=Halt.
    // Target=5 hits LoadConst imm byte index 1, which is the 2nd byte of
    // 0x0000_0023_0000_0000 LE = ... let's encode: LE bytes of u64 are
    // [0x00, 0x00, 0x00, 0x00, 0x23, 0x00, 0x00, 0x00]. So:
    //   pc=4: 0x00
    //   pc=5: 0x00  ← target lands here; 0x00 is unmapped → UnknownOpcode(0)
    let r = run_simple(&code);
    assert!(
        matches!(r, Err(RuntimeError::UnknownOpcode(0x00))),
        "expected UnknownOpcode(0x00), got {r:?}",
    );
}

/// Step limit catches a "wide jump chain" (e.g. every Jmp +0 fires in
/// sequence). With MAX_STEPS = 1024 and instr_len(Jmp) = 3, a bytecode
/// full of `Jmp +0` would consume one step per jump. Verify a chain
/// shorter than MAX_STEPS terminates cleanly with HaltNotReached (no
/// loop), and a chain longer than MAX_STEPS would hit StepLimitExceeded
/// (but that's already covered by other step tests; here we just pin
/// that jumps tick the counter).
#[test]
fn jumps_consume_steps() {
    // Build a chain of 100 `Jmp +0` followed by `LoadConst 7; Halt`.
    let mut code = Vec::new();
    for _ in 0..100 {
        code.push(Op::Jmp as u8);
        code.extend(0i16.to_le_bytes());
    }
    code.push(Op::LoadConst as u8);
    code.extend(7i64.to_le_bytes());
    code.push(Op::Halt as u8);
    // 100 < MAX_STEPS (1024), so this should run to Halt.
    assert_eq!(run_simple(&code).unwrap().amount_out(), 7);
}

#[test]
fn backward_jump_rejected() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    code.extend((-3i16).to_le_bytes());
    assert_eq!(run_simple(&code), Err(RuntimeError::InvalidJumpTarget));
}

#[test]
fn jump_past_end_rejected() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::Jmp as u8);
    code.extend(9999i16.to_le_bytes());
    assert_eq!(run_simple(&code), Err(RuntimeError::InvalidJumpTarget));
}

#[test]
fn load_i64_in_bounds() {
    let mut us = vec![0u8; 16];
    us[0..8].copy_from_slice(&123_i64.to_le_bytes());
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadI64 as u8];
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 123);
}

#[test]
fn load_i64_oob() {
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadI64 as u8];
    code.extend(100u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::UserspaceOutOfBounds)
    );
}

#[test]
fn store_i64_in_bounds_then_load() {
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend(7777i64.to_le_bytes());
    code.push(Op::StoreI64 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::LoadI64 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 7777);
}

#[test]
fn store_i64_oob() {
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::StoreI64 as u8);
    code.extend(100u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::UserspaceOutOfBounds)
    );
}

#[test]
fn load_i64_vs_u64_sign() {
    // Same 8 bytes (all 0xFF = -1 / u64::MAX); LoadI64 sign-extends, LoadU64
    // zero-extends. Observe via `> 0`: signed is -1 (not > 0), unsigned is
    // u64::MAX (> 0). The CmpGt keeps the probe to 0/1 regardless of magnitude.
    let sign_probe = |op: Op| {
        let mut us = vec![0xFFu8; 16];
        let inputs = make_inputs(&mut us, &[]);
        let mut code = vec![op as u8];
        code.extend(0u32.to_le_bytes());
        code.push(Op::LoadConst as u8);
        code.extend(0i64.to_le_bytes());
        code.push(Op::CmpGt as u8);
        code.push(Op::Halt as u8);
        evaluate(&code, inputs).unwrap().amount_out()
    };
    assert_eq!(sign_probe(Op::LoadI64), 0, "LoadI64 sign-extends 0xFF.. to -1");
    assert_eq!(sign_probe(Op::LoadU64), 1, "LoadU64 zero-extends 0xFF.. to u64::MAX");
}

#[test]
fn reject_propagates_reason() {
    let code = [Op::Reject as u8, 0x42];
    assert_eq!(run_simple(&code), Err(RuntimeError::Reject(0x42)));
}

#[test]
fn assert_nonzero_passes_on_nonzero() {
    // LoadConst 1, AssertNonzero 0xAA, Halt → amount_out = 1
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::AssertNonzero as u8);
    code.push(0xAAu8);
    code.push(Op::LoadConst as u8);
    code.extend(42i64.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 42);
}

#[test]
fn assert_nonzero_rejects_on_zero() {
    // LoadConst 0, AssertNonzero 0x05 (StandardReject::SizeOutOfRange), Halt
    let mut code = vec![Op::LoadConst as u8];
    code.extend(0i64.to_le_bytes());
    code.push(Op::AssertNonzero as u8);
    code.push(0x05u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::Reject(0x05)));
}

#[test]
fn assert_nonzero_composes_with_cmp() {
    // assert inv_quote >= 1000 in DSL form:
    //   LoadInvQuote, LoadConst 1000, CmpGe, AssertNonzero 0x05
    // With inv_quote = 500 → CmpGe yields 0 → AssertNonzero rejects.
    let mut us = vec![0u8; 0];
    let mut inputs = make_inputs(&mut us, &[]);
    inputs.inventory_quote = 500;
    let mut code = vec![Op::LoadInvQuote as u8];
    code.push(Op::LoadConst as u8);
    code.extend(1000i64.to_le_bytes());
    code.push(Op::CmpGe as u8);
    code.push(Op::AssertNonzero as u8);
    code.push(0x05u8);
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs), Err(RuntimeError::Reject(0x05)));
}

#[test]
fn tier_lookup_works() {
    // Table at userspace[0]: N=2, (10, 100), (20, 200)
    let mut us = vec![0u8; 64];
    us[0..8].copy_from_slice(&2i64.to_le_bytes());
    us[8..16].copy_from_slice(&10i64.to_le_bytes());
    us[16..24].copy_from_slice(&100i64.to_le_bytes());
    us[24..32].copy_from_slice(&20i64.to_le_bytes());
    us[32..40].copy_from_slice(&200i64.to_le_bytes());
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend(15i64.to_le_bytes());
    code.push(Op::TierLookup as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    // For key=15: t0=10, t1=20. key < t1 → returns v1=200.
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 200);
}

#[test]
fn tier_lookup_bad_n_rejected() {
    let mut us = vec![0u8; 16];
    us[0..8].copy_from_slice(&0i64.to_le_bytes());
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend(0i64.to_le_bytes());
    code.push(Op::TierLookup as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::TierTableMalformed)
    );
}

#[test]
fn lerp_lookup_interpolates() {
    let mut us = vec![0u8; 64];
    us[0..8].copy_from_slice(&2i64.to_le_bytes());
    us[8..16].copy_from_slice(&0i64.to_le_bytes());
    us[16..24].copy_from_slice(&0i64.to_le_bytes());
    us[24..32].copy_from_slice(&100i64.to_le_bytes());
    us[32..40].copy_from_slice(&1000i64.to_le_bytes());
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend(50i64.to_le_bytes());
    code.push(Op::LerpLookup as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 500);
}

#[test]
fn stack_overflow_caught() {
    let mut code = Vec::new();
    for _ in 0..(MAX_STACK_DEPTH as usize + 1) {
        code.push(Op::LoadConst as u8);
        code.extend(1i64.to_le_bytes());
    }
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::StackOverflow));
}

#[test]
fn div_by_zero() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(10i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(0i64.to_le_bytes());
    code.push(Op::Div as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::DivByZero));
}

#[test]
fn quote_load() {
    // LoadQuote sign-extends the i32 slot. Slot 1 = -7; `amount_out` rejects a
    // negative Halt, so negate first and observe 7 — proving the load was -7.
    let mut us = vec![0u8; 16];
    let quotes = [42i32, -7];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuote as u8, 1];
    code.push(Op::Neg as u8);
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 7);
}

#[test]
fn quote_oob() {
    let mut us = vec![0u8; 16];
    let quotes = [42i32];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuote as u8, 99];
    code.push(Op::Halt as u8);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::QuoteIndexOutOfBounds)
    );
}

#[test]
fn quote_u64_two_box() {
    // boxes [2, 1] → (1 << 32) | 2.
    let mut us = vec![0u8; 16];
    let quotes = [2i32, 1];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuoteU64 as u8, 0];
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), (1u64 << 32) | 2);
}

#[test]
fn quote_u64_low_box_masked_not_sign_extended() {
    // Low box = -1 (i32 0xFFFFFFFF) must contribute only its raw 32 bits, NOT
    // sign-extend into bits 32..63. High box = 0 → value is 0x00000000_FFFFFFFF.
    let mut us = vec![0u8; 16];
    let quotes = [-1i32, 0];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuoteU64 as u8, 0];
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 0xFFFF_FFFFu64);
    // LoadQuoteI64 agrees here (high box 0 → positive), proving the low box is
    // masked, not sign-extended, on the signed variant too.
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuoteI64 as u8, 0];
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 0xFFFF_FFFFu64);
}

#[test]
fn quote_i64_high_box_signs() {
    // boxes [0, -1] → 0xFFFFFFFF_00000000 as i64 = -(1 << 32). A negative value
    // can't be Halted (amount_out rejects it), so negate first and observe
    // `1 << 32` — proving the high box signed the combined i64.
    let mut us = vec![0u8; 16];
    let quotes = [0i32, -1];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuoteI64 as u8, 0];
    code.push(Op::Neg as u8);
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 1u64 << 32);
}

#[test]
fn quote_u64_high_box_oob() {
    // Low box in range but slot+1 is not → QuoteIndexOutOfBounds.
    let mut us = vec![0u8; 16];
    let quotes = [1i32];
    let inputs = make_inputs(&mut us, &quotes);
    let mut code = vec![Op::LoadQuoteU64 as u8, 0];
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs), Err(RuntimeError::QuoteIndexOutOfBounds));
}

#[test]
fn load_u64_oob() {
    let mut us = vec![0u8; 4];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadU64 as u8];
    code.extend(0u32.to_le_bytes()); // need 8 bytes; only 4 available
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs), Err(RuntimeError::UserspaceOutOfBounds));
}

#[test]
fn store_u64_round_trips_via_load_u64() {
    // StoreU64 a positive value, read it back with LoadU64.
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend((1i64 << 40).to_le_bytes());
    code.push(Op::StoreU64 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::LoadU64 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 1u64 << 40);
}

#[test]
fn store_u64_rejects_negative() {
    let mut us = vec![0u8; 16];
    let inputs = make_inputs(&mut us, &[]);
    let mut code = vec![Op::LoadConst as u8];
    code.extend((-1i64).to_le_bytes());
    code.push(Op::StoreU64 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    assert_eq!(evaluate(&code, inputs), Err(RuntimeError::ResultOverflowsU64));
}

#[test]
fn pow_basic() {
    // 3 ^ 4 = 81. Push order: base=3, exp=4 → pops exp, base.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(3i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(4i64.to_le_bytes());
    code.push(Op::Pow as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 81);
}

#[test]
fn pow_zero_exponent_is_one() {
    // 12345 ^ 0 = 1.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(12_345i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(0i64.to_le_bytes());
    code.push(Op::Pow as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 1);
}

#[test]
fn pow_negative_exponent_rejected() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(2i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend((-1i64).to_le_bytes());
    code.push(Op::Pow as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::ArithmeticOverflow));
}

#[test]
fn pow_overflow_rejected() {
    // 2 ^ 200 overflows i128 → ArithmeticOverflow.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(2i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(200i64.to_le_bytes());
    code.push(Op::Pow as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::ArithmeticOverflow));
}

#[test]
fn mul_shr_basic() {
    // 100 * 256 >> 8 = 100
    let mut code = vec![Op::LoadConst as u8];
    code.extend(100i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(256i64.to_le_bytes());
    code.push(Op::MulShr as u8);
    code.push(8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 100);
}

#[test]
fn mul_shr_q24() {
    // Encode a Q24 price: 86.378821 * 2^24 = 1_449_372_226
    // Then multiply by amount_in (atomic) and shift back by 24.
    // 100_000_000 (0.1 SOL in atomic) * 1_449_372_226 >> 24 = 8_637_882_100 (≈ $8.64 in 6-dec USDC, but human units here)
    let amount = 100_000_000i64;
    let mid_q24 = 1_449_372_226i64;
    let mut code = vec![Op::LoadConst as u8];
    code.extend(amount.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(mid_q24.to_le_bytes());
    code.push(Op::MulShr as u8);
    code.push(24);
    code.push(Op::Halt as u8);
    let expected = (i128::from(amount) * i128::from(mid_q24)) >> 24;
    assert_eq!(run_simple(&code).unwrap().amount_out() as i128, expected);
}

#[test]
fn mul_shr_rejects_oversized_shift() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(1i64.to_le_bytes());
    code.push(Op::MulShr as u8);
    code.push(128); // > 127 → InvalidShiftAmount
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::InvalidShiftAmount));
}

#[test]
fn mul_q48_matches_mul_shr_48() {
    let a = 1_000_000_000_000i64;
    let b_q48 = 88_495_000i64;
    let mut shr_code = vec![Op::LoadConst as u8];
    shr_code.extend(a.to_le_bytes());
    shr_code.push(Op::LoadConst as u8);
    shr_code.extend(b_q48.to_le_bytes());
    shr_code.push(Op::MulShr as u8);
    shr_code.push(48);
    shr_code.push(Op::Halt as u8);

    let mut q48_code = vec![Op::LoadConst as u8];
    q48_code.extend(a.to_le_bytes());
    q48_code.push(Op::LoadConst as u8);
    q48_code.extend(b_q48.to_le_bytes());
    q48_code.push(Op::MulQ48 as u8);
    q48_code.push(Op::Halt as u8);

    assert_eq!(
        run_simple(&shr_code).unwrap().amount_out(),
        run_simple(&q48_code).unwrap().amount_out(),
    );
}

#[test]
fn div_q48_inverse() {
    // (1 << 48) / 1 = 1 << 48 = 281474976710656
    let mut code = vec![Op::LoadConst as u8];
    code.extend(1i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(1i64.to_le_bytes());
    code.push(Op::DivQ48 as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 1u64 << 48);
}

#[test]
fn div_q48_zero_divisor() {
    let mut code = vec![Op::LoadConst as u8];
    code.extend(100i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(0i64.to_le_bytes());
    code.push(Op::DivQ48 as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code), Err(RuntimeError::DivByZero));
}

#[test]
fn muladd_q48_basic() {
    // ((100 * (2<<48)) >> 48) + 50 = (100 * 2) + 50 = 250
    // Push order: a=100, b=(2<<48), c=50  →  pops c, b, a
    let mut code = vec![Op::LoadConst as u8];
    code.extend(100i64.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend((2i64 << 48).to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(50i64.to_le_bytes());
    code.push(Op::MuladdQ48 as u8);
    code.push(Op::Halt as u8);
    assert_eq!(run_simple(&code).unwrap().amount_out(), 250);
}

#[test]
fn tier_lookup2_basic() {
    // Build a 3-row table at userspace[0]:
    //   (10, 100, 1000)
    //   (20, 200, 2000)
    //   (30, 300, 3000)
    let mut us = vec![0u8; 256];
    let n: i64 = 3;
    us[0..8].copy_from_slice(&n.to_le_bytes());
    let rows: &[(i64, i64, i64)] = &[(10, 100, 1000), (20, 200, 2000), (30, 300, 3000)];
    for (i, (t, a, b)) in rows.iter().enumerate() {
        let off = 8 + i * 24;
        us[off..off + 8].copy_from_slice(&t.to_le_bytes());
        us[off + 8..off + 16].copy_from_slice(&a.to_le_bytes());
        us[off + 16..off + 24].copy_from_slice(&b.to_le_bytes());
    }

    // key = 15 → first row where key < t: t=20, so (200, 2000). value_b on top.
    let mut code = vec![Op::LoadConst as u8];
    code.extend(15i64.to_le_bytes());
    code.push(Op::TierLookup2 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    let inputs = make_inputs(&mut us, &[]);
    let r = evaluate(&code, inputs).unwrap();
    // Halt returns top of stack = value_b = 2000
    assert_eq!(r.amount_out(), 2000);
}

#[test]
fn tier_lookup2_above_all_thresholds() {
    // Same table; key = 999 (above every threshold) → last row (300, 3000)
    let mut us = vec![0u8; 256];
    let n: i64 = 3;
    us[0..8].copy_from_slice(&n.to_le_bytes());
    let rows: &[(i64, i64, i64)] = &[(10, 100, 1000), (20, 200, 2000), (30, 300, 3000)];
    for (i, (t, a, b)) in rows.iter().enumerate() {
        let off = 8 + i * 24;
        us[off..off + 8].copy_from_slice(&t.to_le_bytes());
        us[off + 8..off + 16].copy_from_slice(&a.to_le_bytes());
        us[off + 16..off + 24].copy_from_slice(&b.to_le_bytes());
    }
    let mut code = vec![Op::LoadConst as u8];
    code.extend(999i64.to_le_bytes());
    code.push(Op::TierLookup2 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    let inputs = make_inputs(&mut us, &[]);
    assert_eq!(evaluate(&code, inputs).unwrap().amount_out(), 3000);
}

#[test]
fn tier_lookup2_non_ascending_rejects() {
    // Thresholds not strict-ascending → TierTableMalformed.
    let mut us = vec![0u8; 256];
    let n: i64 = 2;
    us[0..8].copy_from_slice(&n.to_le_bytes());
    let rows: &[(i64, i64, i64)] = &[(20, 100, 1000), (10, 200, 2000)];
    for (i, (t, a, b)) in rows.iter().enumerate() {
        let off = 8 + i * 24;
        us[off..off + 8].copy_from_slice(&t.to_le_bytes());
        us[off + 8..off + 16].copy_from_slice(&a.to_le_bytes());
        us[off + 16..off + 24].copy_from_slice(&b.to_le_bytes());
    }
    let mut code = vec![Op::LoadConst as u8];
    code.extend(50i64.to_le_bytes());
    code.push(Op::TierLookup2 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    let inputs = make_inputs(&mut us, &[]);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::TierTableMalformed)
    );
}

#[test]
fn tier_lookup2_table_too_big_for_userspace() {
    // Claim 3 rows but userspace only has room for 1 row + header.
    let mut us = vec![0u8; 32]; // 8 (header) + 24 (one row) = 32
    let n: i64 = 3;
    us[0..8].copy_from_slice(&n.to_le_bytes());
    let mut code = vec![Op::LoadConst as u8];
    code.extend(5i64.to_le_bytes());
    code.push(Op::TierLookup2 as u8);
    code.extend(0u32.to_le_bytes());
    code.push(Op::Halt as u8);
    let inputs = make_inputs(&mut us, &[]);
    assert_eq!(
        evaluate(&code, inputs),
        Err(RuntimeError::TierTableMalformed)
    );
}

#[test]
fn mid_haircut_style_curve_compiles_and_runs() {
    // Sketch a mid-times-amount curve: load mid (Q48), multiply by amount,
    // shift back by 48. End-to-end test that MulQ48 + Q-format math works in
    // a realistic pricing pattern.
    let mid_q48: i64 = 88_495 * (1i64 << 32); // ~88.495 in Q48-like (compressed)
    let amount: i64 = 1_000_000;

    let mut code = vec![Op::LoadConst as u8];
    code.extend(amount.to_le_bytes());
    code.push(Op::LoadConst as u8);
    code.extend(mid_q48.to_le_bytes());
    code.push(Op::MulQ48 as u8);
    code.push(Op::Halt as u8);

    let expected = (i128::from(amount) * i128::from(mid_q48)) >> 48;
    assert_eq!(run_simple(&code).unwrap().amount_out() as i128, expected);
}

// ── Transaction-introspection opcodes (TxContext) ──────────────────────────

const JUPITER_LIKE: [u8; 32] = [0xAA; 32];
const TAKER_LIKE: [u8; 32] = [0xBB; 32];

fn make_tx_inputs<'a>(userspace: &'a mut [u8], tx: TxContext<'a>) -> Inputs<'a> {
    let mut inputs = make_inputs(userspace, &[]);
    inputs.tx = tx;
    inputs
}

fn push_pubkey_op(code: &mut Vec<u8>, op: Op, key: &[u8; 32]) {
    code.push(op as u8);
    code.extend_from_slice(key);
}

#[test]
fn load_ix_depth_pushes_host_value() {
    let code = [Op::LoadIxDepth as u8, Op::Halt as u8];
    let mut us = vec![];
    let tx = TxContext {
        ix_depth: 3,
        ..TxContext::DIRECT
    };
    let r = evaluate(&code, make_tx_inputs(&mut us, tx)).unwrap();
    assert_eq!(r.amount_out(), 3);
}

#[test]
fn load_tx_flags_pushes_host_bits() {
    use crate::vm::{TX_FLAG_FLASHLOAN, TX_FLAG_JITO_TIP};
    let code = [Op::LoadTxFlags as u8, Op::Halt as u8];
    let mut us = vec![];
    let flags = TX_FLAG_JITO_TIP | TX_FLAG_FLASHLOAN;
    let tx = TxContext {
        tx_flags: flags,
        ..TxContext::DIRECT
    };
    let r = evaluate(&code, make_tx_inputs(&mut us, tx)).unwrap();
    assert_eq!(r.amount_out() as u64, flags);
}

#[test]
fn entrypoint_is_matches_and_mismatches() {
    let mut code = Vec::new();
    push_pubkey_op(&mut code, Op::EntrypointIs, &JUPITER_LIKE);
    code.push(Op::Halt as u8);

    let mut us = vec![];
    let tx = TxContext {
        entrypoint_program: JUPITER_LIKE,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        1
    );

    let tx = TxContext {
        entrypoint_program: TAKER_LIKE,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        0
    );

    // DIRECT default has a zeroed entrypoint — every probe pushes 0.
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, TxContext::DIRECT))
            .unwrap()
            .amount_out(),
        0
    );
}

#[test]
fn is_signed_by_scans_signer_set() {
    let mut code = Vec::new();
    push_pubkey_op(&mut code, Op::IsSignedBy, &TAKER_LIKE);
    code.push(Op::Halt as u8);

    let mut us = vec![];
    let signers = [[0x01u8; 32], TAKER_LIKE, [0x02u8; 32]];
    let tx = TxContext {
        signers: &signers,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        1
    );

    let signers = [[0x01u8; 32]];
    let tx = TxContext {
        signers: &signers,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        0
    );

    // Empty signer set (DIRECT default) → 0.
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, TxContext::DIRECT))
            .unwrap()
            .amount_out(),
        0
    );
}

#[test]
fn truncated_pubkey_immediate_errors() {
    // EntrypointIs expects 32 imm bytes; supply 5.
    let code = [Op::EntrypointIs as u8, 0x01, 0x02, 0x03, 0x04, 0x05];
    assert_eq!(run_simple(&code), Err(RuntimeError::TruncatedImmediate));
}

#[test]
fn direct_caller_pattern_composes_from_depth_and_entrypoint() {
    // The SDK-level `DirectCallerIs(p)` pattern: depth <= 2 AND entrypoint == p.
    // depth, 2, CmpLe → 0/1; EntrypointIs → 0/1; And; Halt.
    let mut code = vec![Op::LoadIxDepth as u8, Op::LoadConst as u8];
    code.extend(2i64.to_le_bytes());
    code.push(Op::CmpLe as u8);
    push_pubkey_op(&mut code, Op::EntrypointIs, &JUPITER_LIKE);
    code.push(Op::And as u8);
    code.push(Op::Halt as u8);

    let mut us = vec![];
    // Router flow: depth 2, entrypoint = jupiter → 1.
    let tx = TxContext {
        ix_depth: 2,
        entrypoint_program: JUPITER_LIKE,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        1
    );
    // Wrapped flow: depth 3, same entrypoint → 0 (caller unknowable).
    let tx = TxContext {
        ix_depth: 3,
        entrypoint_program: JUPITER_LIKE,
        ..TxContext::DIRECT
    };
    assert_eq!(
        evaluate(&code, make_tx_inputs(&mut us, tx))
            .unwrap()
            .amount_out(),
        0
    );
}

