use betex::{
    book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce},
    book::protocol::reject::RejectReason,
    book::{Book, BookEvent},
    types::{AccountId, CorrelationId, MarketId, Money, OddsX10000, RunnerId},
};

fn place_order_cmd(
    market_id: MarketId,
    correlation_id: u64,
    account_id: u64,
    runner_id: u32,
) -> Command {
    Command {
        correlation_id: Some(CorrelationId(correlation_id.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(runner_id),
            account_id: AccountId::from(account_id),
            client_order_id: None,
            side: Side::Yes,
            odds: OddsX10000(20000),
            stake: Money(100),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn batch_cancel_cmd(
    market_id: MarketId,
    batch_max_events: u16,
    metadata: Option<serde_json::Value>,
) -> Command {
    Command {
        correlation_id: Some(CorrelationId(9001.to_string())),
        metadata,
        market_id,
        kind: CommandKind::BatchCancelOrders {
            from_created_at_inclusive: None,
            to_created_at_inclusive: None,
            account_id: None,
            runner_id: None,
            batch_max_events,
            reason: "BET_CANCEL".to_string(),
        },
    }
}

fn continue_batch_cancel_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(9002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueBatchCancelOrders,
    }
}

#[test]
fn batch_cancel_no_orders_still_emits_final_event() {
    let market_id = MarketId(101);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let (events, _) = book
        .handle(&batch_cancel_cmd(market_id, 100, None))
        .expect("batch cancel should be accepted");
    assert_eq!(events.len(), 3);
    assert!(matches!(
        events[0].event,
        BookEvent::BatchCancelStarted { .. }
    ));
    match &events[1].event {
        BookEvent::OrderCancelledBatched {
            cancelled_orders, ..
        } => {
            assert!(cancelled_orders.is_empty());
        }
        _ => panic!("expected OrdersCancelledBatch"),
    }
    assert!(matches!(
        events[2].event,
        BookEvent::BatchProcessCompleted { .. }
    ));
}

#[test]
fn batch_cancel_metadata_attaches_only_to_final_event() {
    let market_id = MarketId(102);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    for i in 0..3 {
        let (events, _) = book
            .handle(&place_order_cmd(market_id, i + 1, 10_000 + i, 1))
            .expect("place should succeed");
        book.apply_all_events(&events);
    }

    let metadata = serde_json::json!({"source":"bet-cancel","trace":"t-1"});
    let (start_events, _) = book
        .handle(&batch_cancel_cmd(market_id, 1, Some(metadata.clone())))
        .expect("batch start should succeed");
    assert_eq!(start_events.len(), 2);
    assert!(matches!(
        start_events[0].event,
        BookEvent::BatchCancelStarted { .. }
    ));
    match &start_events[1].event {
        BookEvent::OrderCancelledBatched { .. } => {}
        _ => panic!("expected OrdersCancelledBatch"),
    }
    assert!(start_events[1].metadata.is_none());
    book.apply_all_events(&start_events);

    loop {
        let (events, _) = book
            .handle(&continue_batch_cancel_cmd(market_id))
            .expect("continue should succeed");
        assert!(events.len() == 1 || events.len() == 2);
        match (
            events.get(0).map(|e| &e.event),
            events.get(1).map(|e| &e.event),
        ) {
            (
                Some(BookEvent::OrderCancelledBatched { .. }),
                Some(BookEvent::BatchProcessCompleted { .. }),
            ) => {
                assert_eq!(events[0].metadata, Some(metadata.clone()));
                break;
            }
            (Some(BookEvent::OrderCancelledBatched { .. }), _) => {
                assert!(events[0].metadata.is_none());
            }
            _ => panic!("expected OrdersCancelledBatch"),
        }
        book.apply_all_events(&events);
    }
}

#[test]
fn batch_cancel_blocks_manual_cancel_while_processing() {
    let market_id = MarketId(103);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let (events, _) = book
        .handle(&place_order_cmd(market_id, 1, 100, 1))
        .expect("place should succeed");
    let oid = match events[0].event {
        BookEvent::OrderAccepted { order_id, .. } => order_id,
        _ => panic!("expected OrderAccepted"),
    };
    book.apply_all_events(&events);

    let (events, _) = book
        .handle(&place_order_cmd(market_id, 2, 101, 1))
        .expect("place should succeed");
    book.apply_all_events(&events);

    let (start_events, _) = book
        .handle(&batch_cancel_cmd(market_id, 1, None))
        .expect("batch start should succeed");
    book.apply_all_events(&start_events);

    let err = book
        .handle(&Command {
            correlation_id: Some(CorrelationId(77.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::CancelOrder {
                account_id: AccountId::from(100),
                order_id: oid,
            },
        })
        .expect_err("manual cancel should be blocked while batch-cancel is in progress");
    assert_eq!(err.reason, RejectReason::MarketBatchCancelling);
}

#[test]
fn batch_cancel_is_allowed_when_market_halted() {
    let market_id = MarketId(104);
    let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let (events, _) = book
        .handle(&Command {
            correlation_id: Some(CorrelationId(55.to_string())),
            metadata: None,
            market_id,
            kind: CommandKind::HaltMarket { reason: 1 },
        })
        .expect("halt should succeed");
    book.apply_all_events(&events);

    let (events, _) = book
        .handle(&batch_cancel_cmd(market_id, 10, None))
        .expect("batch-cancel should be accepted while halted");
    assert_eq!(events.len(), 3);
    assert!(matches!(
        events[0].event,
        BookEvent::BatchCancelStarted { .. }
    ));
    match &events[1].event {
        BookEvent::OrderCancelledBatched { .. } => {}
        _ => panic!("expected OrdersCancelledBatch"),
    }
    assert!(matches!(
        events[2].event,
        BookEvent::BatchProcessCompleted { .. }
    ));
}
