use betex::{
    MarketBookSnapshot,
    book::protocol::{
        command::{BinarySide, BinaryTimeInForce, Command, CommandKind, Side, TimeInForce},
        reject::RejectReason,
    },
    book::{BinaryPriceSize, BookEvent, BookMarketState, CancelCause, RunnerResult, TradeRole},
    config::Config,
    engine::root::EngineRoot,
    engine::state::EngineState,
    types::*,
};
use lucidstream::traits::Aggregate;

fn apply_events(state: &mut EngineState, events: &[betex::book::BookEventEnvelope]) {
    for event in events {
        let owned = std::mem::take(state);
        *state = owned.apply(event);
    }
}

fn handle_apply(state: &mut EngineState, cmd: Command) -> Vec<betex::book::BookEventEnvelope> {
    let events = state.handle_command(&cmd).expect("command succeeds");
    apply_events(state, &events);
    events
}

#[test]
fn binary_yes_market_matches_buy_and_sell() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    let create = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::CreateMarket {
            name: "Binary".to_string(),
            market_model: MarketModel::BinaryYes {
                max_price_ticks: 10_000,
            },
            market_kind: MarketKind::PreEventOnly,
            market_phase: MarketPhase::Pre,
            runner_ids: vec![RunnerId(1), RunnerId(2)],
            runner_labels: vec!["YES".to_string(), "NO".to_string()],
        },
    };
    handle_apply(&mut state, create);

    // Maker: sell YES 100 shares @ 6000 (0.60)
    let sell = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceBinaryOrder {
            account_id: AccountId::from(10),
            client_order_id: None,
            side: BinarySide::Sell,
            price_ticks: 6000,
            qty_shares: 100,
            time_in_force: BinaryTimeInForce::Gtc,
        },
    };
    let ev_sell = handle_apply(&mut state, sell);
    assert!(
        ev_sell
            .iter()
            .any(|e| matches!(e.event, betex::book::BookEvent::BinaryOrderAccepted { .. })),
        "expected BinaryOrderAccepted"
    );

    // Taker: buy YES 100 shares @ 6000 (0.60) => full match
    let buy = Command {
        correlation_id: Some(CorrelationId(3.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceBinaryOrder {
            account_id: AccountId::from(20),
            client_order_id: None,
            side: BinarySide::Buy,
            price_ticks: 6000,
            qty_shares: 100,
            time_in_force: BinaryTimeInForce::Gtc,
        },
    };
    let ev_buy = handle_apply(&mut state, buy);
    let trades: Vec<_> = ev_buy
        .iter()
        .filter_map(|e| match &e.event {
            betex::book::BookEvent::BinaryTradeMatched {
                order_id,
                correlation_id,
                role,
                side,
                market_phase,
                price_ticks,
                counter_party,
                remaining_qty_shares,
                matched_delta_shares,
                ..
            } => Some((
                *order_id,
                correlation_id.clone(),
                *role,
                *side,
                *market_phase,
                *price_ticks,
                *counter_party,
                *remaining_qty_shares,
                *matched_delta_shares,
            )),
            _ => None,
        })
        .collect();
    assert_eq!(trades.len(), 2, "expected maker + taker BinaryTradeMatched");
    assert_eq!(
        trades[0],
        (
            OrderId(1),
            Some(CorrelationId(2.to_string())),
            TradeRole::Maker,
            Side::No,
            MarketPhase::Pre,
            6000,
            OrderId(2),
            0,
            100
        )
    );
    assert_eq!(
        trades[1],
        (
            OrderId(2),
            Some(CorrelationId(3.to_string())),
            TradeRole::Taker,
            Side::Yes,
            MarketPhase::Pre,
            6000,
            OrderId(1),
            0,
            100
        )
    );
}

#[test]
fn binary_yes_fok_rejects_when_not_enough_liquidity() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    // Only 10 shares for sale.
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: Side::No,
                price_ticks: 6000,
                qty_shares: 10,
                time_in_force: TimeInForce::Gtc,
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(20),
                client_order_id: None,
                side: Side::Yes,
                price_ticks: 6000,
                qty_shares: 100,
                time_in_force: TimeInForce::FillOrKill {
                    min_fill: Some(Quantity(100)),
                },
            },
        })
        .expect_err("should reject");
    assert_eq!(err, RejectReason::WouldNotFillFok);
}

