#!/usr/bin/env python
# -*-coding:utf-8 -*-
'''
@File    :   test_optimization_logic.py  
@Time    :   2024/12/25 10:00:00
@Author  :   Test Suite
@Version :   1.0
@Desc    :   Comprehensive tests for pick optimization logic
'''

import pytest
from datetime import datetime
from unittest.mock import patch
from src.confpickem.confidence_pickem_sim import ConfidencePickEmSimulator, Game, Player

@pytest.fixture
def basic_simulator():
    """Basic simulator with simple, predictable games"""
    sim = ConfidencePickEmSimulator(num_sims=100)  # Reduced for speed with new confidence logic
    
    # Create games with clear favorites for testing
    sim.games = [
        Game(  # Very clear favorite
            home_team="SF", away_team="ARI",
            vegas_win_prob=0.85, crowd_home_pick_pct=0.90,
            crowd_home_confidence=14.0, crowd_away_confidence=2.0,
            week=1, kickoff_time=datetime(2024, 9, 8, 13, 0)
        ),
        Game(  # Moderate favorite  
            home_team="KC", away_team="DEN",
            vegas_win_prob=0.65, crowd_home_pick_pct=0.70,
            crowd_home_confidence=10.0, crowd_away_confidence=6.0,
            week=1, kickoff_time=datetime(2024, 9, 8, 16, 25)
        ),
        Game(  # Toss-up game
            home_team="BAL", away_team="CIN",
            vegas_win_prob=0.52, crowd_home_pick_pct=0.48,
            crowd_home_confidence=8.0, crowd_away_confidence=8.5,
            week=1, kickoff_time=datetime(2024, 9, 8, 20, 20)
        )
    ]
    
    sim.players = [
        Player("Expert", skill_level=0.9, crowd_following=0.1, confidence_following=0.2),
        Player("Average", skill_level=0.5, crowd_following=0.5, confidence_following=0.5)
    ]
    
    return sim

class TestOptimizationBasics:
    """Test basic optimization functionality"""
    
    def test_optimize_picks_requires_player_name(self, basic_simulator):
        """Test that optimize_picks requires a player_name argument"""
        with pytest.raises(TypeError):
            basic_simulator.optimize_picks()  # Missing required player_name
    
    def test_optimize_picks_returns_valid_confidence_points(self, basic_simulator):
        """Test that optimization returns valid confidence point assignments"""
        optimal = basic_simulator.optimize_picks("Expert")
        
        # Should assign all confidence points from 1 to num_games
        expected_points = set(range(1, len(basic_simulator.games) + 1))
        actual_points = set(optimal.values())
        
        assert actual_points == expected_points, f"Expected {expected_points}, got {actual_points}"
        
        # Should pick exactly one team per game
        assert len(optimal) == len(basic_simulator.games)
        
        # All teams should be valid
        valid_teams = set()
        for game in basic_simulator.games:
            valid_teams.add(game.home_team)
            valid_teams.add(game.away_team)
        assert all(team in valid_teams for team in optimal.keys())
    
    def test_optimize_respects_fixed_picks(self, basic_simulator):
        """Test that optimization respects fixed pick constraints"""
        fixed_picks = {"Expert": {"SF": 3, "KC": 1}}
        optimal = basic_simulator.optimize_picks("Expert", fixed_picks)
        
        # Fixed picks should be preserved
        assert optimal["SF"] == 3
        assert optimal["KC"] == 1
        
        # Remaining points should still be valid
        all_points = set(range(1, len(basic_simulator.games) + 1))
        used_points = set(optimal.values())
        assert used_points == all_points

