//! Comprehensive tests for the pure ELO rating system (v3.0)
//!
//! Tests cover:
//! - Basic ELO calculations (equal ratings, rating differences)
//! - Singles match scenarios (blowout vs close, upsets)
//! - Doubles effective opponent calculations
//! - Teammate impact (strong vs weak)
//! - Edge cases (shutouts, K-factor application)
//! - Rating conservation and symmetry

#[cfg(test)]
mod elo_calculator_tests {
    use crate::elo::{EloCalculator, EloRating};

    // ==================== EQUAL RATINGS TESTS ====================
    
    #[test]
    fn test_equal_ratings_win_gives_plus_16() {
        let calc = EloCalculator::new();  // K=32
        let player = EloRating::new_player();  // 1500
        let opponent = EloRating::new_player();  // 1500
        
        // Perfect win: 1.0 performance, E=0.5
        // ΔR = 32 × (1.0 - 0.5) = +16
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        
        assert!((new_rating.rating - 1516.0).abs() < 0.01,
                "Equal ratings win should give +16, got {}", 
                new_rating.rating - player.rating);
    }
    
    #[test]
    fn test_equal_ratings_loss_gives_minus_16() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Perfect loss: 0.0 performance, E=0.5
        // ΔR = 32 × (0.0 - 0.5) = -16
        let new_rating = calc.update_rating(&player, &opponent, 0.0);
        
