//! BookSimExchange — L2 orderbook matching engine for realistic backtesting.
//!
//! Matches orders against stored L2 orderbook snapshots with:
//! - Three fill models (Deterministic, Probabilistic, GLFT)
//! - Market impact (temporary + permanent)
//! - Latency simulation via pending order queue

use crate::error::HorizonError;
use crate::exchanges::Exchange;
use crate::orderbook::OrderbookSnapshot;
use crate::types::{Fill, Order, OrderRequest, OrderSide, OrderStatus, OrderType, TimeInForce};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};

// ---------------------------------------------------------------------------
// xoshiro256++ PRNG (same as simulation.rs, inlined to avoid coupling)
// ---------------------------------------------------------------------------

struct Xoshiro256PlusPlus {
    s: [u64; 4],
}

impl Xoshiro256PlusPlus {
    fn new(seed: u64) -> Self {
        let mut state = seed;
        let mut s = [0u64; 4];
        for slot in &mut s {
            state = state.wrapping_add(0x9E3779B97F4A7C15);
            let mut z = state;
            z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
            z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
            z ^= z >> 31;
            *slot = z;
        }
        Self { s }
    }

    #[inline]
    fn next_u64(&mut self) -> u64 {
        let result = (self.s[0].wrapping_add(self.s[3]))
            .rotate_left(23)
            .wrapping_add(self.s[0]);
        let t = self.s[1] << 17;
        self.s[2] ^= self.s[0];
        self.s[3] ^= self.s[1];
        self.s[1] ^= self.s[2];
        self.s[0] ^= self.s[3];
        self.s[2] ^= t;
        self.s[3] = self.s[3].rotate_left(45);
        result
    }

    #[inline]
    fn next_f64(&mut self) -> f64 {
        let bits = self.next_u64() >> 11;
        bits as f64 * (1.0 / (1u64 << 53) as f64)
    }
}

// ---------------------------------------------------------------------------
// Fill model
// ---------------------------------------------------------------------------

/// Fill model determines how resting orders interact with book depth.
pub enum FillModel {
    /// Fills if price crosses — same as paper exchange.
    Deterministic,
    /// P(fill) = exp(-lambda * distance) * (1 - queue_frac)
    Probabilistic { lambda: f64, queue_frac: f64 },
    /// Guéant-Lehalle-Fernandez-Tapia: intensity * exp(-kappa * delta)
    GLFT { intensity: f64, kappa: f64 },
}

// ---------------------------------------------------------------------------
// Impact model
// ---------------------------------------------------------------------------

/// Market impact applied during book walks.
pub struct ImpactModel {
    /// Price shift per unit during book walk (in basis points).
    pub temporary_bps: f64,
    /// Fraction of temporary impact that persists across ticks.
    pub permanent_fraction: f64,
}

impl ImpactModel {
    pub fn none() -> Self {
        Self {
            temporary_bps: 0.0,
            permanent_fraction: 0.0,
        }
    }
}

// ---------------------------------------------------------------------------
// Internal state
// ---------------------------------------------------------------------------

struct BookState {
    book: OrderbookSnapshot,
    permanent_impact: f64,
}

struct BookSimInner {
    orders: Vec<Order>,
    fills: Vec<Fill>,
    next_fill_id: u64,
    books: HashMap<String, BookState>,
    pending_orders: Vec<(u32, Order)>, // (ticks_remaining, order)
    rng: Xoshiro256PlusPlus,
    last_book_ts: f64,
}

#[inline]
fn is_open(status: OrderStatus) -> bool {
    matches!(
        status,
        OrderStatus::New
            | OrderStatus::Submitted
            | OrderStatus::Accepted
            | OrderStatus::PartiallyFilled
    )
}

// ---------------------------------------------------------------------------
// BookSimExchange
// ---------------------------------------------------------------------------

/// L2 orderbook simulation exchange for realistic backtesting.
pub struct BookSimExchange {
    inner: Mutex<BookSimInner>,
    maker_fee_rate: f64,
    taker_fee_rate: f64,
    fill_model: FillModel,
    impact: ImpactModel,
    latency_ticks: u32,
    next_order_id: AtomicU64,
}

