use betex::{
    book::close_process::DEFAULT_CLOSE_BATCH_EVENTS,
    book::protocol::command::{Command, CommandKind, MarketState, Persistence, Side, TimeInForce},
    book::{Book, BookEvent, BookMarketState, CancelCause},
    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 close_cmd(market_id: MarketId, batch_max_events: u16) -> Command {
    Command {
        correlation_id: Some(CorrelationId(1001.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::CloseMarket { batch_max_events },
    }
}

fn suspend_cmd(market_id: MarketId) -> Command {
    Command {
        correlation_id: Some(CorrelationId(2001.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(2003.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(2002.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::ContinueLapseOrders,
    }
}

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

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

fn batch_cancel_cmd(
    market_id: MarketId,
    batch_max_events: u16,
    reason: &str,
    metadata: Option<serde_json::Value>,
) -> Command {
    Command {
        correlation_id: Some(CorrelationId(4001.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: reason.to_string(),
        },
    }
}

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

#[test]
fn completion_event_emitted_for_each_batch_cause_even_without_orders() {
    let mid_close = MarketId(301);
    let mut close_book = Book::new_multi_runner(mid_close, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let close_events = exec(&mut close_book, close_cmd(mid_close, 2));
    assert!(close_events.iter().any(|e| {
        matches!(
            e.event,
            BookEvent::BatchProcessCompleted {
                cancel_cause: CancelCause::CloseCancel
            }
        )
    }));

    let mid_batch = MarketId(302);
    let mut batch_book = Book::new_multi_runner(mid_batch, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let batch_events = exec(
        &mut batch_book,
        batch_cancel_cmd(mid_batch, 10, "EMPTY_BATCH", None),
    );
    assert!(batch_events.iter().any(|e| {
        matches!(
            e.event,
            BookEvent::BatchProcessCompleted {
                cancel_cause: CancelCause::BatchCancel
            }
        )
    }));

    let mid_suspend = MarketId(303);
    let mut suspend_book =
        Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let suspend_events = exec(&mut suspend_book, suspend_cmd(mid_suspend));
    assert!(suspend_events.iter().any(|e| {
        matches!(
            e.event,
            BookEvent::BatchProcessCompleted {
                cancel_cause: CancelCause::SuspendLapse
            }
        )
    }));
    assert_eq!(suspend_book.market_state(), BookMarketState::Suspended);

    let mid_in_play = MarketId(304);
    let mut in_play_book =
        Book::new_multi_runner(mid_in_play, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let in_play_events = exec(&mut in_play_book, set_live_phase_cmd(mid_in_play));
    assert!(in_play_events.iter().any(|e| {
        matches!(
            e.event,
            BookEvent::BatchProcessCompleted {
                cancel_cause: CancelCause::InPlayLapse
            }
        )
    }));
    assert_eq!(in_play_book.market_state(), BookMarketState::Open);
    assert_eq!(in_play_book.market_phase(), MarketPhase::Live);

    let mid_void = MarketId(305);
    let mut void_book = Book::new_multi_runner(mid_void, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let void_events = exec(&mut void_book, void_cmd(mid_void, "EMPTY_VOID"));
    assert!(void_events.iter().any(|e| {
        matches!(
            e.event,
            BookEvent::BatchProcessCompleted {
                cancel_cause: CancelCause::MarketVoid
            }
        )
    }));
    assert_eq!(void_book.market_state(), BookMarketState::Voided);
}

#[test]
fn completion_event_emitted_once_for_multi_batch_lapse_and_void() {
    let mid_suspend = MarketId(305);
    let mut suspend_book =
        Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let total = DEFAULT_CLOSE_BATCH_EVENTS as u64 + 3;
    for i in 0..total {
        let _ = exec(
            &mut suspend_book,
            place_order_cmd(mid_suspend, i + 1, 50_000 + i, 1, Persistence::Lapse),
        );
    }

    let mut suspend_all = Vec::new();
    suspend_all.extend(exec(&mut suspend_book, suspend_cmd(mid_suspend)));
    while suspend_book.batch_process_state().is_some() {
        suspend_all.extend(exec(&mut suspend_book, continue_lapse_cmd(mid_suspend)));
    }
    let suspend_completed = suspend_all
        .iter()
        .filter(|e| {
            matches!(
                e.event,
                BookEvent::BatchProcessCompleted {
                    cancel_cause: CancelCause::SuspendLapse
                }
            )
        })
        .count();
    assert_eq!(suspend_completed, 1);

    let mid_in_play = MarketId(306);
    let mut in_play_book =
        Book::new_multi_runner(mid_in_play, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    for i in 0..total {
        let _ = exec(
            &mut in_play_book,
            place_order_cmd(mid_in_play, i + 1, 55_000 + i, 1, Persistence::Lapse),
        );
    }

    let mut in_play_all = Vec::new();
    in_play_all.extend(exec(&mut in_play_book, set_live_phase_cmd(mid_in_play)));
    while in_play_book.batch_process_state().is_some() {
        in_play_all.extend(exec(&mut in_play_book, continue_lapse_cmd(mid_in_play)));
    }
    let in_play_completed = in_play_all
        .iter()
        .filter(|e| {
            matches!(
                e.event,
                BookEvent::BatchProcessCompleted {
                    cancel_cause: CancelCause::InPlayLapse
                }
            )
        })
        .count();
    assert_eq!(in_play_completed, 1);

    let mid_void = MarketId(307);
    let mut void_book = Book::new_multi_runner(mid_void, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    for i in 0..total {
        let _ = exec(
            &mut void_book,
            place_order_cmd(mid_void, i + 1, 60_000 + i, 1, Persistence::Persist),
        );
    }

    let mut void_all = Vec::new();
    void_all.extend(exec(&mut void_book, void_cmd(mid_void, "VOID_BATCH")));
    while void_book.batch_process_state().is_some() {
        void_all.extend(exec(&mut void_book, continue_void_cmd(mid_void)));
    }
    let void_completed = void_all
        .iter()
        .filter(|e| {
            matches!(
                e.event,
                BookEvent::BatchProcessCompleted {
                    cancel_cause: CancelCause::MarketVoid
                }
            )
        })
        .count();
    assert_eq!(void_completed, 1);
}

#[test]
fn batch_cancel_metadata_survives_recovery_and_attaches_on_final_cancel() {
    let market_id = MarketId(308);
    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, 70_000 + i, 1, Persistence::Persist),
        );
    }

    let metadata = serde_json::json!({"trace":"recovery-meta"});
    let start = exec(
        &mut book,
        batch_cancel_cmd(market_id, 1, "RECOVERY_BATCH", Some(metadata.clone())),
    );
    assert!(matches!(
        start.first().map(|e| &e.event),
        Some(BookEvent::BatchCancelStarted { .. })
    ));

    let mut restarted = book.clone();
    let mut final_cancel_with_meta = false;
    while restarted.batch_process_state().is_some() {
        let events = exec(&mut restarted, continue_batch_cancel_cmd(market_id));
        for env in &events {
            if let BookEvent::OrderCancelledBatched { .. } = env.event
                && env.metadata == Some(metadata.clone())
            {
                final_cancel_with_meta = true;
            }
        }
    }
    assert!(final_cancel_with_meta);
}

#[test]
fn cancel_cause_and_detail_distinguish_sources() {
    let mid_user = MarketId(308);
    let mut user_book = Book::new_multi_runner(mid_user, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let placed = exec(
        &mut user_book,
        place_order_cmd(mid_user, 1, 80_000, 1, Persistence::Persist),
    );
    let order_id = match placed.first().map(|e| &e.event) {
        Some(BookEvent::OrderAccepted { order_id, .. }) => *order_id,
        _ => panic!("expected OrderAccepted"),
    };
    let user_cancel = exec(
        &mut user_book,
        Command {
            correlation_id: Some(CorrelationId(2.to_string())),
            metadata: None,
            market_id: mid_user,
            kind: CommandKind::CancelOrder {
                account_id: AccountId::from(80_000),
                order_id,
            },
        },
    );
    assert!(user_cancel.iter().any(|e| {
        matches!(
            &e.event,
            BookEvent::OrderCancelled {
                cancel_cause: CancelCause::UserCancel,
                cause_detail: Some(detail),
                ..
            } if detail == "USER_CANCEL"
        )
    }));

    let mid_batch = MarketId(309);
    let mut batch_book = Book::new_multi_runner(mid_batch, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let _ = exec(
        &mut batch_book,
        place_order_cmd(mid_batch, 1, 81_000, 1, Persistence::Persist),
    );
    let batch_cancel = exec(
        &mut batch_book,
        batch_cancel_cmd(mid_batch, 10, "CAUSE_BATCH", None),
    );
    assert!(batch_cancel.iter().any(|e| {
        matches!(
            &e.event,
            BookEvent::OrderCancelledBatched {
                cancel_cause: CancelCause::BatchCancel,
                cause_detail: Some(detail),
                ..
            } if detail == "CAUSE_BATCH"
        )
    }));

    let mid_close = MarketId(310);
    let mut close_book = Book::new_multi_runner(mid_close, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let _ = exec(
        &mut close_book,
        place_order_cmd(mid_close, 1, 82_000, 1, Persistence::Persist),
    );
    let close_events = exec(&mut close_book, close_cmd(mid_close, 10));
    assert!(close_events.iter().any(|e| {
        matches!(
            &e.event,
            BookEvent::OrderCancelledBatched {
                cancel_cause: CancelCause::CloseCancel,
                cause_detail: None,
                ..
            }
        )
    }));

    let mid_suspend = MarketId(311);
    let mut suspend_book =
        Book::new_multi_runner(mid_suspend, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let _ = exec(
        &mut suspend_book,
        place_order_cmd(mid_suspend, 1, 83_000, 1, Persistence::Lapse),
    );
    let suspend_events = exec(&mut suspend_book, suspend_cmd(mid_suspend));
    assert!(suspend_events.iter().any(|e| {
        matches!(
            &e.event,
            BookEvent::OrderCancelledBatched {
                cancel_cause: CancelCause::SuspendLapse,
                cause_detail: None,
                ..
            }
        )
    }));

    let mid_void = MarketId(312);
    let mut void_book = Book::new_multi_runner(mid_void, [RunnerId(1), RunnerId(2), RunnerId(3)]);
    let _ = exec(
        &mut void_book,
        place_order_cmd(mid_void, 1, 84_000, 1, Persistence::Persist),
    );
    let void_events = exec(&mut void_book, void_cmd(mid_void, "CAUSE_VOID"));
    assert!(void_events.iter().any(|e| {
        matches!(
            &e.event,
            BookEvent::OrderCancelledBatched {
                cancel_cause: CancelCause::MarketVoid,
                cause_detail: Some(detail),
                ..
            } if detail == "CAUSE_VOID"
        )
    }));
}