        assert!((new_rating.rating - 1484.0).abs() < 0.01,
                "Equal ratings loss should give -16, got {}", 
                new_rating.rating - player.rating);
    }
    
    #[test]
    fn test_equal_ratings_draw_gives_zero() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Draw: 0.5 performance, E=0.5
        // ΔR = 32 × (0.5 - 0.5) = 0
        let new_rating = calc.update_rating(&player, &opponent, 0.5);
        
        assert!((new_rating.rating - player.rating).abs() < 0.01,
                "Equal ratings draw should give 0 change");
    }
    
    // ==================== RATING DIFFERENCE TESTS ====================
    
    #[test]
    fn test_200_point_difference_expected_win() {
        let calc = EloCalculator::new();
        let strong = EloRating::new_with_rating(1700.0);
        let weak = EloRating::new_with_rating(1500.0);
        
        // Strong player wins as expected
        // Expected score ≈ 0.76
        // ΔR = 32 × (1.0 - 0.76) = 32 × 0.24 = 7.68
        let new_rating = calc.update_rating(&strong, &weak, 1.0);
        let delta = new_rating.rating - strong.rating;
        
        assert!(delta > 7.0 && delta < 9.0,
                "200-point stronger player winning should gain ~7.7, got {}", delta);
    }
    
    #[test]
    fn test_200_point_difference_unexpected_loss() {
        let calc = EloCalculator::new();
        let strong = EloRating::new_with_rating(1700.0);
        let weak = EloRating::new_with_rating(1500.0);
        
        // Strong player loses unexpectedly
        // Expected score ≈ 0.76, actual = 0.0
        // ΔR = 32 × (0.0 - 0.76) = -24.32
        let new_rating = calc.update_rating(&strong, &weak, 0.0);
        let delta = new_rating.rating - strong.rating;
        
        assert!(delta < -23.0 && delta > -26.0,
                "Strong player losing should lose ~24 points, got {}", delta);
    }
    
    #[test]
    fn test_200_point_difference_upset_win() {
        let calc = EloCalculator::new();
        let weak = EloRating::new_with_rating(1500.0);
        let strong = EloRating::new_with_rating(1700.0);
        
        // Weak player wins unexpectedly
        // Expected score ≈ 0.24, actual = 1.0
        // ΔR = 32 × (1.0 - 0.24) = 32 × 0.76 = 24.32
        let new_rating = calc.update_rating(&weak, &strong, 1.0);
        let delta = new_rating.rating - weak.rating;
        
        assert!(delta > 23.0 && delta < 26.0,
                "Upset win should gain ~24 points, got {}", delta);
    }
    
    // ==================== SINGLES BLOWOUT VS CLOSE GAME ====================
    
    #[test]
    fn test_blowout_win_11_0_vs_close_11_9() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Close win: 11-9 means 11/20 = 0.55 performance
        let close_rating = calc.update_rating(&player, &opponent, 11.0 / 20.0);
        let close_delta = close_rating.rating - player.rating;
        
        // Blowout: 11-0 means 11/11 = 1.0 performance
        let blowout_rating = calc.update_rating(&player, &opponent, 1.0);
        let blowout_delta = blowout_rating.rating - player.rating;
        
        // Blowout should give more points than close win
        assert!(blowout_delta > close_delta,
                "Blowout ({:.1}) should beat close win ({:.1})",
                blowout_delta, close_delta);
        assert!(blowout_delta > 25.0,
                "Blowout should be >25, got {}", blowout_delta);
        assert!(close_delta > 1.0 && close_delta < 10.0,
                "Close win should be 1-10, got {}", close_delta);
    }
    
    #[test]
    fn test_close_loss_11_10_vs_blowout_0_11() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Close loss: 10-11 means 10/21 = 0.476 performance
        let close_rating = calc.update_rating(&player, &opponent, 10.0 / 21.0);
        let close_delta = close_rating.rating - player.rating;
        
        // Blowout loss: 0-11 means 0/11 = 0.0 performance
        let blowout_rating = calc.update_rating(&player, &opponent, 0.0);
        let blowout_delta = blowout_rating.rating - player.rating;
        
        // Blowout loss should cost more than close loss
        assert!(blowout_delta < close_delta,
                "Blowout loss ({:.1}) should be worse than close loss ({:.1})",
                blowout_delta, close_delta);
        assert!(blowout_delta < -15.0,
                "Blowout loss should be <-15, got {}", blowout_delta);
        assert!(close_delta > -3.0 && close_delta < 0.0,
                "Close loss should be -3 to 0, got {}", close_delta);
    }
    
    #[test]
    fn test_margin_matters_11_2_vs_11_8() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // 11-2: 11/13 = 0.846
        let eleven_two = calc.update_rating(&player, &opponent, 11.0 / 13.0);
        
        // 11-8: 11/19 = 0.579
        let eleven_eight = calc.update_rating(&player, &opponent, 11.0 / 19.0);
        
        let delta_2 = eleven_two.rating - player.rating;
        let delta_8 = eleven_eight.rating - player.rating;
        
        // 11-2 should give more credit than 11-8
        assert!(delta_2 > delta_8,
                "11-2 ({:.1}) should beat 11-8 ({:.1})",
                delta_2, delta_8);
        assert!(delta_2 > 20.0, "11-2 should be >20, got {}", delta_2);
    }
    
    // ==================== DOUBLES EFFECTIVE OPPONENT ====================
    
    #[test]
    fn test_effective_opponent_equal_teams() {
        // Team 1: Player + Teammate 1500
        // Team 2: Both opponents 1500
        // Effective = 1500 + 1500 - 1500 = 1500 (same as singles)
        
        let calc = EloCalculator::new();
        let player = EloRating::new_player();  // 1500
        let teammate = EloRating::new_player();  // 1500
        let opp1 = EloRating::new_player();  // 1500
        let opp2 = EloRating::new_player();  // 1500
        
        // Effective opponent = 1500 + 1500 - 1500 = 1500
        let effective_rating = opp1.rating + opp2.rating - teammate.rating;
        let effective_opp = EloRating::new_with_rating(effective_rating);
        
        // Should behave like singles
        let new_rating = calc.update_rating(&player, &effective_opp, 1.0);
        let delta = new_rating.rating - player.rating;
        
        assert!((delta - 16.0).abs() < 0.1,
                "Equal teams should give +16 like singles, got {}", delta);
    }
    
    #[test]
    fn test_doubles_strong_teammate_reduces_credit() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();  // 1500
        let strong_teammate = EloRating::new_with_rating(1700.0);
        let opp1 = EloRating::new_player();  // 1500
        let opp2 = EloRating::new_player();  // 1500
        
        // Effective = 1500 + 1500 - 1700 = 1300
        let effective_rating = opp1.rating + opp2.rating - strong_teammate.rating;
        let effective_opp = EloRating::new_with_rating(effective_rating);
        
        // Winning with strong teammate should give less credit than with equal teammate
        let new_rating = calc.update_rating(&player, &effective_opp, 1.0);
        let delta = new_rating.rating - player.rating;
        
        // With weaker effective opponent, player is favored, so wins less
        assert!(delta < 16.0,
                "Strong teammate should reduce credit, got {}", delta);
        assert!(delta > 10.0,
                "Strong teammate credit should still be positive, got {}", delta);
    }
    
    #[test]
    fn test_doubles_weak_teammate_increases_credit() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();  // 1500
        let weak_teammate = EloRating::new_with_rating(1300.0);
        let opp1 = EloRating::new_player();  // 1500
        let opp2 = EloRating::new_player();  // 1500
        
        // Effective = 1500 + 1500 - 1300 = 1700
        let effective_rating = opp1.rating + opp2.rating - weak_teammate.rating;
        let effective_opp = EloRating::new_with_rating(effective_rating);
        
        // Winning with weak teammate should give more credit
        let new_rating = calc.update_rating(&player, &effective_opp, 1.0);
        let delta = new_rating.rating - player.rating;
        
        // With stronger effective opponent, player is underdog, so wins more
        assert!(delta > 16.0,
                "Weak teammate should increase credit, got {}", delta);
        assert!(delta < 25.0,
                "Weak teammate credit shouldn't exceed 25, got {}", delta);
    }
    
    #[test]
    fn test_doubles_extreme_teammate_difference() {
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1500.0);
        
        // Scenario 1: Superstar teammate (1800)
        let superstar = EloRating::new_with_rating(1800.0);
        let opp1 = EloRating::new_player();  // 1500
        let opp2 = EloRating::new_player();  // 1500
        
        let superstar_effective = opp1.rating + opp2.rating - superstar.rating;  // 1200
        let superstar_rating = calc.update_rating(&player, 
            &EloRating::new_with_rating(superstar_effective), 1.0);
        
        // Scenario 2: Weak teammate (1200)
        let weak = EloRating::new_with_rating(1200.0);
        let weak_effective = opp1.rating + opp2.rating - weak.rating;  // 1800
        let weak_rating = calc.update_rating(&player,
            &EloRating::new_with_rating(weak_effective), 1.0);
        
        // With superstar: lower effective = less credit
        // With weak: higher effective = more credit
        let superstar_delta = superstar_rating.rating - player.rating;
        let weak_delta = weak_rating.rating - player.rating;
        
        assert!(superstar_delta < weak_delta,
                "Superstar teammate ({:.1}) should give less than weak ({:.1})",
                superstar_delta, weak_delta);
    }
    
    // ==================== EDGE CASES ====================
    
    #[test]
    fn test_shutout_loss_0_11() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Shutout loss: 0/11 = 0.0 performance
        let new_rating = calc.update_rating(&player, &opponent, 0.0);
        let delta = new_rating.rating - player.rating;
        
        // Should lose -16 against equal opponent
        assert!((delta - (-16.0)).abs() < 0.1,
                "Shutout loss should give -16, got {}", delta);
    }
    
    #[test]
    fn test_shutout_win_11_0() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Shutout win: 11/11 = 1.0 performance
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        let delta = new_rating.rating - player.rating;
        
        // Should gain +16 against equal opponent
        assert!((delta - 16.0).abs() < 0.1,
                "Shutout win should give +16, got {}", delta);
    }
    
    #[test]
    fn test_rating_floor_never_below_one() {
        let calc = EloCalculator::new_with_k_factor(10000.0);  // Massive K for extreme loss
        let player = EloRating::new_with_rating(5.0);
        let opponent = EloRating::new_with_rating(3000.0);
        
        let new_rating = calc.update_rating(&player, &opponent, 0.0);
        
        assert!(new_rating.rating >= 1.0,
                "Rating should never go below 1, got {}", new_rating.rating);
    }
    
    #[test]
    fn test_very_high_ratings_extreme_difference() {
        let calc = EloCalculator::new();
        let elite = EloRating::new_with_rating(2500.0);
        let beginner = EloRating::new_with_rating(800.0);
        
        // Beginner somehow beats elite
        let new_rating = calc.update_rating(&beginner, &elite, 1.0);
        
        // Should still give large bonus for upset
        let delta = new_rating.rating - beginner.rating;
        assert!(delta > 30.0,
                "Extreme upset should give large bonus, got {}", delta);
    }
    
    // ==================== K-FACTOR TESTS ====================
    
    #[test]
    fn test_k_factor_32_standard() {
        let calc = EloCalculator::new();  // K=32
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Win as expected: ΔR = 32 × (1.0 - 0.5) = 16
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        let delta = new_rating.rating - player.rating;
        
        assert!((delta - 16.0).abs() < 0.1,
                "K=32 win should be +16, got {}", delta);
    }
    
    #[test]
    fn test_k_factor_64_aggressive() {
        let calc = EloCalculator::new_with_k_factor(64.0);
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Win as expected: ΔR = 64 × (1.0 - 0.5) = 32
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        let delta = new_rating.rating - player.rating;
        
        assert!((delta - 32.0).abs() < 0.1,
                "K=64 win should be +32, got {}", delta);
    }
    
    #[test]
    fn test_k_factor_16_conservative() {
        let calc = EloCalculator::new_with_k_factor(16.0);
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Win as expected: ΔR = 16 × (1.0 - 0.5) = 8
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        let delta = new_rating.rating - player.rating;
        
        assert!((delta - 8.0).abs() < 0.1,
                "K=16 win should be +8, got {}", delta);
    }
    
    // ==================== RATING CONSERVATION ====================
    
    #[test]
    fn test_rating_conservation_in_match() {
        let calc = EloCalculator::new();
        let player1 = EloRating::new_player();
        let player2 = EloRating::new_player();
        
        // Player 1 wins
        let p1_new = calc.update_rating(&player1, &player2, 1.0);
        let p2_new = calc.update_rating(&player2, &player1, 0.0);
        
        let p1_delta = p1_new.rating - player1.rating;
        let p2_delta = p2_new.rating - player2.rating;
        
        // In a zero-sum match, gains and losses should nearly cancel
        let total_change = p1_delta + p2_delta;
        assert!(total_change.abs() < 0.1,
                "Rating changes should sum to ~0, got {}", total_change);
    }
    
    #[test]
    fn test_performance_symmetry() {
        let calc = EloCalculator::new();
        let lower = EloRating::new_with_rating(1400.0);
        let higher = EloRating::new_with_rating(1600.0);
        
        // When lower rated player wins
        let lower_new = calc.update_rating(&lower, &higher, 1.0);
        // When higher rated player loses to lower
        let higher_new = calc.update_rating(&higher, &lower, 0.0);
        
        let lower_gain = lower_new.rating - lower.rating;
        let higher_loss = higher.rating - higher_new.rating;
        
        // Magnitudes should be similar (not exactly same due to rating diff)
        assert!((lower_gain - higher_loss).abs() < 5.0,
                "Upset impact should be symmetric");
    }
    
    // ==================== REGRESSION TESTS ====================
    
    #[test]
    fn test_nobody_gets_negative_rating_change() {
        let calc = EloCalculator::new();
        let player1 = EloRating::new_player();
        let player2 = EloRating::new_player();
        
        // Even in worst case (massive K factor + losing badly)
        let calc_extreme = EloCalculator::new_with_k_factor(1000.0);
        let new_rating = calc_extreme.update_rating(&player1, &player2, 0.0);
        
        // Rating should never decrease more than physically possible
        assert!(new_rating.rating > 0.0,
                "Rating should never be negative");
    }
    
    #[test]
    fn test_expected_score_is_always_0_to_1() {
        let calc = EloCalculator::new();
        
        // Test extreme rating differences
        let very_high = EloRating::new_with_rating(3000.0);
        let very_low = EloRating::new_with_rating(500.0);
        
        let expected = calc.expected_score(very_high.rating, very_low.rating);
        assert!(expected >= 0.0 && expected <= 1.0,
                "Expected score should be in [0, 1], got {}", expected);
        
        let expected_reverse = calc.expected_score(very_low.rating, very_high.rating);
        assert!(expected_reverse >= 0.0 && expected_reverse <= 1.0,
                "Expected score reverse should be in [0, 1], got {}", expected_reverse);
    }
}