impl BookSimExchange {
    pub fn new(
        fee_rate: f64,
        fill_model: FillModel,
        impact: ImpactModel,
        latency_ticks: u32,
        seed: u64,
    ) -> Self {
        Self::with_maker_taker_fees(fee_rate, fee_rate, fill_model, impact, latency_ticks, seed)
    }

    pub fn with_maker_taker_fees(
        maker_fee_rate: f64,
        taker_fee_rate: f64,
        fill_model: FillModel,
        impact: ImpactModel,
        latency_ticks: u32,
        seed: u64,
    ) -> Self {
        Self {
            inner: Mutex::new(BookSimInner {
                orders: Vec::new(),
                fills: Vec::new(),
                next_fill_id: 1,
                books: HashMap::new(),
                pending_orders: Vec::new(),
                rng: Xoshiro256PlusPlus::new(seed),
                last_book_ts: 0.0,
            }),
            maker_fee_rate,
            taker_fee_rate,
            fill_model,
            impact,
            latency_ticks,
            next_order_id: AtomicU64::new(1),
        }
    }

    /// Update the L2 orderbook snapshot for a market. Call before tick().
    pub fn update_book(&self, market_id: &str, book: OrderbookSnapshot) {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let entry = inner
            .books
            .entry(market_id.to_string())
            .or_insert_with(|| BookState {
                book: OrderbookSnapshot::default(),
                permanent_impact: 0.0,
            });
        entry.book = book;
        inner.last_book_ts = entry.book.timestamp;
    }

    /// Tick the matching engine against the stored L2 book for a market.
    /// Returns the number of fills generated.
    pub fn tick(&self, market_id: &str) -> usize {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let now = if inner.last_book_ts > 0.0 {
            inner.last_book_ts
        } else {
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs_f64()
        };

        // Phase 0: Promote matured pending orders from latency queue
        let mut promoted = Vec::new();
        let mut still_pending = Vec::new();
        for (ticks_left, order) in std::mem::take(&mut inner.pending_orders) {
            if ticks_left <= 1 {
                promoted.push(order);
            } else {
                still_pending.push((ticks_left - 1, order));
            }
        }
        inner.pending_orders = still_pending;
        inner.orders.extend(promoted);

        // Clone book state to release the borrow on inner.books before mutable rng access
        let (book, permanent_impact) = match inner.books.get(market_id) {
            Some(bs) => (bs.book.clone(), bs.permanent_impact),
            None => return 0, // No book data — can't match
        };

        // Phase 1: Identify candidate orders (collect indices + snapshots to avoid borrow conflict)
        let candidates: Vec<(usize, Order)> = inner
            .orders
            .iter()
            .enumerate()
            .filter(|(_, order)| order.market_id == market_id && is_open(order.status))
            .map(|(i, order)| (i, order.clone()))
            .collect();

        // Phase 2: Walk book for each candidate (needs &mut rng)
        let mut next_id = inner.next_fill_id;
        let maker_fee = self.maker_fee_rate;
        let taker_fee = self.taker_fee_rate;
        let mut new_fills: Vec<Fill> = Vec::new();
        let mut fill_sizes: Vec<(usize, f64)> = Vec::new();
        let mut total_filled_notional = 0.0;

        // Compute mid price from book for maker/taker determination
        let mid_price = {
            let best_bid = book.bids.first().map(|(p, _)| *p).unwrap_or(0.0);
            let best_ask = book.asks.first().map(|(p, _)| *p).unwrap_or(1.0);
            (best_bid + best_ask) / 2.0
        };

        for (i, order) in &candidates {
            let fill_result = self.walk_book_for_order(
                order,
                &book,
                permanent_impact,
                order.remaining_size,
                &mut inner.rng,
            );

            if let Some((fill_size, avg_price)) = fill_result {
                if fill_size > 0.0 {
                    // FOK: must fill entirely or not at all
                    if order.time_in_force == TimeInForce::FOK
                        && (order.remaining_size - fill_size) > f64::EPSILON
                    {
                        continue;
                    }
                    let fill_id = format!("bs_fill_{}", next_id);
                    next_id += 1;

                    // Determine maker vs taker (same logic as paper exchange)
                    let is_maker = match order.order_type {
                        OrderType::Market => false,
                        OrderType::Limit => match order.order_side {
                            OrderSide::Buy => order.price < mid_price,
                            OrderSide::Sell => order.price > mid_price,
                        },
                    };

                    let fee_rate = if is_maker { maker_fee } else { taker_fee };
                    let fee = fill_size * avg_price * fee_rate;
                    new_fills.push(Fill {
                        fill_id,
                        order_id: order.id.clone(),
                        market_id: order.market_id.clone(),
                        side: order.side,
                        order_side: order.order_side,
                        price: avg_price,
                        size: fill_size,
                        fee,
                        timestamp: now,
                        token_id: None,
                        exchange: "book_sim".to_string(),
                        is_maker,
                    });
                    fill_sizes.push((*i, fill_size));
                    total_filled_notional += fill_size * avg_price;
                }
            }
        }

        // Phase 3: Mutate order states
        for &(i, fill_size) in &fill_sizes {
            let order = &mut inner.orders[i];
            order.filled_size += fill_size;
            order.remaining_size = (order.size - order.filled_size).max(0.0);
            if order.remaining_size <= f64::EPSILON {
                order.status = OrderStatus::Filled;
            } else {
                order.status = OrderStatus::PartiallyFilled;
            }
        }

        // Phase 4: Handle TIF semantics — cancel unfilled FOK/FAK/Market orders
        for order in &mut inner.orders {
            if order.market_id == market_id && is_open(order.status) {
                let should_cancel = matches!(
                    order.time_in_force,
                    TimeInForce::FOK | TimeInForce::FAK
                ) || order.order_type == OrderType::Market;
                if should_cancel {
                    order.status = OrderStatus::Canceled;
                    order.status_reason = Some("tif_expired".into());
                }
            }
        }

        // Post-tick: Accumulate permanent impact
        if self.impact.temporary_bps > 0.0 && self.impact.permanent_fraction > 0.0 {
            if let Some(bs) = inner.books.get_mut(market_id) {
                bs.permanent_impact += total_filled_notional
                    * self.impact.permanent_fraction
                    * self.impact.temporary_bps
                    / 10000.0;
            }
        }

        inner.next_fill_id = next_id;
        let fill_count = new_fills.len();
        inner.fills.extend(new_fills);
        fill_count
    }

