use betex::{
    book::common::formulas,
    book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce},
    book::{Book, BookEvent, BookEventEnvelope},
    types::{
        AccountId, CorrelationId, MarketId, Money, OddsX10000, OrderId, RunnerId, odds::TICK_LADDER,
    },
};
use rand::{RngExt, SeedableRng, rngs::StdRng};
use std::collections::BTreeMap;

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

fn accepted_order_id(events: &[BookEventEnvelope]) -> OrderId {
    events
        .iter()
        .find_map(|env| match &env.event {
            BookEvent::OrderAccepted { order_id, .. } => Some(*order_id),
            _ => None,
        })
        .expect("expected OrderAccepted event")
}

fn last_trade_remaining(events: &[BookEventEnvelope], order_id: OrderId) -> Money {
    events
        .iter()
        .filter_map(|env| match &env.event {
            BookEvent::TradeMatched {
                order_id: trade_order_id,
                remaining_stake,
                ..
            } if *trade_order_id == order_id => Some(*remaining_stake),
            _ => None,
        })
        .next_back()
        .expect("expected TradeMatched event for order")
}

fn place_order_cmd(
    market_id: MarketId,
    correlation_id: u64,
    account_id: u64,
    runner_id: u32,
    side: Side,
    odds_x10000: u32,
    stake: i64,
) -> 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,
            odds: OddsX10000(odds_x10000),
            stake: Money(stake),
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn assert_no_one_quantum_levels(book: &Book, runner_id: RunnerId) {
    let prices = book.runner_prices(runner_id, 10);
    assert!(
        prices
            .available_to_back
            .iter()
            .chain(prices.available_to_lay.iter())
            .all(|level| level.size != Money(1)),
        "runner {runner_id:?} still exposes a 1-quantum price level: {prices:?}"
    );
}

#[test]
fn cent_aligned_implied_conversion_does_not_need_clamp() {
    let effective_price = OddsX10000(10_300);
    let maker_stake = Money::from_cents(1);
    let taker_capacity = formulas::implied_taker_capacity_from_maker(maker_stake, effective_price);
    let maker_fill = formulas::implied_maker_stake_from_taker(taker_capacity, effective_price);

    assert_eq!(taker_capacity, Money::from_cents(33));
    assert_eq!(maker_fill, maker_stake);
}

#[test]
fn exhaustive_cent_aligned_round_trip_does_not_need_clamp() {
    for &price in &TICK_LADDER {
        let effective_price = OddsX10000(price);
        if effective_price.0 <= OddsX10000::MIN {
            continue;
        }

        for cents in 1..=50_000i64 {
            let maker_stake = Money::from_cents(cents);
            let taker_capacity =
                formulas::implied_taker_capacity_from_maker(maker_stake, effective_price);
            let maker_fill =
                formulas::implied_maker_stake_from_taker(taker_capacity, effective_price);

            assert!(
                maker_fill.0 <= maker_stake.0,
                "round-trip exceeded maker stake at price={} maker_cents={} taker_capacity={} maker_fill={}",
                effective_price.0,
                cents,
                taker_capacity.0,
                maker_fill.0
            );
        }
    }
}

