//! Scenario-based tests using the test harness.
//!
//! These tests verify Betfair exchange rules including:
//! - Price-time priority (FIFO)
//! - Partial fills and multi-level sweeps
//! - Settlement outcomes (winner/loser/void)
//! - Implied matching (two-runner)
//! - Fill-or-Kill behavior

mod harness;

use betex::book::BookMarketState;
use betex::book::protocol::reject::RejectReason;
use harness::*;

// ============================================================================
// Basic Matching Tests
// ============================================================================

mod basic_matching {
    use super::*;

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

        // Lay order rests on book
        let lay = h.place(mm(), lay(0).at(2.0).stake(100));
        h.assert_order(lay).is_unmatched();
        h.assert_no_trades();

        // Back order matches
        let back = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(lay).is_filled();
        h.assert_order(back).is_filled();
        h.assert_trades_count(1);

        h.assert_last_trade()
            .stake(100)
            .at(2.0)
            .runner(0)
            .back_order(back)
            .lay_order(lay);
    }

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

        // Lay at 2.0 (layer offers 2.0)
        h.place(mm(), lay(0).at(2.0).stake(100));

        // Back at 2.1 means the backer wants at least 2.1 odds.
        // A resting lay at 2.0 is worse for the backer, so it should not match.
        let back = h.place(alice(), back(0).at(2.1).stake(100));

        h.assert_order(back).is_unmatched();
        h.assert_no_trades();
    }

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

        // Lay resting at 2.0
        let lay = h.place(mm(), lay(0).at(2.0).stake(100));

        // Back comes in at 1.9 - should match at 2.0 because higher odds are
        // better for the backer and the trade executes at the resting price.
        let back = h.place(alice(), back(0).at(1.9).stake(100));

        h.assert_order(lay).is_filled();
        h.assert_order(back).is_filled();
        h.assert_last_trade().at(2.0); // Matched at resting price
    }
}

// ============================================================================
// Partial Fill Tests
// ============================================================================

mod partial_fills {
    use super::*;

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

        // Lay 100
        let lay = h.place(mm(), lay(0).at(2.0).stake(100));

        // Back only 50
        let back = h.place(alice(), back(0).at(2.0).stake(50));

        h.assert_order(lay).is_partial(50, 50); // 50 matched, 50 remaining
        h.assert_order(back).is_filled(); // fully filled
        h.assert_trades_count(1);
        h.assert_last_trade().stake(50);
    }

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

        // Lay 50
        let lay = h.place(mm(), lay(0).at(2.0).stake(50));

        // Back 100 - only 50 available
        let back = h.place(alice(), back(0).at(2.0).stake(100));

        h.assert_order(lay).is_filled();
        h.assert_order(back).is_partial(50, 50); // 50 matched, 50 resting
        h.assert_trades_count(1);
    }

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

        // Multiple price levels of liquidity
        let lay1 = h.place(mm(), lay(0).at(2.0).stake(50));
        let lay2 = h.place(mm(), lay(0).at(2.02).stake(50));
        let lay3 = h.place(mm(), lay(0).at(2.04).stake(50));

        // Aggressor sweeps through all acceptable levels, taking the highest
        // odds first and then walking down to the limit price.
        let back = h.place(alice(), back(0).at(2.0).stake(120));

        h.assert_order(lay1).is_partial(20, 30); // only 20 filled from this level
        h.assert_order(lay2).is_filled();
        h.assert_order(lay3).is_filled();
        h.assert_order(back).is_filled();
        h.assert_trades_count(3);

        // Verify trades at each price level
        h.assert_trade(0).at(2.04).stake(50);
        h.assert_trade(1).at(2.02).stake(50);
        h.assert_trade(2).at(2.0).stake(20);
    }
}

// ============================================================================
// FIFO Priority Tests
// ============================================================================

mod fifo_priority {
    use super::*;

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

        // Two lays at same price - order1 first
        let lay1 = h.place(alice(), lay(0).at(2.0).stake(100));
        let lay2 = h.place(bob(), lay(0).at(2.0).stake(100));

        // Back 100 - should match lay1 first (FIFO)
        let back = h.place(carol(), back(0).at(2.0).stake(100));

        h.assert_order(lay1).is_filled();
        h.assert_order(lay2).is_unmatched();
        h.assert_order(back).is_filled();

