//! Ring 3 — production-day replay validation. See real-LOB spec §8 Ring 3.
//!
//! Two scenarios:
//!
//!   buy_and_hold_full_day
//!     Walk a populated MBP-10 fixture from first event to last, submit
//!     a market buy of 1 lot at open, close with a market sell at the
//!     last event. Assert simulated realized P&L matches
//!     (close_mid - open_mid) within a slippage budget (a few ticks).
//!     This proves the book-walking + position accounting + step-loop
//!     orchestration are wire-level correct against real data, not
//!     just hand-crafted JSON fixtures.
//!
//!   walk_book_one_full_day
//!     Same fixture; no trades. Just iterates every event through
//!     apply_snapshot + step_resting_orders and asserts the LOB never
//!     violates the bid/ask monotonicity + no-crossed-book invariants
//!     at any point in the day. A stress test of the kernel against
//!     real market microstructure (gaps, locked markets, etc).
//!
//! Both skip gracefully if FOXHUNT_TEST_DATA lacks populated predecoded
//! sidecars — the placeholder-empty 40-byte sidecars committed in the
//! test fixture tree won't drive these tests, but a real Databento
//! decode will. The Ring 3 gate is operational, not CI-mandatory.

use anyhow::Result;
use ml_alpha::data::loader::{
    discover_mbp10_files_sorted, MultiHorizonLoader, MultiHorizonLoaderConfig,
};
use ml_backtesting::sim::LobSimCuda;
use ml_core::device::MlDevice;
use std::path::PathBuf;

fn try_loader() -> Option<MultiHorizonLoader> {
    let root = std::env::var("FOXHUNT_TEST_DATA").ok()?;
    let mbp10 = PathBuf::from(&root).join("ES.FUT");
    if !mbp10.exists() {
        return None;
    }
    let files = discover_mbp10_files_sorted(&mbp10).ok()?;
    // Take just the first file to keep the test bounded.
    let files = files.into_iter().take(1).collect::<Vec<_>>();
    let cfg = MultiHorizonLoaderConfig {
        files,
        predecoded_dir: mbp10.clone(),
        seq_len: 1,
        horizons: [30, 100, 300, 1000, 6000],
        n_max_sequences: 0,
        seed: 0xCAFE_F00D,
        decision_stride: 1,
        inference_only: true,
    };
    match MultiHorizonLoader::new(&cfg) {
        Ok(l) => Some(l),
        Err(e) => {
            eprintln!("skipping: fixture data not usable ({e})");
            None
        }
    }
}

fn try_device() -> Option<MlDevice> {
    match MlDevice::cuda(0) {
        Ok(d) => Some(d),
        Err(e) => {
            eprintln!("skipping: cuda device unavailable ({e})");
            None
        }
    }
}

fn mid_of(raw: &ml_alpha::cfc::snap_features::Mbp10RawInput) -> f32 {
    (raw.bid_px[0] + raw.ask_px[0]) * 0.5
}

#[test]
#[ignore = "requires populated FOXHUNT_TEST_DATA"]
fn buy_and_hold_full_day() -> Result<()> {
    let Some(mut loader) = try_loader() else { return Ok(()); };
    let Some(dev) = try_device() else { return Ok(()); };
    let mut sim = LobSimCuda::new(1, &dev)?;

    // Snapshot the OPENING price as soon as we see the first event.
    let first = match loader.next_inference_input()? {
        Some(r) => r,
        None => {
            eprintln!("skipping: empty stream");
            return Ok(());
        }
    };
    sim.apply_snapshot(&first.bid_px, &first.bid_sz, &first.ask_px, &first.ask_sz)?;
    let open_mid = mid_of(&first);
    // Aggressive buy 1 lot at the open's best ask.
    sim.submit_market(0, 0 /*buy*/, 1, first.ts_ns)?;

    let pos_after_open = sim.read_pos(0)?;
    assert_eq!(
        pos_after_open.position_lots, 1,
        "open buy didn't fill (likely fixture data has zero ask depth)"
    );
    let entry_vwap = pos_after_open.vwap_entry;

    // Walk every subsequent event through book_update + resting_orders.
    let mut last_raw = first;
    let mut event_count = 1u64;
    while let Some(raw) = loader.next_inference_input()? {
        sim.apply_snapshot(&raw.bid_px, &raw.bid_sz, &raw.ask_px, &raw.ask_sz)?;
        sim.step_resting_orders(raw.ts_ns, raw.trade_signed_vol)?;
        event_count += 1;
        last_raw = raw;
    }

    // Close at the last event: aggressive sell 1 lot.
    sim.submit_market(0, 1 /*sell*/, 1, last_raw.ts_ns)?;
    let final_pos = sim.read_pos(0)?;
    assert_eq!(
        final_pos.position_lots, 0,
        "close sell didn't fully unwind position"
    );

    let close_mid = mid_of(&last_raw);
    let expected_pnl_price_units = close_mid - open_mid;
    let realized = final_pos.realized_pnl;
    let drift_ticks = (realized - expected_pnl_price_units).abs() / 0.25;

    eprintln!(
        "ring3 buy_and_hold: events={} entry_vwap={:.2} open_mid={:.2} close_mid={:.2} expected_pnl={:.4} realized={:.4} drift_ticks={:.2}",
        event_count, entry_vwap, open_mid, close_mid, expected_pnl_price_units, realized, drift_ticks
    );

    // Slippage budget: ±2 ticks (= 0.5 price units) per spec §8 Ring 3.
    // The simulator's market-order path walks the book at both ends, so
    // the difference vs mid-to-mid pricing accumulates the open-side
    // half-spread + the close-side half-spread, plus any in-day book
    // movement we observed via realized_pnl bookkeeping.
    assert!(
        drift_ticks <= 2.0,
        "realized P&L drift {drift_ticks:.2} ticks > 2-tick budget — sim books vs hand-computed mid diverge"
    );
    Ok(())
}

#[test]
#[ignore = "requires populated FOXHUNT_TEST_DATA"]
fn walk_book_one_full_day() -> Result<()> {
    let Some(mut loader) = try_loader() else { return Ok(()); };
    let Some(dev) = try_device() else { return Ok(()); };
    let mut sim = LobSimCuda::new(1, &dev)?;

    let mut event_count = 0u64;
    while let Some(raw) = loader.next_inference_input()? {
        sim.apply_snapshot(&raw.bid_px, &raw.bid_sz, &raw.ask_px, &raw.ask_sz)?;
        sim.step_resting_orders(raw.ts_ns, raw.trade_signed_vol)?;
        event_count += 1;

        // Spot-check invariants every 10_000 events to bound test time.
        if event_count % 10_000 == 0 {
            let books = sim.read_books()?;
            for (b, book) in books.iter().enumerate() {
                for k in 1..10 {
                    assert!(
                        book.bid_px[k] <= book.bid_px[k - 1] + 1e-4,
                        "event {event_count} backtest {b}: bid monotonicity violated at level {k}"
                    );
                    assert!(
                        book.ask_px[k] >= book.ask_px[k - 1] - 1e-4,
                        "event {event_count} backtest {b}: ask monotonicity violated at level {k}"
                    );
                }
                assert!(
                    book.ask_px[0] > book.bid_px[0],
                    "event {event_count} backtest {b}: crossed book"
                );
            }
        }
    }
    eprintln!("ring3 walk_book: walked {event_count} events without invariant violation");
    assert!(event_count > 0, "fixture produced no events");
    Ok(())
}
