//! Test harness for comprehensive scenario-based testing of the order book.
//!
//! This module provides a fluent API for setting up market scenarios,
//! executing commands, and asserting expected outcomes including:
//! - Partial fills and multi-level sweeps
//! - Settlement with various runner results
//! - Market voiding and cancellation
//! - Implied matching (two-runner) verification
//!
//! # Example
//!
//! ```ignore
//! let mut h = TestHarness::two_runner();
//!
//! // Setup orders
//! let lay = h.place(MM, lay(0).at(2.0).stake(100));
//! let back = h.place(ALICE, back(0).at(2.0).stake(50));
//!
//! // Assert matching
//! h.assert_order(lay).is_partial(50, 50);
//! h.assert_order(back).is_filled();
//! h.assert_trades_count(1);
//!
//! // Test branching outcomes
//! let cp = h.checkpoint();
//!
//! cp.clone().settle(winner(0))
//!     .assert_payout(ALICE, 50);  // Won bet at 2.0
//!
//! cp.clone().void_market()
//!     .assert_all_refunded();
//! ```

#![allow(dead_code)]
#![allow(clippy::wrong_self_convention)]

use betex::{
    book::protocol::{
        command::{Command, CommandKind, MarketState, Persistence, Side, TimeInForce},
        reject::RejectReason,
    },
    book::{CommandResponseKind, *},
    types::*,
};
use std::collections::HashMap;

// ============================================================================
// Internal Types
// ============================================================================

struct ExecuteResult {
    events: Vec<BookEventEnvelope>,
    #[allow(dead_code)]
    response: CommandResponse,
}

type PendingTrade = (OrderId, TradeRole, RunnerId, Side, OddsX10000, Money, Money);
type PendingTradeMap = HashMap<(OrderId, OrderId), PendingTrade>;

// ============================================================================
// Account Constants
// ============================================================================

/// Market maker account
pub fn mm() -> AccountId {
    AccountId::from(1)
}
/// Test account Alice
pub fn alice() -> AccountId {
    AccountId::from(100)
}
/// Test account Bob
pub fn bob() -> AccountId {
    AccountId::from(200)
}
/// Test account Carol
pub fn carol() -> AccountId {
    AccountId::from(300)
}

// ============================================================================
// Order Instruction Builders
// ============================================================================

/// Builder for order instructions.
#[derive(Debug, Clone)]
pub struct OrderInstruction {
    pub runner_id: RunnerId,
    pub side: Side,
    pub odds: OddsX10000,
    pub stake: Money,
    pub persistence: Persistence,
    pub time_in_force: TimeInForce,
}

impl OrderInstruction {
    fn new(runner: u32, side: Side) -> Self {
        Self {
            runner_id: RunnerId(runner),
            side,
            odds: OddsX10000(20000), // default 2.0
            stake: Money(100),       // default 100
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        }
    }

    /// Set the odds (as decimal, e.g., 2.0)
    pub fn at(mut self, odds: f64) -> Self {
        self.odds = OddsX10000((odds * 10000.0) as u32);
        self
    }

    /// Set the odds as raw OddsX10000
    pub fn at_raw(mut self, odds: u32) -> Self {
        self.odds = OddsX10000(odds);
        self
    }

    /// Set the stake
    pub fn stake(mut self, stake: i64) -> Self {
        self.stake = Money(stake);
        self
    }

    /// Set persistence to Lapse
    pub fn lapse(mut self) -> Self {
        self.persistence = Persistence::Lapse;
        self
    }

    /// Set time-in-force to Fill-or-Kill
    pub fn fok(mut self) -> Self {
        self.time_in_force = TimeInForce::FillOrKill { min_fill: None };
        self
    }

    /// Set time-in-force to Fill-or-Kill with minimum fill size
    pub fn fok_min(mut self, min_fill: i64) -> Self {
        self.time_in_force = TimeInForce::FillOrKill {
            min_fill: Some(Quantity(min_fill as u64)),
        };
        self
    }
}

/// Create a BACK order instruction for a runner
pub fn back(runner: u32) -> OrderInstruction {
    OrderInstruction::new(runner, Side::Yes)
}

/// Create a LAY order instruction for a runner
pub fn lay(runner: u32) -> OrderInstruction {
    OrderInstruction::new(runner, Side::No)
}