        // Verify it matched with alice()'s order, not bob()'s
        h.assert_last_trade().lay_order(lay1);
    }

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

        // Three lays at same price
        let lay1 = h.place(alice(), lay(0).at(2.0).stake(50));
        let lay2 = h.place(bob(), lay(0).at(2.0).stake(50));
        let lay3 = h.place(carol(), lay(0).at(2.0).stake(50));

        // Back sweeps through first two
        let back = h.place(mm(), back(0).at(2.0).stake(100));

        h.assert_order(lay1).is_filled();
        h.assert_order(lay2).is_filled();
        h.assert_order(lay3).is_unmatched(); // third didn't get filled
        h.assert_order(back).is_filled();

        h.assert_trades_count(2);
        h.assert_trade(0).lay_order(lay1);
        h.assert_trade(1).lay_order(lay2);
    }
}

// ============================================================================
// Settlement Outcome Tests
// ============================================================================

mod settlement {
    use super::*;

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

        // Setup: Alice backs runner 0 at 2.0 for 100
        let _lay = h.place(mm(), lay(0).at(2.0).stake(100));
        let _back = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_trades_count(1);

        // Checkpoint before settlement (after closing market)
        h.close_market();
        let cp = h.checkpoint();

        // Branch 1: Runner 0 wins
        let mut h1 = cp.clone();
        h1.settle(&[winner(0), loser(1)]);
        // Alice backed at 2.0 for 100, wins 100 profit
        h1.assert_payout(alice(), 100);
        // mm() was layer, loses 100
        h1.assert_payout(mm(), -100);

        // Branch 2: Runner 1 wins (runner 0 loses)
        let mut h2 = cp.clone();
        h2.settle(&[loser(0), winner(1)]);
        // Alice loses her stake
        h2.assert_payout(alice(), -100);
        // mm() wins the stake
        h2.assert_payout(mm(), 100);
    }

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

        // Setup matched order
        h.place(mm(), lay(0).at(2.0).stake(100));
        h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_trades_count(1);

        // Void the market
        h.void_market();

        h.assert_market_state(BookMarketState::Voided);
        h.assert_trades_count(1);
    }

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

        // Alice matches against multiple lay prices
        h.place(mm(), lay(0).at(2.0).stake(50));
        h.place(mm(), lay(0).at(2.1).stake(50));

        h.place(alice(), back(0).at(2.0).stake(100)); // sweeps both levels

        h.assert_trades_count(2);

        // Close market before settlement
        h.close_market();

        // Runner 0 wins
        h.settle(&[winner(0), loser(1)]);

        // Alice's P&L:
        // Trade 1: backed 50 at 2.0 → profit = 50 * (2.0 - 1) = 50
        // Trade 2: backed 50 at 2.1 → profit = 50 * (2.1 - 1) = 55
        // Total = 105
        h.assert_payout(alice(), 105);
    }
}

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

    // Setup: Alice and Bob back both runners at same odds
    let _back0 = h.place(mm(), lay(0).at(2.0).stake(100));
    let _back1 = h.place(alice(), back(0).at(2.0).stake(100));
    let _back2 = h.place(bob(), back(1).at(2.0).stake(100));
    h.assert_trades_count(1); // One implied trade

    // Close market before settlement
    h.close_market();

    // Settle with dead heat (divisor 2)
    h.settle_dead_heat(&[winner(0), winner(1)], 2);

    // Verify MarketSettled event records dead heat divisor
    let dead_heat = h.events().iter().rev().find_map(|env| match &env.event {
        betex::book::BookEvent::MarketSettled {
            dead_heat_divisor, ..
        } => *dead_heat_divisor,
        _ => None,
    });

    assert_eq!(dead_heat, Some(2));
    h.assert_market_state(BookMarketState::Settled);
}

// ============================================================================
// Implied Matching Tests (Two-Runner Only)
// ============================================================================

mod implied_matching {
    use super::*;

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

        // Alice backs runner 0 at 2.0
        let back0 = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(back0).is_unmatched();

        // Bob backs runner 1 at 2.0
        // In two-runner market: BACK runner 1 @ 2.0 ≡ LAY runner 0 @ 2.0
        let back1 = h.place(bob(), back(1).at(2.0).stake(100));

        // Should have implied matched
        h.assert_order(back0).is_filled();
        h.assert_order(back1).is_filled();
        h.assert_trades_count(1);
    }

    #[test]
    fn no_implied_matching_in_multi_runner() {
        let mut h = TestHarness::three_runner();

        // Alice backs runner 0 at 2.0
        let back0 = h.place(alice(), back(0).at(2.0).stake(100));

        // Bob backs runner 1 at 2.0
        let back1 = h.place(bob(), back(1).at(2.0).stake(100));

        // In multi-runner market, no implied matching
        h.assert_order(back0).is_unmatched();
        h.assert_order(back1).is_unmatched();
        h.assert_no_trades();
    }
}

// ============================================================================
// Fill-or-Kill Tests
// ============================================================================