#[test]
fn binary_yes_fok_rejection_emits_no_book_event() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: Side::No,
                price_ticks: 6000,
                qty_shares: 10,
                time_in_force: TimeInForce::Gtc,
            },
        },
    );

    let before_seq = state
        .markets
        .get(&market_id)
        .expect("market exists")
        .last_market_seq;

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(20),
                client_order_id: None,
                side: Side::Yes,
                price_ticks: 6000,
                qty_shares: 100,
                time_in_force: TimeInForce::FillOrKill {
                    min_fill: Some(Quantity(100)),
                },
            },
        })
        .expect_err("should reject without emitting events");

    assert_eq!(err, RejectReason::WouldNotFillFok);
    assert_eq!(
        state
            .markets
            .get(&market_id)
            .expect("market exists")
            .last_market_seq,
        before_seq,
        "rejected command must not advance market event sequence"
    );
}

#[test]
fn binary_yes_fok_rejects_when_only_self_liquidity_is_available() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: Side::No,
                price_ticks: 6000,
                qty_shares: 10,
                time_in_force: TimeInForce::Gtc,
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: Side::Yes,
                price_ticks: 6000,
                qty_shares: 10,
                time_in_force: TimeInForce::FillOrKill {
                    min_fill: Some(Quantity(10)),
                },
            },
        })
        .expect_err("self liquidity should not satisfy FOK");
    assert_eq!(err, RejectReason::WouldNotFillFok);
}

#[test]
fn binary_yes_rejects_invalid_price_ticks() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 0,
                qty_shares: 1,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        })
        .expect_err("should reject");
    assert_eq!(err, RejectReason::InvalidPriceTicks);
}

#[test]
fn exchange_odds_market_rejects_binary_orders() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Odds".to_string(),
                market_model: MarketModel::ExchangeOdds,
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["A".to_string(), "B".to_string()],
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 6000,
                qty_shares: 1,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        })
        .expect_err("should reject");
    assert_eq!(err, RejectReason::MarketModelMismatch);
}

#[test]
fn binary_yes_market_snapshot_includes_binary_depth() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    // Resting bids: aggregate at 4000.
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 5,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(11),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 7,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(4.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(12),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 3500,
                qty_shares: 9,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    // Resting asks.
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(5.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(20),
                client_order_id: None,
                side: BinarySide::Sell,
                price_ticks: 6000,
                qty_shares: 3,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(6.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(21),
                client_order_id: None,
                side: BinarySide::Sell,
                price_ticks: 6500,
                qty_shares: 4,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    let root = EngineRoot::new(state.clone());
    let snap = root
        .market_snapshot(market_id, 10)
        .expect("snapshot exists");

    assert_eq!(
        snap.market_model(),
        MarketModel::BinaryYes {
            max_price_ticks: 10_000
        }
    );
    assert_eq!(snap.name, "Binary");
    assert_eq!(snap.metadata, None);

    let MarketBookSnapshot::BinaryYes(bin) = &snap.book else {
        panic!("binary snapshot present");
    };
    assert_eq!(bin.yes_runner.runner_label, "YES");
    assert_eq!(bin.no_runner.runner_label, "NO");
    assert_eq!(bin.max_price_ticks, 10_000);
    assert_eq!(bin.bids.len(), 2);
    assert_eq!(
        bin.bids[0],
        BinaryPriceSize {
            price_ticks: 4000,
            size_shares: 12
        }
    );
    assert_eq!(
        bin.bids[1],
        BinaryPriceSize {
            price_ticks: 3500,
            size_shares: 9
        }
    );

    assert_eq!(bin.asks.len(), 2);
    assert_eq!(
        bin.asks[0],
        BinaryPriceSize {
            price_ticks: 6000,
            size_shares: 3
        }
    );
    assert_eq!(
        bin.asks[1],
        BinaryPriceSize {
            price_ticks: 6500,
            size_shares: 4
        }
    );

    let json = serde_json::to_value(&snap).expect("serialize snapshot");
    assert_eq!(json["name"], "Binary");
    assert_eq!(json["metadata"], serde_json::Value::Null);
    assert_eq!(json["book"]["kind"], "BINARY_YES");
    assert_eq!(json["book"]["data"]["yes_runner"]["runner_label"], "YES");
    assert_eq!(json["book"]["data"]["no_runner"]["runner_label"], "NO");
    assert!(json.get("market_model").is_none());
}

#[test]
fn exchange_market_snapshot_includes_runner_labels() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Odds".to_string(),
                market_model: MarketModel::ExchangeOdds,
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["Home".to_string(), "Away".to_string()],
            },
        },
    );

    let snap = EngineRoot::new(state)
        .market_snapshot(market_id, 5)
        .expect("snapshot exists");

    let MarketBookSnapshot::ExchangeOdds(book) = &snap.book else {
        panic!("exchange snapshot present");
    };
    assert_eq!(snap.name, "Odds");
    assert_eq!(snap.metadata, None);
    assert_eq!(book.runners.len(), 2);
    assert_eq!(book.runners[0].runner_label, "Home");
    assert_eq!(book.runners[1].runner_label, "Away");

    let json = serde_json::to_value(&snap).expect("serialize snapshot");
    assert_eq!(json["name"], "Odds");
    assert_eq!(json["metadata"], serde_json::Value::Null);
    assert_eq!(json["book"]["kind"], "EXCHANGE_ODDS");
    assert_eq!(json["book"]["data"]["runners"][0]["runner_label"], "Home");
}