// ============================================================================
// Settlement Helpers
// ============================================================================

/// Create a Winner result for a runner
pub fn winner(runner: u32) -> (RunnerId, RunnerResult) {
    (RunnerId(runner), RunnerResult::Winner)
}

/// Create a Loser result for a runner
pub fn loser(runner: u32) -> (RunnerId, RunnerResult) {
    (RunnerId(runner), RunnerResult::Loser)
}

/// Create a Removed result for a runner
pub fn removed(runner: u32) -> (RunnerId, RunnerResult) {
    (RunnerId(runner), RunnerResult::Removed)
}

// ============================================================================
// Tracked State
// ============================================================================

/// Tracked order state for assertions
#[derive(Debug, Clone)]
pub struct TrackedOrder {
    pub order_id: OrderId,
    pub account_id: AccountId,
    pub instruction: OrderInstruction,
    pub matched: FillQuantity,
    pub remaining: FillQuantity,
    pub state: BookOrderState,
    pub trades: Vec<OrderId>,
}

/// Tracked trade for assertions
#[derive(Debug, Clone)]
pub struct TrackedTrade {
    pub back_order_id: OrderId,
    pub lay_order_id: OrderId,
    pub runner_id: RunnerId,
    pub odds: OddsX10000,
    pub stake: Money,
    pub matched_at: DateTime,
    pub voided: bool,
}

/// Tracked account state
#[derive(Debug, Clone, Default)]
pub struct TrackedAccount {
    pub liability: Money,
    pub orders: Vec<OrderId>,
}

// ============================================================================
// Test Harness
// ============================================================================

/// The main test harness for scenario-based testing.
#[derive(Debug, Clone)]
pub struct TestHarness {
    pub book: Book,
    market_id: MarketId,
    runner_ids: Vec<RunnerId>,
    orders: HashMap<OrderId, TrackedOrder>,
    trades: Vec<TrackedTrade>,
    accounts: HashMap<AccountId, TrackedAccount>,
    events: Vec<BookEventEnvelope>,
    next_correlation_id: u64,
    settled: bool,
    settlement_results: Option<Vec<(RunnerId, RunnerResult)>>,
}

impl TestHarness {
    // ========================================================================
    // Constructors
    // ========================================================================

    /// Create a two-runner market harness (implied matching enabled)
    pub fn two_runner() -> Self {
        let market_id = MarketId(1);
        let runner_ids = vec![RunnerId(0), RunnerId(1)];
        let book = Book::new_two_runner(market_id, runner_ids[0], runner_ids[1]);
        Self::new_with_book(book, market_id, runner_ids)
    }

    /// Create a three-runner market harness (e.g., Home/Draw/Away)
    pub fn three_runner() -> Self {
        let market_id = MarketId(1);
        let runner_ids = vec![RunnerId(0), RunnerId(1), RunnerId(2)];
        let book = Book::new_multi_runner(market_id, runner_ids.clone());
        Self::new_with_book(book, market_id, runner_ids)
    }

    /// Create a multi-runner market harness with specified runner count
    pub fn multi_runner(count: u32) -> Self {
        assert!(count >= 2, "Need at least 2 runners");
        let market_id = MarketId(1);
        let runner_ids: Vec<RunnerId> = (0..count).map(RunnerId).collect();
        let book = Book::new(market_id, runner_ids.clone());
        Self::new_with_book(book, market_id, runner_ids)
    }

    fn new_with_book(book: Book, market_id: MarketId, runner_ids: Vec<RunnerId>) -> Self {
        Self {
            book,
            market_id,
            runner_ids,
            orders: HashMap::new(),
            trades: Vec::new(),
            accounts: HashMap::new(),
            events: Vec::new(),
            next_correlation_id: 1,
            settled: false,
            settlement_results: None,
        }
    }

    // ========================================================================
    // Order Placement
    // ========================================================================

