#![cfg(feature = "test")]

use percolator::*;
use percolator::wide_math::U256;

// ============================================================================
// Helpers
// ============================================================================

fn default_params() -> RiskParams {
    RiskParams {
        warmup_period_slots: 100,
        maintenance_margin_bps: 500,    // 5%
        initial_margin_bps: 1000,       // 10% — MUST be > maintenance
        trading_fee_bps: 10,
        max_accounts: 64,
        new_account_fee: U128::new(1000),
        maintenance_fee_per_slot: U128::new(1),
        max_crank_staleness_slots: 1000,
        liquidation_fee_bps: 100,
        liquidation_fee_cap: U128::new(1_000_000),
        liquidation_buffer_bps: 50,
        min_liquidation_abs: U128::new(0),
        min_initial_deposit: U128::new(1000),
        min_nonzero_mm_req: 1,
        min_nonzero_im_req: 2,
        insurance_floor: U128::ZERO,
    }
}

/// Build a size_q from a quantity in base units.
/// size_q = quantity * POS_SCALE  (signed)
fn make_size_q(quantity: i64) -> i128 {
    let abs_qty = (quantity as i128).unsigned_abs();
    let scaled = abs_qty.checked_mul(POS_SCALE).expect("make_size_q overflow");
    assert!(scaled <= i128::MAX as u128, "make_size_q: exceeds i128");
    if quantity < 0 {
        -(scaled as i128)
    } else {
        scaled as i128
    }
}

/// Helper: create engine, add two users with deposits, run initial crank.
/// Returns (engine, user_a_idx, user_b_idx).
fn setup_two_users(deposit_a: u128, deposit_b: u128) -> (RiskEngine, u16, u16) {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add user a");
    let b = engine.add_user(1000).expect("add user b");

    // Deposit before crank so accounts have capital and are not GC'd
    if deposit_a > 0 {
        engine.deposit(a, deposit_a, oracle, slot).expect("deposit a");
    }
    if deposit_b > 0 {
        engine.deposit(b, deposit_b, oracle, slot).expect("deposit b");
    }

    // Initial crank so trades/withdrawals pass freshness check
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("initial crank");

    (engine, a, b)
}

// ============================================================================
// 1. Basic engine creation and parameter validation
// ============================================================================

#[test]
fn test_engine_creation() {
    let engine = RiskEngine::new(default_params());
    assert_eq!(engine.vault.get(), 0);
    assert_eq!(engine.insurance_fund.balance.get(), 0);
    assert_eq!(engine.current_slot, 0);
    assert_eq!(engine.num_used_accounts, 0);
    assert!(engine.check_conservation());
}

#[test]
#[should_panic(expected = "maintenance_margin_bps must be strictly less than initial_margin_bps")]
fn test_params_require_mm_lt_im() {
    let mut params = default_params();
    params.maintenance_margin_bps = 1000;
    params.initial_margin_bps = 1000; // equal => should panic
    let _ = RiskEngine::new(params);
}

#[test]
#[should_panic(expected = "maintenance_margin_bps must be strictly less than initial_margin_bps")]
fn test_params_require_mm_lt_im_greater() {
    let mut params = default_params();
    params.maintenance_margin_bps = 1500;
    params.initial_margin_bps = 1000; // mm > im => should panic
    let _ = RiskEngine::new(params);
}

// ============================================================================
// 2. add_user and add_lp
// ============================================================================

#[test]
fn test_add_user() {
    let mut engine = RiskEngine::new(default_params());
    let idx = engine.add_user(1000).expect("add_user");
    assert_eq!(idx, 0);
    assert!(engine.is_used(idx as usize));
    assert_eq!(engine.num_used_accounts, 1);
    // Fee of 1000 goes to insurance; excess = 0
    assert_eq!(engine.accounts[idx as usize].capital.get(), 0);
    assert_eq!(engine.insurance_fund.balance.get(), 1000);
    assert_eq!(engine.vault.get(), 1000);
    assert!(engine.accounts[idx as usize].is_user());
}

#[test]
fn test_add_user_with_excess() {
    let mut engine = RiskEngine::new(default_params());
    let idx = engine.add_user(5000).expect("add_user");
    // excess = 5000 - 1000 = 4000 goes to capital
    assert_eq!(engine.accounts[idx as usize].capital.get(), 4000);
    assert_eq!(engine.insurance_fund.balance.get(), 1000);
    assert_eq!(engine.vault.get(), 5000);
}

#[test]
fn test_add_user_insufficient_fee() {
    let mut engine = RiskEngine::new(default_params());
    let result = engine.add_user(500); // less than new_account_fee (1000)
    assert_eq!(result, Err(RiskError::InsufficientBalance));
}

#[test]
fn test_add_lp() {
    let mut engine = RiskEngine::new(default_params());
    let program = [1u8; 32];
    let context = [2u8; 32];
    let idx = engine.add_lp(program, context, 2000).expect("add_lp");
    assert!(engine.is_used(idx as usize));
    assert!(engine.accounts[idx as usize].is_lp());
    assert_eq!(engine.accounts[idx as usize].matcher_program, program);
    assert_eq!(engine.accounts[idx as usize].matcher_context, context);
    assert_eq!(engine.accounts[idx as usize].capital.get(), 1000); // 2000 - 1000 fee
}

// ============================================================================
// 3. deposit and withdraw
// ============================================================================

#[test]
fn test_deposit() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;
    let idx = engine.add_user(1000).expect("add_user");

    let vault_before = engine.vault.get();
    engine.deposit(idx, 10_000, oracle, slot).expect("deposit");
    assert_eq!(engine.accounts[idx as usize].capital.get(), 10_000);
    assert_eq!(engine.vault.get(), vault_before + 10_000);
    assert!(engine.check_conservation());
}

#[test]
fn test_withdraw_no_position() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;
    let idx = engine.add_user(1000).expect("add_user");

    // Deposit before crank so account is not GC'd
    engine.deposit(idx, 10_000, oracle, slot).expect("deposit");

    // Initial crank needed for freshness
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    engine.withdraw(idx, 5_000, oracle, slot).expect("withdraw");
    assert_eq!(engine.accounts[idx as usize].capital.get(), 5_000);
    assert!(engine.check_conservation());
}

#[test]
fn test_withdraw_exceeds_balance() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;
    let idx = engine.add_user(1000).expect("add_user");
    engine.deposit(idx, 5_000, oracle, slot).expect("deposit");
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    let result = engine.withdraw(idx, 10_000, oracle, slot);
    assert_eq!(result, Err(RiskError::InsufficientBalance));
}

#[test]
fn test_withdraw_succeeds_without_fresh_crank() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let idx = engine.add_user(1000).expect("add_user");
    engine.deposit(idx, 10_000, oracle, 1).expect("deposit");

    // Spec §10.4 + §0 goal 6: withdraw must not require a recent keeper crank.
    // touch_account_full accrues market state directly from the caller's oracle.
    let result = engine.withdraw(idx, 1_000, oracle, 5000);
    assert!(result.is_ok(), "withdraw must succeed without fresh crank (spec §0 goal 6)");
}

// ============================================================================
// 4. execute_trade basics
// ============================================================================

#[test]
fn test_basic_trade() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Trade: a goes long 100 units, b goes short 100 units
    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Both should have positions of the correct magnitude
    let eff_a = engine.effective_pos_q(a as usize);
    let eff_b = engine.effective_pos_q(b as usize);
    assert_eq!(eff_a, make_size_q(100), "account a must be long 100 units");
    assert_eq!(eff_b, make_size_q(-100), "account b must be short 100 units");
    assert!(engine.oi_eff_long_q > 0, "open interest must be nonzero after trade");
    assert!(engine.check_conservation());
}

#[test]
fn test_trade_succeeds_without_fresh_crank() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let a = engine.add_user(1000).expect("add user a");
    let b = engine.add_user(1000).expect("add user b");
    engine.deposit(a, 100_000, oracle, 1).expect("deposit a");
    engine.deposit(b, 100_000, oracle, 1).expect("deposit b");

    // Spec §10.5 + §0 goal 6: execute_trade must not require a recent keeper crank.
    let size_q = make_size_q(10);
    let result = engine.execute_trade(a, b, oracle, 5000, size_q, oracle);
    assert!(result.is_ok(), "trade must succeed without fresh crank (spec §0 goal 6)");
}

#[test]
fn test_trade_undercollateralized_rejected() {
    let (mut engine, a, b) = setup_two_users(1_000, 1_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Try to open a huge position that exceeds margin
    // 1000 capital, 10% IM => max notional = 10000
    // notional = |size| * oracle / POS_SCALE, so for oracle=1000,
    // 11 units => notional = 11000, requires 1100 IM
    let size_q = make_size_q(11);
    let result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    assert_eq!(result, Err(RiskError::Undercollateralized));
}

#[test]
fn test_trade_with_different_exec_price() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let exec = 990u64;
    let slot = 1u64;

    // Trade at exec_price=990 vs oracle=1000
    // trade_pnl for long = size * (oracle - exec) / POS_SCALE
    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, exec).expect("trade");

    // Account a (long) bought at exec=990 vs oracle=1000, so should have positive PnL
    // trade_pnl = floor(100 * POS_SCALE * (1000 - 990) / POS_SCALE) = 1000
    assert!(engine.accounts[a as usize].pnl > 0,
        "long PnL must be positive when exec < oracle: pnl={}",
        engine.accounts[a as usize].pnl);

    // Account b (short) had negative trade PnL of -1000, but settle_losses
    // absorbs it from capital. Verify b's capital decreased instead.
    // b started with 100_000 deposit, minus trading fee. After settle_losses,
    // the 1000 loss is paid from capital.
    let cap_b = engine.accounts[b as usize].capital.get();
    assert!(cap_b < 100_000,
        "short capital must decrease when exec < oracle (loss settled): cap={}",
        cap_b);
    assert!(engine.check_conservation());
}

// ============================================================================
// 5. Conservation invariant
// ============================================================================

