//! Ring 1 fixture tests: N=1 (or small N) bit-exact GPU oracle assertions.
//! See docs/superpowers/specs/2026-05-18-real-lob-integration-design.md §8.
//!
//! Run with: `cargo test -p ml-backtesting --test lob_sim_fixtures -- --ignored --nocapture`

use anyhow::{Context, Result};
use ml_backtesting::lob::BOOK_LEVELS;
use ml_backtesting::policy::IsvKellyStateHost;
use ml_backtesting::sim::LobSimCuda;
use ml_core::device::MlDevice;
use serde::Deserialize;
use std::path::Path;

#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
enum FixtureEvent {
    Snapshot {
        bid_px: [f32; BOOK_LEVELS],
        bid_sz: [f32; BOOK_LEVELS],
        ask_px: [f32; BOOK_LEVELS],
        ask_sz: [f32; BOOK_LEVELS],
    },
    SubmitMarket {
        backtest_idx: usize,
        side: String,
        size: u16,
        #[serde(default = "default_ts_ns")]
        ts_ns: u64,
    },
    Decision {
        alpha_probs: [f32; 5],
        target_annual_vol_units: f32,
        annualisation_factor: f32,
        max_lots: u16,
        #[serde(default = "default_ts_ns")]
        ts_ns: u64,
    },
    SeedLimit {
        backtest_idx: usize,
        slot_idx: usize,
        side: String,
        kind: String,
        active_state: u8,
        oco_pair: u8,
        price: f32,
        size: f32,
        #[serde(default)] queue_position_init: f32,
        #[serde(default)] arrival_ts_ns: u64,
    },
    SeedStop {
        backtest_idx: usize,
        slot_idx: usize,
        side: String,
        kind: String,
        active_state: u8,
        oco_pair: u8,
        trigger_price: f32,
        #[serde(default)] limit_price: f32,
        size: f32,
        #[serde(default)] arrival_ts_ns: u64,
    },
    StepResting {
        ts_ns: u64,
        #[serde(default)] trade_signed_vol: f32,
    },
}

#[derive(Debug, Deserialize)]
struct ExpectedSlotState {
    backtest_idx: usize,
    slot_idx: usize,
    active: u8,
}

fn default_ts_ns() -> u64 { 1 }

#[derive(Debug, Deserialize, Default)]
struct IsvKellyFixture {
    #[serde(default)] pnl_ema_win: f32,
    #[serde(default)] pnl_ema_loss: f32,
    #[serde(default)] win_rate_ema: f32,
    #[serde(default)] n_trades_seen: u32,
    #[serde(default)] realised_return_var: f32,
    #[serde(default)] recent_sharpe: f32,
}

impl IsvKellyFixture {
    fn to_host(&self) -> IsvKellyStateHost {
        IsvKellyStateHost {
            pnl_ema_win: self.pnl_ema_win,
            pnl_ema_loss: self.pnl_ema_loss,
            win_rate_ema: self.win_rate_ema,
            n_trades_seen: self.n_trades_seen,
            realised_return_var: self.realised_return_var,
            recent_sharpe: self.recent_sharpe,
        }
    }
}

#[derive(Debug, Deserialize, Default)]
struct ExpectedIsvKelly {
    #[serde(default)] n_trades_seen_min: u32,
    #[serde(default = "u32_max")] n_trades_seen_max: u32,
}
fn u32_max() -> u32 { u32::MAX }

#[derive(Debug, Deserialize)]
struct ExpectedPos {
    position_lots: i32,
    #[serde(default)]
    vwap_entry: f32,
    #[serde(default = "default_tol")]
    vwap_tol: f32,
    #[serde(default)]
    realized_pnl: f32,
    #[serde(default = "default_tol")]
    realized_pnl_tol: f32,
}

fn default_tol() -> f32 { 1e-3 }

#[derive(Debug, Deserialize)]
struct FixtureBook {
    bid_px: [f32; BOOK_LEVELS],
    bid_sz: [f32; BOOK_LEVELS],
    ask_px: [f32; BOOK_LEVELS],
    ask_sz: [f32; BOOK_LEVELS],
}