class TestOptimizationBehavior:
    """Test optimization behavior and decision-making"""
    
    def test_optimization_picks_favorites_with_high_confidence(self, basic_simulator):
        """Test that optimization tends to put high confidence on clear favorites"""
        # Suppress print statements during test
        with patch('builtins.print'):
            optimal = basic_simulator.optimize_picks("Expert", confidence_range=3)
        
        # SF is the clearest favorite (85% win prob), should get high confidence
        sf_confidence = optimal.get("SF", 0)
        
        # Should be above average confidence (which would be 2 for 3 games)
        assert sf_confidence >= 2, f"Expected SF to get high confidence, got {sf_confidence}"
    
    def test_optimization_consistency(self, basic_simulator):
        """Test that optimization gives consistent results with same inputs"""
        with patch('builtins.print'):
            optimal1 = basic_simulator.optimize_picks("Expert", confidence_range=2)
            optimal2 = basic_simulator.optimize_picks("Expert", confidence_range=2)
        
        # Results should be identical (same random seed for simulations)
        assert optimal1 == optimal2
    
    def test_different_players_get_different_picks(self, basic_simulator):
        """Test that different player types can produce different optimal picks"""
        with patch('builtins.print'):
            expert_picks = basic_simulator.optimize_picks("Expert", confidence_range=2)
            average_picks = basic_simulator.optimize_picks("Average", confidence_range=2)
        
        # At minimum, both should be valid
        assert len(expert_picks) == len(basic_simulator.games)
        assert len(average_picks) == len(basic_simulator.games)

class TestOptimizationEdgeCases:
    """Test edge cases and error conditions"""
    
    def test_optimize_unknown_player(self, basic_simulator):
        """Test optimization with unknown player name"""
        with pytest.raises(ValueError, match="Unknown player"):
            basic_simulator.optimize_picks("Unknown Player")
    
    def test_optimize_with_invalid_fixed_picks(self, basic_simulator):
        """Test optimization with invalid fixed pick constraints"""
        # Invalid team name
        with pytest.raises(ValueError):
            basic_simulator.optimize_picks("Expert", {"Expert": {"INVALID": 1}})
        
        # Invalid confidence points
        with pytest.raises(ValueError):
            basic_simulator.optimize_picks("Expert", {"Expert": {"SF": 10}})  # Only 3 games
        
        # Duplicate confidence points
        with pytest.raises(ValueError):
            basic_simulator.optimize_picks("Expert", {"Expert": {"SF": 1, "KC": 1}})
    
    def test_optimize_all_games_fixed(self, basic_simulator):
        """Test optimization when all games have fixed picks"""
        fixed_picks = {"Expert": {"SF": 3, "KC": 2, "BAL": 1}}
        optimal = basic_simulator.optimize_picks("Expert", fixed_picks)
        
        # Should return exactly the fixed picks
        assert optimal == fixed_picks["Expert"]
    
    def test_confidence_range_behavior(self, basic_simulator):
        """Test how confidence_range parameter affects optimization"""
        with patch('builtins.print'):
            # Very limited range
            optimal_narrow = basic_simulator.optimize_picks("Expert", confidence_range=1)
            # Broader range  
            optimal_broad = basic_simulator.optimize_picks("Expert", confidence_range=3)
        
        # Both should be valid
        assert len(optimal_narrow) == len(basic_simulator.games)
        assert len(optimal_broad) == len(basic_simulator.games)
        
        # Confidence points should be distributed correctly
        assert set(optimal_narrow.values()) == set(range(1, len(basic_simulator.games) + 1))
        assert set(optimal_broad.values()) == set(range(1, len(basic_simulator.games) + 1))