#[test]
fn test_conservation_after_deposits() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(5000).expect("add user a");
    engine.deposit(a, 100_000, oracle, slot).expect("deposit");
    let b = engine.add_user(3000).expect("add user b");
    engine.deposit(b, 50_000, oracle, slot).expect("deposit");

    assert!(engine.check_conservation());
    // V >= C_tot + I
    let senior = engine.c_tot.get() + engine.insurance_fund.balance.get();
    assert!(engine.vault.get() >= senior);
}

#[test]
fn test_conservation_after_trade() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");
    assert!(engine.check_conservation());
}

// ============================================================================
// 6. Haircut ratio computation
// ============================================================================

#[test]
fn test_haircut_ratio_no_positive_pnl() {
    let engine = RiskEngine::new(default_params());
    let (h_num, h_den) = engine.haircut_ratio();
    // When pnl_pos_tot == 0, returns (1, 1)
    assert_eq!(h_num, 1u128);
    assert_eq!(h_den, 1u128);
}

#[test]
fn test_haircut_ratio_with_surplus() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Execute a trade, then move price to give one side positive PnL
    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Now accrue market with a higher price
    engine.accrue_market_to(2, 1100).expect("accrue");
    // Touch accounts to realize PnL
    engine.touch_account_full(a as usize, 1100, 2).expect("touch a");
    engine.touch_account_full(b as usize, 1100, 2).expect("touch b");

    let (h_num, h_den) = engine.haircut_ratio();
    // h_num <= h_den always
    assert!(h_num <= h_den);
    // Verify the haircut is actually computed (not just the default (1,1))
    assert!(h_num > 0, "h_num must be positive when PnL exists");
    assert!(h_den > 0, "h_den must be positive when PnL exists");
}

// ============================================================================
// 7. Liquidation at oracle
// ============================================================================

#[test]
fn test_liquidation_eligible_account() {
    // Use a smaller capital so we can trigger liquidation more easily
    let (mut engine, a, b) = setup_two_users(50_000, 200_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open a position near the margin limit
    // 50_000 capital, 10% IM => max notional = 500_000
    // 480 units * 1000 = 480_000 notional, IM = 48_000
    let size_q = make_size_q(480);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Move the price against the long (a) to trigger liquidation
    // Use accrue_market_to to update price state without running the full crank
    // (the crank would itself liquidate the account before we can test it explicitly)
    let new_oracle = 890u64;
    let slot2 = 2u64;

    // Call liquidate_at_oracle directly - it calls touch_account_full internally
    // which runs accrue_market_to
    let result = engine.liquidate_at_oracle(a, slot2, new_oracle, LiquidationPolicy::FullClose).expect("liquidate");
    assert!(result, "account a should have been liquidated");
    // Position should be closed
    let eff = engine.effective_pos_q(a as usize);
    assert!(eff == 0);
    assert!(engine.check_conservation());
}

#[test]
fn test_liquidation_healthy_account() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Account is well collateralized, liquidation should return false
    let result = engine.liquidate_at_oracle(a, slot, oracle, LiquidationPolicy::FullClose).expect("liquidate attempt");
    assert!(!result, "healthy account should not be liquidated");
}

#[test]
fn test_liquidation_flat_account() {
    let (mut engine, a, _b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // No position open, liquidation should return false
    let result = engine.liquidate_at_oracle(a, slot, oracle, LiquidationPolicy::FullClose).expect("liquidate flat");
    assert!(!result);
}

// ============================================================================
// 8. Warmup and profit conversion
// ============================================================================

#[test]
fn test_warmup_slope_set_on_new_profit() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Advance and accrue at higher price so long (a) gets positive PnL
    let slot2 = 10u64;
    let new_oracle = 1100u64;
    engine.keeper_crank(slot2, new_oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    engine.touch_account_full(a as usize, new_oracle, slot2).expect("touch");

    // If PnL is positive and warmup_period > 0, slope should be set
    if engine.accounts[a as usize].pnl > 0 {
        assert!(engine.accounts[a as usize].warmup_slope_per_step != 0,
            "warmup slope should be nonzero for positive PnL");
    }
}

#[test]
fn test_warmup_full_conversion_after_period() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Move price up to give account a profit
    let slot2 = 10u64;
    let new_oracle = 1200u64;
    engine.keeper_crank(slot2, new_oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    engine.touch_account_full(a as usize, new_oracle, slot2).expect("touch");

    // Close position so profit conversion can happen (only for flat accounts)
    let close_q = make_size_q(-50);
    engine.execute_trade(a, b, new_oracle, slot2, close_q, new_oracle).expect("close");

    let capital_before = engine.accounts[a as usize].capital.get();

    // Wait beyond warmup period (100 slots) and touch again
    let slot3 = slot2 + 200;
    engine.keeper_crank(slot3, new_oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank2");
    engine.touch_account_full(a as usize, new_oracle, slot3).expect("touch2");

    let capital_after = engine.accounts[a as usize].capital.get();
    // Capital should increase after warmup conversion (position is flat now)
    assert!(capital_after > capital_before,
        "after full warmup period, profit must be converted to capital");
    assert!(engine.check_conservation());
}

// ============================================================================
// 9. Insurance fund operations
// ============================================================================

#[test]
fn test_top_up_insurance_fund() {
    let mut engine = RiskEngine::new(default_params());
    let before_vault = engine.vault.get();
    let before_ins = engine.insurance_fund.balance.get();

    let result = engine.top_up_insurance_fund(5000, 0).expect("top_up");
    assert_eq!(engine.vault.get(), before_vault + 5000);
    assert_eq!(engine.insurance_fund.balance.get(), before_ins + 5000);
    assert!(result); // above floor (floor = 0)
    assert!(engine.check_conservation());
}


// ============================================================================
// 10. Fee operations
// ============================================================================

#[test]
fn test_deposit_fee_credits() {
    let mut engine = RiskEngine::new(default_params());
    let slot = 1u64;
    engine.current_slot = slot;
    let idx = engine.add_user(1000).expect("add_user");

    // Give the account fee debt first (spec §2.1: fee_credits <= 0)
    engine.accounts[idx as usize].fee_credits = I128::new(-5000);

    // Pay off 3000 of the 5000 debt
    engine.deposit_fee_credits(idx, 3000, slot).expect("deposit_fee_credits");
    assert_eq!(engine.accounts[idx as usize].fee_credits.get(), -2000,
        "fee_credits must reflect partial payoff");

    // Pay off the remaining 2000
    engine.deposit_fee_credits(idx, 2000, slot).expect("deposit_fee_credits");
    assert_eq!(engine.accounts[idx as usize].fee_credits.get(), 0,
        "fee_credits must be zero after full payoff");

    // Over-payment is capped — fee_credits stays at 0
    engine.deposit_fee_credits(idx, 9999, slot).expect("no-op succeeds");
    assert_eq!(engine.accounts[idx as usize].fee_credits.get(), 0,
        "fee_credits must not go positive");
}

#[test]
fn test_add_fee_credits() {
    let mut engine = RiskEngine::new(default_params());
    let slot = 1u64;
    engine.current_slot = slot;
    let idx = engine.add_user(1000).expect("add_user");

    // Give the account debt, then add credits to pay it off
    engine.accounts[idx as usize].fee_credits = I128::new(-5000);
    engine.add_fee_credits(idx, 3000).expect("add_fee_credits");
    assert_eq!(engine.accounts[idx as usize].fee_credits.get(), -2000);
}

#[test]
fn test_trading_fee_charged() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let capital_before = engine.accounts[a as usize].capital.get();

    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    let capital_after = engine.accounts[a as usize].capital.get();
    // Trading fee should reduce capital of account a
    // fee = ceil(|100| * 1000 * 10 / 10000) = ceil(100) = 100
    assert!(capital_after < capital_before, "trading fee should reduce capital");
    assert!(engine.check_conservation());
}

#[test]
fn test_lp_fees_earned_tracking() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add user");
    let lp = engine.add_lp([1; 32], [2; 32], 1000).expect("add lp");

    // Deposit before crank so accounts are not GC'd
    engine.deposit(a, 100_000, oracle, slot).expect("deposit a");
    engine.deposit(lp, 100_000, oracle, slot).expect("deposit lp");
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    let size_q = make_size_q(100);
    engine.execute_trade(a, lp, oracle, slot, size_q, oracle).expect("trade");

    // LP (account b) should track fees earned
    assert!(engine.accounts[lp as usize].fees_earned_total.get() > 0,
        "LP should track fees earned");
}

// ============================================================================
// 11. Close account
// ============================================================================

#[test]
fn test_close_account_flat() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let idx = engine.add_user(1000).expect("add_user");
    engine.deposit(idx, 10_000, oracle, slot).expect("deposit");

    let capital_returned = engine.close_account(idx, slot, oracle).expect("close");
    assert_eq!(capital_returned, 10_000);
    assert!(!engine.is_used(idx as usize));
    assert!(engine.check_conservation());
}

#[test]
fn test_close_account_with_position_fails() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    let result = engine.close_account(a, slot, oracle);
    assert_eq!(result, Err(RiskError::Undercollateralized));
}

#[test]
fn test_close_account_not_found() {
    let mut engine = RiskEngine::new(default_params());
    let result = engine.close_account(99, 1, 1000);
    assert_eq!(result, Err(RiskError::AccountNotFound));
}

// ============================================================================
// 12. Keeper crank
// ============================================================================

#[test]
fn test_keeper_crank_advances_slot() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 10u64;
    let _caller = engine.add_user(1000).expect("add_user");

    let outcome = engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    assert!(outcome.advanced);
    assert_eq!(engine.last_crank_slot, slot);
}

#[test]
fn test_keeper_crank_same_slot_not_advanced() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 10u64;
    let _caller = engine.add_user(1000).expect("add_user");

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank1");
    let outcome = engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank2");
    assert!(!outcome.advanced);
}