#[test]
fn postcard_can_roundtrip_optional_snapshot_metadata_when_absent() {
    #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
    struct MetadataWrapper {
        metadata: Option<serde_json::Value>,
    }

    let value = MetadataWrapper { metadata: None };
    let bytes = postcard::to_allocvec(&value).expect("encode wrapper");
    let roundtrip: MetadataWrapper = postcard::from_bytes(&bytes).expect("decode wrapper");
    assert_eq!(roundtrip, value);
}

#[test]
fn binary_yes_close_market_does_not_skip_live_orders() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    // Two live orders, same account for ownership during cancellation.
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 1,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 1,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    // Small close batch: enough overhead + cancel only 1 binary order (2 events).
    let close = Command {
        correlation_id: Some(CorrelationId(4.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::CloseMarket {
            batch_max_events: 6,
        },
    };
    handle_apply(&mut state, close);

    // Drive close process to completion.
    for i in 0..10 {
        let state_now = state.markets.get(&market_id).unwrap().book.market_state();
        if matches!(state_now, betex::book::BookMarketState::Closed) {
            break;
        }
        handle_apply(
            &mut state,
            Command {
                correlation_id: Some(CorrelationId((10 + i).to_string())),
                metadata: None,
                market_id,
                kind: CommandKind::ContinueCloseMarket,
            },
        );
    }

    let st = state.markets.get(&market_id).unwrap().book.market_state();
    assert_eq!(st, betex::book::BookMarketState::Closed);
    assert_eq!(
        state
            .markets
            .get(&market_id)
            .unwrap()
            .book
            .active_order_count(),
        0
    );
}

#[test]
fn binary_yes_void_market_cancels_live_orders() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 3,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(20),
                client_order_id: None,
                side: BinarySide::Sell,
                price_ticks: 7000,
                qty_shares: 4,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    let events = handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(4.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::VoidMarket {
                reason: "VOID".to_string(),
            },
        },
    );

    assert!(
        events.iter().any(|e| {
            matches!(
                e.event,
                BookEvent::OrderCancelledBatched {
                    cancel_cause: CancelCause::MarketVoid,
                    ..
                }
            )
        }),
        "expected market-void cancellations"
    );
    assert!(
        events.iter().any(|e| {
            matches!(
                e.event,
                BookEvent::BatchProcessCompleted {
                    cancel_cause: CancelCause::MarketVoid
                }
            )
        }),
        "expected market-void completion marker"
    );
    assert_eq!(
        state.markets.get(&market_id).unwrap().book.market_state(),
        BookMarketState::Voided
    );
    assert_eq!(
        state
            .markets
            .get(&market_id)
            .unwrap()
            .book
            .active_order_count(),
        0
    );
}

#[test]
fn binary_yes_self_match_is_blocked() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 6000,
                qty_shares: 5,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    let second = handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Sell,
                price_ticks: 6000,
                qty_shares: 5,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    assert!(
        !second
            .iter()
            .any(|e| matches!(e.event, BookEvent::BinaryTradeMatched { .. })),
        "same-account crossing should not match"
    );
    assert_eq!(
        state
            .markets
            .get(&market_id)
            .unwrap()
            .book
            .active_order_count(),
        2
    );
}