#[cfg(test)]
mod elo_integration_tests {
    use crate::elo::{EloCalculator, EloRating};
    
    #[test]
    fn test_tournament_convergence() {
        // Simulate a tournament where skill levels should converge to rankings
        let calc = EloCalculator::new();
        
        let mut elite = EloRating::new_with_rating(1800.0);
        let mut strong = EloRating::new_with_rating(1600.0);
        let mut average = EloRating::new_player();  // 1500
        let mut weak = EloRating::new_with_rating(1400.0);
        
        // Simulate 30 matches with expected skill distribution
        for _ in 0..30 {
            // Elite usually beats everyone
            elite = calc.update_rating(&elite, &strong, 0.75);
            elite = calc.update_rating(&elite, &average, 0.85);
            elite = calc.update_rating(&elite, &weak, 0.95);
            
            // Strong beats most
            strong = calc.update_rating(&strong, &average, 0.65);
            strong = calc.update_rating(&strong, &weak, 0.80);
            strong = calc.update_rating(&strong, &elite, 0.25);
            
            // Average plays normally
            average = calc.update_rating(&average, &weak, 0.60);
            average = calc.update_rating(&average, &elite, 0.15);
            average = calc.update_rating(&average, &strong, 0.35);
            
            // Weak loses more
            weak = calc.update_rating(&weak, &elite, 0.05);
            weak = calc.update_rating(&weak, &strong, 0.20);
            weak = calc.update_rating(&weak, &average, 0.40);
        }
        
        // After convergence, rankings should be maintained
        assert!(elite.rating > strong.rating,
                "Elite ({:.0}) should beat strong ({:.0})",
                elite.rating, strong.rating);
        assert!(strong.rating > average.rating,
                "Strong ({:.0}) should beat average ({:.0})",
                strong.rating, average.rating);
        assert!(average.rating > weak.rating,
                "Average ({:.0}) should beat weak ({:.0})",
                average.rating, weak.rating);
    }
    
    #[test]
    fn test_rating_change_magnitude_consistency() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Test that rating changes scale linearly with performance
        let r_0_0 = calc.update_rating(&player, &opponent, 0.0);
        let r_0_25 = calc.update_rating(&player, &opponent, 0.25);
        let r_0_5 = calc.update_rating(&player, &opponent, 0.5);
        let r_0_75 = calc.update_rating(&player, &opponent, 0.75);
        let r_1_0 = calc.update_rating(&player, &opponent, 1.0);
        
        let delta_0 = r_0_0.rating - player.rating;
        let delta_25 = r_0_25.rating - player.rating;
        let delta_5 = r_0_5.rating - player.rating;
        let delta_75 = r_0_75.rating - player.rating;
        let delta_100 = r_1_0.rating - player.rating;
        
        // Deltas should increase monotonically
        assert!(delta_0 < delta_25, "0.0 should < 0.25");
        assert!(delta_25 < delta_5, "0.25 should < 0.5");
        assert!(delta_5 < delta_75, "0.5 should < 0.75");
        assert!(delta_75 < delta_100, "0.75 should < 1.0");
    }
}
