use betex::{
    book::BookEvent,
    book::BookEventEnvelope,
    book::protocol::command::{Command, CommandKind, Side},
    book::{EventMetadata, Persistence, TimeInForce},
    config::Config,
    engine::{
        root::EngineRoot,
        state::{EngineState, Market},
    },
    types::*,
};
mod common;
use common::{apply_outcome, binary_market_config, two_runner_market_config};

#[test]
fn place_order_can_match_multiple_resting_orders() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let _ts = unix_epoch();

    let market_id = MarketId(1);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let mut root = EngineRoot::new_with(state, 0);

    // Two resting lays @ 2.0, each 100.
    let mk1 = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(10),
            client_order_id: None,
            side: Side::No,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    let mk2 = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(10),
            client_order_id: None,
            side: Side::No,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    for cmd in [mk1, mk2] {
        root.handle(cmd).expect("command handler succeeds");
        let events = root.take_changes();
        apply_outcome(&mut root, events);
    }

    // Taker back order for 200 should match both makers (2 trades).
    let taker = Command {
        correlation_id: Some(CorrelationId(3.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(20),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(200),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    root.handle(taker).expect("command handler succeeds");
    let events = root.take_changes();
    let trade_count = events
        .iter()
        .filter(|e| matches!(e.event, BookEvent::TradeMatched { .. }))
        .count();
    assert_eq!(trade_count, 4);

    let mut matched_by_order = std::collections::HashMap::<OrderId, i64>::new();
    for env in &events {
        if let BookEvent::TradeMatched {
            order_id,
            matched_delta,
            ..
        } = env.event
        {
            *matched_by_order.entry(order_id).or_insert(0) += matched_delta.0;
        }
    }

    apply_outcome(&mut root, events);

    let market = root.state().markets.get(&market_id).unwrap();
    // Maker and taker orders should be fully filled (not active/resting).
    assert_eq!(market.book.active_order_count(), 0);
    assert!(market.book.get_order(OrderId(1)).is_none());
    assert!(market.book.get_order(OrderId(2)).is_none());
    assert!(market.book.get_order(OrderId(3)).is_none());

    assert_eq!(matched_by_order.get(&OrderId(1)).copied().unwrap_or(0), 100);
    assert_eq!(matched_by_order.get(&OrderId(2)).copied().unwrap_or(0), 100);
    assert_eq!(matched_by_order.get(&OrderId(3)).copied().unwrap_or(0), 200);
    assert_eq!(market.book.total_matched(), Money(200));
}

#[test]
fn replace_order_defaults_stake_to_remaining_when_omitted() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let _ts = unix_epoch();

    let market_id = MarketId(1);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let mut root = EngineRoot::new_with(state, 0);

    // Maker lays 100.
    let maker = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(10),
            client_order_id: None,
            side: Side::No,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    root.handle(maker).expect("command handler succeeds");
    let events = root.take_changes();
    apply_outcome(&mut root, events);

    // Taker backs 40 to partially fill the maker (remaining should be 60).
    let taker = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(20),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(40),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    root.handle(taker).expect("command handler succeeds");
    let events = root.take_changes();
    apply_outcome(&mut root, events);

    let market = root.state().markets.get(&market_id).unwrap();
    let old = market.book.get_order(OrderId(1)).unwrap();
    assert_eq!(old.matched, Money(40));
    assert_eq!(old.remaining(), Money(60));

    // Replace without specifying new stake should preserve remaining (60), not resurrect full stake (100).
    let replace = Command {
        correlation_id: Some(CorrelationId(3.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ReplaceOrder {
            account_id: AccountId::from(10),
            order_id: OrderId(1),
            new_odds: Some(OddsX10000(21000)),
            new_stake: None,
        },
    };
    root.handle(replace).expect("command handler succeeds");
    let events = root.take_changes();
    assert!(
        events.iter().any(|e| matches!(
            e.event,
            BookEvent::OrderAccepted {
                stake: Money(60),
                ..
            }
        )),
        "expected replacement OrderAccepted stake to equal remaining (60)"
    );
    apply_outcome(&mut root, events);

    let market = root.state().markets.get(&market_id).unwrap();
    let new = market.book.get_order(OrderId(3)).unwrap();
    assert_eq!(new.stake, Money(60));
    assert_eq!(new.matched, Money(0));
}

#[test]
fn replace_order_preserves_existing_persistence() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let _ts = unix_epoch();

    let market_id = MarketId(1);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let mut root = EngineRoot::new_with(state, 0);

    // Seed an existing order with Lapse persistence (cannot be created via protocol commands today).
    apply_outcome(
        &mut root,
        [BookEventEnvelope {
            market_id,
            market_name: String::new(),
            market_seq: 1,
            timestamp: unix_epoch(),
            metadata: EventMetadata::default(),
            event: BookEvent::OrderAccepted {
                correlation_id: Some(CorrelationId(0.to_string())),
                order_id: OrderId(1),
                account_id: AccountId::from(10),
                runner_id: RunnerId(1),
                runner_label: "1".to_string(),
                side: Side::Lay,
                price: OddsX10000(20000),
                stake: Money(100),
                persistence: Persistence::Lapse,
                time_in_force: TimeInForce::Gtc,
            },
        }],
    );

    let replace = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ReplaceOrder {
            account_id: AccountId::from(10),
            order_id: OrderId(1),
            new_odds: Some(OddsX10000(21000)),
            new_stake: None,
        },
    };

    root.handle(replace).expect("command handler succeeds");
    let events = root.take_changes();
    assert!(
        events.iter().any(|e| matches!(
            e.event,
            BookEvent::OrderAccepted {
                order_id: OrderId(2),
                persistence: Persistence::Lapse,
                ..
            }
        )),
        "expected replacement OrderAccepted to preserve old persistence (Lapse)"
    );
    apply_outcome(&mut root, events);

    let market = root.state().markets.get(&market_id).unwrap();
    let new = market.book.get_order(OrderId(2)).unwrap();
    assert_eq!(new.persistence, Persistence::Lapse);
}

#[test]
fn create_market_event_carries_command_correlation_id() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let correlation_id = CorrelationId("11111111-1111-1111-1111-111111111111".to_string());
    let metadata = serde_json::json!({
        "source": "api",
        "request_id": "req-create-1"
    });
    let cmd = Command {
        correlation_id: Some(correlation_id.clone()),
        metadata: Some(metadata.clone()),
        market_id: MarketId(42),
        kind: CommandKind::CreateMarket {
            name: "Correlation Test".to_string(),
            market_model: MarketModel::ExchangeOdds,
            market_kind: MarketKind::InPlayCapable,
            market_phase: MarketPhase::Pre,
            runner_ids: vec![RunnerId(1), RunnerId(2)],
            runner_labels: vec!["A".to_string(), "B".to_string()],
        },
    };

    let events = state.handle_command(&cmd).expect("create market accepted");
    let Some(env) = events.first() else {
        panic!("expected at least one event");
    };
    assert_eq!(env.metadata, Some(metadata.clone()));
    match &env.event {
        BookEvent::MarketCreated {
            correlation_id: actual,
            ..
        } => {
            assert_eq!(actual, &Some(correlation_id));
        }
        other => panic!("expected MarketCreated, got {other:?}"),
    }
}