#[test]
fn randomized_multi_match_cent_aligned_fills_do_not_need_clamp() {
    let mut rng = StdRng::seed_from_u64(0xC1A0_C1A0);
    let candidates: Vec<u32> = TICK_LADDER
        .iter()
        .copied()
        .filter(|px| (15_000..=600_000).contains(px))
        .collect();
    let mut verified_multi_match_cases = 0usize;

    for case_idx in 0..512u64 {
        let market_id = MarketId(40_000 + case_idx);
        let runner_a = RunnerId(1);
        let runner_b = RunnerId(2);
        let mut book = Book::new_two_runner(market_id, runner_a, runner_b);
        let maker_side = if rng.random_bool(0.5) {
            Side::Back
        } else {
            Side::Lay
        };
        let taker_side = maker_side;
        let maker_count = rng.random_range(2..=5);
        let mut maker_stakes = BTreeMap::new();

        for maker_idx in 0..maker_count {
            let maker_odds = OddsX10000(candidates[rng.random_range(0..candidates.len())]);
            let maker_stake = Money::from_cents(rng.random_range(1..=2_000));
            let events = exec(
                &mut book,
                place_order_cmd(
                    market_id,
                    case_idx * 10 + maker_idx as u64 + 1,
                    1_000 + maker_idx as u64,
                    runner_b.0,
                    maker_side,
                    maker_odds.0,
                    maker_stake.0,
                ),
            );
            maker_stakes.insert(accepted_order_id(&events), maker_stake);
        }

        let ladder = match taker_side {
            Side::Back => book.runner_prices(runner_a, 16).available_to_back,
            Side::Lay => book.runner_prices(runner_a, 16).available_to_lay,
        };
        assert!(
            !ladder.is_empty(),
            "expected implied ladder in multi-match case {case_idx}"
        );

        let total_visible = Money(ladder.iter().map(|level| level.size.0).sum());
        assert!(
            total_visible.0 >= Money::MONEY_SCALE_PER_CENT * 2,
            "expected at least two cents of visible capacity in case {case_idx}"
        );
        let taker_price = ladder
            .last()
            .expect("expected worst acceptable price")
            .price;
        let total_cents = total_visible.to_cents_trunc();
        let taker_stake = Money::from_cents(rng.random_range(2..=total_cents));

        let events = exec(
            &mut book,
            place_order_cmd(
                market_id,
                case_idx * 10 + 9,
                9_999,
                runner_a.0,
                taker_side,
                taker_price.0,
                taker_stake.0,
            ),
        );

        let trade_events: Vec<_> = events
            .iter()
            .filter_map(|env| match &env.event {
                BookEvent::TradeMatched { .. } => Some(&env.event),
                _ => None,
            })
            .collect();
        assert_eq!(
            trade_events.len() % 2,
            0,
            "trade events should arrive in maker/taker pairs in case {case_idx}"
        );
        if trade_events.len() < 4 {
            continue;
        }
        verified_multi_match_cases += 1;

        let mut maker_totals = BTreeMap::<OrderId, i64>::new();
        let mut taker_total = 0i64;
        for pair in trade_events.chunks_exact(2) {
            let (maker_order_id, maker_price, maker_matched_delta) = match pair[0] {
                BookEvent::TradeMatched {
                    role: betex::book::TradeRole::Maker,
                    order_id,
                    price,
                    matched_delta,
                    ..
                } => (order_id, price, matched_delta),
                _ => panic!("expected maker event first in case {case_idx}"),
            };
            let (taker_price_seen, taker_matched_delta) = match pair[1] {
                BookEvent::TradeMatched {
                    role: betex::book::TradeRole::Taker,
                    price,
                    matched_delta,
                    ..
                } => (price, matched_delta),
                _ => panic!("expected taker event second in case {case_idx}"),
            };

            let expected_maker_fill =
                formulas::implied_maker_stake_from_taker(*taker_matched_delta, *taker_price_seen);
            assert_eq!(
                *maker_matched_delta, expected_maker_fill,
                "maker fill should be the unclamped formula result in case {case_idx} at maker price {} and taker price {}",
                maker_price.0, taker_price_seen.0
            );

            *maker_totals.entry(*maker_order_id).or_insert(0) += maker_matched_delta.0;
            taker_total += taker_matched_delta.0;
        }

        for (order_id, matched) in maker_totals {
            let maker_stake = maker_stakes
                .get(&order_id)
                .expect("maker order should exist in source placements");
            assert!(
                matched <= maker_stake.0,
                "maker overfilled in case {case_idx}: order={order_id:?} matched={} stake={}",
                matched,
                maker_stake.0
            );
        }
        assert!(
            taker_total <= taker_stake.0,
            "taker matched more than requested in case {case_idx}: matched={} requested={}",
            taker_total,
            taker_stake.0
        );
    }

    assert!(
        verified_multi_match_cases >= 100,
        "expected to verify a substantial number of true multi-match cases, saw {}",
        verified_multi_match_cases
    );
}