    /// Place an order and return its OrderId
    pub fn place(&mut self, account: AccountId, instruction: OrderInstruction) -> OrderId {
        let cmd = self.make_place_command(account.clone(), &instruction);
        let result = self.execute_command(cmd);

        let Some(CommandResponseKind::PlaceOrder(ref pr)) = result.response.kind else {
            panic!("expected place order response");
        };
        let order_id = pr.order_id;
        let matched = pr.matched;
        let remaining = pr.remaining;
        let state = pr
            .final_order_state
            .unwrap_or(BookOrderState::ExecutableUnmatched);

        let tracked = TrackedOrder {
            order_id,
            account_id: account.clone(),
            instruction: instruction.clone(),
            matched,
            remaining,
            state,
            trades: Vec::new(),
        };
        self.orders.insert(order_id, tracked);

        self.accounts
            .entry(account)
            .or_default()
            .orders
            .push(order_id);

        self.process_events_for_tracking(&result.events);

        order_id
    }

    /// Place an order expecting rejection
    pub fn place_rejected(
        &mut self,
        account: AccountId,
        instruction: OrderInstruction,
    ) -> RejectReason {
        let cmd = self.make_place_command(account, &instruction);
        match self.book.handle(&cmd) {
            Ok((events, _)) => {
                self.book.apply_all_events(&events);
                panic!("Expected rejection but order was accepted");
            }
            Err(e) => e.reason,
        }
    }

    fn make_place_command(
        &mut self,
        account: AccountId,
        instruction: &OrderInstruction,
    ) -> Command {
        let key_seed = self.next_correlation_id;
        let correlation_id = CorrelationId(key_seed.to_string());
        self.next_correlation_id += 1;

        Command {
            correlation_id: Some(correlation_id),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::PlaceOrder {
                runner_id: instruction.runner_id,
                account_id: account,
                client_order_id: None,
                side: instruction.side,
                odds: instruction.odds,
                stake: instruction.stake,
                persistence: instruction.persistence,
                time_in_force: instruction.time_in_force,
            },
        }
    }

    // ========================================================================
    // Order Management
    // ========================================================================

    /// Cancel an order
    pub fn cancel(&mut self, order_id: OrderId) -> &mut Self {
        let order = self.orders.get(&order_id).expect("Order not found");
        let account_id = order.account_id.clone();

        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::CancelOrder {
                account_id,
                order_id,
            },
        };
        self.next_correlation_id += 1;

        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    // ========================================================================
    // Market State Management
    // ========================================================================