class TestOptimizationPerformanceComparison:
    """Test optimization performance against baselines"""
    
    def test_optimization_beats_baseline_with_larger_pool(self):
        """Test optimization with realistic player pool size (5 players)"""
        sim = ConfidencePickEmSimulator(num_sims=500)  # Balance between speed and accuracy
        
        # Create realistic games
        sim.games = [
            Game("KC", "LV", 0.75, 0.80, 12.5, 3.5, 1, datetime(2024, 9, 8, 13, 0)),
            Game("SF", "ARI", 0.70, 0.75, 11.8, 4.2, 1, datetime(2024, 9, 8, 16, 0)),  
            Game("BUF", "MIA", 0.65, 0.70, 10.2, 5.8, 1, datetime(2024, 9, 8, 20, 0)),
            Game("PHI", "NYG", 0.60, 0.65, 9.5, 6.5, 1, datetime(2024, 9, 9, 13, 0)),
            Game("BAL", "CIN", 0.52, 0.48, 8.0, 8.0, 1, datetime(2024, 9, 9, 16, 0))
        ]
        
        # Create 5-player pool with different skill levels
        sim.players = [
            Player("Expert", skill_level=0.85, crowd_following=0.2, confidence_following=0.3),
            Player("Above Avg", skill_level=0.65, crowd_following=0.4, confidence_following=0.5),
            Player("Average", skill_level=0.50, crowd_following=0.5, confidence_following=0.5),
            Player("Below Avg", skill_level=0.35, crowd_following=0.6, confidence_following=0.7),
            Player("Novice", skill_level=0.20, crowd_following=0.8, confidence_following=0.8)
        ]
        
        # Test Expert player optimization
        with patch('builtins.print'):
            optimal_picks = sim.optimize_picks("Expert", confidence_range=2)  # Faster
        
        # Compare optimized vs random performance
        optimal_fixed = {"Expert": optimal_picks}
        optimal_stats = sim.simulate_all(optimal_fixed)
        random_stats = sim.simulate_all({})  # No constraints
        
        optimal_win_pct = optimal_stats['win_pct']['Expert']
        random_win_pct = random_stats['win_pct']['Expert']
        baseline_pct = 1.0 / len(sim.players)  # 20% for 5 players
        
        print(f"\n5-Player Pool Results:")
        print(f"  Baseline (20%): {baseline_pct:.3f}")
        print(f"  Random Expert: {random_win_pct:.3f}")
        print(f"  Optimized Expert: {optimal_win_pct:.3f}")
        print(f"  Improvement: {optimal_win_pct - random_win_pct:.3f}")
        
        # Expert should outperform baseline (relaxed margin due to simulation noise)
        assert optimal_win_pct >= baseline_pct - 0.05, \
            f"Expert with optimization ({optimal_win_pct:.3f}) should beat baseline ({baseline_pct:.3f}) by margin"
        
        # Expert should generally outperform their random performance
        # (Though we allow some tolerance due to simulation variance)
        assert optimal_win_pct >= random_win_pct - 0.05, \
            f"Optimization shouldn't significantly hurt performance: {optimal_win_pct:.3f} vs {random_win_pct:.3f}"
    
    def test_skill_vs_optimization_interaction(self):
        """Test how player skill interacts with optimization benefits"""
        sim = ConfidencePickEmSimulator(num_sims=500)  # Increased to reduce variance
        
        # Simple 3-game setup
        sim.games = [
            Game("FAV1", "DOG1", 0.80, 0.85, 13.0, 3.0, 1, datetime(2024, 9, 8, 13, 0)),
            Game("FAV2", "DOG2", 0.65, 0.70, 10.0, 6.0, 1, datetime(2024, 9, 8, 16, 0)),
            Game("EVEN1", "EVEN2", 0.50, 0.50, 8.0, 8.0, 1, datetime(2024, 9, 8, 20, 0))
        ]
        
        # Test players with different skill levels
        test_players = [
            Player("High Skill", skill_level=0.9, crowd_following=0.1, confidence_following=0.2),
            Player("Low Skill", skill_level=0.3, crowd_following=0.7, confidence_following=0.8)
        ]
        
        for player in test_players:
            sim.players = [player, Player("Opponent", 0.5, 0.5, 0.5)]  # 2-player pool
            
            with patch('builtins.print'):
                optimal_picks = sim.optimize_picks(player.name, confidence_range=2)
            
            optimal_fixed = {player.name: optimal_picks}
            optimal_stats = sim.simulate_all(optimal_fixed)
            random_stats = sim.simulate_all({})
            
            optimal_win_pct = optimal_stats['win_pct'][player.name]
            random_win_pct = random_stats['win_pct'][player.name]
            baseline = 0.5  # 2-player pool
            
            print(f"\n{player.name} (skill={player.skill_level}):")
            print(f"  Random: {random_win_pct:.3f}")
            print(f"  Optimized: {optimal_win_pct:.3f}")
            print(f"  Improvement: {optimal_win_pct - random_win_pct:.3f}")
            
            # Both players should benefit from optimization relative to baseline
            assert optimal_win_pct >= baseline - 0.1, \
                f"{player.name} optimization should be near baseline: {optimal_win_pct:.3f} vs {baseline:.3f}"