mod fill_or_kill {
    use super::*;

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

        // Only 50 available
        h.place(mm(), lay(0).at(2.0).stake(50));

        // FOK for 100 should fail - not enough liquidity
        let reason = h.place_rejected(alice(), back(0).at(2.0).stake(100).fok());
        assert_eq!(reason, RejectReason::WouldNotFillFok);
    }

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

        // Exactly 100 available
        let lay = h.place(mm(), lay(0).at(2.0).stake(100));

        // FOK for 100 should succeed
        let back = h.place(alice(), back(0).at(2.0).stake(100).fok());

        h.assert_order(lay).is_filled();
        h.assert_order(back).is_filled();
        h.assert_trades_count(1);
    }

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

        // 50 available
        let lay = h.place(mm(), lay(0).at(2.0).stake(50));

        // FOK for 100 with min_fill=50 should succeed with partial fill
        let back = h.place(alice(), back(0).at(2.0).stake(100).fok_min(50));

        h.assert_order(lay).is_filled();
        // With FOK, the remainder is cancelled, not rested
        h.assert_order(back).matched(50);
        h.assert_trades_count(1);
    }

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

        // Only 30 available
        h.place(mm(), lay(0).at(2.0).stake(30));

        // FOK for 100 with min_fill=50 should fail - only 30 available
        let reason = h.place_rejected(alice(), back(0).at(2.0).stake(100).fok_min(50));
        assert_eq!(reason, RejectReason::WouldNotFillFok);
    }
}

// ============================================================================
// Market State Tests
// ============================================================================

mod market_state {
    use super::*;

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

        h.suspend();
        h.assert_market_state(BookMarketState::Suspended);

        let reason = h.place_rejected(alice(), back(0).at(2.0).stake(100));
        assert_eq!(reason, RejectReason::MarketNotOpen);
    }

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

        h.suspend();
        h.resume();
        h.assert_market_state(BookMarketState::Open);

        // Should succeed now
        let order = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(order).is_unmatched();
    }

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

        // Place a lapse order
        let order = h.place(alice(), back(0).at(2.0).stake(100).lapse());
        h.assert_order(order).is_unmatched();

        // Move to live phase
        h.set_live_phase();

        // Order should be lapsed
        h.assert_order(order).is_lapsed();
    }

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

        let order = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(order).is_unmatched();

        h.close_market();

        h.assert_order(order).is_cancelled();
        h.assert_market_state(BookMarketState::Closed);
    }
}

// ============================================================================
// Runner Removal Tests
// ============================================================================

mod runner_removal {
    use super::*;

    #[test]
    fn remove_runner_cancels_orders_on_that_runner() {
        let mut h = TestHarness::three_runner();

        // Orders on runners 0 and 1
        let order0 = h.place(alice(), back(0).at(2.0).stake(100));
        let order1 = h.place(bob(), back(1).at(2.0).stake(100));

        // Remove runner 0
        h.remove_runner(0, None);

        // Order on runner 0 should be cancelled
        h.assert_order(order0).is_cancelled();
        // Order on runner 1 should be unaffected
        h.assert_order(order1).is_unmatched();
    }
}

// ============================================================================
// Cancel Order Tests
// ============================================================================

mod cancellation {
    use super::*;

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

        let order = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(order).is_unmatched();

        h.cancel(order);
        h.assert_order(order).is_cancelled();
    }

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

        // Lay 50
        h.place(mm(), lay(0).at(2.0).stake(50));

        // Back 100 - 50 matches, 50 rests
        let back = h.place(alice(), back(0).at(2.0).stake(100));
        h.assert_order(back).is_partial(50, 50);

        // Cancel the rest
        h.cancel(back);
        h.assert_order(back).is_cancelled();
        h.assert_order(back).matched(50); // Still has the matched portion
    }

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

        // Two lays at same price
        let lay1 = h.place(alice(), lay(0).at(2.0).stake(50));
        let lay2 = h.place(bob(), lay(0).at(2.0).stake(50));

        // Cancel first one
        h.cancel(lay1);
        h.assert_order(lay1).is_cancelled();

        // Back should match second order, not cancelled one
        let back = h.place(carol(), back(0).at(2.0).stake(50));

        h.assert_order(lay2).is_filled();
        h.assert_order(back).is_filled();
        h.assert_trades_count(1);
        h.assert_last_trade().lay_order(lay2);
    }
}

// ============================================================================
// Validation Tests
// ============================================================================

mod validation {
    use super::*;

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