#[test]
fn test_keeper_crank_caller_touch_no_fee() {
    // Spec §8.2: maintenance fees disabled — keeper crank touch does not reduce capital.
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let caller = engine.add_user(1000).expect("add_user");
    engine.deposit(caller, 10_000, oracle, slot).expect("deposit");

    let capital_before = engine.accounts[caller as usize].capital.get();

    // Advance some slots and crank
    let slot2 = 200u64;
    let outcome = engine.keeper_crank(slot2, oracle, &[(caller, None)], 64).expect("crank");
    assert!(outcome.advanced);

    let capital_after = engine.accounts[caller as usize].capital.get();
    assert_eq!(capital_after, capital_before,
        "maintenance fee disabled: capital must not change");
}

// ============================================================================
// 13. Side mode gating (DrainOnly, ResetPending)
// ============================================================================

#[test]
fn test_drain_only_blocks_new_trades() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Manually set long side to DrainOnly
    engine.side_mode_long = SideMode::DrainOnly;

    // Try to open a new long position (a goes long) — should be blocked
    let size_q = make_size_q(50);
    let result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    assert_eq!(result, Err(RiskError::SideBlocked));
}

#[test]
fn test_drain_only_allows_reducing_trade() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open a position first in Normal mode
    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("open trade");

    // Now set long side to DrainOnly
    engine.side_mode_long = SideMode::DrainOnly;

    // Reducing trade (a goes short = reducing long) should work
    let reduce_q = make_size_q(-50);
    engine.execute_trade(a, b, oracle, slot, reduce_q, oracle)
        .expect("reducing trade should succeed in DrainOnly");
}

#[test]
fn test_reset_pending_blocks_new_trades() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // ResetPending with stale_account_count > 0 is NOT auto-finalizable,
    // so it must still block OI-increasing trades.
    engine.side_mode_short = SideMode::ResetPending;
    engine.stale_account_count_short = 1;

    // b would go long (opposite of short blocked), a goes short — short increase blocked
    let size_q = make_size_q(-50); // a goes short
    let result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    assert_eq!(result, Err(RiskError::SideBlocked));
}

// ============================================================================
// 14. ADL mechanics
// ============================================================================

#[test]
fn test_adl_triggered_by_liquidation() {
    let (mut engine, a, b) = setup_two_users(50_000, 50_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open large positions near margin
    // 50k capital, 10% IM => max notional = 500k
    // 450 units * 1000 = 450k notional, IM = 45k
    let size_q = make_size_q(450);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Move price down sharply to make long (a) deeply underwater
    // Call liquidate_at_oracle directly (the crank would liquidate first)
    let slot2 = 2u64;
    let crash_oracle = 870u64;

    let result = engine.liquidate_at_oracle(a, slot2, crash_oracle, LiquidationPolicy::FullClose).expect("liquidate");
    assert!(result, "account a should be liquidated");
    assert!(engine.check_conservation());

    // After liquidation, the position is closed. ADL state may have changed.
    let eff_a = engine.effective_pos_q(a as usize);
    assert!(eff_a == 0, "liquidated position should be zero");
}

#[test]
fn test_adl_epoch_changes() {
    let mut engine = RiskEngine::new(default_params());
    let epoch_long_before = engine.adl_epoch_long;

    // Begin a full drain reset on long side (requires OI=0)
    assert!(engine.oi_eff_long_q == 0);
    engine.begin_full_drain_reset(Side::Long);

    assert_eq!(engine.adl_epoch_long, epoch_long_before + 1);
    assert_eq!(engine.side_mode_long, SideMode::ResetPending);
    assert_eq!(engine.adl_mult_long, ADL_ONE);
}

#[test]
fn test_effective_pos_epoch_mismatch() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open position
    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Manually bump the long epoch to simulate a reset
    engine.adl_epoch_long += 1;

    // Effective position should be zero due to epoch mismatch
    let eff = engine.effective_pos_q(a as usize);
    assert!(eff == 0, "epoch mismatch should zero effective position");
}

// ============================================================================
// Additional edge-case tests
// ============================================================================

#[test]
fn test_set_owner() {
    let mut engine = RiskEngine::new(default_params());
    let idx = engine.add_user(1000).expect("add_user");
    let owner = [42u8; 32];
    engine.set_owner(idx, owner).expect("set_owner");
    assert_eq!(engine.accounts[idx as usize].owner, owner);
}

#[test]
fn test_set_owner_invalid_idx() {
    let mut engine = RiskEngine::new(default_params());
    let result = engine.set_owner(99, [0u8; 32]);
    assert_eq!(result, Err(RiskError::Unauthorized));
}

#[test]
fn test_notional_computation() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    let notional = engine.notional(a as usize, oracle);
    // notional = |100 * POS_SCALE| * 1000 / POS_SCALE = 100_000
    assert_eq!(notional, 100_000);
}

#[test]
fn test_advance_slot() {
    let mut engine = RiskEngine::new(default_params());
    assert_eq!(engine.current_slot, 0);
    engine.advance_slot(42);
    assert_eq!(engine.current_slot, 42);
    engine.advance_slot(8);
    assert_eq!(engine.current_slot, 50);
}

#[test]
fn test_recompute_aggregates() {
    let (mut engine, a, b) = setup_two_users(50_000, 50_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let size_q = make_size_q(30);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    let c_before = engine.c_tot.get();
    let pnl_before = engine.pnl_pos_tot;

    engine.recompute_aggregates();

    // Aggregates should be consistent after recompute
    assert_eq!(engine.c_tot.get(), c_before);
    assert_eq!(engine.pnl_pos_tot, pnl_before);
}

#[test]
fn test_multiple_accounts() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    // Create several accounts
    for _ in 0..10 {
        let idx = engine.add_user(1000).expect("add_user");
        engine.deposit(idx, 10_000, oracle, slot).expect("deposit");
    }

    assert_eq!(engine.num_used_accounts, 10);
    assert_eq!(engine.count_used(), 10);
    assert!(engine.check_conservation());
}

#[test]
fn test_trade_then_close_round_trip() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open position
    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("open");

    // Close position (reverse trade)
    let close_q = make_size_q(-50);
    engine.execute_trade(a, b, oracle, slot, close_q, oracle).expect("close");

    let eff_a = engine.effective_pos_q(a as usize);
    let eff_b = engine.effective_pos_q(b as usize);
    assert!(eff_a == 0, "position a should be flat after close");
    assert!(eff_b == 0, "position b should be flat after close");
    assert!(engine.check_conservation());
}

#[test]
fn test_withdraw_with_position_margin_check() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open position: 100 units * 1000 = 100k notional, 10% IM = 10k required
    let size_q = make_size_q(100);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Try to withdraw so much that IM is violated
    // capital ~ 100k (minus fees), need at least 10k for IM
    let result = engine.withdraw(a, 95_000, oracle, slot);
    assert_eq!(result, Err(RiskError::Undercollateralized));
}

#[test]
fn test_zero_size_trade_rejected() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    let result = engine.execute_trade(a, b, oracle, slot, 0i128, oracle);
    assert_eq!(result, Err(RiskError::Overflow));
}

#[test]
fn test_zero_oracle_rejected() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let slot = 1u64;

    let size_q = make_size_q(10);
    let result = engine.execute_trade(a, b, 0, slot, size_q, 1000);
    assert_eq!(result, Err(RiskError::Overflow));
}