class TestOptimizationIntegration:
    """Test optimization integration with other simulator methods"""
    
    def test_optimal_picks_improve_win_probability(self, basic_simulator):
        """Test that optimized picks improve win probability vs random picks"""
        # Get optimized picks for Expert player
        with patch('builtins.print'):
            optimal_picks = basic_simulator.optimize_picks("Expert")
        
        # Simulate with optimized picks (multiple runs for statistical significance)
        optimal_win_rates = []
        random_win_rates = []
        
        # Run multiple simulations to reduce random variance
        for _ in range(5):
            # Test optimized picks
            optimal_fixed = {"Expert": optimal_picks}
            optimal_stats = basic_simulator.simulate_all(optimal_fixed)
            optimal_win_rates.append(optimal_stats['win_pct']['Expert'])
            
            # Test random picks (no fixed picks - let simulator choose randomly)
            random_stats = basic_simulator.simulate_all({})
            random_win_rates.append(random_stats['win_pct']['Expert'])
        
        # Calculate average win rates
        avg_optimal_win_rate = sum(optimal_win_rates) / len(optimal_win_rates)
        avg_random_win_rate = sum(random_win_rates) / len(random_win_rates)
        
        # Theoretical baseline: 1/num_players for completely random performance
        theoretical_baseline = 1.0 / len(basic_simulator.players)
        
        print(f"\nWin Rate Comparison:")
        print(f"  Optimized picks: {avg_optimal_win_rate:.3f}")
        print(f"  Random picks: {avg_random_win_rate:.3f}")
        print(f"  Theoretical baseline: {theoretical_baseline:.3f}")
        
        # Test 1: Optimized should not be dramatically below baseline
        # Note: With only 5 simulation runs, variance is high. We use a generous tolerance
        # to avoid flaky tests while still catching catastrophic optimizer failures.
        assert avg_optimal_win_rate >= theoretical_baseline - 0.15, \
            f"Optimized picks ({avg_optimal_win_rate:.3f}) should be near or above baseline ({theoretical_baseline:.3f})"

        # Test 2: Optimized should generally outperform random picks for skilled player
        # (Allow generous tolerance since this involves significant randomness with few runs)
        improvement_margin = avg_optimal_win_rate - avg_random_win_rate
        print(f"  Improvement margin: {improvement_margin:.3f}")

        # For a skilled player (Expert has skill_level=0.9), optimized picks should
        # generally perform better than random, though we allow negative margin due to randomness
        assert improvement_margin >= -0.15, \
            f"Optimized picks shouldn't be significantly worse than random (margin: {improvement_margin:.3f})"
    
    def test_game_importance_with_optimized_picks(self, basic_simulator):
        """Test game importance analysis using optimized picks"""
        with patch('builtins.print'):
            optimal_picks = basic_simulator.optimize_picks("Expert")
        
        # Create picks dataframe with optimal picks
        optimal_fixed = {"Expert": optimal_picks}
        picks_df = basic_simulator.simulate_picks(optimal_fixed)
        
        # Analyze game importance
        importance_df = basic_simulator.assess_game_importance("Expert", picks_df, optimal_fixed)
        
        # Should have one row per game
        assert len(importance_df) == len(basic_simulator.games)
        
        # Should have proper columns
        expected_cols = ['game', 'points_bid', 'pick', 'win_probability', 'loss_probability', 
                        'win_delta', 'loss_delta', 'total_impact', 'is_fixed']
        assert all(col in importance_df.columns for col in expected_cols)
        
        # Games with higher confidence should generally have higher impact
        high_conf_games = importance_df[importance_df['points_bid'] >= 2]
        low_conf_games = importance_df[importance_df['points_bid'] == 1]
        
        if len(high_conf_games) > 0 and len(low_conf_games) > 0:
            avg_high_impact = high_conf_games['total_impact'].abs().mean()
            avg_low_impact = low_conf_games['total_impact'].abs().mean()
            # This relationship should generally hold (very relaxed due to simulation variance)
            # In CI environments or with small sample sizes, this can be noisy
            assert avg_high_impact >= avg_low_impact * 0.3, \
                "High confidence games should generally have higher impact"

