//! Stress tests and extreme scenarios for the ELO system
//!
//! Tests that verify the rating system behaves correctly under unusual conditions

#[cfg(test)]
mod tests {
    use crate::elo::calculator::EloCalculator;
    use crate::elo::doubles::calculate_effective_opponent_rating;
    use crate::elo::rating::EloRating;
    use crate::elo::score_weight::calculate_weighted_score;

    // ============================================
    // EXTREME RATING DIFFERENCES
    // ============================================

    #[test]
    fn test_1000_point_rating_gap() {
        let calc = EloCalculator::new();
        let elite = EloRating::new_with_rating(2500.0);
        let beginner = EloRating::new_with_rating(1500.0);
        
        // Elite expected to win ~99.7% of points
        let elite_perf = calculate_weighted_score(elite.rating, beginner.rating, 11, 0);
        let new_elite = calc.update_rating(&elite, &beginner, elite_perf);
        
        // Even with a shutout, elite shouldn't gain much (expected result)
        let gain = new_elite.rating - elite.rating;
        assert!(gain < 1.0, "Elite gained too much for expected shutout: {}", gain);
        
        println!("1000pt gap shutout: Elite {} -> {} ({:+.2})", elite.rating, new_elite.rating, gain);
    }

    #[test]
    fn test_1000_point_upset() {
        let calc = EloCalculator::new();
        let beginner = EloRating::new_with_rating(1500.0);
        let elite = EloRating::new_with_rating(2500.0);
        
        // Beginner pulls off impossible upset
        let beginner_perf = calculate_weighted_score(beginner.rating, elite.rating, 11, 9);
        let new_beginner = calc.update_rating(&beginner, &elite, beginner_perf);
        
        // Should be massive gain
        let gain = new_beginner.rating - beginner.rating;
        assert!(gain > 15.0, "Beginner didn't gain enough for major upset: {}", gain);
        
        println!("1000pt upset: Beginner {} -> {} (+{:.2})", beginner.rating, new_beginner.rating, gain);
    }

    #[test]
    fn test_minimum_rating_player() {
        let calc = EloCalculator::new();
        let min_player = EloRating::new_with_rating(1.0); // Minimum possible
        let opponent = EloRating::new_player();
        
        // Even losing badly shouldn't go below 1
        let perf = calculate_weighted_score(min_player.rating, opponent.rating, 0, 11);
        let new_rating = calc.update_rating(&min_player, &opponent, perf);
        
        assert!(new_rating.rating >= 1.0, "Rating went below minimum: {}", new_rating.rating);
    }

    #[test]
    fn test_very_high_rating_player() {
        let calc = EloCalculator::new();
        let max_player = EloRating::new_with_rating(3000.0);
        let opponent = EloRating::new_player();
        
        // Losing to much lower rated player
        let perf = calculate_weighted_score(max_player.rating, opponent.rating, 5, 11);
        let new_rating = calc.update_rating(&max_player, &opponent, perf);
        
        // Should lose significant rating
        let loss = max_player.rating - new_rating.rating;
        assert!(loss > 10.0, "High rated player didn't lose enough: {}", loss);
        
        println!("3000 rated loses to 1500: {} -> {} (-{:.2})", max_player.rating, new_rating.rating, loss);
    }

    // ============================================
    // SCORE EXTREMES
    // ============================================

    #[test]
    fn test_11_0_shutout_performance() {
        let perf = calculate_weighted_score(1500.0, 1500.0, 11, 0);
        assert!((perf - 1.0).abs() < 0.001, "11-0 should be 100% performance");
    }

    #[test]
    fn test_0_11_shutout_loss_performance() {
        let perf = calculate_weighted_score(1500.0, 1500.0, 0, 11);
        assert!(perf.abs() < 0.001, "0-11 should be 0% performance");
    }

    #[test]
    fn test_deuce_game_performance() {
        // 15-13 overtime game
        let perf = calculate_weighted_score(1500.0, 1500.0, 15, 13);
        let expected = 15.0 / 28.0;
        assert!((perf - expected).abs() < 0.001);
        
        println!("15-13 overtime: {:.3} performance", perf);
    }

    #[test]
    fn test_one_point_game() {
        // Weird edge case: 1-0 (shouldn't happen, but system should handle)
        let perf = calculate_weighted_score(1500.0, 1500.0, 1, 0);
        assert!((perf - 1.0).abs() < 0.001, "1-0 should be 100% performance");
    }