#[test]
fn test_close_account_after_trade_and_unwind() {
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open and close position
    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("open");
    let close_q = make_size_q(-50);
    engine.execute_trade(a, b, oracle, slot, close_q, oracle).expect("close");

    // Wait beyond warmup to let PnL settle
    let slot2 = slot + 200;
    engine.keeper_crank(slot2, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    engine.touch_account_full(a as usize, oracle, slot2).expect("touch");

    // PnL should be zero or converted by now
    let pnl = engine.accounts[a as usize].pnl;
    if pnl == 0 {
        let cap = engine.close_account(a, slot2, oracle).expect("close account");
        assert!(cap > 0);
        assert!(!engine.is_used(a as usize));
    }
    // If PnL is not zero, closing might fail — that is expected behavior
}

#[test]
fn test_insurance_absorbs_loss_on_liquidation() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add user a");
    let b = engine.add_user(1000).expect("add user b");

    // Deposit before crank so accounts are not GC'd
    engine.deposit(a, 20_000, oracle, slot).expect("deposit a");
    engine.deposit(b, 100_000, oracle, slot).expect("deposit b");

    // Top up insurance fund
    engine.top_up_insurance_fund(50_000, slot).expect("top up");

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("initial crank");

    // Open near-max position
    let size_q = make_size_q(180);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Crash price to make a deeply underwater
    let slot2 = 2u64;
    let crash = 850u64;
    engine.keeper_crank(slot2, crash, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    engine.liquidate_at_oracle(a, slot2, crash, LiquidationPolicy::FullClose).expect("liquidate");
    assert!(engine.check_conservation());
}

#[test]
fn test_maintenance_fee_disabled() {
    // Spec §8.2: Recurring account-local maintenance fees are disabled in this revision.
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let idx = engine.add_user(1000).expect("add_user");
    engine.deposit(idx, 10_000, oracle, slot).expect("deposit");

    let capital_before = engine.accounts[idx as usize].capital.get();
    let fc_before = engine.accounts[idx as usize].fee_credits.get();

    // Advance 500 slots and touch
    let slot2 = 501u64;
    engine.keeper_crank(slot2, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    engine.touch_account_full(idx as usize, oracle, slot2).expect("touch");

    let capital_after = engine.accounts[idx as usize].capital.get();
    let fc_after = engine.accounts[idx as usize].fee_credits.get();
    // No fees charged — capital and fee_credits unchanged
    assert_eq!(capital_after, capital_before, "maintenance fees disabled: capital must not change");
    assert_eq!(fc_after, fc_before, "maintenance fees disabled: fee_credits must not change");
}

#[test]
fn test_keeper_crank_liquidates_underwater_accounts() {
    let (mut engine, a, b) = setup_two_users(50_000, 50_000);
    let oracle = 1000u64;
    let slot = 1u64;

    // Open near-margin positions
    let size_q = make_size_q(450);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");

    // Crash price
    let slot2 = 2u64;
    let crash = 870u64;
    let outcome = engine.keeper_crank(slot2, crash, &[(a, Some(LiquidationPolicy::FullClose)), (b, Some(LiquidationPolicy::FullClose))], 64).expect("crank");
    // The crank should have liquidated the underwater account
    assert!(outcome.num_liquidations > 0, "crank must liquidate underwater account");
    assert!(engine.check_conservation());
}

#[test]
fn test_i128_size_q_construction() {
    // Verify our make_size_q helper produces correct values
    let pos = make_size_q(1);
    let neg = make_size_q(-1);

    assert!(pos > 0);
    assert!(neg < 0);

    // |pos| should equal POS_SCALE
    let abs_pos = pos.unsigned_abs();
    assert_eq!(abs_pos, POS_SCALE);
}

#[test]
fn test_deposit_fee_credits_invalid_account() {
    let mut engine = RiskEngine::new(default_params());
    let result = engine.deposit_fee_credits(99, 1000, 1);
    assert_eq!(result, Err(RiskError::Unauthorized));
}

#[test]
fn test_finalize_side_reset() {
    let mut engine = RiskEngine::new(default_params());

    // Set up for reset
    engine.begin_full_drain_reset(Side::Long);
    assert_eq!(engine.side_mode_long, SideMode::ResetPending);

    // All stored_pos_count and stale_count must be 0 for finalize
    // Since no accounts with long positions exist, they should already be 0
    let result = engine.finalize_side_reset(Side::Long);
    assert!(result.is_ok());
    assert_eq!(engine.side_mode_long, SideMode::Normal);
}

#[test]
fn test_finalize_side_reset_wrong_mode() {
    let mut engine = RiskEngine::new(default_params());
    // Side is Normal, finalize should fail
    let result = engine.finalize_side_reset(Side::Long);
    assert_eq!(result, Err(RiskError::CorruptState));
}

#[test]
fn test_account_equity_net_positive() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let idx = engine.add_user(1000).expect("add_user");
    engine.deposit(idx, 50_000, oracle, slot).expect("deposit");

    let eq = engine.account_equity_net(&engine.accounts[idx as usize], oracle);
    // With only capital and no PnL, equity = capital = 50_000
    let expected: i128 = 50_000;
    assert_eq!(eq, expected);
}

#[test]
fn test_count_used() {
    let mut engine = RiskEngine::new(default_params());
    assert_eq!(engine.count_used(), 0);

    engine.add_user(1000).expect("add_user");
    assert_eq!(engine.count_used(), 1);

    engine.add_user(1000).expect("add_user");
    assert_eq!(engine.count_used(), 2);
}

#[test]
fn test_conservation_maintained_through_lifecycle() {
    // Full lifecycle: create, deposit, trade, move price, crank, close
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    let b = engine.add_user(1000).expect("add b");

    // Deposit before crank so accounts are not GC'd
    engine.deposit(a, 100_000, oracle, slot).expect("dep a");
    engine.deposit(b, 100_000, oracle, slot).expect("dep b");
    assert!(engine.check_conservation());

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");
    assert!(engine.check_conservation());

    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade");
    assert!(engine.check_conservation());

    // Price move
    let slot2 = 10u64;
    engine.keeper_crank(slot2, 1050, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank2");
    assert!(engine.check_conservation());

    // Close positions
    let close_q = make_size_q(-50);
    engine.execute_trade(a, b, 1050, slot2, close_q, 1050).expect("close");
    assert!(engine.check_conservation());
}

// ============================================================================
// Spec property #23: immediate fee seniority after restart conversion
// ============================================================================

/// If restart-on-new-profit converts matured entitlement into C_i while fee debt
/// is outstanding, the fee-debt sweep occurs immediately — before later
/// loss-settlement or margin logic can consume that new capital.
///
/// This test verifies that after a trade triggers restart-on-new-profit,
/// fee debt is properly swept (capital reduced, fee_credits less negative,
/// insurance fund receives payment).
#[test]
fn test_fee_seniority_after_restart_on_new_profit_in_trade() {
    // Use zero-fee params to isolate the restart-on-new-profit / fee-sweep interaction
    let mut params = default_params();
    params.trading_fee_bps = 0;
    params.maintenance_fee_per_slot = U128::new(0);
    // Use zero warmup so all positive PnL is immediately warmable
    params.warmup_period_slots = 0;

    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    let b = engine.add_user(1000).expect("add b");

    // Large deposits so margin is not an issue
    engine.deposit(a, 1_000_000, oracle, slot).expect("dep a");
    engine.deposit(b, 1_000_000, oracle, slot).expect("dep b");

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    // Open position: a buys 10 from b
    let size_q = make_size_q(10);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).expect("trade1");
    assert!(engine.check_conservation());

    // Price rises: a now has positive PnL (profit)
    let slot2 = 50u64;
    let oracle2 = 1100u64;
    engine.keeper_crank(slot2, oracle2, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank2");
    assert!(engine.check_conservation());

    // Inject fee debt on account a: fee_credits = -5000
    // (In production this happens from maintenance fees exceeding credits)
    engine.accounts[a as usize].fee_credits = I128::new(-5000);

    let cap_before = engine.accounts[a as usize].capital.get();
    let ins_before = engine.insurance_fund.balance.get();

    // Execute another trade that will trigger restart-on-new-profit for a
    // (a buys 1 more at favorable price = market, AvailGross increases)
    let size_q2 = make_size_q(1);
    engine.execute_trade(a, b, oracle2, slot2, size_q2, oracle2).expect("trade2");
    assert!(engine.check_conservation());

    // After trade: fee debt should have been swept
    let fc_after = engine.accounts[a as usize].fee_credits.get();
    // Fee debt was 5000. After sweep, fee_credits should be less negative (or zero).
    assert!(fc_after > -5000, "fee debt was not swept after restart-on-new-profit: fc={}", fc_after);

    // Insurance fund should have received the swept amount
    let ins_after = engine.insurance_fund.balance.get();
    assert!(ins_after > ins_before, "insurance fund did not receive fee sweep payment");

    // Capital should have decreased by the swept amount
    // (restart conversion adds to capital, fee sweep subtracts)
    // We can't easily check exact amounts without knowing warmable, but we can
    // verify conservation holds
    assert!(engine.check_conservation());
}

// ============================================================================
// Issue #4: Maintenance fee settle must not clamp fee_credits to i128::MIN
// ============================================================================

#[test]
fn test_maintenance_fee_disabled_extreme_params() {
    // Spec §8.2: maintenance fees disabled — even extreme params don't charge fees.
    let mut params = default_params();
    params.maintenance_fee_per_slot = U128::new(i128::MAX as u128);
    let mut engine = RiskEngine::new(params);
    let slot = 1u64;
    engine.current_slot = slot;

    let idx = engine.add_user(1000).expect("add user");
    engine.deposit(idx, 100_000, 1000, slot).expect("deposit");

    // With fees disabled, touch must succeed even with extreme fee params
    engine.last_oracle_price = 1000;
    engine.last_market_slot = 100;
    let fc_before = engine.accounts[idx as usize].fee_credits.get();
    let result = engine.touch_account_full(idx as usize, 1000, 100);
    assert!(result.is_ok(), "touch must succeed — maintenance fees disabled");
    // fee_credits unchanged (no fee charge) — only last_fee_slot stamped
    assert_eq!(engine.accounts[idx as usize].fee_credits.get(), fc_before,
        "fee_credits must not change with maintenance fees disabled");
}

// ============================================================================
// Issue #5: charge_fee_safe must not panic on PnL underflow
// ============================================================================

#[test]
fn test_charge_fee_safe_does_not_panic_on_extreme_pnl() {
    let mut params = default_params();
    params.trading_fee_bps = 100; // 1% fee
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    let b = engine.add_user(1000).expect("add b");

    // Give a zero capital (so fee shortfall goes to PnL),
    // and b large capital for margin
    engine.deposit(a, 1, oracle, slot).expect("dep a");
    engine.deposit(b, 10_000_000, oracle, slot).expect("dep b");

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    // Set account a's PnL to near i128::MIN so fee subtraction would overflow.
    // The charge_fee_safe path: if capital < fee, shortfall = fee - capital,
    // then PnL -= shortfall. If PnL is near i128::MIN, this could overflow.
    let near_min = i128::MIN.checked_add(1i128).unwrap();
    engine.set_pnl(a as usize, near_min);

    // Executing a trade charges a fee. If capital is 0, fee goes to PnL.
    // With PnL near i128::MIN, subtracting the fee must not panic.
    // (The trade will likely fail for margin reasons, but must not panic.)
    let size_q = make_size_q(1);
    let _result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    // We don't care if it succeeds or returns Err — just that it doesn't panic.
}

// ============================================================================
// Issue #1: keeper_crank must propagate errors from state-mutating functions
// ============================================================================

#[test]
fn test_keeper_crank_propagates_corruption() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    engine.deposit(a, 100_000, oracle, slot).expect("dep a");
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    // Set up a corrupt state: a_basis = 0 triggers CorruptState error
    // in settle_side_effects (called by touch_account_full)
    engine.accounts[a as usize].position_basis_q = POS_SCALE as i128;
    engine.accounts[a as usize].adl_a_basis = 0; // CORRUPT: a_basis must be > 0
    engine.stored_pos_count_long = 1;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;

    // keeper_crank must propagate the CorruptState error, not swallow it
    let result = engine.keeper_crank(2, oracle, &[(a, None)], 64);
    assert!(result.is_err(), "keeper_crank must propagate corruption errors");
}

// ============================================================================
// Self-trade rejection
// ============================================================================

#[test]
fn test_self_trade_rejected() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    engine.deposit(a, 100_000, oracle, slot).expect("dep a");
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    let size_q = make_size_q(1);
    let result = engine.execute_trade(a, a, oracle, slot, size_q, oracle);
    assert!(result.is_err(), "self-trade (a == b) must be rejected");
}