class TestHillClimbingOptimization:
    """Test hill climbing optimization method"""

    def test_hill_climb_returns_valid_picks(self, basic_simulator):
        """Test that hill climbing returns valid confidence point assignments"""
        with patch('builtins.print'):
            optimal, summary_stats = basic_simulator.optimize_picks_hill_climb(
                "Expert", iterations=50, restarts=2
            )

        # Should assign all confidence points from 1 to num_games
        expected_points = set(range(1, len(basic_simulator.games) + 1))
        actual_points = set(optimal.values())

        assert actual_points == expected_points, f"Expected {expected_points}, got {actual_points}"

        # Should pick exactly one team per game
        assert len(optimal) == len(basic_simulator.games)

        # Summary stats should be a DataFrame
        assert hasattr(summary_stats, 'shape'), "Should return summary statistics DataFrame"

    def test_hill_climb_respects_fixed_picks(self, basic_simulator):
        """Test that hill climbing respects fixed pick constraints"""
        fixed_picks = {"Expert": {"SF": 3, "KC": 1}}

        with patch('builtins.print'):
            optimal, _ = basic_simulator.optimize_picks_hill_climb(
                "Expert", fixed_picks=fixed_picks, iterations=50, restarts=2
            )

        # Fixed picks should be preserved
        assert optimal["SF"] == 3
        assert optimal["KC"] == 1

        # Remaining points should still be valid
        all_points = set(range(1, len(basic_simulator.games) + 1))
        used_points = set(optimal.values())
        assert used_points == all_points

    def test_hill_climb_vs_greedy_comparison(self, basic_simulator):
        """Test that hill climbing can find solutions at least as good as greedy"""
        with patch('builtins.print'):
            greedy_picks = basic_simulator.optimize_picks("Expert", confidence_range=3)
            hill_picks, _ = basic_simulator.optimize_picks_hill_climb(
                "Expert", iterations=100, restarts=3
            )

        # Evaluate both solutions
        greedy_fixed = {"Expert": greedy_picks}
        hill_fixed = {"Expert": hill_picks}

        greedy_stats = basic_simulator.simulate_all(greedy_fixed)
        hill_stats = basic_simulator.simulate_all(hill_fixed)

        greedy_win_pct = greedy_stats['win_pct']['Expert']
        hill_win_pct = hill_stats['win_pct']['Expert']

        print(f"\nGreedy win %: {greedy_win_pct:.3f}")
        print(f"Hill climb win %: {hill_win_pct:.3f}")

        # Hill climbing should be at least competitive with greedy (allow variance)
        # Note: Due to stochastic nature with small iteration counts (100 iters, 3 restarts),
        # we allow hill climb to be somewhat worse. In practice, with more iterations,
        # hill climbing typically matches or exceeds greedy.
        assert hill_win_pct >= greedy_win_pct - 0.2, \
            f"Hill climbing ({hill_win_pct:.3f}) should be competitive with greedy ({greedy_win_pct:.3f})"

    def test_hill_climb_with_completed_games(self):
        """Test hill climbing with some completed games (mid-week scenario)"""
        sim = ConfidencePickEmSimulator(num_sims=100)

        sim.games = [
            Game("SF", "ARI", 0.85, 0.90, 14.0, 2.0, 1,
                 datetime(2024, 9, 8, 13, 0), actual_outcome=True),  # Completed
            Game("KC", "DEN", 0.65, 0.70, 10.0, 6.0, 1,
                 datetime(2024, 9, 8, 16, 25)),  # Not completed
            Game("BAL", "CIN", 0.52, 0.48, 8.0, 8.5, 1,
                 datetime(2024, 9, 8, 20, 20))  # Not completed
        ]

        sim.players = [
            Player("Expert", skill_level=0.9, crowd_following=0.1, confidence_following=0.2)
        ]

        # First game already picked with confidence 3
        available_points = {1, 2}  # Only 2 points left for 2 remaining games

        with patch('builtins.print'):
            optimal, _ = sim.optimize_picks_hill_climb(
                "Expert", iterations=50, restarts=2, available_points=available_points
            )

        # Should only pick for incomplete games
        assert len(optimal) == 2
        assert set(optimal.values()) == {1, 2}

    def test_hill_climb_returns_summary_statistics(self):
        """Test that hill climbing returns summary statistics for top combinations"""
        sim = ConfidencePickEmSimulator(num_sims=200)

        sim.games = [
            Game("SF", "ARI", 0.85, 0.90, 14.0, 2.0, 1,
                 datetime(2024, 9, 8, 13, 0)),
            Game("KC", "DEN", 0.65, 0.70, 10.0, 6.0, 1,
                 datetime(2024, 9, 8, 16, 25)),
            Game("BAL", "CIN", 0.52, 0.48, 8.0, 8.5, 1,
                 datetime(2024, 9, 8, 20, 20)),
            Game("BUF", "MIA", 0.58, 0.55, 9.0, 7.0, 1,
                 datetime(2024, 9, 9, 13, 0))
        ]

        sim.players = [
            Player("TestPlayer", skill_level=0.7, crowd_following=0.3, confidence_following=0.4),
            Player("Opponent1", skill_level=0.5, crowd_following=0.5, confidence_following=0.5)
        ]

        with patch('builtins.print'):
            optimal_picks, summary_stats = sim.optimize_picks_hill_climb(
                "TestPlayer", iterations=50, restarts=3, top_n=30
            )

        # Verify return types
        assert isinstance(optimal_picks, dict), "First return value should be dict of picks"
        assert hasattr(summary_stats, 'shape'), "Second return value should be DataFrame"

        # Verify picks are valid
        assert len(optimal_picks) == 4, "Should have picks for all 4 games"
        assert set(optimal_picks.values()) == {1, 2, 3, 4}, "Should use each confidence level once"

        # Verify summary stats structure
        required_columns = {'team', 'frequency', 'appearances', 'avg_confidence',
                          'min_confidence', 'max_confidence'}
        assert required_columns.issubset(set(summary_stats.columns)), \
            f"Summary stats missing columns. Has: {summary_stats.columns.tolist()}"

        # Verify summary stats content
        assert len(summary_stats) > 0, "Summary stats should have at least one team"
        assert all(0 <= summary_stats['frequency']) and all(summary_stats['frequency'] <= 1), \
            "Frequencies should be between 0 and 1"
        assert all(summary_stats['appearances'] > 0), "All teams should appear at least once"
        assert all(1 <= summary_stats['avg_confidence']) and all(summary_stats['avg_confidence'] <= 4), \
            "Average confidence should be between 1 and number of games"

        # Verify the optimal picks appear in the summary stats
        for team in optimal_picks.keys():
            assert team in summary_stats['team'].values, \
                f"Team {team} from optimal picks should appear in summary stats"

