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

fn exec(book: &mut Book, cmd: Command) -> Vec<betex::book::BookEventEnvelope> {
    let (events, _) = book.handle(&cmd).expect("command should succeed");
    book.apply_all_events(&events);
    events
}

fn place_order_cmd(
    market_id: MarketId,
    correlation_id: u64,
    account_id: u64,
    runner_id: u32,
    persistence: Persistence,
) -> 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,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn place_binary_order_cmd(market_id: MarketId, correlation_id: u64, account_id: u64) -> Command {
    Command {
        correlation_id: Some(CorrelationId(correlation_id.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceBinaryOrder {
            account_id: AccountId::from(account_id),
            client_order_id: None,
            side: Side::Yes,
            price_ticks: 4000,
            qty_shares: 1,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn close_cmd(market_id: MarketId, batch_max_events: u16) -> Command {
    Command {
        correlation_id: Some(CorrelationId(5001.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::CloseMarket { batch_max_events },
    }
}

fn continue_close_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(5002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueCloseMarket,
    }
}

fn batch_cancel_cmd(market_id: MarketId, batch_max_events: u16) -> Command {
    Command {
        correlation_id: Some(CorrelationId(6001.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,
            reason: "TEST_BATCH_CANCEL".to_string(),
        },
    }
}

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

fn suspend_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(7001.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::SetMarketState {
            state: MarketState::Suspended,
        },
    }
}

fn set_live_phase_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(7003.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::SetMarketPhase {
            phase: MarketPhase::Live,
        },
    }
}

fn continue_lapse_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(7002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueLapseOrders,
    }
}

fn void_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(8001.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::VoidMarket {
            reason: "TEST_VOID".to_string(),
        },
    }
}

fn continue_void_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(8002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueVoidOrders,
    }
}

#[test]
fn binary_yes_close_recovery_from_mid_batch_process() {
    let market_id = MarketId(201);
    let mut book = Book::new_binary_yes(market_id, RunnerId(1), RunnerId(2), 10_000);

    for i in 0..5 {
        let _ = exec(
            &mut book,
            place_binary_order_cmd(market_id, i + 1, 90_000 + i),
        );
    }

    let _start_events = exec(&mut book, close_cmd(market_id, 2));
    assert!(matches!(
        book.batch_process_state(),
        Some(BatchProcessState::Close(_))
    ));

    let mut restarted = book.clone();
    while restarted.close_process_state().is_some() {
        let _ = exec(&mut restarted, continue_close_cmd(market_id));
    }

    assert_eq!(restarted.market_state(), BookMarketState::Closed);
    assert_eq!(restarted.active_order_count(), 0);
    assert!(restarted.batch_process_state().is_none());
}

#[test]
fn binary_yes_batch_cancel_recovery_from_mid_batch_process() {
    let market_id = MarketId(202);
    let mut book = Book::new_binary_yes(market_id, RunnerId(1), RunnerId(2), 10_000);

    for i in 0..5 {
        let _ = exec(
            &mut book,
            place_binary_order_cmd(market_id, i + 1, 91_000 + i),
        );
    }

    let _start_events = exec(&mut book, batch_cancel_cmd(market_id, 1));
    assert!(matches!(
        book.batch_process_state(),
        Some(BatchProcessState::Cancel(_))
    ));

    let mut restarted = book.clone();
    while restarted.cancel_process_state().is_some() {
        let _ = exec(&mut restarted, continue_batch_cancel_cmd(market_id));
    }

    assert_eq!(restarted.active_order_count(), 0);
    assert!(restarted.batch_process_state().is_none());
}

#[test]
fn suspend_and_void_recovery_complete_from_mid_batch_process() {
    let market_id_suspend = MarketId(203);
    let market_id_in_play = MarketId(204);
    let market_id_void = MarketId(205);
    let mut suspend_book =
        Book::new_multi_runner(market_id_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let mut in_play_book =
        Book::new_multi_runner(market_id_in_play, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let mut void_book =
        Book::new_multi_runner(market_id_void, [RunnerId(1), RunnerId(2), RunnerId(3)]);

    let total = betex::book::close_process::DEFAULT_CLOSE_BATCH_EVENTS as u64 + 3;
    for i in 0..total {
        let _ = exec(
            &mut suspend_book,
            place_order_cmd(market_id_suspend, i + 1, 92_000 + i, 1, Persistence::Lapse),
        );
        let _ = exec(
            &mut void_book,
            place_order_cmd(market_id_void, i + 1, 93_000 + i, 1, Persistence::Persist),
        );
        let _ = exec(
            &mut in_play_book,
            place_order_cmd(market_id_in_play, i + 1, 92_500 + i, 1, Persistence::Lapse),
        );
    }

    let _ = exec(&mut suspend_book, suspend_cmd(market_id_suspend));
    assert!(matches!(
        suspend_book.batch_process_state(),
        Some(BatchProcessState::Lapse(_))
    ));

    let _ = exec(&mut in_play_book, set_live_phase_cmd(market_id_in_play));
    assert!(matches!(
        in_play_book.batch_process_state(),
        Some(BatchProcessState::Lapse(_))
    ));

    let _ = exec(&mut void_book, void_cmd(market_id_void));
    assert!(matches!(
        void_book.batch_process_state(),
        Some(BatchProcessState::Void(_))
    ));

    let mut suspend_restarted = suspend_book.clone();
    while suspend_restarted.batch_process_state().is_some() {
        let events = exec(
            &mut suspend_restarted,
            continue_lapse_cmd(market_id_suspend),
        );
        assert!(
            events.iter().any(|e| {
                matches!(
                    e.event,
                    BookEvent::OrderCancelledBatched { .. }
                        | BookEvent::BatchProcessCompleted { .. }
                )
            }),
            "continue lapse should emit progress/completion events"
        );
    }
    assert_eq!(suspend_restarted.market_state(), BookMarketState::Suspended);
    assert_eq!(suspend_restarted.active_order_count(), 0);

    let mut in_play_restarted = in_play_book.clone();
    while in_play_restarted.batch_process_state().is_some() {
        let events = exec(
            &mut in_play_restarted,
            continue_lapse_cmd(market_id_in_play),
        );
        assert!(
            events.iter().any(|e| {
                matches!(
                    e.event,
                    BookEvent::OrderCancelledBatched { .. }
                        | BookEvent::BatchProcessCompleted { .. }
                )
            }),
            "continue live-phase lapse should emit progress/completion events"
        );
    }
    assert_eq!(in_play_restarted.market_state(), BookMarketState::Open);
    assert_eq!(in_play_restarted.market_phase(), MarketPhase::Live);
    assert_eq!(in_play_restarted.active_order_count(), 0);

    let mut void_restarted = void_book.clone();
    while void_restarted.batch_process_state().is_some() {
        let events = exec(&mut void_restarted, continue_void_cmd(market_id_void));
        assert!(
            events.iter().any(|e| {
                matches!(
                    e.event,
                    BookEvent::OrderCancelledBatched { .. }
                        | BookEvent::BatchProcessCompleted { .. }
                )
            }),
            "continue void should emit progress/completion events"
        );
    }
    assert_eq!(void_restarted.market_state(), BookMarketState::Voided);
    assert_eq!(void_restarted.active_order_count(), 0);
}

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

    for i in 0..3 {
        let _ = exec(
            &mut book,
            place_order_cmd(market_id, i + 1, 94_000 + i, 1, Persistence::Persist),
        );
    }

    let _ = exec(&mut book, close_cmd(market_id, 2));
    assert!(matches!(
        book.batch_process_state(),
        Some(BatchProcessState::Close(_))
    ));
    let err = book
        .handle(&continue_batch_cancel_cmd(market_id))
        .expect_err("continue batch cancel should reject during close process");
    assert_eq!(err.reason, RejectReason::MarketNotBatchCancelling);

    let market_id_2 = MarketId(206);
    let mut book2 = Book::new_multi_runner(market_id_2, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    for i in 0..3 {
        let _ = exec(
            &mut book2,
            place_order_cmd(market_id_2, i + 1, 95_000 + i, 1, Persistence::Persist),
        );
    }
    let _ = exec(&mut book2, batch_cancel_cmd(market_id_2, 1));
    assert!(matches!(
        book2.batch_process_state(),
        Some(BatchProcessState::Cancel(_))
    ));
    let err = book2
        .handle(&continue_close_cmd(market_id_2))
        .expect_err("continue close should reject during cancel process");
    assert_eq!(err.reason, RejectReason::MarketNotBatchCancelling);
}

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

    for cmd in [
        continue_close_cmd(market_id),
        continue_batch_cancel_cmd(market_id),
        continue_lapse_cmd(market_id),
        continue_void_cmd(market_id),
    ] {
        let err = book
            .handle(&cmd)
            .expect_err("continue command should reject without active process");
        assert_eq!(err.reason, RejectReason::MarketNotBatchCancelling);
    }
}