#[derive(Debug, Deserialize)]
struct ExpectedTradeRecord {
    entry_ts_ns: u64,
    exit_ts_ns: u64,
    size_lots: i32,
    realised_pnl_usd_fp: i32,
    #[serde(default = "default_pnl_fp_tol")]
    realised_pnl_tol_usd_fp: i32,
}
fn default_pnl_fp_tol() -> i32 { 100 }

#[derive(Debug, Deserialize)]
struct Fixture {
    name: String,
    n_backtests: usize,
    events: Vec<FixtureEvent>,
    #[serde(default)]
    expected_books: Vec<FixtureBook>,
    #[serde(default)]
    expected_books_apply_to_all: bool,
    #[serde(default)]
    expected_pos: Vec<ExpectedPos>,
    #[serde(default)]
    expected_trade_records: Vec<ExpectedTradeRecord>,
    #[serde(default)]
    warm_start_isv_kelly: Vec<[IsvKellyFixture; 5]>,
    #[serde(default)]
    expected_min_trade_records: usize,
    #[serde(default)]
    expected_isv_kelly_after: Vec<[ExpectedIsvKelly; 5]>,
    #[serde(default)]
    expected_limit_slot_states: Vec<ExpectedSlotState>,
    #[serde(default)]
    expected_stop_slot_states: Vec<ExpectedSlotState>,
    #[serde(default)]
    fill_all_limit_slots: bool,
    #[serde(default)]
    expect_33rd_overflow: bool,
}

fn parse_kind_limit(s: &str) -> u8 {
    match s {
        "Limit" => 1,
        "Ioc" => 2,
        "Fok" => 3,
        other => panic!("unknown limit kind: {other}"),
    }
}
fn parse_kind_stop(s: &str) -> u8 {
    match s {
        "StopMarket" => 4,
        "StopLimit" => 5,
        other => panic!("unknown stop kind: {other}"),
    }
}
fn parse_side(s: &str) -> u8 {
    match s {
        "buy" => 0,
        "sell" => 1,
        other => panic!("unknown side: {other}"),
    }
}