class TestOptimizationPerformance:
    """Test optimization performance characteristics"""

    def test_optimization_runtime_reasonable(self, basic_simulator):
        """Test that optimization completes in reasonable time"""
        import time

        with patch('builtins.print'):
            start_time = time.time()
            basic_simulator.optimize_picks("Expert", confidence_range=2)
            end_time = time.time()

        # Should complete in under 30 seconds for basic case
        runtime = end_time - start_time
        assert runtime < 30, f"Optimization took too long: {runtime:.2f}s"
    
    def test_optimization_with_larger_game_set(self):
        """Test optimization behavior with more realistic game count"""
        sim = ConfidencePickEmSimulator(num_sims=500)  # Fewer sims for speed
        
        # Create 16 games (typical NFL week)
        games = []
        for i in range(16):
            games.append(Game(
                home_team=f"H{i}", away_team=f"A{i}",
                vegas_win_prob=0.5 + (i % 10) * 0.04,  # Vary win probabilities
                crowd_home_pick_pct=0.45 + (i % 8) * 0.06,
                crowd_home_confidence=8 + (i % 5),
                crowd_away_confidence=8 - (i % 5),
                week=1, kickoff_time=datetime(2024, 9, 8, 13, 0)
            ))
        
        sim.games = games
        sim.players = [Player("Test", skill_level=0.7, crowd_following=0.3, confidence_following=0.4)]
        
        with patch('builtins.print'):
            optimal = sim.optimize_picks("Test", confidence_range=2)  # Limited range for speed
        
        # Should still produce valid results
        assert len(optimal) == 16
        assert set(optimal.values()) == set(range(1, 17))