use betex::{
    book::Book,
    book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce},
    cross_match::{
        CrossMatchConfig, CrossMatchEngine, CrossMatchResult, HedgeInput, HedgeResult,
        calculate_3runner_hedge,
    },
    types::{AccountId, CorrelationId, MarketId, Money, OddsX10000, OrderId, 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,
    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,
        },
    }
}

#[test]
fn cross_match_back_hedge_finds_integer_solution_for_edge_case() {
    let input = HedgeInput {
        target_runner: RunnerId(1),
        user_side: Side::Yes,
        user_odds: OddsX10000(30_000),
        user_stake: Money(1),
        other_runners: vec![
            (RunnerId(2), OddsX10000(10_100), Money(1)),
            (RunnerId(3), OddsX10000(30_000), Money(1)),
        ],
        max_loss: Money(0),
    };

    match calculate_3runner_hedge(&input) {
        HedgeResult::Success {
            legs,
            worst_case_pnl,
        } => {
            assert_eq!(legs.len(), 2);
            assert!(worst_case_pnl.0 >= 0, "risk-free hedge should not lose");
        }
        HedgeResult::NoSolution { reason } => {
            panic!("expected feasible hedge, got NoSolution: {reason}");
        }
    }
}

#[test]
fn cross_match_lay_hedge_accepts_feasible_zero_hedge_case() {
    let input = HedgeInput {
        target_runner: RunnerId(1),
        user_side: Side::No,
        user_odds: OddsX10000(20_000),
        user_stake: Money(358),
        other_runners: vec![
            (RunnerId(2), OddsX10000(10_500), Money(76)),
            (RunnerId(3), OddsX10000(12_000), Money(3935)),
        ],
        max_loss: Money(931),
    };

    match calculate_3runner_hedge(&input) {
        HedgeResult::Success {
            legs,
            worst_case_pnl,
        } => {
            assert_eq!(legs.len(), 2);
            assert!(
                worst_case_pnl.0 >= -input.max_loss.0,
                "worst pnl {} exceeds tolerance {}",
                worst_case_pnl.0,
                input.max_loss.0
            );
        }
        HedgeResult::NoSolution { reason } => {
            panic!("expected feasible hedge, got NoSolution: {reason}");
        }
    }
}

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

    // User LAY on target runner (resting order that cross-match will take).
    let _ = exec(
        &mut book,
        place_order_cmd(market_id, 1, 10, 1, Side::No, 20_000, 358),
    );
    // LAY liquidity on other runners for hedge discovery.
    let _ = exec(
        &mut book,
        place_order_cmd(market_id, 2, 20, 2, Side::No, 10_500, 76),
    );
    let _ = exec(
        &mut book,
        place_order_cmd(market_id, 3, 30, 3, Side::No, 12_000, 3935),
    );

    let mut config = CrossMatchConfig::default();
    config.risk.max_loss_pct = rust_decimal::Decimal::new(3, 0);
    config.risk.max_loss_absolute = Money(931);
    let mut engine = CrossMatchEngine::new(config);

    let res = engine.attempt_cross_match(&mut book, OrderId(1));
    match res {
        CrossMatchResult::Success {
            hedge_legs, events, ..
        } => {
            assert!(
                hedge_legs.is_empty(),
                "zero-hedge case should not execute stake=0 legs"
            );
            assert!(
                events
                    .iter()
                    .any(|e| matches!(e.event, betex::book::BookEvent::TradeMatched { .. })),
                "user order should still be matched by the house leg"
            );
        }
        CrossMatchResult::NotPossible { reason } => {
            panic!("expected success for zero-hedge case, got NotPossible: {reason}");
        }
        CrossMatchResult::Failed { reason, .. } => {
            panic!("expected success for zero-hedge case, got Failed: {reason}");
        }
    }
}

#[test]
fn cross_match_enforces_max_runners_before_3_runner_constraint() {
    let market_id = MarketId(501);
    let mut book = Book::new_multi_runner(
        market_id,
        [RunnerId(1), RunnerId(2), RunnerId(3), RunnerId(4)],
    );
    let _ = exec(
        &mut book,
        place_order_cmd(market_id, 1, 100, 1, Side::Yes, 20_000, 100),
    );
    let config = CrossMatchConfig {
        max_runners: 3,
        ..CrossMatchConfig::default()
    };
    let mut engine = CrossMatchEngine::new(config);

    let res = engine.attempt_cross_match(&mut book, OrderId(1));
    match res {
        CrossMatchResult::NotPossible { reason } => {
            assert_eq!(reason, "Too many runners for cross-matching");
        }
        _ => panic!("expected NotPossible due to max_runners"),
    }
}

#[test]
fn default_house_account_uses_nil_uuid() {
    let config = CrossMatchConfig::default();
    assert_eq!(config.house_account, AccountId::from(uuid::Uuid::nil()));
}