// ============================================================================
// Same-slot price change applies mark-to-market
// ============================================================================

#[test]
fn test_same_slot_price_change_applies_mark() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;
    engine.last_oracle_price = oracle;
    engine.last_market_slot = slot; // same slot
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;

    let k_long_before = engine.adl_coeff_long;
    let k_short_before = engine.adl_coeff_short;

    // Same slot, different price: mark-only update must apply
    let new_oracle = 1100u64;
    engine.accrue_market_to(slot, new_oracle).expect("accrue");

    // K_long must increase (price went up, longs gain)
    assert!(engine.adl_coeff_long > k_long_before,
        "K_long must increase on same-slot price rise");
    // K_short must decrease (shorts lose)
    assert!(engine.adl_coeff_short < k_short_before,
        "K_short must decrease on same-slot price rise");
    // Oracle price must be updated
    assert!(engine.last_oracle_price == new_oracle,
        "last_oracle_price must be updated");
}

// ============================================================================
// schedule_end_of_instruction_resets error propagation
// ============================================================================

#[test]
fn test_schedule_reset_error_propagated_in_withdraw() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).expect("add a");
    engine.deposit(a, 100_000, oracle, slot).expect("dep a");
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).expect("crank");

    // Corrupt state: stored_pos_count says 0 but OI is non-zero and unequal.
    // This makes schedule_end_of_instruction_resets return CorruptState.
    engine.stored_pos_count_long = 0;
    engine.stored_pos_count_short = 0;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE * 2; // unequal OI

    let result = engine.withdraw(a, 1, oracle, slot);
    assert!(result.is_err(), "withdraw must propagate reset error on corrupt state");
}

// ============================================================================
// Wide arithmetic: U512-backed mul_div with large operands
// ============================================================================

#[test]
fn test_wide_signed_mul_div_floor_large_operands() {
    use percolator::wide_math::{wide_signed_mul_div_floor, I256};

    // Large basis * large positive K_diff
    let abs_basis = U256::from_u128(u128::MAX);
    let k_diff = I256::from_i128(i128::MAX);
    let denom = U256::from_u128(POS_SCALE);
    let result = wide_signed_mul_div_floor(abs_basis, k_diff, denom);
    // Must not panic; result should be positive (positive * positive / positive)
    assert!(!result.is_negative(), "positive inputs must give non-negative result");

    // Large basis * large negative K_diff (floor toward -inf)
    let k_neg = I256::from_i128(-1_000_000_000);
    let result_neg = wide_signed_mul_div_floor(abs_basis, k_neg, denom);
    assert!(result_neg.is_negative(), "negative k_diff must give negative result");

    // Verify floor rounding: for negative results with remainder, result should
    // be strictly more negative than truncation toward zero.
    // (-1 * 3) / 2 => floor = -2, not -1 (truncation).
    let basis_3 = U256::from_u128(3);
    let k_neg1 = I256::from_i128(-1);
    let denom_2 = U256::from_u128(2);
    let floored = wide_signed_mul_div_floor(basis_3, k_neg1, denom_2);
    assert_eq!(floored, I256::from_i128(-2), "floor(-3/2) must be -2");
}

#[test]
fn test_wide_signed_mul_div_floor_zero_cases() {
    use percolator::wide_math::{wide_signed_mul_div_floor, I256};

    // Zero basis
    let result = wide_signed_mul_div_floor(U256::ZERO, I256::from_i128(42), U256::from_u128(1));
    assert_eq!(result, I256::ZERO);

    // Zero k_diff
    let result = wide_signed_mul_div_floor(U256::from_u128(42), I256::ZERO, U256::from_u128(1));
    assert_eq!(result, I256::ZERO);
}

#[test]
fn test_mul_div_floor_u256_large_product() {
    use percolator::wide_math::mul_div_floor_u256;

    // (u128::MAX * u128::MAX) / 1 should not panic — uses U512 internally
    let a = U256::from_u128(u128::MAX);
    let b = U256::from_u128(u128::MAX);
    let d = U256::from_u128(u128::MAX); // dividing by same magnitude keeps in range
    let result = mul_div_floor_u256(a, b, d);
    assert_eq!(result, U256::from_u128(u128::MAX), "u128::MAX * u128::MAX / u128::MAX = u128::MAX");

    // Small a * large b / large d => small result
    let result2 = mul_div_floor_u256(U256::from_u128(1), U256::from_u128(u128::MAX), U256::from_u128(u128::MAX));
    assert_eq!(result2, U256::from_u128(1));
}

#[test]
fn test_mul_div_ceil_u256_rounding() {
    use percolator::wide_math::mul_div_ceil_u256;

    // Exact division: 6 * 2 / 3 = 4 (no rounding needed)
    let exact = mul_div_ceil_u256(U256::from_u128(6), U256::from_u128(2), U256::from_u128(3));
    assert_eq!(exact, U256::from_u128(4));

    // Rounding up: 7 * 1 / 3 = ceil(7/3) = 3
    let ceiled = mul_div_ceil_u256(U256::from_u128(7), U256::from_u128(1), U256::from_u128(3));
    assert_eq!(ceiled, U256::from_u128(3), "ceil(7/3) must be 3");

    // Minimal remainder: 4 * 1 / 3 = ceil(4/3) = 2
    let min_rem = mul_div_ceil_u256(U256::from_u128(4), U256::from_u128(1), U256::from_u128(3));
    assert_eq!(min_rem, U256::from_u128(2), "ceil(4/3) must be 2");
}

// ============================================================================
// Multi-step funding accrual over large dt
// ============================================================================

#[test]
fn test_accrue_market_to_multi_substep_large_dt() {
    let mut engine = RiskEngine::new(default_params());
    engine.last_oracle_price = 1000;
    engine.last_market_slot = 0;
    engine.funding_price_sample_last = 1000;
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;

    // High funding rate, large time gap requiring multiple sub-steps
    engine.funding_rate_bps_per_slot_last = 5000; // 50% bps/slot
    let large_dt = MAX_FUNDING_DT * 3 + 100; // triggers 4 sub-steps

    let result = engine.accrue_market_to(large_dt, 1100);
    assert!(result.is_ok(), "multi-substep accrual must not overflow: {:?}", result);

    // Price increased, so K_long must increase (mark + funding payer = long)
    // K_short must also change from receiving funding
    assert!(engine.last_market_slot == large_dt);
    assert!(engine.last_oracle_price == 1100);
}

#[test]
fn test_accrue_market_funding_rate_zero_no_funding_applied() {
    let mut engine = RiskEngine::new(default_params());
    engine.last_oracle_price = 1000;
    engine.last_market_slot = 0;
    engine.funding_price_sample_last = 1000;
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;
    engine.funding_rate_bps_per_slot_last = 0;

    let k_long_before = engine.adl_coeff_long;
    let k_short_before = engine.adl_coeff_short;

    // Same price, time passes: with zero rate, only mark applies (0 delta_p)
    engine.accrue_market_to(100, 1000).unwrap();

    // No price change + no funding → K unchanged
    assert_eq!(engine.adl_coeff_long, k_long_before);
    assert_eq!(engine.adl_coeff_short, k_short_before);
}

#[test]
fn test_accrue_market_no_funding_transfer() {
    // Spec §4.12 / §5.4: zero-rate core profile — no funding transfer in this revision.
    let mut engine = RiskEngine::new(default_params());
    engine.last_oracle_price = 1000;
    engine.last_market_slot = 0;
    engine.funding_price_sample_last = 1000;
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;

    // Even with a nonzero stored rate, no funding transfer occurs
    engine.funding_rate_bps_per_slot_last = -1000;

    let k_long_before = engine.adl_coeff_long;
    let k_short_before = engine.adl_coeff_short;

    engine.accrue_market_to(10, 1000).unwrap(); // same price, time passes

    // Zero-rate core profile: K coefficients must NOT change from funding
    assert_eq!(engine.adl_coeff_short, k_short_before,
        "zero-rate: short K must not change from funding");
    assert_eq!(engine.adl_coeff_long, k_long_before,
        "zero-rate: long K must not change from funding");
}

// ============================================================================
// Keeper crank: cursor advancement and fairness
// ============================================================================

#[test]
fn test_keeper_crank_processes_candidates() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);

    // Crank with explicit candidates processes them
    let outcome = engine.keeper_crank(5, 1000, &[(a, None), (b, None)], 64).unwrap();
    assert!(outcome.advanced, "crank must advance slot");
}

#[test]
fn test_keeper_crank_caller_fee_discount_multi_slot() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 10_000_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    // Advance many slots to accumulate maintenance fee debt
    let far_slot = 1000u64;
    engine.accounts[a as usize].last_fee_slot = slot;

    // Run crank at far_slot with account a as candidate
    engine.keeper_crank(far_slot, oracle, &[(a, None)], 64).unwrap();

    // Account's last_fee_slot should be updated to far_slot (post-settlement)
    assert_eq!(engine.accounts[a as usize].last_fee_slot, far_slot,
        "account's last_fee_slot must be updated after crank settlement");
}

// ============================================================================
// Liquidation edge cases
// ============================================================================

#[test]
fn test_liquidation_triggers_on_underwater_account() {
    // Small deposits + large position = high leverage → easily liquidated
    let (mut engine, a, b) = setup_two_users(100_000, 100_000);
    let oracle = 1000u64;
    let slot = 2u64;

    // Trade at maximum leverage the margin allows
    // With 100k capital, 10% IM, max notional ≈ 1M → ~1000 units at price 1000
    let size_q = make_size_q(900);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Price crashes — longs deeply underwater
    let crash_price = 500u64; // 50% drop
    let slot2 = 3;

    // Crank at crash price — accrues market internally then liquidates
    let outcome = engine.keeper_crank(slot2, crash_price, &[(a, Some(LiquidationPolicy::FullClose)), (b, Some(LiquidationPolicy::FullClose))], 64).unwrap();
    assert!(outcome.num_liquidations > 0, "crank must liquidate underwater account after 50% price drop");
}