#[test]
fn place_order_event_carries_command_correlation_id() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let market_id = MarketId(1);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let correlation_id = CorrelationId("22222222-2222-2222-2222-222222222222".to_string());
    let metadata = serde_json::json!({
        "source": "api",
        "request_id": "req-place-1"
    });
    let cmd = Command {
        correlation_id: Some(correlation_id.clone()),
        metadata: Some(metadata.clone()),
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(10),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    let events = state.handle_command(&cmd).expect("place order accepted");
    let accepted = events.iter().find_map(|env| match &env.event {
        BookEvent::OrderAccepted { correlation_id, .. } => Some(correlation_id),
        _ => None,
    });
    assert_eq!(accepted, Some(&Some(correlation_id)));
    assert!(
        events
            .iter()
            .all(|env| env.metadata == Some(metadata.clone())),
        "all emitted events must carry command metadata"
    );
}

#[test]
fn stored_orders_and_trade_events_carry_correlation_id() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let market_id = MarketId(9);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let mut root = EngineRoot::new_with(state, 0);

    let maker_correlation = CorrelationId("maker-trace".to_string());
    root.handle(Command {
        correlation_id: Some(maker_correlation.clone()),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(10),
            client_order_id: None,
            side: Side::No,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    })
    .expect("maker place accepted");
    let maker_events = root.take_changes();
    apply_outcome(&mut root, maker_events);

    let market = root.state().markets.get(&market_id).expect("market exists");
    assert_eq!(
        market
            .book
            .get_order(OrderId(1))
            .expect("maker order stored")
            .info
            .correlation_id,
        Some(maker_correlation.clone())
    );

    let taker_correlation = CorrelationId("taker-trace".to_string());
    root.handle(Command {
        correlation_id: Some(taker_correlation.clone()),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: AccountId::from(20),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    })
    .expect("taker place accepted");
    let events = root.take_changes();

    let trade_correlations: std::collections::HashMap<_, _> = events
        .iter()
        .filter_map(|env| match &env.event {
            BookEvent::TradeMatched {
                order_id,
                correlation_id,
                ..
            } => Some((*order_id, correlation_id.clone())),
            _ => None,
        })
        .collect();
    assert_eq!(
        trade_correlations.get(&OrderId(1)),
        Some(&Some(maker_correlation))
    );
    assert_eq!(
        trade_correlations.get(&OrderId(2)),
        Some(&Some(taker_correlation))
    );
}