#[test]
fn binary_yes_set_market_state_closed_uses_close_flow() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceBinaryOrder {
                account_id: AccountId::from(10),
                client_order_id: None,
                side: BinarySide::Buy,
                price_ticks: 4000,
                qty_shares: 2,
                time_in_force: BinaryTimeInForce::Gtc,
            },
        },
    );

    let events = handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SetMarketState {
                state: betex::book::protocol::command::MarketState::Closed,
            },
        },
    );

    assert!(
        events.iter().any(|e| {
            matches!(
                e.event,
                BookEvent::OrderCancelledBatched {
                    cancel_cause: CancelCause::CloseCancel,
                    ..
                }
            )
        }),
        "expected close-cancel event from SetMarketState::Closed"
    );
    assert_eq!(
        state
            .markets
            .get(&market_id)
            .unwrap()
            .book
            .active_order_count(),
        0
    );
}

#[test]
fn binary_yes_settle_requires_closed_market() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SettleMarket {
                runner_results: vec![
                    (RunnerId(1), RunnerResult::Winner),
                    (RunnerId(2), RunnerResult::Loser),
                ],
                dead_heat_divisor: None,
            },
        })
        .expect_err("settlement should require closed market");
    assert_eq!(err, RejectReason::MarketNotClosed);
}

#[test]
fn binary_yes_settle_rejects_duplicate_runners() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CloseMarket {
                batch_max_events: betex::config::DEFAULT_SET_MARKET_STATE_BATCH,
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SettleMarket {
                runner_results: vec![
                    (RunnerId(1), RunnerResult::Winner),
                    (RunnerId(1), RunnerResult::Loser),
                ],
                dead_heat_divisor: None,
            },
        })
        .expect_err("duplicate runner results should reject");
    assert_eq!(err, RejectReason::DuplicateRunner);
}

#[test]
fn binary_yes_settle_rejects_incomplete_results() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CloseMarket {
                batch_max_events: betex::config::DEFAULT_SET_MARKET_STATE_BATCH,
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SettleMarket {
                runner_results: vec![(RunnerId(1), RunnerResult::Winner)],
                dead_heat_divisor: None,
            },
        })
        .expect_err("incomplete runner results should reject");
    assert_eq!(err, RejectReason::IncompleteResults);
}

#[test]
fn binary_yes_settle_rejects_duplicate_settlement() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CloseMarket {
                batch_max_events: betex::config::DEFAULT_SET_MARKET_STATE_BATCH,
            },
        },
    );
    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(3.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SettleMarket {
                runner_results: vec![
                    (RunnerId(1), RunnerResult::Winner),
                    (RunnerId(2), RunnerResult::Loser),
                ],
                dead_heat_divisor: None,
            },
        },
    );

    let err = state
        .handle_command(&Command {
            correlation_id: Some(CorrelationId(4.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::SettleMarket {
                runner_results: vec![
                    (RunnerId(1), RunnerResult::Winner),
                    (RunnerId(2), RunnerResult::Loser),
                ],
                dead_heat_divisor: None,
            },
        })
        .expect_err("duplicate settlement should reject");
    assert_eq!(err, RejectReason::NoChange);
}

#[test]
fn binary_yes_replace_forces_gtc_time_in_force() {
    let mut state = EngineState::new(Config::default());
    let market_id = MarketId(1);

    handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(1.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CreateMarket {
                name: "Binary".to_string(),
                market_model: MarketModel::BinaryYes {
                    max_price_ticks: 10_000,
                },
                market_kind: MarketKind::PreEventOnly,
                market_phase: MarketPhase::Pre,
                runner_ids: vec![RunnerId(1), RunnerId(2)],
                runner_labels: vec!["YES".to_string(), "NO".to_string()],
            },
        },
    );

    apply_events(
        &mut state,
        &[betex::book::BookEventEnvelope {
            market_id,
            market_name: String::new(),
            market_seq: 2,
            timestamp: unix_epoch(),
            metadata: None,
            event: BookEvent::BinaryOrderAccepted {
                correlation_id: Some(CorrelationId(999.to_string())),
                order_id: OrderId(99),
                account_id: AccountId::from(55),
                side: Side::Yes,
                price_ticks: 4500,
                qty_shares: 5,
                time_in_force: TimeInForce::FillOrKill { min_fill: None },
            },
        }],
    );

    let events = handle_apply(
        &mut state,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::ReplaceBinaryOrder {
                account_id: AccountId::from(55),
                order_id: OrderId(99),
                new_price_ticks: Some(4600),
                new_qty_shares: Some(5),
            },
        },
    );

    let replacement_tif = events.iter().find_map(|e| match &e.event {
        BookEvent::BinaryOrderAccepted {
            order_id,
            time_in_force,
            ..
        } if *order_id != OrderId(99) => Some(*time_in_force),
        _ => None,
    });
    assert_eq!(replacement_tif, Some(TimeInForce::Gtc));
}