#[test]
fn test_direct_liquidation_returns_to_insurance() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);
    let oracle = 1000u64;
    let slot = 2u64;

    let size_q = make_size_q(10);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    let ins_before = engine.insurance_fund.balance.get();

    // Price crashes — a (long) underwater
    let crash_price = 100u64;
    let slot2 = 3;
    engine.liquidate_at_oracle(a, slot2, crash_price, LiquidationPolicy::FullClose).unwrap();

    let ins_after = engine.insurance_fund.balance.get();
    // Insurance should receive liquidation fee (or absorb loss)
    assert!(ins_after >= ins_before, "insurance fund must not decrease on liquidation");
}

// ============================================================================
// Conservation law: full lifecycle
// ============================================================================

#[test]
fn test_conservation_full_lifecycle() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);
    assert!(engine.check_conservation(), "conservation must hold after setup");

    let oracle = 1000u64;
    let slot = 2u64;

    // Trade
    let size_q = make_size_q(5);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();
    assert!(engine.check_conservation(), "conservation must hold after trade");

    // Price change + crank
    let slot2 = 3;
    engine.keeper_crank(slot2, 1200, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();
    assert!(engine.check_conservation(), "conservation must hold after crank with price change");

    // Withdraw
    engine.withdraw(a, 1_000, 1200, slot2).unwrap();
    assert!(engine.check_conservation(), "conservation must hold after withdraw");

    // Another crank at different price
    let slot3 = 4;
    engine.keeper_crank(slot3, 800, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();
    assert!(engine.check_conservation(), "conservation must hold after second crank");
}

// ============================================================================
// Position boundary: max position enforcement
// ============================================================================

#[test]
fn test_trade_at_reasonable_size_succeeds() {
    let (mut engine, a, b) = setup_two_users(100_000_000, 100_000_000);
    let oracle = 1000u64;
    let slot = 2u64;

    // Reasonable trade should succeed
    let size_q = make_size_q(1);
    let result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    assert!(result.is_ok(), "reasonable trade must succeed");
    assert!(engine.check_conservation());
}

// ============================================================================
// Maintenance fee: overflow on large dt
// ============================================================================

#[test]
fn test_maintenance_fee_disabled_large_dt_succeeds() {
    // Spec §8.2: maintenance fees disabled — even extreme fee params with large dt succeed.
    let mut params = default_params();
    params.maintenance_fee_per_slot = U128::new(u128::MAX / 2);
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 10_000_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    let far_slot = slot + 200_000;
    engine.last_market_slot = far_slot - 1;
    engine.last_oracle_price = oracle;
    engine.funding_price_sample_last = oracle;

    // With fees disabled, this must succeed (no overflow from fee calculation)
    let result = engine.keeper_crank(far_slot, oracle, &[(a, None)], 64);
    assert!(result.is_ok(), "maintenance fees disabled — large dt must not fail");
}

// ============================================================================
// charge_fee_safe: PnL near i128::MIN boundary
// ============================================================================

#[test]
fn test_charge_fee_safe_rejects_pnl_at_i256_min() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 0, oracle, slot).unwrap(); // zero capital so shortfall goes to PnL

    // Set PnL very close to i128::MIN
    let near_min = i128::MIN.checked_add(1i128).unwrap();
    engine.set_pnl(a as usize, near_min);

    // Liquidation fee would push PnL to exactly i128::MIN — must return Err
    // We test via the public liquidate path, but first set up the conditions
    // for an underwater account with a position.
    engine.accounts[a as usize].position_basis_q = POS_SCALE as i128;
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.adl_epoch_long = 0;
    engine.adl_epoch_short = 0;
    engine.accounts[a as usize].adl_a_basis = ADL_ONE;
    engine.accounts[a as usize].adl_k_snap = 0i128;
    engine.accounts[a as usize].adl_epoch_snap = 0;
    engine.stored_pos_count_long = 1;
    engine.oi_eff_long_q = POS_SCALE;
    engine.oi_eff_short_q = POS_SCALE;
    engine.last_oracle_price = oracle;
    engine.last_market_slot = slot;
    engine.last_crank_slot = slot;
    engine.funding_price_sample_last = oracle;

    // Liquidation should handle this gracefully (return Err or succeed without i128::MIN)
    let result = engine.liquidate_at_oracle(a, slot, oracle, LiquidationPolicy::FullClose);
    // Either it errors out or it succeeds but PnL is not i128::MIN
    if result.is_ok() {
        assert!(engine.accounts[a as usize].pnl != i128::MIN,
            "PnL must never reach i128::MIN");
    }
}

// ============================================================================
// Side mode gating prevents OI increase during DrainOnly
// ============================================================================

#[test]
fn test_drain_only_blocks_oi_increase() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);
    let oracle = 1000u64;
    let slot = 2u64;

    // Set long side to DrainOnly
    engine.side_mode_long = SideMode::DrainOnly;

    // Try to open a new long position — should fail
    let size_q = make_size_q(1); // a goes long
    let result = engine.execute_trade(a, b, oracle, slot, size_q, oracle);
    assert!(result.is_err(), "DrainOnly side must reject OI-increasing trades");
}

// ============================================================================
// Oracle price: zero and max boundary
// ============================================================================

#[test]
fn test_oracle_price_zero_rejected() {
    let (mut engine, a, _b) = setup_two_users(10_000_000, 10_000_000);
    let result = engine.accrue_market_to(2, 0);
    assert!(result.is_err(), "oracle price 0 must be rejected");
}

#[test]
fn test_oracle_price_max_accepted() {
    let mut engine = RiskEngine::new(default_params());
    engine.last_oracle_price = 1000;
    engine.last_market_slot = 0;
    engine.funding_price_sample_last = 1000;
    engine.adl_mult_long = ADL_ONE;
    engine.adl_mult_short = ADL_ONE;
    engine.funding_rate_bps_per_slot_last = 0;

    let result = engine.accrue_market_to(1, MAX_ORACLE_PRICE);
    assert!(result.is_ok(), "MAX_ORACLE_PRICE must be accepted");

    let result2 = engine.accrue_market_to(2, MAX_ORACLE_PRICE + 1);
    assert!(result2.is_err(), "above MAX_ORACLE_PRICE must be rejected");
}

// ============================================================================
// Deposit/withdraw roundtrip: conservation on single account
// ============================================================================

#[test]
fn test_deposit_withdraw_roundtrip_same_slot() {
    let (mut engine, a, _b) = setup_two_users(10_000_000, 10_000_000);
    // Use same slot as setup (slot=1) to avoid maintenance fee deduction
    let oracle = 1000;
    let slot = 1;

    let cap_before = engine.accounts[a as usize].capital.get();
    engine.deposit(a, 5_000_000, oracle, slot).unwrap();
    assert_eq!(engine.accounts[a as usize].capital.get(), cap_before + 5_000_000);

    // Withdraw full extra amount at same slot — no fee should apply
    engine.withdraw(a, 5_000_000, oracle, slot).unwrap();
    assert_eq!(engine.accounts[a as usize].capital.get(), cap_before,
        "same-slot deposit+withdraw roundtrip must return exact capital");
    assert!(engine.check_conservation());
}

// ============================================================================
// Multiple cranks don't double-process accounts
// ============================================================================

#[test]
fn test_double_crank_same_slot_is_safe() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);
    let oracle = 1000u64;
    let slot = 2u64;

    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    let cap_a = engine.accounts[a as usize].capital.get();
    let cap_b = engine.accounts[b as usize].capital.get();

    // Second crank same slot — should be a no-op (no double fee charges etc.)
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    // Capital shouldn't change from a redundant crank
    // (small tolerance for rounding if any fees apply)
    let cap_a_after = engine.accounts[a as usize].capital.get();
    let cap_b_after = engine.accounts[b as usize].capital.get();
    assert!(cap_a_after == cap_a, "redundant crank must not change capital");
    assert!(cap_b_after == cap_b, "redundant crank must not change capital");
    assert!(engine.check_conservation());
}

// ============================================================================
// Issue #1: Withdraw simulation must not inflate haircut ratio
// ============================================================================

#[test]
fn test_withdraw_simulation_does_not_inflate_haircut() {
    let (mut engine, a, b) = setup_two_users(10_000_000, 10_000_000);
    let oracle = 1000u64;
    let slot = 2u64;

    // Open a position so the margin check path is exercised
    let size_q = make_size_q(50);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Give a some positive PnL so haircut matters
    engine.set_pnl(a as usize, 5_000_000i128);

    // Record haircut before
    let (h_num_before, h_den_before) = engine.haircut_ratio();

    // Simulate what the FIXED withdraw() does: adjust both capital AND vault
    let old_cap = engine.accounts[a as usize].capital.get();
    let old_vault = engine.vault;
    let withdraw_amount = 1_000_000u128;
    let new_cap = old_cap - withdraw_amount;
    engine.set_capital(a as usize, new_cap);
    engine.vault = U128::new(engine.vault.get() - withdraw_amount);

    let (h_num_sim, h_den_sim) = engine.haircut_ratio();

    // Revert both
    engine.set_capital(a as usize, old_cap);
    engine.vault = old_vault;

    // Compare: h_sim <= h_before (cross-multiply)
    // h_num_sim / h_den_sim <= h_num_before / h_den_before
    let lhs = h_num_sim.checked_mul(h_den_before).unwrap();
    let rhs = h_num_before.checked_mul(h_den_sim).unwrap();
    assert!(lhs <= rhs,
        "haircut must not increase during withdraw simulation (Residual inflation)");
}

// ============================================================================
// Issue #2: Funding rate must be validated before storage
// ============================================================================

#[test]
fn test_multiple_cranks_do_not_brick_protocol() {
    let (mut engine, _a, _b) = setup_two_users(10_000_000, 10_000_000);

    // Run crank at slot 2
    let _ = engine.keeper_crank(2, 1000, &[] as &[(u16, Option<LiquidationPolicy>)], 64);

    // Protocol must not be bricked — next crank must succeed
    let result = engine.keeper_crank(3, 1000, &[] as &[(u16, Option<LiquidationPolicy>)], 64);
    assert!(result.is_ok(),
        "protocol must not be bricked by a previous crank");
}

