//! Ring 2 — property-based fuzz at N ∈ {1, 16, 256}.
//!
//! Random-walk MBP-10 sequences applied to the LOB simulator; assert
//! invariants that must hold regardless of input or scheduling:
//!
//!   * bid_px monotonically non-increasing across levels
//!   * ask_px monotonically non-decreasing across levels
//!   * ask_px[0] > bid_px[0] (no crossed book)
//!   * Position lots stays within plausible bounds even under
//!     random market orders
//!   * Realised P&L finite (no NaN/Inf leaks)
//!
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §8 Ring 2.

use anyhow::Result;
use ml_backtesting::sim::LobSimCuda;
use ml_core::device::MlDevice;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

const BOOK_LEVELS: usize = 10;
const TICK: f32 = 0.25;

fn make_initial_book() -> ([f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LEVELS], [f32; BOOK_LEVELS]) {
    let mut bid_px = [0.0; BOOK_LEVELS];
    let mut bid_sz = [0.0; BOOK_LEVELS];
    let mut ask_px = [0.0; BOOK_LEVELS];
    let mut ask_sz = [0.0; BOOK_LEVELS];
    let mid = 5500.00_f32;
    for k in 0..BOOK_LEVELS {
        bid_px[k] = mid - TICK * (k as f32 + 1.0);
        ask_px[k] = mid + TICK * (k as f32 + 1.0);
        bid_sz[k] = 5.0 + 5.0 * k as f32;
        ask_sz[k] = 5.0 + 5.0 * k as f32;
    }
    (bid_px, bid_sz, ask_px, ask_sz)
}

fn step_random_walk(
    rng: &mut ChaCha8Rng,
    bid_px: &mut [f32; BOOK_LEVELS],
    bid_sz: &mut [f32; BOOK_LEVELS],
    ask_px: &mut [f32; BOOK_LEVELS],
    ask_sz: &mut [f32; BOOK_LEVELS],
) {
    // Random mid drift in {-TICK, 0, +TICK} with mild bias.
    let drift_choices = [-TICK, 0.0, 0.0, 0.0, TICK];
    let drift = drift_choices[rng.gen_range(0..drift_choices.len())];
    // Shift the whole book by drift, preserving level structure.
    for k in 0..BOOK_LEVELS {
        bid_px[k] += drift;
        ask_px[k] += drift;
        // Jitter sizes; clamp positive.
        let dbz: f32 = rng.gen_range(-3.0..3.0);
        let daz: f32 = rng.gen_range(-3.0..3.0);
        bid_sz[k] = (bid_sz[k] + dbz).max(1.0);
        ask_sz[k] = (ask_sz[k] + daz).max(1.0);
    }
}

fn assert_book_invariants(b_idx: usize, bid_px: &[f32], bid_sz: &[f32], ask_px: &[f32], ask_sz: &[f32]) {
    for k in 1..BOOK_LEVELS {
        assert!(
            bid_px[k] <= bid_px[k - 1] + 1e-4,
            "backtest {b_idx}: bid_px non-monotone at level {k}: {} > {}",
            bid_px[k], bid_px[k - 1]
        );
        assert!(
            ask_px[k] >= ask_px[k - 1] - 1e-4,
            "backtest {b_idx}: ask_px non-monotone at level {k}: {} < {}",
            ask_px[k], ask_px[k - 1]
        );
    }
    assert!(
        ask_px[0] > bid_px[0],
        "backtest {b_idx}: crossed book — ask[0]={}, bid[0]={}",
        ask_px[0], bid_px[0]
    );
    for k in 0..BOOK_LEVELS {
        assert!(bid_sz[k] > 0.0, "backtest {b_idx}: bid_sz[{k}] non-positive");
        assert!(ask_sz[k] > 0.0, "backtest {b_idx}: ask_sz[{k}] non-positive");
        assert!(bid_px[k].is_finite() && ask_px[k].is_finite());
    }
}

fn run_fuzz_n(n_backtests: usize, n_events: usize, with_orders: bool, seed: u64) -> Result<()> {
    let dev = MlDevice::cuda(0).map_err(|e| anyhow::anyhow!("cuda dev: {e}"))?;
    let mut sim = LobSimCuda::new(n_backtests, &dev)?;
    let mut rng = ChaCha8Rng::seed_from_u64(seed);

    let (mut bid_px, mut bid_sz, mut ask_px, mut ask_sz) = make_initial_book();

    let mut ts_ns: u64 = 1_000_000_000;
    for event_idx in 0..n_events {
        step_random_walk(&mut rng, &mut bid_px, &mut bid_sz, &mut ask_px, &mut ask_sz);
        sim.apply_snapshot(&bid_px, &bid_sz, &ask_px, &ask_sz)?;

        // 1 in 8 events: each backtest submits a random market order.
        if with_orders && event_idx % 8 == 0 {
            for b in 0..n_backtests {
                let side: u8 = if rng.gen_bool(0.5) { 0 } else { 1 };
                let size: u16 = rng.gen_range(1..=3);
                sim.submit_market(b, side, size, ts_ns)?;
            }
        }

        // Invariants on book state.
        let books = sim.read_books()?;
        for (b, book) in books.iter().enumerate() {
            assert_book_invariants(b, &book.bid_px, &book.bid_sz, &book.ask_px, &book.ask_sz);
        }

        if with_orders {
            // Position + P&L sanity per backtest.
            for b in 0..n_backtests {
                let pos = sim.read_pos(b)?;
                assert!(
                    pos.position_lots.abs() <= 30,
                    "backtest {b}: position_lots out of plausible range: {}",
                    pos.position_lots
                );
                assert!(pos.realized_pnl.is_finite(), "backtest {b}: realized_pnl NaN/Inf");
                assert!(pos.vwap_entry.is_finite(), "backtest {b}: vwap_entry NaN/Inf");
            }
        }

        ts_ns += 1_000_000;
    }
    Ok(())
}

#[test]
#[ignore = "requires CUDA"]
fn fuzz_n1_book_only() -> Result<()> {
    run_fuzz_n(1, 200, /*with_orders=*/ false, 0xCAFE_F00D)
}

#[test]
#[ignore = "requires CUDA"]
fn fuzz_n16_with_orders() -> Result<()> {
    run_fuzz_n(16, 200, /*with_orders=*/ true, 0xDEAD_BEEF)
}

#[test]
#[ignore = "requires CUDA"]
fn fuzz_n256_with_orders() -> Result<()> {
    run_fuzz_n(256, 100, /*with_orders=*/ true, 0x1234_5678)
}