    /// Walk the opposite book side for an order, applying fill model and impact.
    /// Returns Some((total_fill_size, volume_weighted_avg_price)) or None.
    fn walk_book_for_order(
        &self,
        order: &Order,
        book: &OrderbookSnapshot,
        permanent_impact: f64,
        max_size: f64,
        rng: &mut Xoshiro256PlusPlus,
    ) -> Option<(f64, f64)> {
        let levels: &[(f64, f64)] = match order.order_side {
            OrderSide::Buy => &book.asks,  // Buy walks asks ascending
            OrderSide::Sell => &book.bids, // Sell walks bids descending
        };

        if levels.is_empty() {
            return None;
        }

        let mut filled_so_far = 0.0;
        let mut notional_so_far = 0.0;
        let mut remaining = max_size;

        for &(level_price, level_size) in levels {
            if remaining <= f64::EPSILON {
                break;
            }

            // Apply permanent impact to level price
            let adjusted_price = match order.order_side {
                OrderSide::Buy => level_price + permanent_impact,
                OrderSide::Sell => level_price - permanent_impact,
            };

            // Check limit price constraint
            if order.order_type == OrderType::Limit {
                match order.order_side {
                    OrderSide::Buy => {
                        if adjusted_price > order.price {
                            break; // Rest of book is too expensive
                        }
                    }
                    OrderSide::Sell => {
                        if adjusted_price < order.price {
                            break; // Rest of book is too cheap
                        }
                    }
                }
            }

            // Available size at this level
            let available = level_size.min(remaining);

            // Apply fill model to determine actual fill amount
            let model_fill = self.apply_fill_model(
                available,
                adjusted_price,
                order,
                filled_so_far,
                rng,
            );

            if model_fill <= f64::EPSILON {
                continue;
            }

            // Apply temporary impact: effective price shifts based on cumulative fill
            let impact_shift = if self.impact.temporary_bps > 0.0 {
                filled_so_far * self.impact.temporary_bps / 10000.0
            } else {
                0.0
            };

            let effective_price = match order.order_side {
                OrderSide::Buy => adjusted_price + impact_shift,
                OrderSide::Sell => (adjusted_price - impact_shift).max(0.0),
            };

            // Re-check limit after impact
            if order.order_type == OrderType::Limit {
                match order.order_side {
                    OrderSide::Buy => {
                        if effective_price > order.price {
                            break;
                        }
                    }
                    OrderSide::Sell => {
                        if effective_price < order.price {
                            break;
                        }
                    }
                }
            }

            filled_so_far += model_fill;
            notional_so_far += model_fill * effective_price;
            remaining -= model_fill;
        }

        if filled_so_far > f64::EPSILON {
            let avg_price = notional_so_far / filled_so_far;
            Some((filled_so_far, avg_price))
        } else {
            None
        }
    }