fn run_book_fixture(path: &Path) -> Result<()> {
    let s = std::fs::read_to_string(path)
        .with_context(|| format!("read {}", path.display()))?;
    let fx: Fixture = serde_json::from_str(&s).context("parse fixture")?;
    eprintln!("running fixture: {}", fx.name);

    let dev = MlDevice::cuda(0)
        .map_err(|e| anyhow::anyhow!("cuda device: {e}"))?;
    let mut sim = LobSimCuda::new(fx.n_backtests, &dev)?;

    // Apply ISV-Kelly warm-start if provided (one row per backtest).
    for (b, states) in fx.warm_start_isv_kelly.iter().enumerate() {
        let host_states: [IsvKellyStateHost; 5] = [
            states[0].to_host(),
            states[1].to_host(),
            states[2].to_host(),
            states[3].to_host(),
            states[4].to_host(),
        ];
        sim.write_isv_kelly(b, &host_states)?;
    }

    for ev in &fx.events {
        match ev {
            FixtureEvent::Snapshot { bid_px, bid_sz, ask_px, ask_sz } => {
                sim.apply_snapshot(bid_px, bid_sz, ask_px, ask_sz)?;
            }
            FixtureEvent::SubmitMarket { backtest_idx, side, size, ts_ns } => {
                let side_u8 = match side.as_str() {
                    "buy" => 0u8,
                    "sell" => 1u8,
                    other => anyhow::bail!("submit_market: unknown side {other}"),
                };
                sim.submit_market(*backtest_idx, side_u8, *size, *ts_ns)?;
            }
            FixtureEvent::Decision {
                alpha_probs,
                target_annual_vol_units,
                annualisation_factor,
                max_lots,
                ts_ns,
            } => {
                sim.broadcast_alpha(alpha_probs)?;
                sim.step_decision(*ts_ns, *target_annual_vol_units, *annualisation_factor, *max_lots)?;
            }
            FixtureEvent::SeedLimit {
                backtest_idx, slot_idx, side, kind, active_state,
                oco_pair, price, size, queue_position_init, arrival_ts_ns,
            } => {
                sim.seed_limit_order(
                    *backtest_idx, *slot_idx,
                    parse_side(side), parse_kind_limit(kind), *active_state,
                    *oco_pair, *price, *size, *queue_position_init, *arrival_ts_ns,
                )?;
            }
            FixtureEvent::SeedStop {
                backtest_idx, slot_idx, side, kind, active_state,
                oco_pair, trigger_price, limit_price, size, arrival_ts_ns,
            } => {
                sim.seed_stop_order(
                    *backtest_idx, *slot_idx,
                    parse_side(side), parse_kind_stop(kind), *active_state,
                    *oco_pair, *trigger_price, *limit_price, *size, *arrival_ts_ns,
                )?;
            }
            FixtureEvent::StepResting { ts_ns, trade_signed_vol } => {
                sim.step_resting_orders(*ts_ns, *trade_signed_vol)?;
            }
        }
    }

    if fx.expected_min_trade_records > 0 {
        let records = sim.read_trade_records(0)?;
        assert!(
            records.len() >= fx.expected_min_trade_records,
            "expected >= {} trade records, got {}",
            fx.expected_min_trade_records,
            records.len()
        );
    }

    if fx.fill_all_limit_slots {
        // Seed all 32 limit slots successfully; expect each call to return Ok.
        for slot in 0..ml_backtesting::lob::MAX_LIMITS {
            sim.seed_limit_order(
                0, slot, 0 /*buy*/, 1 /*Limit*/, 1 /*resting*/, 0xFF,
                5400.00 /* deep below the book — never marketable */,
                1.0, 0.0, 0,
            )?;
        }
        // The 33rd seed (re-seeding slot 0 which is in use) must fail.
        if fx.expect_33rd_overflow {
            let res = sim.seed_limit_order(
                0, 0, 0, 1, 1, 0xFF, 5400.00, 1.0, 0.0, 0,
            );
            assert!(
                res.is_err(),
                "expected slot-already-in-use error, got Ok"
            );
        }
    }

    for want in &fx.expected_limit_slot_states {
        let got = sim.read_limit_slot(want.backtest_idx, want.slot_idx)?;
        assert_eq!(
            got.active, want.active,
            "limit_slot[b={},s={}].active got {} want {}",
            want.backtest_idx, want.slot_idx, got.active, want.active
        );
    }
    for want in &fx.expected_stop_slot_states {
        let got = sim.read_stop_slot(want.backtest_idx, want.slot_idx)?;
        assert_eq!(
            got.active, want.active,
            "stop_slot[b={},s={}].active got {} want {}",
            want.backtest_idx, want.slot_idx, got.active, want.active
        );
    }

    if !fx.expected_isv_kelly_after.is_empty() {
        for (b, expected) in fx.expected_isv_kelly_after.iter().enumerate() {
            let got = sim.read_isv_kelly(b)?;
            for h in 0..5 {
                let n = got[h].n_trades_seen;
                assert!(
                    n >= expected[h].n_trades_seen_min && n <= expected[h].n_trades_seen_max,
                    "backtest {b} horizon {h}: n_trades_seen got {n}, want in [{}..{}]",
                    expected[h].n_trades_seen_min,
                    expected[h].n_trades_seen_max
                );
            }
        }
    }

    if !fx.expected_books.is_empty() {
        let books = sim.read_books()?;
        let expected: Vec<&FixtureBook> = if fx.expected_books_apply_to_all {
            (0..fx.n_backtests).map(|_| &fx.expected_books[0]).collect()
        } else {
            anyhow::ensure!(
                fx.expected_books.len() == fx.n_backtests,
                "expected_books len {} ≠ n_backtests {}",
                fx.expected_books.len(),
                fx.n_backtests
            );
            fx.expected_books.iter().collect()
        };

        for (b, (got, want)) in books.iter().zip(expected.iter()).enumerate() {
            for k in 0..BOOK_LEVELS {
                assert_eq!(got.bid_px[k], want.bid_px[k], "backtest {b} bid_px[{k}]");
                assert_eq!(got.bid_sz[k], want.bid_sz[k], "backtest {b} bid_sz[{k}]");
                assert_eq!(got.ask_px[k], want.ask_px[k], "backtest {b} ask_px[{k}]");
                assert_eq!(got.ask_sz[k], want.ask_sz[k], "backtest {b} ask_sz[{k}]");
            }
        }
    }

    if !fx.expected_trade_records.is_empty() {
        let records = sim.read_trade_records(0)?;
        assert_eq!(
            records.len(),
            fx.expected_trade_records.len(),
            "trade record count mismatch: got {}, want {}",
            records.len(),
            fx.expected_trade_records.len()
        );
        for (i, want) in fx.expected_trade_records.iter().enumerate() {
            let got = &records[i];
            assert_eq!(got.entry_ts_ns, want.entry_ts_ns, "trade[{i}].entry_ts_ns");
            assert_eq!(got.exit_ts_ns, want.exit_ts_ns, "trade[{i}].exit_ts_ns");
            assert_eq!(got.size_lots, want.size_lots, "trade[{i}].size_lots");
            let diff = (got.realised_pnl_usd_fp - want.realised_pnl_usd_fp).abs();
            assert!(
                diff <= want.realised_pnl_tol_usd_fp,
                "trade[{i}].realised_pnl_usd_fp got {} want {} (tol {})",
                got.realised_pnl_usd_fp, want.realised_pnl_usd_fp, want.realised_pnl_tol_usd_fp
            );
        }
    }

    if !fx.expected_pos.is_empty() {
        anyhow::ensure!(
            fx.expected_pos.len() == fx.n_backtests,
            "expected_pos len {} ≠ n_backtests {}",
            fx.expected_pos.len(),
            fx.n_backtests
        );
        for (b, want) in fx.expected_pos.iter().enumerate() {
            let got = sim.read_pos(b)?;
            assert_eq!(
                got.position_lots, want.position_lots,
                "backtest {b} position_lots"
            );
            if want.position_lots != 0 {
                let diff = (got.vwap_entry - want.vwap_entry).abs();
                assert!(
                    diff <= want.vwap_tol,
                    "backtest {b} vwap_entry got {} want {} (tol {})",
                    got.vwap_entry, want.vwap_entry, want.vwap_tol
                );
            }
            let pnl_diff = (got.realized_pnl - want.realized_pnl).abs();
            assert!(
                pnl_diff <= want.realized_pnl_tol,
                "backtest {b} realized_pnl got {} want {} (tol {})",
                got.realized_pnl, want.realized_pnl, want.realized_pnl_tol
            );
        }
    }

    Ok(())
}