// ============================================================================
// Issue #3: GC must not delete accounts with fee_credits
// ============================================================================

#[test]
fn test_gc_dust_preserves_fee_credits() {
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 10_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    // Set up dust-like state: 0 capital, 0 position, but positive fee_credits
    engine.set_capital(a as usize, 0);
    engine.accounts[a as usize].position_basis_q = 0i128;
    engine.set_pnl(a as usize, 0i128);
    engine.accounts[a as usize].fee_credits = I128::new(5_000);

    assert!(engine.is_used(a as usize), "account must exist before GC");

    engine.garbage_collect_dust();

    assert!(engine.is_used(a as usize),
        "GC must not delete account with non-zero fee_credits");
    assert_eq!(engine.accounts[a as usize].fee_credits.get(), 5_000,
        "fee_credits must be preserved");
}

// ============================================================================
// Bug fix #1: GC must collect dead accounts with negative fee_credits (debt)
// ============================================================================

#[test]
fn test_gc_collects_dead_account_with_negative_fee_credits() {
    // Before the fix: settle_maintenance_fee pushes fee_credits negative,
    // then !fee_credits.is_zero() causes GC to skip the dead account forever.
    let mut params = default_params();
    params.maintenance_fee_per_slot = U128::new(100); // high fee
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 10_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    // Simulate abandoned account: zero everything
    engine.set_capital(a as usize, 0);
    engine.accounts[a as usize].position_basis_q = 0i128;
    engine.set_pnl(a as usize, 0i128);
    engine.accounts[a as usize].fee_credits = I128::new(0);
    engine.accounts[a as usize].last_fee_slot = slot;

    // Advance time so maintenance fee accrues → pushes fee_credits negative
    let gc_slot = slot + 100;
    engine.current_slot = gc_slot;

    let num_used_before = engine.num_used_accounts;
    engine.garbage_collect_dust();

    // Account must be collected despite negative fee_credits
    assert!(!engine.is_used(a as usize),
        "dead account with negative fee_credits must be collected by GC");
    assert!(engine.num_used_accounts < num_used_before,
        "used account count must decrease");
}

#[test]
fn test_gc_still_protects_positive_fee_credits() {
    // Regression: the fix must not break protection of prepaid credits
    let mut engine = RiskEngine::new(default_params());
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 10_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 64).unwrap();

    engine.set_capital(a as usize, 0);
    engine.accounts[a as usize].position_basis_q = 0i128;
    engine.set_pnl(a as usize, 0i128);
    // Large positive prepaid credits
    engine.accounts[a as usize].fee_credits = I128::new(1_000_000);

    engine.garbage_collect_dust();

    assert!(engine.is_used(a as usize),
        "GC must protect accounts with positive (prepaid) fee_credits");
}

// ============================================================================
// Bug fix #2: Maintenance fee must NOT eagerly sweep capital
// (trading loss seniority over fee debt)
// ============================================================================

#[test]
fn test_maintenance_fee_disabled_no_capital_sweep() {
    // Spec §8.2: maintenance fees disabled — touch_account_full does NOT
    // charge fees or sweep capital for fee debt.
    let mut params = default_params();
    params.maintenance_fee_per_slot = U128::new(100);
    params.new_account_fee = U128::ZERO;
    params.trading_fee_bps = 0;
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;

    let a = engine.add_user(0).unwrap();
    engine.deposit(a, 10_000, oracle, slot).unwrap();
    engine.last_oracle_price = oracle;
    engine.last_market_slot = slot;
    engine.accounts[a as usize].last_fee_slot = slot;

    // Advance 50 slots — no fee charge
    let touch_slot = slot + 50;
    let _ = engine.touch_account_full(a as usize, oracle, touch_slot);

    let fc = engine.accounts[a as usize].fee_credits.get();
    let cap_after = engine.accounts[a as usize].capital.get();

    // Capital unchanged — no fees charged
    assert_eq!(cap_after, 10_000, "capital unchanged: fees disabled");
    // fee_credits unchanged
    assert_eq!(fc, 0, "fee_credits unchanged: fees disabled");
}

// ============================================================================
// Bug fix #3: Minimum absolute liquidation fee must be enforced
// ============================================================================

#[test]
fn test_min_liquidation_fee_enforced() {
    // Before the fix: dust positions liquidated with zero penalty because
    // min_liquidation_abs was defined but never referenced.
    // Use proper trade flow so all invariants are maintained.
    let mut params = default_params();
    params.min_liquidation_abs = U128::new(500);
    params.liquidation_fee_bps = 100; // 1%
    params.liquidation_fee_cap = U128::new(1_000_000);
    params.maintenance_fee_per_slot = U128::ZERO;
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    let b = engine.add_user(1000).unwrap();
    // Large capital so account stays solvent even after price drop
    engine.deposit(a, 1_000_000, oracle, slot).unwrap();
    engine.deposit(b, 1_000_000, oracle, slot).unwrap();

    // Small position: 1 unit. Notional = 1000, 1% bps fee = 10.
    // min_liquidation_abs = 500 → fee = max(10, 500) = 500.
    let size_q = make_size_q(1);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Now make account underwater but still solvent (has capital to pay fee).
    // Directly set PnL to push below maintenance margin.
    // Equity = capital + PnL. Maintenance = 5% * |notional|.
    // At oracle 1000, 1 unit: notional = 1000, maint = 50.
    // Capital ~ 1M (minus trading fee). Set PnL so equity < maint margin.
    // PnL = -(capital - 40) makes equity = 40 < 50 maintenance.
    let cap = engine.accounts[a as usize].capital.get();
    engine.set_pnl(a as usize, -((cap as i128) - 40));

    let ins_before = engine.insurance_fund.balance.get();

    let slot2 = 2;
    let result = engine.liquidate_at_oracle(a, slot2, oracle, LiquidationPolicy::FullClose);
    assert!(result.is_ok(), "liquidation must succeed: {:?}", result);
    assert!(result.unwrap(), "account must be liquidated");

    let ins_after = engine.insurance_fund.balance.get();

    // Fee = max(10, 500) = 500, min(500, 1M) = 500.
    // Account has 40 units of equity → charge_fee_safe pays 40 from cap, 460 from PnL.
    // Insurance gets 40 from cap directly.
    // Then deficit gets absorbed from insurance.
    // Net insurance change: +40 (fee from cap) - deficit_absorbed.
    // The key: the FEE AMOUNT itself is 500 (not 10). Test the formula is correct.
    // Since we can't isolate fee vs loss, just verify the overall flow doesn't panic
    // and conservation holds.
    assert!(engine.check_conservation(), "conservation must hold after min-fee liquidation");
}

#[test]
fn test_min_liquidation_fee_does_not_exceed_cap() {
    // Verify: min(max(bps_fee, min_abs), cap) → cap wins when min > cap
    let mut params = default_params();
    params.liquidation_fee_cap = U128::new(200);     // low cap
    params.min_liquidation_abs = U128::new(150);     // below cap (valid per §1.4)
    params.liquidation_fee_bps = 100;
    params.maintenance_fee_per_slot = U128::ZERO;
    let mut engine = RiskEngine::new(params);
    let oracle = 1000u64;
    let slot = 1u64;
    engine.current_slot = slot;

    let a = engine.add_user(1000).unwrap();
    let b = engine.add_user(1000).unwrap();
    engine.deposit(a, 50_000, oracle, slot).unwrap();
    engine.deposit(b, 50_000, oracle, slot).unwrap();

    // 10-unit position: notional = 10000, 1% bps = 100
    // max(100, 150) = 150, but cap = 200 → fee = 150
    // The cap wins when fee would exceed it
    let size_q = make_size_q(10);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Crash price to trigger liquidation
    let crash_price = 100u64;
    let slot2 = 2;

    // Record insurance before. Trading fee from execute_trade already credited.
    let ins_before = engine.insurance_fund.balance.get();
    let result = engine.liquidate_at_oracle(a, slot2, crash_price, LiquidationPolicy::FullClose);
    assert!(result.is_ok(), "liquidation must succeed: {:?}", result);

    let ins_after = engine.insurance_fund.balance.get();

    // The net insurance change includes: +liq_fee, -absorbed_loss.
    // We can't isolate the fee directly, but we verify conservation holds
    // and the code path executed min(max(bps, min_abs), cap).
    assert!(engine.check_conservation(), "conservation must hold after liquidation");
}

// ============================================================================
// Property 49: Profit-conversion reserve preservation
// consume_released_pnl leaves R_i unchanged, reduces pnl_pos_tot and
// pnl_matured_pos_tot by exactly x.
// ============================================================================

#[test]
fn test_property_49_consume_released_pnl_preserves_reserve() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let mut engine = RiskEngine::new(default_params());
    let a = engine.add_user(1000).unwrap();
    engine.deposit(a, 100_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    // Give account positive PnL with some matured (released) portion
    let idx = a as usize;
    engine.set_pnl(idx, 5_000);
    // After set_pnl, the increase goes to reserved_pnl; simulate warmup completion
    engine.set_reserved_pnl(idx, 0); // all matured

    let r_before = engine.accounts[idx].reserved_pnl;
    let ppt_before = engine.pnl_pos_tot;
    let pmpt_before = engine.pnl_matured_pos_tot;

    assert_eq!(r_before, 0, "all profit should be released");

    let x = 2_000u128;
    engine.consume_released_pnl(idx, x);

    assert_eq!(engine.accounts[idx].reserved_pnl, r_before,
        "R_i must be unchanged after consume_released_pnl");
    assert_eq!(engine.pnl_pos_tot, ppt_before - x,
        "pnl_pos_tot must decrease by x");
    assert_eq!(engine.pnl_matured_pos_tot, pmpt_before - x,
        "pnl_matured_pos_tot must decrease by x");
    assert_eq!(engine.accounts[idx].pnl, 3_000i128,
        "PNL_i must decrease by x");
}