    /// Apply the fill model to determine how much of `available` actually fills.
    #[inline]
    fn apply_fill_model(
        &self,
        available: f64,
        level_price: f64,
        order: &Order,
        _filled_so_far: f64,
        rng: &mut Xoshiro256PlusPlus,
    ) -> f64 {
        match &self.fill_model {
            FillModel::Deterministic => available,
            FillModel::Probabilistic { lambda, queue_frac } => {
                // Distance from order price to level price
                let distance = (level_price - order.price).abs();
                let p_fill = (-lambda * distance).exp() * (1.0 - queue_frac);
                let p_fill = p_fill.clamp(0.0, 1.0);
                if rng.next_f64() < p_fill {
                    available
                } else {
                    0.0
                }
            }
            FillModel::GLFT { intensity, kappa } => {
                // delta = distance from mid to order price (approximated as distance to level)
                let delta = (level_price - order.price).abs();
                let p_fill = (intensity * (-kappa * delta).exp()).min(1.0);
                let p_fill = p_fill.clamp(0.0, 1.0);
                if rng.next_f64() < p_fill {
                    available
                } else {
                    0.0
                }
            }
        }
    }

    /// Evict terminal (non-open) orders to bound memory.
    pub fn evict_terminal(&self) {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        inner.orders.retain(|o| is_open(o.status));
    }

    /// Get all resting (open) orders.
    pub fn resting_orders(&self) -> Vec<Order> {
        let inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        inner
            .orders
            .iter()
            .filter(|o| is_open(o.status))
            .cloned()
            .collect()
    }
}

impl Exchange for BookSimExchange {
    fn submit_order(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        if !req.price.is_finite() || !req.size.is_finite() || req.size <= 0.0 {
            return Err(HorizonError::Order(format!(
                "book_sim: invalid order price={} size={}", req.price, req.size
            )));
        }
        let seq = self.next_order_id.fetch_add(1, Ordering::Relaxed);
        let id = format!("bs{}", seq);
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let now = if inner.last_book_ts > 0.0 {
            inner.last_book_ts
        } else {
            SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs_f64()
        };

        let order = Order {
            id: id.clone(),
            market_id: req.market_id.clone(),
            side: req.side,
            order_side: req.order_side,
            price: req.price,
            size: req.size,
            filled_size: 0.0,
            remaining_size: req.size,
            order_type: req.order_type,
            time_in_force: req.time_in_force,
            status: OrderStatus::Accepted,
            created_at: now,
            status_reason: None,
            exchange: "book_sim".to_string(),
            amendment_count: 0,
            token_id: req.token_id.clone(),
            neg_risk: req.neg_risk,
        };

        if self.latency_ticks > 0 {
            inner.pending_orders.push((self.latency_ticks, order));
        } else {
            inner.orders.push(order);
        }
        Ok(id)
    }