#[test]
fn cent_aligned_implied_match_fills_without_clamp_artifact() {
    let market_id = MarketId(1);
    let runner_a = RunnerId(1);
    let runner_b = RunnerId(2);
    let mut book = Book::new_two_runner(market_id, runner_a, runner_b);
    let maker_odds = OddsX10000(360_000);
    let taker_odds = formulas::back_to_lay_odds(maker_odds);

    let maker = exec(
        &mut book,
        place_order_cmd(
            market_id,
            1,
            10,
            runner_a.0,
            Side::Back,
            maker_odds.0,
            Money::from_cents(1).0,
        ),
    );
    let maker_order_id = accepted_order_id(&maker);

    let implied = book.runner_prices(runner_b, 10);
    assert_eq!(implied.available_to_back.len(), 1);
    assert_eq!(implied.available_to_back[0].price, taker_odds);
    assert_eq!(implied.available_to_back[0].size, Money::from_cents(33));

    let taker = exec(
        &mut book,
        place_order_cmd(
            market_id,
            2,
            20,
            runner_b.0,
            Side::Back,
            taker_odds.0,
            Money::from_cents(33).0,
        ),
    );
    let taker_order_id = accepted_order_id(&taker);

    assert_eq!(last_trade_remaining(&taker, maker_order_id), Money(0));
    assert_eq!(last_trade_remaining(&taker, taker_order_id), Money(0));
    assert!(!book.is_resting(maker_order_id));
    assert!(!book.is_resting(taker_order_id));

    let runner_a_prices = book.runner_prices(runner_a, 10);
    let runner_b_prices = book.runner_prices(runner_b, 10);
    assert!(runner_a_prices.available_to_back.is_empty());
    assert!(runner_a_prices.available_to_lay.is_empty());
    assert!(runner_b_prices.available_to_back.is_empty());
    assert!(runner_b_prices.available_to_lay.is_empty());
}

#[test]
fn cent_aligned_implied_remainder_matches_visible_taker_remainder() {
    let market_id = MarketId(1);
    let runner_a = RunnerId(1);
    let runner_b = RunnerId(2);
    let mut book = Book::new_two_runner(market_id, runner_a, runner_b);
    let maker_odds = OddsX10000(24_400);
    let taker_odds = formulas::back_to_lay_odds(maker_odds);

    let maker = exec(
        &mut book,
        place_order_cmd(
            market_id,
            1,
            10,
            runner_a.0,
            Side::Back,
            maker_odds.0,
            Money::from_cents(133).0,
        ),
    );
    let maker_order_id = accepted_order_id(&maker);

    let taker = exec(
        &mut book,
        place_order_cmd(
            market_id,
            2,
            20,
            runner_b.0,
            Side::Back,
            taker_odds.0,
            Money::from_cents(300).0,
        ),
    );
    let taker_order_id = accepted_order_id(&taker);

    assert_eq!(last_trade_remaining(&taker, maker_order_id), Money(0));
    assert_eq!(
        last_trade_remaining(&taker, taker_order_id),
        Money::from_cents(110)
    );
    assert!(!book.is_resting(maker_order_id));
    assert!(book.is_resting(taker_order_id));
    assert_eq!(
        book.get_order(taker_order_id)
            .expect("taker order should exist")
            .remaining(),
        Money::from_cents(110)
    );

    let runner_a_prices = book.runner_prices(runner_a, 10);
    let runner_b_prices = book.runner_prices(runner_b, 10);

    assert!(
        runner_a_prices.available_to_lay.is_empty(),
        "filled maker should not leave direct dust on runner A: {runner_a_prices:?}"
    );
    assert!(
        runner_b_prices.available_to_back.is_empty(),
        "filled maker should not leave implied dust on runner B: {runner_b_prices:?}"
    );
    assert_eq!(runner_b_prices.available_to_lay.len(), 1);
    assert_eq!(runner_b_prices.available_to_lay[0].price, taker_odds);
    assert_eq!(
        runner_b_prices.available_to_lay[0].size,
        Money::from_cents(110)
    );

    assert_no_one_quantum_levels(&book, runner_a);
    assert_no_one_quantum_levels(&book, runner_b);
}