        // 2.01 is not a valid tick (should be 2.00 or 2.02)
        let reason = h.place_rejected(alice(), back(0).at_raw(20100).stake(100));
        assert_eq!(reason, RejectReason::InvalidOdds);
    }

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

        let reason = h.place_rejected(alice(), back(0).at(2.0).stake(0));
        assert_eq!(reason, RejectReason::InvalidStake);
    }

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

        // Runner 99 doesn't exist
        let reason = h.place_rejected(alice(), back(99).at(2.0).stake(100));
        assert_eq!(reason, RejectReason::RunnerNotFound);
    }

    #[test]
    fn reject_order_on_removed_runner() {
        let mut h = TestHarness::three_runner();

        h.remove_runner(0, None);

        // Can't place order on removed runner
        let reason = h.place_rejected(alice(), back(0).at(2.0).stake(100));
        assert_eq!(reason, RejectReason::RunnerNotFound);
    }
}

// ============================================================================
// Price Priority Tests
// ============================================================================

mod price_priority {
    use super::*;

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

        // Multiple lay prices - higher is better for backer
        h.place(mm(), lay(0).at(2.0).stake(50));
        h.place(mm(), lay(0).at(2.1).stake(50)); // Better price
        h.place(mm(), lay(0).at(1.9).stake(50));

        // Back at 2.0 should match against 2.1 first (best available)
        let back = h.place(alice(), back(0).at(2.0).stake(30));

        h.assert_order(back).is_filled();
        h.assert_trades_count(1);
        h.assert_last_trade().at(2.1); // Matched at best price
    }

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

        // Multiple back prices - lower is better for layer
        h.place(alice(), back(0).at(2.0).stake(50));
        h.place(bob(), back(0).at(1.9).stake(50)); // Better price for layer
        h.place(carol(), back(0).at(2.1).stake(50));

        // Lay at 2.0 should match against 1.9 first (best for layer)
        let lay = h.place(mm(), lay(0).at(2.0).stake(30));

        h.assert_order(lay).is_filled();
        h.assert_trades_count(1);
        h.assert_last_trade().at(1.9); // Matched at best price
    }

    #[test]
    fn multi_runner_back_matches_highest_acceptable_lay_price() {
        let mut h = TestHarness::three_runner();

        h.place(mm(), lay(0).at(2.0).stake(50));
        h.place(bob(), lay(0).at(2.1).stake(50)); // Better for backer
        h.place(carol(), lay(0).at(1.9).stake(50));

        let back = h.place(alice(), back(0).at(2.0).stake(30));

        h.assert_order(back).is_filled();
        h.assert_trades_count(1);
        h.assert_last_trade().at(2.1);
    }

    #[test]
    fn multi_runner_lay_matches_lowest_acceptable_back_price() {
        let mut h = TestHarness::three_runner();

        h.place(alice(), back(0).at(2.0).stake(50));
        h.place(bob(), back(0).at(1.9).stake(50)); // Better for layer
        h.place(carol(), back(0).at(2.1).stake(50));

        let lay = h.place(mm(), lay(0).at(2.0).stake(30));

        h.assert_order(lay).is_filled();
        h.assert_trades_count(1);
        h.assert_last_trade().at(1.9);
    }
}

// ============================================================================
// Self-Match Prevention Tests
// ============================================================================

mod self_match {
    use super::*;

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

        // Same account places both sides
        let lay = h.place(alice(), lay(0).at(2.0).stake(100));
        let back = h.place(alice(), back(0).at(2.0).stake(100));

        // Should NOT match - self-trade prevention
        h.assert_order(lay).is_unmatched();
        h.assert_order(back).is_unmatched();
        h.assert_no_trades();
    }

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

        // Different accounts
        let lay = h.place(alice(), lay(0).at(2.0).stake(100));
        let back = h.place(bob(), back(0).at(2.0).stake(100));

        // Should match
        h.assert_order(lay).is_filled();
        h.assert_order(back).is_filled();
        h.assert_trades_count(1);
    }
}

// ============================================================================
// Account-Specific Scenarios
// ============================================================================

mod account_scenarios {
    use super::*;

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

        // Alice backs both runners
        let back0 = h.place(alice(), back(0).at(2.0).stake(100));
        let back1 = h.place(alice(), back(1).at(2.0).stake(100));

        h.assert_order(back0).is_unmatched();
        h.assert_order(back1).is_unmatched();
        h.assert_trades_count(0);
    }

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

        // Alice places two orders
        let order1 = h.place(alice(), back(0).at(2.0).stake(100));
        let order2 = h.place(alice(), lay(0).at(2.0).stake(100));

        h.assert_order(order1).is_unmatched();
        h.assert_order(order2).is_unmatched();

        // Cancel own order
        h.cancel(order2);

        h.assert_order(order2).is_cancelled();
        h.assert_order(order1).is_unmatched();
    }
}