    #[test]
    fn test_zero_zero_game() {
        // No points played (edge case)
        let perf = calculate_weighted_score(1500.0, 1500.0, 0, 0);
        assert!((perf - 0.5).abs() < 0.001, "0-0 should default to 50%");
    }

    // ============================================
    // DOUBLES EFFECTIVE OPPONENT EXTREMES
    // ============================================

    #[test]
    fn test_effective_opponent_huge_gap() {
        // Pro (2000) paired with beginner (1000) vs two 1500s
        let eff_pro = calculate_effective_opponent_rating(1500.0, 1500.0, 1000.0);
        let eff_beginner = calculate_effective_opponent_rating(1500.0, 1500.0, 2000.0);
        
        // Pro faces 1500+1500-1000 = 2000 (hard)
        assert!((eff_pro - 2000.0).abs() < 0.01);
        
        // Beginner faces 1500+1500-2000 = 1000 (easy)
        assert!((eff_beginner - 1000.0).abs() < 0.01);
        
        println!("Pro (2000) + Beginner (1000) vs 1500+1500:");
        println!("  Pro eff_opp: {:.0}, Beginner eff_opp: {:.0}", eff_pro, eff_beginner);
    }

    #[test]
    fn test_effective_opponent_negative() {
        // Extremely strong teammate makes effective opponent very low
        let eff = calculate_effective_opponent_rating(1200.0, 1200.0, 2000.0);
        
        // 1200+1200-2000 = 400
        assert!((eff - 400.0).abs() < 0.01);
        
        // System should still work with low effective opponent
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1500.0);
        let eff_opp = EloRating::new_with_rating(eff);
        
        let perf = calculate_weighted_score(player.rating, eff_opp.rating, 11, 5);
        let new_rating = calc.update_rating(&player, &eff_opp, perf);
        
