mod harness;

use betex::{
    book::common::formulas,
    book::{BookEvent, TradeRole},
    prelude::{Command, CommandKind, Side, TimeInForce},
    types::odds::TICK_LADDER,
    types::{CorrelationId, MarketId, MarketPhase, Money, OddsX10000, OrderId, RunnerId},
};
use harness::*;
use rand::{RngExt, SeedableRng, rngs::StdRng};

fn pnl_for_back(stake: Money, odds: OddsX10000, wins: bool) -> i64 {
    if wins {
        formulas::backer_profit(stake, odds).0
    } else {
        -stake.0
    }
}

fn place_raw(
    book: &mut betex::book::Book,
    correlation: u64,
    market_id: MarketId,
    account_id: betex::types::AccountId,
    runner_id: RunnerId,
    side: Side,
    odds: OddsX10000,
    stake: Money,
) -> Vec<betex::book::BookEventEnvelope> {
    let cmd = Command {
        correlation_id: Some(CorrelationId(correlation.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id,
            account_id,
            client_order_id: None,
            side,
            odds,
            stake,
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    let (events, _) = book.handle(&cmd).expect("place should succeed");
    book.apply_all_events(&events);
    events
}

#[test]
fn implied_matching_converts_stake_and_partially_fills_taker() {
    let mut h = TestHarness::two_runner();

    let maker = h.place(mm(), back(1).at(3.0).stake(100));
    let taker = h.place(alice(), back(0).at(1.5).stake(300));

    h.assert_order(maker).is_filled();
    h.assert_order(taker).is_partial(200, 100);
    h.assert_trades_count(1);
    h.assert_last_trade().runner(0).at(1.5).stake(200);
}

#[test]
fn fok_uses_implied_taker_capacity_after_stake_conversion() {
    let mut h = TestHarness::two_runner();

    let maker = h.place(mm(), back(1).at(3.0).stake(100));
    let taker = h.place(alice(), back(0).at(1.5).stake(150).fok());

    h.assert_order(taker).is_filled();
    h.assert_order(maker).is_filled();
    h.assert_trades_count(1);
    h.assert_last_trade().at(1.5).stake(150);
}

#[test]
fn implied_back_prefers_highest_effective_odds_for_taker() {
    let mut h = TestHarness::two_runner();

    let better = h.place(mm(), back(1).at(2.0).stake(100)); // implies 2.0 on runner 0
    let worse = h.place(bob(), back(1).at(3.0).stake(100)); // implies 1.5 on runner 0

    let taker = h.place(alice(), back(0).at(1.5).stake(50));

    h.assert_order(better).is_filled();
    h.assert_order(worse).is_unmatched();
    h.assert_order(taker).is_filled();
    h.assert_trades_count(1);
    h.assert_last_trade().at(2.0).runner(0).back_order(taker);
}

#[test]
fn implied_lay_prefers_lowest_effective_odds_for_taker() {
    let mut h = TestHarness::two_runner();

    let worse = h.place(mm(), lay(1).at(2.0).stake(100)); // implies 2.0 on runner 0
    let better = h.place(bob(), lay(1).at(3.0).stake(100)); // implies 1.5 on runner 0

    let taker = h.place(alice(), lay(0).at(2.0).stake(50));

    h.assert_order(worse).is_unmatched();
    h.assert_order(better).is_filled();
    h.assert_order(taker).is_filled();
    h.assert_trades_count(1);
    h.assert_last_trade().at(1.5).runner(0).lay_order(taker);
}

#[test]
fn implied_back_b_vs_back_a_is_zero_sum_and_no_free_arb() {
    let market_id = MarketId(99);
    let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

    let maker_cmd = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(2),
            account_id: mm(),
            client_order_id: None,
            side: Side::Back,
            odds: OddsX10000(30_000),
            stake: Money(100),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    let taker_cmd = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: alice(),
            client_order_id: None,
            side: Side::Back,
            odds: OddsX10000(15_000),
            stake: Money(200),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    let (e1, _) = book.handle(&maker_cmd).expect("maker place should succeed");
    book.apply_all_events(&e1);
    let (e2, _) = book.handle(&taker_cmd).expect("taker place should succeed");
    book.apply_all_events(&e2);

    let trade = e2
        .iter()
        .find_map(|env| match env.event {
            BookEvent::TradeMatched {
                order_id: OrderId(2),
                market_phase,
                price,
                stake,
                ..
            } => Some((market_phase, price, stake)),
            _ => None,
        })
        .expect("expected a matched trade");

    let (trade_market_phase, trade_price, taker_stake) = trade;
    assert_eq!(trade_market_phase, MarketPhase::Pre);
    let maker_stake = formulas::implied_maker_stake_from_taker(taker_stake, trade_price);
    assert_eq!(trade_price, OddsX10000(15_000));
    assert_eq!(taker_stake, Money(200));
    assert_eq!(maker_stake, Money(100));

    // Outcome A wins (runner 1).
    let taker_a_wins = pnl_for_back(taker_stake, OddsX10000(15_000), true);
    let maker_a_wins = pnl_for_back(maker_stake, OddsX10000(30_000), false);
    assert_eq!(taker_a_wins + maker_a_wins, 0);

    // Outcome B wins (runner 2).
    let taker_b_wins = pnl_for_back(taker_stake, OddsX10000(15_000), false);
    let maker_b_wins = pnl_for_back(maker_stake, OddsX10000(30_000), true);
    assert_eq!(taker_b_wins + maker_b_wins, 0);

    // No free arbitrage.
    assert!(!(taker_a_wins >= 0 && taker_b_wins >= 0));
    assert!(!(maker_a_wins >= 0 && maker_b_wins >= 0));
}

#[test]
fn implied_match_emits_each_side_price_and_stake_from_own_order_view() {
    let market_id = MarketId(100);
    let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

    let maker_cmd = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(2),
            account_id: mm(),
            client_order_id: None,
            side: Side::Back,
            odds: OddsX10000(30_000),
            stake: Money(100),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    let taker_cmd = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: alice(),
            client_order_id: None,
            side: Side::Back,
            odds: OddsX10000(15_000),
            stake: Money(200),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    let (e1, _) = book.handle(&maker_cmd).expect("maker place should succeed");
    book.apply_all_events(&e1);
    let (e2, _) = book.handle(&taker_cmd).expect("taker place should succeed");
    book.apply_all_events(&e2);

    let mut maker_view = None;
    let mut taker_view = None;
    for env in e2 {
        if let BookEvent::TradeMatched {
            order_id,
            role,
            market_phase,
            price,
            stake,
            ..
        } = env.event
        {
            if order_id == OrderId(1) {
                maker_view = Some((role, market_phase, price, stake));
            } else if order_id == OrderId(2) {
                taker_view = Some((role, market_phase, price, stake));
            }
        }
    }

    assert_eq!(
        maker_view,
        Some((
            TradeRole::Maker,
            MarketPhase::Pre,
            OddsX10000(30_000),
            Money(100)
        ))
    );
    assert_eq!(
        taker_view,
        Some((
            TradeRole::Taker,
            MarketPhase::Pre,
            OddsX10000(15_000),
            Money(200)
        ))
    );
}

#[test]
fn trade_events_record_live_market_phase_when_matched_live() {
    let market_id = MarketId(101);
    let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

    let maker_cmd = Command {
        correlation_id: Some(CorrelationId(1.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: mm(),
            client_order_id: None,
            side: Side::Lay,
            odds: OddsX10000(20_000),
            stake: Money(100),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };
    let set_live_phase = Command {
        correlation_id: Some(CorrelationId(2.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::SetMarketPhase {
            phase: MarketPhase::Live,
        },
    };
    let taker_cmd = Command {
        correlation_id: Some(CorrelationId(3.to_string())),
        metadata: None,
        market_id,
        kind: CommandKind::PlaceOrder {
            runner_id: RunnerId(1),
            account_id: alice(),
            client_order_id: None,
            side: Side::Back,
            odds: OddsX10000(20_000),
            stake: Money(100),
            persistence: betex::book::Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    };

    let (place_events, _) = book.handle(&maker_cmd).expect("maker place should succeed");
    book.apply_all_events(&place_events);

    let (state_events, _) = book
        .handle(&set_live_phase)
        .expect("set live phase should succeed");
    book.apply_all_events(&state_events);
    assert!(matches!(
        state_events.first().map(|env| &env.event),
        Some(BookEvent::MarketPhaseChanged {
            to: MarketPhase::Live,
            ..
        })
    ));

    let (trade_events, _) = book.handle(&taker_cmd).expect("taker place should succeed");
    let market_phases: Vec<_> = trade_events
        .iter()
        .filter_map(|env| match env.event {
            BookEvent::TradeMatched { market_phase, .. } => Some(market_phase),
            _ => None,
        })
        .collect();

    assert_eq!(market_phases, vec![MarketPhase::Live, MarketPhase::Live]);
}

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

    for case_idx in 0..256u64 {
        let market_id = MarketId(10_000 + case_idx);
        let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

        let maker_side = if rng.random_bool(0.5) {
            Side::Back
        } else {
            Side::Lay
        };
        let maker_odds = OddsX10000(candidates[rng.random_range(0..candidates.len())]);
        let maker_stake = Money::from_cents(rng.random_range(5..5_000));

        place_raw(
            &mut book,
            case_idx * 2 + 1,
            market_id,
            mm(),
            RunnerId(2),
            maker_side,
            maker_odds,
            maker_stake,
        );

        let (taker_side, expected_price, prices) = match maker_side {
            Side::Back => (
                Side::Back,
                formulas::back_to_lay_odds(maker_odds),
                book.runner_prices(RunnerId(1), 1).available_to_back,
            ),
            Side::Lay => (
                Side::Lay,
                formulas::lay_to_back_odds(maker_odds),
                book.runner_prices(RunnerId(1), 1).available_to_lay,
            ),
        };

        assert!(
            !prices.is_empty(),
            "expected implied liquidity for case {case_idx}"
        );
        assert_eq!(prices[0].price, expected_price, "case {case_idx}");
        assert_eq!(
            prices[0].size.0 % Money::MONEY_SCALE_PER_CENT,
            0,
            "capacity must round down to cents in case {case_idx}"
        );

        let taker_stake = prices[0].size;
        let events = place_raw(
            &mut book,
            case_idx * 2 + 2,
            market_id,
            alice(),
            RunnerId(1),
            taker_side,
            expected_price,
            taker_stake,
        );

        let mut maker_view = None;
        let mut taker_view = None;
        for env in events {
            if let BookEvent::TradeMatched {
                role,
                price,
                stake,
                remaining_stake,
                matched_delta,
                ..
            } = env.event
            {
                match role {
                    TradeRole::Maker => {
                        maker_view = Some((price, stake, remaining_stake, matched_delta));
                    }
                    TradeRole::Taker => {
                        taker_view = Some((price, stake, remaining_stake, matched_delta));
                    }
                }
            }
        }

        let (maker_price, _maker_view_stake, maker_remaining, maker_matched_delta) =
            maker_view.unwrap_or_else(|| {
                panic!(
                    "maker trade view missing in case {case_idx}: maker_side={maker_side:?} maker_odds={} maker_stake={} expected_price={} taker_stake={}",
                    maker_odds.0,
                    maker_stake.0,
                    expected_price.0,
                    taker_stake.0
                )
            });
        let (taker_price, taker_view_stake, taker_remaining, taker_matched_delta) =
            taker_view.unwrap_or_else(|| {
                panic!(
                    "taker trade view missing in case {case_idx}: maker_side={maker_side:?} maker_odds={} maker_stake={} expected_price={} taker_stake={}",
                    maker_odds.0,
                    maker_stake.0,
                    expected_price.0,
                    taker_stake.0
                )
            });

        assert_eq!(
            maker_price, maker_odds,
            "maker sees original odds in case {case_idx}"
        );
        assert_eq!(
            taker_price, expected_price,
            "taker sees implied odds in case {case_idx}"
        );
        assert_eq!(taker_view_stake, taker_stake, "case {case_idx}");
        assert_eq!(taker_matched_delta, taker_stake, "case {case_idx}");
        assert_eq!(taker_remaining, Money::zero(), "case {case_idx}");

        let expected_maker_match =
            formulas::implied_maker_stake_from_taker(taker_stake, expected_price);
        assert_eq!(
            maker_matched_delta,
            Money(expected_maker_match.0.min(maker_stake.0)),
            "maker matched delta mismatch in case {case_idx}"
        );
        assert_eq!(
            maker_remaining.0,
            maker_stake.0 - maker_matched_delta.0,
            "maker remaining mismatch in case {case_idx}"
        );
        assert!(
            maker_remaining.0 >= 0,
            "maker overfilled in case {case_idx}: stake={}, matched={}",
            maker_stake.0,
            maker_matched_delta.0
        );
    }
}

#[test]
fn zero_implied_capacity_does_not_appear_on_opposite_runner() {
    let market_id = MarketId(20_000);
    let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

    place_raw(
        &mut book,
        1,
        market_id,
        mm(),
        RunnerId(2),
        Side::Back,
        OddsX10000(60_000),
        Money(1),
    );

    let implied = book.runner_prices(RunnerId(1), 10);
    assert!(
        implied.available_to_back.is_empty(),
        "zero-capacity implied liquidity must not be shown on opposite runner"
    );
}

#[test]
fn taker_back_at_min_tick_can_match_high_opposite_back_prices() {
    let market_id = MarketId(25_000);
    let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));

    place_raw(
        &mut book,
        1,
        market_id,
        mm(),
        RunnerId(2),
        Side::Back,
        OddsX10000(1_100_000),
        Money::from_cents(1),
    );

    let implied = book.runner_prices(RunnerId(1), 4).available_to_back;
    assert_eq!(implied.len(), 1);
    assert_eq!(implied[0].price, OddsX10000(10_100));
    assert_eq!(implied[0].size, Money::from_cents(100));

    let events = place_raw(
        &mut book,
        2,
        market_id,
        alice(),
        RunnerId(1),
        Side::Back,
        OddsX10000(10_100),
        Money::from_cents(1),
    );

    let taker_fill = events.iter().find_map(|env| match env.event {
        BookEvent::TradeMatched {
            role: TradeRole::Taker,
            matched_delta,
            ..
        } => Some(matched_delta),
        _ => None,
    });
    assert_eq!(taker_fill, Some(Money::from_cents(1)));
}

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

    for case_idx in 0..96u64 {
        let market_id = MarketId(30_000 + case_idx);
        let mut book = betex::book::Book::new_two_runner(market_id, RunnerId(1), RunnerId(2));
        let maker_side = if rng.random_bool(0.5) {
            Side::Back
        } else {
            Side::Lay
        };
        let maker_count = rng.random_range(2..=4);

        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(10..1_000));
            place_raw(
                &mut book,
                case_idx * 10 + maker_idx as u64 + 1,
                market_id,
                betex::types::AccountId::from(1_000 + maker_idx as u64),
                RunnerId(2),
                maker_side,
                maker_odds,
                maker_stake,
            );
        }

        let (taker_side, ladder) = match maker_side {
            Side::Back => (
                Side::Back,
                book.runner_prices(RunnerId(1), 16).available_to_back,
            ),
            Side::Lay => (
                Side::Lay,
                book.runner_prices(RunnerId(1), 16).available_to_lay,
            ),
        };

        assert!(
            !ladder.is_empty(),
            "expected implied ladder for multi-match case {case_idx}"
        );

        let taker_price = match taker_side {
            Side::Back => ladder
                .last()
                .map(|ps| ps.price)
                .expect("back ladder should have worst acceptable price"),
            Side::Lay => ladder
                .last()
                .map(|ps| ps.price)
                .expect("lay ladder should have worst acceptable price"),
        };
        let taker_stake = Money(ladder.iter().map(|ps| ps.size.0).sum());

        let events = place_raw(
            &mut book,
            case_idx * 10 + 9,
            market_id,
            alice(),
            RunnerId(1),
            taker_side,
            taker_price,
            taker_stake,
        );

        let taker_matches: Vec<_> = events
            .iter()
            .filter_map(|env| match env.event {
                BookEvent::TradeMatched {
                    role: TradeRole::Taker,
                    stake,
                    ..
                } => Some(stake),
                _ => None,
            })
            .collect();
        let maker_matches: Vec<_> = events
            .iter()
            .filter_map(|env| match env.event {
                BookEvent::TradeMatched {
                    role: TradeRole::Maker,
                    matched_delta,
                    ..
                } => Some(matched_delta),
                _ => None,
            })
            .collect();

        assert!(
            taker_matches.len() >= 2,
            "expected taker to sweep multiple makers in case {case_idx}"
        );
        assert_eq!(
            taker_matches.iter().map(|m| m.0).sum::<i64>(),
            taker_stake.0,
            "taker matched total mismatch in case {case_idx}"
        );
        assert_eq!(
            taker_matches.len(),
            maker_matches.len(),
            "maker/taker fill count mismatch in case {case_idx}"
        );
    }
}
