use super::rating::EloRating;

/// Pure ELO rating calculator
/// Uses standard ELO formula without rating deviation or volatility
pub struct EloCalculator {
    k_factor: f64,  // Standard: 32 for casual play
}

impl EloCalculator {
    pub fn new() -> Self {
        Self { k_factor: 32.0 }
    }
    
    pub fn new_with_k_factor(k_factor: f64) -> Self {
        Self { k_factor }
    }
    
    /// Calculate expected score for player against opponent
    /// E = 1 / (1 + 10^((R_opp - R_self)/400))
    pub fn expected_score(&self, player_rating: f64, opponent_rating: f64) -> f64 {
        let rating_diff = opponent_rating - player_rating;
        1.0 / (1.0 + 10.0_f64.powf(rating_diff / 400.0))
    }
    
    /// Update a single player's rating
    /// 
    /// Arguments:
    /// - player: Current rating
    /// - opponent: Opponent's rating
    /// - actual_performance: Actual score (0.0-1.0) from match results
    pub fn update_rating(
        &self,
        player: &EloRating,
        opponent: &EloRating,
        actual_performance: f64,
    ) -> EloRating {
        let expected = self.expected_score(player.rating, opponent.rating);
        let rating_change = self.k_factor * (actual_performance - expected);
        
        EloRating {
            rating: (player.rating + rating_change).max(1.0),  // Never go below 1
        }
    }
    
    /// Update multiple opponents in one call (average expected value)
    /// Useful for doubles where a player faces an "effective opponent"
    pub fn update_rating_multiple(
        &self,
        player: &EloRating,
        opponents: &[EloRating],
        actual_performance: f64,
    ) -> EloRating {
        if opponents.is_empty() {
            return *player;
        }
        
        // Average expected score across all opponents
        let avg_expected: f64 = opponents.iter()
            .map(|opp| self.expected_score(player.rating, opp.rating))
            .sum::<f64>() / opponents.len() as f64;
        
        let rating_change = self.k_factor * (actual_performance - avg_expected);
        
        EloRating {
            rating: (player.rating + rating_change).max(1.0),
        }
    }
}

impl Default for EloCalculator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_expected_score_equal_ratings() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        let expected = calc.expected_score(player.rating, opponent.rating);
        assert!((expected - 0.5).abs() < 0.001);
    }
    
    #[test]
    fn test_expected_score_higher_rated() {
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1600.0);
        let opponent = EloRating::new_with_rating(1500.0);
        
        let expected = calc.expected_score(player.rating, opponent.rating);
        // Expected should be > 0.5 for higher rated player
        assert!(expected > 0.5);
        // Roughly 0.64
        assert!((expected - 0.64).abs() < 0.02);
    }
    
    #[test]
    fn test_expected_score_lower_rated() {
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1400.0);
        let opponent = EloRating::new_with_rating(1500.0);
        
        let expected = calc.expected_score(player.rating, opponent.rating);
        // Expected should be < 0.5 for lower rated player
        assert!(expected < 0.5);
        // Roughly 0.36
        assert!((expected - 0.36).abs() < 0.02);
    }
    
    #[test]
    fn test_rating_update_win_as_expected() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Win (1.0 performance) as expected (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.1);
    }
    
    #[test]
    fn test_rating_update_loss_as_expected() {
        let calc = EloCalculator::new();
        let player = EloRating::new_player();
        let opponent = EloRating::new_player();
        
        // Loss (0.0 performance) as expected (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.1);
    }
    
    #[test]
    fn test_rating_update_upset_win() {
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1400.0);
        let opponent = EloRating::new_with_rating(1500.0);
        
        // Lower rated player wins (unexpected)
        // E ≈ 0.36, actual = 1.0
        // ΔR = 32 × (1.0 - 0.36) ≈ 20.5
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        assert!(new_rating.rating > player.rating + 20.0);
    }
    
    #[test]
    fn test_rating_update_expected_win() {
        let calc = EloCalculator::new();
        let player = EloRating::new_with_rating(1600.0);
        let opponent = EloRating::new_with_rating(1500.0);
        
        // Higher rated player wins (expected)
        // E ≈ 0.64, actual = 1.0
        // ΔR = 32 × (1.0 - 0.64) ≈ 11.5
        let new_rating = calc.update_rating(&player, &opponent, 1.0);
        assert!(new_rating.rating > player.rating + 11.0);
        assert!(new_rating.rating < player.rating + 12.0);
    }
    
    #[test]
    fn test_rating_update_draw() {
        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.1);
    }
    
    #[test]
    fn test_rating_never_below_one() {
        let calc = EloCalculator::new_with_k_factor(1000.0);  // Huge K
        let player = EloRating::new_with_rating(10.0);
        let opponent = EloRating::new_with_rating(2500.0);
        
        // Massive loss, but should never go below 1
        let new_rating = calc.update_rating(&player, &opponent, 0.0);
        assert!(new_rating.rating >= 1.0);
    }
}