#[test]
fn batch_cancel_events_carry_order_correlation_ids() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let market_id = MarketId(10);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, two_runner_market_config(market_id)),
    );
    let mut root = EngineRoot::new_with(state, 0);

    for (cid, order_account_id) in [("cancel-1", 10_u64), ("cancel-2", 20_u64)] {
        root.handle(Command {
            correlation_id: Some(CorrelationId(cid.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceOrder {
                runner_id: RunnerId(1),
                account_id: AccountId::from(order_account_id),
                client_order_id: None,
                side: Side::Yes,
                odds: OddsX10000(20000),
                stake: Money(100),
                persistence: Persistence::Persist,
                time_in_force: TimeInForce::Gtc,
            },
        })
        .expect("place accepted");
        let events = root.take_changes();
        apply_outcome(&mut root, events);
    }

    root.handle(Command {
        correlation_id: Some(CorrelationId("batch-cancel".to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::BatchCancelOrders {
            from_created_at_inclusive: None,
            to_created_at_inclusive: None,
            account_id: None,
            runner_id: None,
            batch_max_events: 10,
            reason: "TEST_BATCH_CANCEL".to_string(),
        },
    })
    .expect("batch cancel accepted");
    let events = root.take_changes();

    let cancelled = events.iter().find_map(|env| match &env.event {
        BookEvent::OrderCancelledBatched {
            cancelled_orders, ..
        } => Some((
            cancelled_orders
                .iter()
                .map(|entry| entry.order_id)
                .collect::<Vec<_>>(),
            cancelled_orders
                .iter()
                .map(|entry| entry.correlation_id.clone())
                .collect::<Vec<_>>(),
        )),
        _ => None,
    });
    assert_eq!(
        cancelled,
        Some((
            vec![OrderId(1), OrderId(2)],
            vec![
                Some(CorrelationId("cancel-1".to_string())),
                Some(CorrelationId("cancel-2".to_string()))
            ]
        ))
    );
}

#[test]
fn place_binary_order_event_carries_command_correlation_id() {
    let cfg = Config::default();
    let mut state = EngineState::new(cfg);
    let market_id = MarketId(7);
    state.markets.insert(
        market_id,
        Market::from_config_with_cfg(&state.cfg, binary_market_config(market_id)),
    );
    let correlation_id = CorrelationId("33333333-3333-3333-3333-333333333333".to_string());
    let cmd = Command {
        correlation_id: Some(correlation_id.clone()),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceBinaryOrder {
            account_id: AccountId::from(1001),
            client_order_id: None,
            side: Side::Yes,
            price_ticks: 40,
            qty_shares: 10,
            time_in_force: TimeInForce::Gtc,
        },
    };

    let events = state
        .handle_command(&cmd)
        .expect("place binary order accepted");
    let accepted = events.iter().find_map(|env| match &env.event {
        BookEvent::BinaryOrderAccepted { correlation_id, .. } => Some(correlation_id),
        _ => None,
    });
    assert_eq!(accepted, Some(&Some(correlation_id)));
}