    fn cancel_order(&self, order_id: &str) -> Result<(), HorizonError> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        // Check active orders
        for order in inner.orders.iter_mut() {
            if order.id == order_id && is_open(order.status) {
                order.status = OrderStatus::Canceled;
                order.status_reason = Some("canceled".to_string());
                return Ok(());
            }
        }
        // Check pending orders (latency queue)
        if let Some(pos) = inner.pending_orders.iter().position(|(_, o)| o.id == order_id) {
            inner.pending_orders.remove(pos);
            return Ok(());
        }
        Err(HorizonError::Order(format!(
            "order not found: {}",
            order_id
        )))
    }

    fn cancel_all(&self) -> Result<usize, HorizonError> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let mut count = 0;
        for order in inner.orders.iter_mut() {
            if is_open(order.status) {
                order.status = OrderStatus::Canceled;
                order.status_reason = Some("cancel_all".to_string());
                count += 1;
            }
        }
        count += inner.pending_orders.len();
        inner.pending_orders.clear();
        Ok(count)
    }

    fn cancel_for_market(&self, market_id: &str) -> Result<usize, HorizonError> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        let mut count = 0;
        for order in inner.orders.iter_mut() {
            if order.market_id == market_id && is_open(order.status) {
                order.status = OrderStatus::Canceled;
                order.status_reason = Some("cancel_market".to_string());
                count += 1;
            }
        }
        let before = inner.pending_orders.len();
        inner
            .pending_orders
            .retain(|(_, o)| o.market_id != market_id);
        count += before - inner.pending_orders.len();
        Ok(count)
    }

    fn drain_fills(&self) -> Vec<Fill> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        std::mem::take(&mut inner.fills)
    }

    fn name(&self) -> &str {
        "book_sim"
    }

    fn amend_order(
        &self,
        order_id: &str,
        new_price: Option<f64>,
        new_size: Option<f64>,
        _original_request: &OrderRequest,
    ) -> Result<String, HorizonError> {
        let mut inner = self.inner.lock().unwrap_or_else(|e| e.into_inner());
        for order in inner.orders.iter_mut() {
            if order.id == order_id && is_open(order.status) {
                if let Some(p) = new_price {
                    order.price = p;
                }
                if let Some(s) = new_size {
                    order.size = s;
                    order.remaining_size = (s - order.filled_size).max(0.0);
                }
                order.amendment_count += 1;
                return Ok(order_id.to_string());
            }
        }
        Err(HorizonError::Order(format!(
            "order not found for amendment: {}",
            order_id
        )))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::Side;

    fn sample_book() -> OrderbookSnapshot {
        OrderbookSnapshot::from_levels(
            vec![(0.55, 100.0), (0.54, 200.0), (0.53, 50.0)],
            vec![(0.56, 80.0), (0.57, 150.0), (0.58, 300.0)],
            1000.0,
            "test".to_string(),
        )
    }

    fn buy_req(market: &str, price: f64, size: f64) -> OrderRequest {
        OrderRequest {
            market_id: market.into(),
            side: Side::Yes,
            order_side: OrderSide::Buy,
            size,
            price,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::GTC,
            post_only: true,
            token_id: None,
            neg_risk: false,
        }
    }

    fn sell_req(market: &str, price: f64, size: f64) -> OrderRequest {
        OrderRequest {
            market_id: market.into(),
            side: Side::Yes,
            order_side: OrderSide::Sell,
            size,
            price,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::GTC,
            post_only: true,
            token_id: None,
            neg_risk: false,
        }
    }

    #[test]
    fn deterministic_buy_fills_through_asks() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.update_book("mkt", sample_book());
        // Buy at 0.57 should fill through ask levels 0.56 (80) and 0.57 (150)
        exch.submit_order(&buy_req("mkt", 0.57, 50.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 1);
        let drained = exch.drain_fills();
        assert_eq!(drained.len(), 1);
        assert!((drained[0].size - 50.0).abs() < 1e-10);
        // Price should be at first ask level (0.56) since 50 < 80
        assert!((drained[0].price - 0.56).abs() < 1e-10);
    }

    #[test]
    fn deterministic_buy_walks_multiple_levels() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.update_book("mkt", sample_book());
        // Buy at 0.58 with size 100: walks 0.56 (80) + 0.57 (20 of 150)
        exch.submit_order(&buy_req("mkt", 0.58, 100.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 1);
        let drained = exch.drain_fills();
        assert_eq!(drained.len(), 1);
        assert!((drained[0].size - 100.0).abs() < 1e-10);
        // VWAP: (80*0.56 + 20*0.57) / 100 = (44.8 + 11.4) / 100 = 0.562
        assert!((drained[0].price - 0.562).abs() < 0.001);
    }

    #[test]
    fn sell_fills_through_bids() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.update_book("mkt", sample_book());
        // Sell at 0.54 should fill through bid levels 0.55 (100) and 0.54 (partial)
        exch.submit_order(&sell_req("mkt", 0.54, 50.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 1);
        let drained = exch.drain_fills();
        assert_eq!(drained.len(), 1);
        assert!((drained[0].size - 50.0).abs() < 1e-10);
        // All filled at best bid 0.55
        assert!((drained[0].price - 0.55).abs() < 1e-10);
    }

    #[test]
    fn no_fill_if_book_price_worse() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.update_book("mkt", sample_book());
        // Buy at 0.50 — all asks are above 0.50, so no fill
        exch.submit_order(&buy_req("mkt", 0.50, 10.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 0);
    }

    #[test]
    fn no_fill_without_book() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        // No book update — should not fill
        exch.submit_order(&buy_req("mkt", 0.60, 10.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 0);
    }

    #[test]
    fn latency_delays_order() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            2, // 2 tick latency
            42,
        );
        exch.update_book("mkt", sample_book());
        exch.submit_order(&buy_req("mkt", 0.58, 10.0)).unwrap();

        // Tick 1: order still in latency queue
        assert_eq!(exch.tick("mkt"), 0);
        assert!(exch.drain_fills().is_empty());

        // Tick 2: order promoted and fills
        assert_eq!(exch.tick("mkt"), 1);
        let drained = exch.drain_fills();
        assert_eq!(drained.len(), 1);
        assert!((drained[0].size - 10.0).abs() < 1e-10);
    }

    #[test]
    fn impact_increases_effective_price() {
        let exch = BookSimExchange::new(
            0.0,
            FillModel::Deterministic,
            ImpactModel {
                temporary_bps: 50.0, // 50 bps per unit
                permanent_fraction: 0.0,
            },
            0,
            42,
        );
        // Book with a single ask level
        let book = OrderbookSnapshot::from_levels(
            vec![(0.50, 100.0)],
            vec![(0.52, 200.0)],
            0.0,
            String::new(),
        );
        exch.update_book("mkt", book);
        // Buy 100 at 0.60: fills at ask 0.52, but impact shifts price up
        exch.submit_order(&buy_req("mkt", 0.60, 100.0)).unwrap();
        let fills = exch.tick("mkt");
        assert_eq!(fills, 1);
        let drained = exch.drain_fills();
        // With temporary impact, effective price should be slightly above 0.52
        // First unit: impact = 0, price = 0.52
        // As we fill more, impact accumulates. Average should be above 0.52.
        assert!(drained[0].price >= 0.52);
    }

    #[test]
    fn fok_cancels_if_not_fully_filled() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        // Book with only 50 available at best ask
        let book = OrderbookSnapshot::from_levels(
            vec![(0.50, 100.0)],
            vec![(0.52, 50.0)],
            0.0,
            String::new(),
        );
        exch.update_book("mkt", book);
        // FOK buy for 100 — only 50 available, should partially fill then cancel remainder
        let req = OrderRequest {
            market_id: "mkt".into(),
            side: Side::Yes,
            order_side: OrderSide::Buy,
            size: 100.0,
            price: 0.52,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::FOK,
            post_only: false,
            token_id: None,
            neg_risk: false,
        };
        exch.submit_order(&req).unwrap();
        exch.tick("mkt");
        let drained = exch.drain_fills();
        assert!(drained.is_empty(), "FOK should produce zero fills when book depth is insufficient");
        // FOK: the unfilled remainder gets canceled
        assert_eq!(exch.resting_orders().len(), 0);
    }

    #[test]
    fn cancel_order_works() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        let id = exch.submit_order(&buy_req("mkt", 0.50, 10.0)).unwrap();
        assert_eq!(exch.resting_orders().len(), 1);
        exch.cancel_order(&id).unwrap();
        assert_eq!(exch.resting_orders().len(), 0);
    }

    #[test]
    fn cancel_all_works() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.submit_order(&buy_req("mkt", 0.50, 10.0)).unwrap();
        exch.submit_order(&buy_req("mkt", 0.51, 5.0)).unwrap();
        let count = exch.cancel_all().unwrap();
        assert_eq!(count, 2);
        assert_eq!(exch.resting_orders().len(), 0);
    }

    #[test]
    fn cancel_for_market_works() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            0,
            42,
        );
        exch.submit_order(&buy_req("mkt1", 0.50, 10.0)).unwrap();
        exch.submit_order(&buy_req("mkt2", 0.51, 5.0)).unwrap();
        let count = exch.cancel_for_market("mkt1").unwrap();
        assert_eq!(count, 1);
        assert_eq!(exch.resting_orders().len(), 1);
        assert_eq!(exch.resting_orders()[0].market_id, "mkt2");
    }

    #[test]
    fn probabilistic_fill_is_stochastic() {
        let exch = BookSimExchange::new(
            0.0,
            FillModel::Probabilistic {
                lambda: 5.0,
                queue_frac: 0.5,
            },
            ImpactModel::none(),
            0,
            42,
        );
        let book = OrderbookSnapshot::from_levels(
            vec![(0.50, 100.0)],
            vec![(0.52, 1000.0)],
            0.0,
            String::new(),
        );

        // Run many trials and check that sometimes fills, sometimes doesn't
        let mut fill_count = 0;
        for i in 0..100 {
            let e = BookSimExchange::new(
                0.0,
                FillModel::Probabilistic {
                    lambda: 5.0,
                    queue_frac: 0.5,
                },
                ImpactModel::none(),
                0,
                i,
            );
            e.update_book("mkt", book.clone());
            e.submit_order(&buy_req("mkt", 0.52, 10.0)).unwrap();
            if e.tick("mkt") > 0 {
                fill_count += 1;
            }
        }
        // Should fill sometimes but not always
        assert!(fill_count > 0, "should fill at least once");
        assert!(fill_count < 100, "should not fill every time");
    }

    #[test]
    fn permanent_impact_accumulates() {
        let exch = BookSimExchange::new(
            0.0,
            FillModel::Deterministic,
            ImpactModel {
                temporary_bps: 10.0,
                permanent_fraction: 0.5,
            },
            0,
            42,
        );
        let book = OrderbookSnapshot::from_levels(
            vec![(0.50, 1000.0)],
            vec![(0.52, 1000.0)],
            0.0,
            String::new(),
        );
        exch.update_book("mkt", book.clone());
        // First buy: fills, generates permanent impact
        exch.submit_order(&buy_req("mkt", 0.60, 100.0)).unwrap();
        exch.tick("mkt");
        exch.drain_fills();

        // Update book again (same levels), submit another order
        exch.update_book("mkt", book);
        exch.submit_order(&buy_req("mkt", 0.60, 100.0)).unwrap();
        exch.tick("mkt");
        let fills = exch.drain_fills();
        // Second fill price should be higher due to permanent impact
        assert_eq!(fills.len(), 1);
        assert!(fills[0].price > 0.52, "permanent impact should raise price");
    }

    #[test]
    fn cancel_pending_order_in_latency_queue() {
        let exch = BookSimExchange::new(
            0.001,
            FillModel::Deterministic,
            ImpactModel::none(),
            5, // 5 tick latency
            42,
        );
        let id = exch.submit_order(&buy_req("mkt", 0.50, 10.0)).unwrap();
        // Order is in latency queue, not yet active
        assert_eq!(exch.resting_orders().len(), 0);
        // Cancel from latency queue
        exch.cancel_order(&id).unwrap();
        // After many ticks, should not appear
        exch.update_book("mkt", sample_book());
        for _ in 0..10 {
            exch.tick("mkt");
        }
        assert!(exch.drain_fills().is_empty());
    }
}