// ============================================================================
// Property 50: Flat-only automatic conversion
// touch_account_full on a flat account converts matured released profit;
// touch_account_full on an open-position account does NOT auto-convert.
// ============================================================================

#[test]
fn test_property_50_flat_only_auto_conversion() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let mut params = default_params();
    params.maintenance_fee_per_slot = U128::ZERO;
    params.trading_fee_bps = 0;
    params.new_account_fee = U128::ZERO;
    let mut engine = RiskEngine::new(params);

    let a = engine.add_user(0).unwrap();
    let b = engine.add_user(0).unwrap();
    engine.deposit(a, 100_000, oracle, slot).unwrap();
    engine.deposit(b, 100_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    // Give 'a' an open position
    let size_q = make_size_q(1);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Manually give 'a' released matured profit and fund vault to cover it
    let idx_a = a as usize;
    engine.set_pnl(idx_a, 10_000);
    engine.set_reserved_pnl(idx_a, 0); // all matured
    engine.vault = U128::new(engine.vault.get() + 10_000); // fund the PnL

    // Touch with open position — should NOT auto-convert
    engine.touch_account_full(idx_a, oracle, slot + 1).unwrap();

    let pnl_after = engine.accounts[idx_a].pnl;
    assert!(pnl_after > 0, "open-position touch must not zero out released profit via auto-convert");

    // Now test flat account: close the position first
    engine.execute_trade(a, b, oracle, slot + 1, -size_q, oracle).unwrap();
    // Give released profit and fund vault
    let idx_a = a as usize;
    engine.set_pnl(idx_a, 5_000);
    engine.set_reserved_pnl(idx_a, 0);
    engine.vault = U128::new(engine.vault.get() + 5_000);

    let cap_before_flat = engine.accounts[idx_a].capital.get();
    engine.touch_account_full(idx_a, oracle, slot + 2).unwrap();

    // After flat touch, released profit should have been converted to capital
    let pnl_after_flat = engine.accounts[idx_a].pnl;
    let cap_after_flat = engine.accounts[idx_a].capital.get();
    assert_eq!(pnl_after_flat, 0, "flat touch must convert released profit (PNL → 0)");
    assert!(cap_after_flat > cap_before_flat, "flat touch must increase capital from conversion");
}

// ============================================================================
// Property 51: Universal withdrawal dust guard
// Withdrawal must leave either 0 capital or >= MIN_INITIAL_DEPOSIT.
// ============================================================================

#[test]
fn test_property_51_universal_withdrawal_dust_guard() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let min_deposit = 1_000u128;

    let mut params = default_params();
    params.min_initial_deposit = U128::new(min_deposit);
    params.maintenance_fee_per_slot = U128::ZERO;
    params.new_account_fee = U128::ZERO;
    let mut engine = RiskEngine::new(params);

    let a = engine.add_user(0).unwrap();
    engine.deposit(a, 5_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    let cap = engine.accounts[a as usize].capital.get();
    assert_eq!(cap, 5_000);

    // Try withdrawing to leave dust (< MIN_INITIAL_DEPOSIT but > 0)
    let withdraw_dust = cap - 500; // leaves 500, which is < 1000 MIN_INITIAL_DEPOSIT
    let result = engine.withdraw(a, withdraw_dust, oracle, slot);
    assert!(result.is_err(), "withdrawal leaving dust below MIN_INITIAL_DEPOSIT must be rejected");

    // Withdrawing to leave exactly 0 must succeed
    let result2 = engine.withdraw(a, cap, oracle, slot);
    assert!(result2.is_ok(), "full withdrawal to 0 must succeed");

    // Re-deposit and test partial withdrawal leaving >= MIN_INITIAL_DEPOSIT
    engine.deposit(a, 5_000, oracle, slot).unwrap();
    let cap2 = engine.accounts[a as usize].capital.get();
    let withdraw_ok = cap2 - min_deposit; // leaves exactly MIN_INITIAL_DEPOSIT
    let result3 = engine.withdraw(a, withdraw_ok, oracle, slot);
    assert!(result3.is_ok(), "withdrawal leaving >= MIN_INITIAL_DEPOSIT must succeed");
}

// ============================================================================
// Property 52: Explicit open-position profit conversion
// convert_released_pnl consumes only ReleasedPos_i, leaves R_i unchanged,
// sweeps fee debt, and rejects if post-conversion state is unhealthy.
// ============================================================================

#[test]
fn test_property_52_convert_released_pnl_explicit() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let mut engine = RiskEngine::new(default_params());
    let a = engine.add_user(1000).unwrap();
    let b = engine.add_user(1000).unwrap();
    engine.deposit(a, 100_000, oracle, slot).unwrap();
    engine.deposit(b, 100_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    // Give 'a' an open position
    let size_q = make_size_q(1);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Set released matured profit
    let idx = a as usize;
    engine.set_pnl(idx, 10_000);
    engine.set_reserved_pnl(idx, 3_000); // 7000 released

    let r_before = engine.accounts[idx].reserved_pnl;

    // Convert some released profit
    let result = engine.convert_released_pnl(a, 5_000, oracle, slot + 1);
    assert!(result.is_ok(), "convert_released_pnl must succeed: {:?}", result);

    // R_i must be unchanged
    assert_eq!(engine.accounts[idx].reserved_pnl, r_before,
        "R_i must be unchanged after convert_released_pnl");

    // Requesting more than released must fail
    let released_now = {
        let pnl = engine.accounts[idx].pnl;
        let pos = if pnl > 0 { pnl as u128 } else { 0u128 };
        pos.saturating_sub(engine.accounts[idx].reserved_pnl)
    };
    let result2 = engine.convert_released_pnl(a, released_now + 1, oracle, slot + 1);
    assert!(result2.is_err(), "requesting more than released must fail");
}

// ============================================================================
// Property 53: Phantom-dust ADL ordering awareness
// If a keeper zeroes the last stored position on a side while phantom OI
// remains, opposite-side bankruptcies after that lose K-socialization capacity.
// ============================================================================

#[test]
fn test_property_53_phantom_dust_adl_ordering() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let mut params = default_params();
    params.trading_fee_bps = 0;
    params.maintenance_fee_per_slot = U128::ZERO;
    params.new_account_fee = U128::ZERO;
    let mut engine = RiskEngine::new(params);

    let a = engine.add_user(0).unwrap();
    let b = engine.add_user(0).unwrap();
    // Give 'a' small capital so it goes bankrupt on crash; give 'b' large capital
    engine.deposit(a, 50_000, oracle, slot).unwrap();
    engine.deposit(b, 1_000_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    // Open near-maximum-leverage position for 'a':
    // 50k capital, 10% IM => max notional ~500k => ~480 units at price 1000
    let size_q = make_size_q(480);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Verify balanced OI before crash
    assert_eq!(engine.oi_eff_long_q, engine.oi_eff_short_q, "OI must be balanced");
    assert!(engine.oi_eff_long_q > 0, "OI must be nonzero");
    assert!(engine.stored_pos_count_long > 0, "should have stored long positions");

    // Crash the price to make 'a' (long) deeply underwater, triggering
    // liquidation + ADL (bankruptcy). This closes a's position and creates
    // phantom dust on the long side.
    let crash_price = 870u64;
    let slot2 = slot + 1;
    let result = engine.liquidate_at_oracle(a, slot2, crash_price, LiquidationPolicy::FullClose);
    assert!(result.is_ok(), "liquidation must succeed: {:?}", result);
    assert!(result.unwrap(), "account a must be liquidated");

    // After liquidation, a's position is closed; stored_pos_count_long should be 0
    assert_eq!(engine.stored_pos_count_long, 0,
        "long stored_pos_count must be 0 after sole long is liquidated");

    // Conservation must hold even in this phantom-dust ADL scenario
    assert!(engine.check_conservation(),
        "conservation must hold after phantom-dust ADL scenario");
}

// ============================================================================
// Property 54: Unilateral exact-drain reset scheduling
// If enqueue_adl drives OI_eff_opp to 0 while OI_eff_liq_side remains
// positive, it schedules pending_reset_opp = true.
// ============================================================================

#[test]
fn test_property_54_unilateral_exact_drain_reset() {
    let oracle = 1_000u64;
    let slot = 1u64;
    let mut params = default_params();
    params.trading_fee_bps = 0;
    params.maintenance_fee_per_slot = U128::ZERO;
    params.new_account_fee = U128::ZERO;
    let mut engine = RiskEngine::new(params);

    let a = engine.add_user(0).unwrap();
    let b = engine.add_user(0).unwrap();
    engine.deposit(a, 100_000, oracle, slot).unwrap();
    engine.deposit(b, 100_000, oracle, slot).unwrap();
    engine.keeper_crank(slot, oracle, &[] as &[(u16, Option<LiquidationPolicy>)], 0).unwrap();

    // a long, b short
    let size_q = make_size_q(1);
    engine.execute_trade(a, b, oracle, slot, size_q, oracle).unwrap();

    // Crash the price to make account 'a' deeply underwater
    let crash_price = 100u64;
    let slot2 = slot + 1;

    // Liquidate 'a' — the long position is closed, ADL may drain the long side
    let result = engine.liquidate_at_oracle(a, slot2, crash_price, LiquidationPolicy::FullClose);
    assert!(result.is_ok(), "liquidation must succeed: {:?}", result);

    // After liquidation, the long side should be drained (only long was 'a').
    // The key property: no underflow or panic, and conservation holds
    // even when OI_eff on one side goes to 0.
    assert!(engine.check_conservation(), "conservation must hold after exact-drain scenario");

    // If long OI went to 0, the side should have a reset scheduled or already finalized
    if engine.oi_eff_long_q == 0 && engine.stored_pos_count_long == 0 {
        // Side was fully drained — mode should transition appropriately
        assert!(engine.side_mode_long != SideMode::Normal
                || engine.stored_pos_count_short == 0,
            "drained side should transition from Normal unless both sides empty");
    }
}