        // Player expected to dominate, so should gain little or lose rating
        println!("Eff opp 400: Player {} -> {} for 11-5 win", player.rating, new_rating.rating);
    }

    #[test]
    fn test_effective_opponent_very_high() {
        // Weak teammate against strong opponents
        let eff = calculate_effective_opponent_rating(1800.0, 1800.0, 1200.0);
        
        // 1800+1800-1200 = 2400
        assert!((eff - 2400.0).abs() < 0.01);
        
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1500.0);
        let eff_opp = EloRating::new_with_rating(eff);
        
        let perf = calculate_weighted_score(player.rating, eff_opp.rating, 11, 9);
        let new_rating = calc.update_rating(&player, &eff_opp, perf);
        
        // Beating 2400 effective opponent should give big gains
        let gain = new_rating.rating - player.rating;
        assert!(gain > 10.0, "Should gain big for beating 2400 eff opp");
        
        println!("Eff opp 2400: Player {} -> {} (+{:.2}) for 11-9 win", 
            player.rating, new_rating.rating, gain);
    }

    // ============================================
    // K-FACTOR EXTREMES
    // ============================================

    #[test]
    fn test_k_factor_zero() {
        let calc = EloCalculator::new_with_k_factor(0.0);
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // No rating change with K=0
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        assert!((new_rating.rating - player.rating).abs() < 0.001);
    }

    #[test]
    fn test_k_factor_very_high() {
        let calc = EloCalculator::new_with_k_factor(100.0);
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Big swings with high K
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        let gain = new_rating.rating - player.rating;
        
        // K=100, E=0.5, S=1.0 → ΔR = 100*(1.0-0.5) = 50
        assert!((gain - 50.0).abs() < 0.1);
    }

    // ============================================
    // CONSISTENCY TESTS
    // ============================================

    #[test]
    fn test_symmetry_singles() {
        let calc = EloCalculator::new();
        let p1 = EloRating::new_player();
        let p2 = EloRating::new_player();
        
        let p1_perf = calculate_weighted_score(p1.rating, p2.rating, 11, 7);
        let p2_perf = calculate_weighted_score(p2.rating, p1.rating, 7, 11);
        
        let new_p1 = calc.update_rating(&p1, &p2, p1_perf);
        let new_p2 = calc.update_rating(&p2, &p1, p2_perf);
        
        // Changes should be equal and opposite
        let p1_change = new_p1.rating - p1.rating;
        let p2_change = new_p2.rating - p2.rating;
        
        assert!((p1_change + p2_change).abs() < 0.001, 
            "Rating changes not symmetric: {} + {} = {}", p1_change, p2_change, p1_change + p2_change);
    }

    #[test]
    fn test_rating_bounds_after_many_matches() {
        let calc = EloCalculator::new();
        let mut player = EloRating::new_player();
        let opponent = EloRating::new_with_rating(2000.0);
        
        // Lose 100 matches badly
        for _ in 0..100 {
            let perf = calculate_weighted_score(player.rating, opponent.rating, 0, 11);
            player = calc.update_rating(&player, &opponent, perf);
        }
        
        // Should never go below 1
        assert!(player.rating >= 1.0, "Rating below minimum after losses: {}", player.rating);
        
        println!("After 100 shutout losses to 2000: {:.2}", player.rating);
    }

    #[test]
    fn test_convergence_to_true_skill() {
        let calc = EloCalculator::new();
        
        // Two players where one is truly better (wins 70% of points consistently)
        let mut better = EloRating::new_player();
        let mut worse = EloRating::new_player();
        
        for _ in 0..50 {
            // Better player consistently gets 70% of points
            let better_perf = 0.7;
            let worse_perf = 0.3;
            
            let new_better = calc.update_rating(&better, &worse, better_perf);
            let new_worse = calc.update_rating(&worse, &better, worse_perf);
            
            better = new_better;
            worse = new_worse;
        }
        
        // Rating gap should stabilize around ~150-200 points for 70/30 split
        let gap = better.rating - worse.rating;
        assert!(gap > 100.0, "Gap too small for consistent 70/30: {}", gap);
        assert!(gap < 400.0, "Gap too large: {}", gap);
        
        println!("After 50 matches (70/30 split): Better={:.0}, Worse={:.0}, Gap={:.0}",
            better.rating, worse.rating, gap);
    }

    #[test]
    fn test_unstable_player() {
        let calc = EloCalculator::new();
        let mut player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Alternating wins and losses
        for i in 0..20 {
            let perf = if i % 2 == 0 {
                calculate_weighted_score(player.rating, opponent.rating, 11, 5)
            } else {
                calculate_weighted_score(player.rating, opponent.rating, 5, 11)
            };
            player = calc.update_rating(&player, &opponent, perf);
        }
        
        // Should stay near 1500
        assert!((player.rating - 1500.0).abs() < 50.0, 
            "Unstable player drifted too far: {}", player.rating);
        
        println!("After 20 alternating matches: {:.2}", player.rating);
    }

    // ============================================
    // FLOATING POINT EDGE CASES
    // ============================================

    #[test]
    fn test_no_nans_or_infinities() {
        let calc = EloCalculator::new();
        
        // Test various extreme inputs
        let test_cases: Vec<(f64, f64)> = vec![
            (0.0, 1.0),
            (1.0, 0.0),
            (0.0, 0.0),
            (1.0, 1.0),
            (0.5, 0.5),
            (0.999999, 0.000001),
        ];
        
        for (p1_rating_mult, p2_rating_mult) in test_cases {
            let p1 = EloRating::new_with_rating(1500.0 * p1_rating_mult.max(0.001));
            let p2 = EloRating::new_with_rating(1500.0 * p2_rating_mult.max(0.001));
            
            for perf in [0.0, 0.5, 1.0] {
                let new_rating = calc.update_rating(&p1, &p2, perf);
                
                assert!(!new_rating.rating.is_nan(), 
                    "NaN detected: p1={}, p2={}, perf={}", p1.rating, p2.rating, perf);
                assert!(!new_rating.rating.is_infinite(), 
                    "Infinity detected: p1={}, p2={}, perf={}", p1.rating, p2.rating, perf);
            }
        }
    }

    #[test]
    fn test_expected_score_bounds() {
        // Expected score should always be between 0 and 1
        let calc = EloCalculator::new();
        
        let test_ratings = vec![1.0, 100.0, 500.0, 1000.0, 1500.0, 2000.0, 2500.0, 3000.0];
        
        for r1 in &test_ratings {
            for r2 in &test_ratings {
                let p1 = EloRating::new_with_rating(*r1);
                let p2 = EloRating::new_with_rating(*r2);
                
                // Test by checking rating change for 0.5 performance (should equal expected)
                let new_rating = calc.update_rating(&p1, &p2, 0.5);
                let change = new_rating.rating - p1.rating;
                
                // For perf=0.5, change = K * (0.5 - E)
                // So E = 0.5 - change/K
                let implied_expected = 0.5 - change / 32.0;
                
                assert!(implied_expected >= 0.0 && implied_expected <= 1.0,
                    "Expected score out of bounds: {} for r1={}, r2={}", implied_expected, r1, r2);
            }
        }
    }
}