#[test]
#[ignore = "requires CUDA"]
fn fix_book_update_replace() {
    run_book_fixture(Path::new("tests/fixtures/book_update_replace.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_book_update_two_step() {
    run_book_fixture(Path::new("tests/fixtures/book_update_two_step.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_book_update_n_backtests() {
    run_book_fixture(Path::new("tests/fixtures/book_update_n_backtests.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_market_order_consumes_top() {
    run_book_fixture(Path::new("tests/fixtures/market_order_consumes_top.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_pnl_accounting_buy_close() {
    run_book_fixture(Path::new("tests/fixtures/pnl_accounting_buy_close.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_decision_alpha_buy_close() {
    run_book_fixture(Path::new("tests/fixtures/decision_alpha_buy_close.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_limit_rest_marketable_fill() {
    run_book_fixture(Path::new("tests/fixtures/limit_rest_marketable_fill.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_stop_trigger() {
    run_book_fixture(Path::new("tests/fixtures/stop_trigger.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_oco_one_cancels_other() {
    run_book_fixture(Path::new("tests/fixtures/oco_one_cancels_other.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_submission_overflow() {
    run_book_fixture(Path::new("tests/fixtures/submission_overflow.json")).unwrap();
}

#[test]
#[ignore = "requires CUDA"]
fn fix_latency_in_flight_miss() {
    run_book_fixture(Path::new("tests/fixtures/latency_in_flight_miss.json")).unwrap();
}