    /// Suspend the market
    pub fn suspend(&mut self) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SetMarketState {
                state: MarketState::Suspended,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    /// Resume (unsuspend) the market
    pub fn resume(&mut self) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SetMarketState {
                state: MarketState::Open,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    /// Move the market into the live trading phase.
    pub fn set_live_phase(&mut self) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SetMarketPhase {
                phase: MarketPhase::Live,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    /// Void the market (cancels all orders, voids all trades)
    pub fn void_market(&mut self) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::VoidMarket {
                reason: "TEST_VOID".to_string(),
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    /// Close the market (cancels all resting orders)
    pub fn close_market(&mut self) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SetMarketState {
                state: MarketState::Closed,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    /// Settle the market with given runner results
    pub fn settle(&mut self, results: &[(RunnerId, RunnerResult)]) -> &mut Self {
        let results = results.to_vec();
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SettleMarket {
                runner_results: results.clone(),
                dead_heat_divisor: None,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self.settled = true;
        self.settlement_results = Some(results);
        self
    }

    /// Settle with dead heat
    pub fn settle_dead_heat(
        &mut self,
        results: &[(RunnerId, RunnerResult)],
        divisor: u32,
    ) -> &mut Self {
        let results = results.to_vec();
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::SettleMarket {
                runner_results: results.clone(),
                dead_heat_divisor: Some(divisor),
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self.settled = true;
        self.settlement_results = Some(results);
        self
    }

    /// Remove a runner with optional reduction factor (in basis points)
    pub fn remove_runner(&mut self, runner: u32, reduction_factor_bps: Option<u32>) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::RemoveRunner {
                runner_id: RunnerId(runner),
                reduction_factor_bps,
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    // ========================================================================
    // Trade Voiding
    // ========================================================================

    /// Void all trades matched at or after the given timestamp
    pub fn void_trades_from_time(&mut self, from_timestamp: DateTime) -> &mut Self {
        let cmd = Command {
            correlation_id: Some(CorrelationId(self.next_correlation_id.to_string())),
            metadata: None,
            market_id: self.market_id,
            kind: CommandKind::VoidTradesFromTime {
                from_matched_at_inclusive: from_timestamp,
                reason: "TEST_VOID_FROM_TIME".to_string(),
            },
        };
        self.next_correlation_id += 1;
        let result = self.execute_command(cmd);
        self.process_events_for_tracking(&result.events);
        self
    }

    // ========================================================================
    // Checkpointing
    // ========================================================================

    /// Create a checkpoint that can be cloned for testing multiple outcomes
    pub fn checkpoint(&self) -> Self {
        self.clone()
    }

    // ========================================================================
    // Internal Execution
    // ========================================================================

    fn execute_command(&mut self, cmd: Command) -> ExecuteResult {
        match self.book.handle(&cmd) {
            Ok((events, response)) => {
                self.book.apply_all_events(&events);
                self.events.extend(events.clone());
                ExecuteResult { events, response }
            }
            Err(e) => panic!("Command failed: {:?}", e),
        }
    }

    fn process_events_for_tracking(&mut self, events: &[BookEventEnvelope]) {
        let mut pending_trades: PendingTradeMap = HashMap::new();
        for env in events {
            match &env.event {
                BookEvent::OrderCancelled {
                    cancelled_order,
                    cancel_cause,
                    ..
                } => {
                    let next_state = match cancel_cause {
                        CancelCause::SuspendLapse | CancelCause::InPlayLapse => {
                            BookOrderState::Lapsed
                        }
                        CancelCause::MarketVoid => BookOrderState::Voided,
                        _ => BookOrderState::Cancelled,
                    };
                    if let Some(order) = self.orders.get_mut(&cancelled_order.order_id) {
                        order.state = next_state;
                    }
                }
                BookEvent::OrderCancelledBatched {
                    cancelled_orders,
                    cancel_cause,
                    ..
                } => {
                    let next_state = match cancel_cause {
                        CancelCause::SuspendLapse | CancelCause::InPlayLapse => {
                            BookOrderState::Lapsed
                        }
                        CancelCause::MarketVoid => BookOrderState::Voided,
                        _ => BookOrderState::Cancelled,
                    };
                    for cancelled_order in cancelled_orders {
                        if let Some(order) = self.orders.get_mut(&cancelled_order.order_id) {
                            order.state = next_state;
                        }
                    }
                }
                BookEvent::TradeMatched {
                    order_id,
                    role,
                    runner_id,
                    side,
                    price,
                    stake,
                    counter_party,
                    matched_delta,
                    ..
                } => {
                    let key = if *order_id <= *counter_party {
                        (*order_id, *counter_party)
                    } else {
                        (*counter_party, *order_id)
                    };
                    if let Some((
                        first_order_id,
                        first_role,
                        first_runner_id,
                        first_side,
                        first_price,
                        first_stake,
                        first_matched_delta,
                    )) = pending_trades.remove(&key)
                    {
                        if first_order_id != *counter_party {
                            continue;
                        }
                        let (
                            maker_order_id,
                            maker_side,
                            maker_matched_delta,
                            taker_order_id,
                            taker_runner_id,
                            taker_side,
                            taker_price,
                            taker_stake,
                        ) = match (first_role, *role) {
                            (TradeRole::Maker, TradeRole::Taker) => (
                                first_order_id,
                                first_side,
                                first_matched_delta,
                                *order_id,
                                *runner_id,
                                *side,
                                *price,
                                *stake,
                            ),
                            (TradeRole::Taker, TradeRole::Maker) => (
                                *order_id,
                                *side,
                                *matched_delta,
                                first_order_id,
                                first_runner_id,
                                first_side,
                                first_price,
                                first_stake,
                            ),
                            _ => continue,
                        };
                        let (back_order_id, lay_order_id) = match (maker_side, taker_side) {
                            (Side::Yes, Side::No) => (maker_order_id, taker_order_id),
                            (Side::No, Side::Yes) => (taker_order_id, maker_order_id),
                            // Implied in two-runner: maker BACK on opposite runner is synthetic LAY
                            // on taker runner; maker LAY on opposite runner is synthetic BACK.
                            (Side::Yes, Side::Yes) => (taker_order_id, maker_order_id),
                            (Side::No, Side::No) => (maker_order_id, taker_order_id),
                        };

                        let trade = TrackedTrade {
                            back_order_id,
                            lay_order_id,
                            runner_id: taker_runner_id,
                            odds: taker_price,
                            stake: taker_stake,
                            matched_at: env.timestamp,
                            voided: false,
                        };
                        self.trades.push(trade);

                        if let Some(order) = self.orders.get_mut(&maker_order_id) {
                            order.trades.push(taker_order_id);
                            if let (FillQuantity::Stake(matched), FillQuantity::Stake(remaining)) =
                                (&mut order.matched, &mut order.remaining)
                            {
                                matched.0 = matched.0.saturating_add(maker_matched_delta.0);
                                remaining.0 = remaining.0.saturating_sub(maker_matched_delta.0);
                                if remaining.0 == 0 {
                                    order.state = BookOrderState::ExecutionComplete;
                                } else {
                                    order.state = BookOrderState::ExecutablePartiallyMatched;
                                }
                            }
                        }
                        if let Some(order) = self.orders.get_mut(&taker_order_id) {
                            order.trades.push(maker_order_id);
                        }
                    } else {
                        pending_trades.insert(
                            key,
                            (
                                *order_id,
                                *role,
                                *runner_id,
                                *side,
                                *price,
                                *stake,
                                *matched_delta,
                            ),
                        );
                    }
                }
                BookEvent::VoidTradesFromTime { .. } => {}
                _ => {}
            }
        }
    }

    // ========================================================================
    // Accessors
    // ========================================================================

    /// Get all events emitted so far
    pub fn events(&self) -> &[BookEventEnvelope] {
        &self.events
    }

    /// Get all tracked trades
    pub fn trades(&self) -> &[TrackedTrade] {
        &self.trades
    }

    /// Get live (non-voided) trades
    pub fn live_trades(&self) -> Vec<&TrackedTrade> {
        self.trades.iter().filter(|t| !t.voided).collect()
    }

    /// Get runner IDs
    pub fn runners(&self) -> &[RunnerId] {
        &self.runner_ids
    }

    // ========================================================================
    // Assertions - Order
    // ========================================================================

    /// Start asserting on an order
    pub fn assert_order(&self, order_id: OrderId) -> OrderAssert<'_> {
        OrderAssert {
            harness: self,
            order_id,
        }
    }

    // ========================================================================
    // Assertions - Trade Count
    // ========================================================================

    /// Assert the total number of trades
    pub fn assert_trades_count(&self, expected: usize) -> &Self {
        let actual = self.trades.len();
        assert_eq!(
            actual, expected,
            "Expected {} trades, got {}",
            expected, actual
        );
        self
    }

    /// Assert no trades occurred
    pub fn assert_no_trades(&self) -> &Self {
        self.assert_trades_count(0)
    }

    /// Assert the number of live (non-voided) trades
    pub fn assert_live_trades_count(&self, expected: usize) -> &Self {
        let actual = self.live_trades().len();
        assert_eq!(
            actual, expected,
            "Expected {} live trades, got {}",
            expected, actual
        );
        self
    }

    // ========================================================================
    // Assertions - Market State
    // ========================================================================

    /// Assert the current market state
    pub fn assert_market_state(&self, expected: BookMarketState) -> &Self {
        let actual = self.book.market_state();
        assert_eq!(
            actual, expected,
            "Expected market state {:?}, got {:?}",
            expected, actual
        );
        self
    }

    // ========================================================================
    // Assertions - Trade Details
    // ========================================================================

    /// Assert the last trade has specific properties
    pub fn assert_last_trade(&self) -> TradeAssert<'_> {
        assert!(!self.trades.is_empty(), "No trades to assert on");
        TradeAssert {
            harness: self,
            trade: self.trades.last().unwrap(),
        }
    }

    /// Assert a specific trade by index
    pub fn assert_trade(&self, index: usize) -> TradeAssert<'_> {
        assert!(
            index < self.trades.len(),
            "Trade index {} out of bounds",
            index
        );
        TradeAssert {
            harness: self,
            trade: &self.trades[index],
        }
    }

    // ========================================================================
    // Assertions - All Trades Voided (for void_market)
    // ========================================================================

    /// Assert all trades have been voided
    pub fn assert_all_trades_voided(&self) -> &Self {
        for trade in &self.trades {
            assert!(trade.voided, "Trade was not voided");
        }
        self
    }

    // ========================================================================
    // Payout Calculation Helpers
    // ========================================================================

    /// Calculate expected payout for an account based on settlement results
    /// Returns (profit/loss, explanation)
    pub fn calculate_payout(&self, account: AccountId) -> (i64, String) {
        let results = self
            .settlement_results
            .as_ref()
            .expect("Market not settled");

        let mut total_pnl: i64 = 0;
        let mut explanations = Vec::new();

        // Find all trades involving this account's orders
        for trade in self.live_trades() {
            let back_order = self.orders.get(&trade.back_order_id);
            let lay_order = self.orders.get(&trade.lay_order_id);

            let is_backer = back_order.map(|o| o.account_id.clone()) == Some(account.clone());
            let is_layer = lay_order.map(|o| o.account_id.clone()) == Some(account.clone());

            if !is_backer && !is_layer {
                continue;
            }

            // Find runner result
            let runner_result = results
                .iter()
                .find(|(r, _)| *r == trade.runner_id)
                .map(|(_, res)| res);

            let stake = trade.stake.0;
            let odds = trade.odds.0 as i64;
            let profit = (stake * (odds - 10000)) / 10000; // (odds - 1) * stake

            match runner_result {
                Some(RunnerResult::Winner) => {
                    if is_backer {
                        total_pnl += profit;
                        explanations.push(format!(
                            "BACK won: +{} (stake {} @ {})",
                            profit,
                            stake,
                            odds as f64 / 10000.0
                        ));
                    } else if is_layer {
                        total_pnl -= profit;
                        explanations.push(format!(
                            "LAY lost: -{} (stake {} @ {})",
                            profit,
                            stake,
                            odds as f64 / 10000.0
                        ));
                    }
                }
                Some(RunnerResult::Loser) => {
                    if is_backer {
                        total_pnl -= stake;
                        explanations.push(format!("BACK lost: -{} (stake)", stake));
                    } else if is_layer {
                        total_pnl += stake;
                        explanations.push(format!("LAY won: +{} (stake)", stake));
                    }
                }
                Some(RunnerResult::Removed) | Some(RunnerResult::RemovedVacant) | None => {
                    // Voided - no P&L
                    explanations.push("Runner removed/void: 0".to_string());
                }
            }
        }

        (total_pnl, explanations.join("; "))
    }

    /// Assert expected payout for an account after settlement
    pub fn assert_payout(&self, account: AccountId, expected_pnl: i64) -> &Self {
        let (actual_pnl, explanation) = self.calculate_payout(account.clone());
        assert_eq!(
            actual_pnl, expected_pnl,
            "Expected P&L {} for {:?}, got {} ({})",
            expected_pnl, account, actual_pnl, explanation
        );
        self
    }
}

// ============================================================================
// Order Assertion Builder
// ============================================================================

pub struct OrderAssert<'a> {
    harness: &'a TestHarness,
    order_id: OrderId,
}

impl<'a> OrderAssert<'a> {
    fn qty_value(quantity: FillQuantity) -> i64 {
        match quantity {
            FillQuantity::Stake(money) => money.0,
            FillQuantity::Shares(shares) => shares as i64,
        }
    }

    fn order(&self) -> &TrackedOrder {
        self.harness
            .orders
            .get(&self.order_id)
            .expect("Order not found in harness")
    }

    /// Assert order is fully matched
    pub fn is_filled(self) -> Self {
        let order = self.order();
        assert_eq!(
            Self::qty_value(order.remaining),
            0,
            "Order {:?} not fully filled: remaining={}",
            self.order_id,
            Self::qty_value(order.remaining)
        );
        self
    }

    /// Assert order is fully unmatched
    pub fn is_unmatched(self) -> Self {
        let order = self.order();
        assert_eq!(
            Self::qty_value(order.matched),
            0,
            "Order {:?} has matches: matched={}",
            self.order_id,
            Self::qty_value(order.matched)
        );
        self
    }

    /// Assert order has specific matched amount
    pub fn matched(self, expected: i64) -> Self {
        let order = self.order();
        assert_eq!(
            Self::qty_value(order.matched),
            expected,
            "Order {:?} matched={}, expected={}",
            self.order_id,
            Self::qty_value(order.matched),
            expected
        );
        self
    }

    /// Assert order has specific remaining amount
    pub fn remaining(self, expected: i64) -> Self {
        let order = self.order();
        assert_eq!(
            Self::qty_value(order.remaining),
            expected,
            "Order {:?} remaining={}, expected={}",
            self.order_id,
            Self::qty_value(order.remaining),
            expected
        );
        self
    }

    /// Assert order is partially filled with specific matched and remaining
    pub fn is_partial(self, matched: i64, remaining: i64) -> Self {
        let order = self.order();
        assert_eq!(
            Self::qty_value(order.matched),
            matched,
            "Order {:?} matched={}, expected={}",
            self.order_id,
            Self::qty_value(order.matched),
            matched
        );
        assert_eq!(
            Self::qty_value(order.remaining),
            remaining,
            "Order {:?} remaining={}, expected={}",
            self.order_id,
            Self::qty_value(order.remaining),
            remaining
        );
        self
    }

    /// Assert order state is Cancelled
    pub fn is_cancelled(self) -> Self {
        let order = self.order();
        assert_eq!(
            order.state,
            BookOrderState::Cancelled,
            "Order {:?} state={:?}, expected Cancelled",
            self.order_id,
            order.state
        );
        self
    }

    /// Assert order state is Lapsed
    pub fn is_lapsed(self) -> Self {
        let order = self.order();
        assert_eq!(
            order.state,
            BookOrderState::Lapsed,
            "Order {:?} state={:?}, expected Lapsed",
            self.order_id,
            order.state
        );
        self
    }

    /// Assert order has specific number of trades
    pub fn has_trades(self, expected: usize) -> Self {
        let order = self.order();
        assert_eq!(
            order.trades.len(),
            expected,
            "Order {:?} has {} trades, expected {}",
            self.order_id,
            order.trades.len(),
            expected
        );
        self
    }

    /// Return the harness for chaining
    pub fn and(self) -> &'a TestHarness {
        self.harness
    }
}

// ============================================================================
// Trade Assertion Builder
// ============================================================================

pub struct TradeAssert<'a> {
    #[allow(dead_code)]
    harness: &'a TestHarness,
    trade: &'a TrackedTrade,
}

impl<'a> TradeAssert<'a> {
    /// Assert trade has specific stake
    pub fn stake(self, expected: i64) -> Self {
        assert_eq!(
            self.trade.stake.0, expected,
            "Trade stake={}, expected={}",
            self.trade.stake.0, expected
        );
        self
    }

    /// Assert trade has specific odds (as decimal)
    pub fn at(self, expected_odds: f64) -> Self {
        let expected = (expected_odds * 10000.0) as u32;
        assert_eq!(
            self.trade.odds.0,
            expected,
            "Trade odds={}, expected={}",
            self.trade.odds.0 as f64 / 10000.0,
            expected_odds
        );
        self
    }

    /// Assert trade is on specific runner
    pub fn runner(self, expected: u32) -> Self {
        assert_eq!(
            self.trade.runner_id.0, expected,
            "Trade runner={}, expected={}",
            self.trade.runner_id.0, expected
        );
        self
    }

    /// Assert trade involves specific back order
    pub fn back_order(self, expected: OrderId) -> Self {
        assert_eq!(
            self.trade.back_order_id, expected,
            "Trade back_order={:?}, expected={:?}",
            self.trade.back_order_id, expected
        );
        self
    }

    /// Assert trade involves specific lay order
    pub fn lay_order(self, expected: OrderId) -> Self {
        assert_eq!(
            self.trade.lay_order_id, expected,
            "Trade lay_order={:?}, expected={:?}",
            self.trade.lay_order_id, expected
        );
        self
    }

    /// Assert trade is voided
    pub fn is_voided(self) -> Self {
        assert!(self.trade.voided, "Trade is not voided");
        self
    }

    /// Assert trade is live (not voided)
    pub fn is_live(self) -> Self {
        assert!(!self.trade.voided, "Trade is voided");
        self
    }
}
