"""
Unit Tests for Result Charts

Tests result chart abstraction, hit location calculation, and PD auto mode.
"""
import pytest
from unittest.mock import Mock
from pydantic import ValidationError

from app.config.result_charts import (
    PlayOutcome,
    calculate_hit_location,
    ResultChart,
    ManualResultChart,
    PdAutoResultChart,
)
from app.models.game_models import ManualOutcomeSubmission


# ============================================================================
# HELPER FUNCTION TESTS
# ============================================================================

class TestCalculateHitLocation:
    """Test calculate_hit_location() helper function"""

    def test_groundball_returns_infield_position(self):
        """Test groundballs return infield positions"""
        outcome = PlayOutcome.GROUNDBALL_C

        # Run 100 times to test distribution
        locations = set()
        for _ in range(100):
            location = calculate_hit_location(outcome, 'R')
            locations.add(location)

        # Should only get infield positions
        valid_infield = {'1B', '2B', 'SS', '3B', 'P', 'C'}
        assert locations.issubset(valid_infield)

    def test_flyout_returns_outfield_position(self):
        """Test flyouts return outfield positions"""
        outcome = PlayOutcome.FLYOUT_B

        # Run 100 times to test distribution
        locations = set()
        for _ in range(100):
            location = calculate_hit_location(outcome, 'R')
            locations.add(location)

        # Should only get outfield positions
        valid_outfield = {'LF', 'CF', 'RF'}
        assert locations.issubset(valid_outfield)

    def test_returns_none_for_outcomes_not_requiring_location(self):
        """Test returns None for strikeouts, walks, etc."""
        assert calculate_hit_location(PlayOutcome.STRIKEOUT, 'R') is None
        assert calculate_hit_location(PlayOutcome.WALK, 'R') is None
        assert calculate_hit_location(PlayOutcome.HOMERUN, 'R') is None
        assert calculate_hit_location(PlayOutcome.SINGLE_1, 'R') is None


# ============================================================================
# PLAYOUTCOME HELPER TESTS
# ============================================================================

class TestPlayOutcomeRequiresHitLocation:
    """Test PlayOutcome.requires_hit_location() method"""

    def test_groundballs_require_location(self):
        """Test all groundball variants require location"""
        assert PlayOutcome.GROUNDBALL_A.requires_hit_location() is True
        assert PlayOutcome.GROUNDBALL_B.requires_hit_location() is True
        assert PlayOutcome.GROUNDBALL_C.requires_hit_location() is True

    def test_flyouts_require_location(self):
        """Test all flyout variants require location"""
        assert PlayOutcome.FLYOUT_A.requires_hit_location() is True
        assert PlayOutcome.FLYOUT_B.requires_hit_location() is True
        assert PlayOutcome.FLYOUT_C.requires_hit_location() is True

    def test_other_outcomes_do_not_require_location(self):
        """Test strikes, walks, hits don't require location"""
        assert PlayOutcome.STRIKEOUT.requires_hit_location() is False
        assert PlayOutcome.WALK.requires_hit_location() is False
        assert PlayOutcome.HOMERUN.requires_hit_location() is False
        assert PlayOutcome.SINGLE_1.requires_hit_location() is False
        assert PlayOutcome.DOUBLE_2.requires_hit_location() is False


# ============================================================================
# MANUAL RESULT CHART TESTS
# ============================================================================

class TestManualResultChart:
    """Test ManualResultChart (should not be used)"""

    def test_raises_not_implemented_error(self):
        """Test ManualResultChart.get_outcome() raises NotImplementedError"""
        chart = ManualResultChart()

        with pytest.raises(NotImplementedError) as exc_info:
            chart.get_outcome(
                roll=Mock(),
                state=Mock(),
                batter=Mock(),
                pitcher=Mock()
            )

        assert "manual mode" in str(exc_info.value).lower()
        assert "websocket" in str(exc_info.value).lower()


# ============================================================================
# PD AUTO RESULT CHART TESTS
# ============================================================================

class TestPdAutoResultChart:
    """Test PdAutoResultChart auto mode implementation"""

    def create_mock_player(self, name, card_type, ratings_data):
        """Helper to create mock player with card data"""
        player = Mock()
        player.name = name

        if card_type == 'batting':
            card = Mock()
            card.hand = 'R'
            card.ratings = {
                'R': Mock(**ratings_data),
                'L': Mock(**ratings_data)
            }
            player.batting_card = card
            player.pitching_card = None
        else:  # pitching
            card = Mock()
            card.hand = 'R'
            card.ratings = {
                'R': Mock(**ratings_data),
                'L': Mock(**ratings_data)
            }
            player.pitching_card = card
            player.batting_card = Mock(hand='R')

        return player

    def test_select_card_coin_flip_distribution(self):
        """Test _select_card() has ~50/50 distribution"""
        chart = PdAutoResultChart()

        # Create mock players with valid cards
        batter = self.create_mock_player('Batter', 'batting', {'homerun': 10.0})
        pitcher = self.create_mock_player('Pitcher', 'pitching', {'homerun': 5.0})

        # Run 1000 trials
        batting_count = 0
        for _ in range(1000):
            _, is_batting = chart._select_card(batter, pitcher)
            if is_batting:
                batting_count += 1

        # Should be approximately 50% (allow 10% variance)
        assert 400 <= batting_count <= 600

    def test_select_card_uses_correct_handedness(self):
        """Test _select_card() selects rating for correct handedness"""
        chart = PdAutoResultChart()

        # Create batter with batting card
        batter = self.create_mock_player('Batter', 'batting', {'homerun': 10.0})

        # Create pitcher with specific hand
        pitcher = Mock()
        pitcher.name = 'Pitcher'
        pitcher.pitching_card = Mock(hand='L')  # LHP
        pitcher.batting_card = None

        # Force batting card selection
        rating, is_batting = chart._select_card(batter, pitcher)

        # Should have selected rating vs LHP
        assert rating is not None

    def test_build_distribution_creates_cumulative_values(self):
        """Test _build_distribution() builds cumulative thresholds"""
        chart = PdAutoResultChart()

        # Create mock rating
        rating = Mock(
            homerun=5.0,
            triple=2.0,
            double_two=8.0,
            single_one=15.0,
            walk=10.0,
            strikeout=20.0,
            groundout_a=10.0,
            groundout_b=15.0,
            groundout_c=15.0,
            # Zero out others
            bp_homerun=0.0,
            double_three=0.0,
            single_two=0.0,
            bp_single=0.0,
            hbp=0.0,
            lineout=0.0,
            popout=0.0,
            flyout_a=0.0,
            flyout_bq=0.0,
            flyout_lf_b=0.0,
            flyout_rf_b=0.0,
            double_pull=0.0,
            single_center=0.0,
        )

        distribution = chart._build_distribution(rating, is_batting_card=True)

        # Should have entries
        assert len(distribution) > 0

        # Cumulative values should be increasing
        prev_threshold = 0
        for threshold, outcome in distribution:
            assert threshold > prev_threshold
            prev_threshold = threshold

        # Total should be ~100% (within rounding)
        assert distribution[-1][0] >= 95.0  # Allow some missing outcomes

    def test_select_outcome_uses_distribution(self):
        """Test _select_outcome() selects based on cumulative distribution"""
        chart = PdAutoResultChart()

        # Simple distribution: 0-50 = STRIKEOUT, 50-100 = WALK
        distribution = [
            (50.0, PlayOutcome.STRIKEOUT),
            (100.0, PlayOutcome.WALK),
        ]

        # Run 1000 trials
        results = {'strikeout': 0, 'walk': 0}
        for _ in range(1000):
            outcome = chart._select_outcome(distribution)
            if outcome == PlayOutcome.STRIKEOUT:
                results['strikeout'] += 1
            elif outcome == PlayOutcome.WALK:
                results['walk'] += 1

        # Should be approximately 50/50 (allow 10% variance)
        assert 400 <= results['strikeout'] <= 600
        assert 400 <= results['walk'] <= 600

    def test_get_outcome_returns_tuple(self):
        """Test get_outcome() returns (outcome, location) tuple"""
        chart = PdAutoResultChart()

        batter = self.create_mock_player('Batter', 'batting', {
            'homerun': 100.0,  # Always HR
            'triple': 0.0,
            'double_two': 0.0,
            'single_one': 0.0,
            'walk': 0.0,
            'strikeout': 0.0,
            'groundout_a': 0.0,
            'groundout_b': 0.0,
            'groundout_c': 0.0,
            'bp_homerun': 0.0,
            'double_three': 0.0,
            'single_two': 0.0,
            'bp_single': 0.0,
            'hbp': 0.0,
            'lineout': 0.0,
            'popout': 0.0,
            'flyout_a': 0.0,
            'flyout_bq': 0.0,
            'flyout_lf_b': 0.0,
            'flyout_rf_b': 0.0,
            'double_pull': 0.0,
            'single_center': 0.0,
        })
        pitcher = self.create_mock_player('Pitcher', 'pitching', {
            'homerun': 100.0, 'triple': 0.0, 'double_two': 0.0, 'single_one': 0.0,
            'walk': 0.0, 'strikeout': 0.0, 'groundout_a': 0.0, 'groundout_b': 0.0,
            'groundout_c': 0.0, 'bp_homerun': 0.0, 'double_three': 0.0, 'single_two': 0.0,
            'bp_single': 0.0, 'hbp': 0.0, 'double_pull': 0.0, 'single_center': 0.0,
            'double_cf': 0.0, 'flyout_lf_b': 0.0, 'flyout_cf_b': 0.0, 'flyout_rf_b': 0.0
        })

        outcome, location = chart.get_outcome(
            roll=Mock(),
            state=Mock(),
            batter=batter,
            pitcher=pitcher
        )

        assert isinstance(outcome, PlayOutcome)
        assert outcome == PlayOutcome.HOMERUN
        assert location is None  # HRs don't need location

    def test_get_outcome_calculates_location_for_groundballs(self):
        """Test get_outcome() calculates location for groundballs"""
        chart = PdAutoResultChart()

        batter = self.create_mock_player('Batter', 'batting', {
            'groundout_a': 100.0,  # Always groundball
            'homerun': 0.0,
            'triple': 0.0,
            'double_two': 0.0,
            'single_one': 0.0,
            'walk': 0.0,
            'strikeout': 0.0,
            'groundout_b': 0.0,
            'groundout_c': 0.0,
            'bp_homerun': 0.0,
            'double_three': 0.0,
            'single_two': 0.0,
            'bp_single': 0.0,
            'hbp': 0.0,
            'lineout': 0.0,
            'popout': 0.0,
            'flyout_a': 0.0,
            'flyout_bq': 0.0,
            'flyout_lf_b': 0.0,
            'flyout_rf_b': 0.0,
            'double_pull': 0.0,
            'single_center': 0.0,
        })
        pitcher = self.create_mock_player('Pitcher', 'pitching', {
            'homerun': 0.0, 'triple': 0.0, 'double_two': 0.0, 'single_one': 0.0,
            'walk': 0.0, 'strikeout': 0.0, 'groundout_a': 100.0, 'groundout_b': 0.0,
            'groundout_c': 0.0, 'bp_homerun': 0.0, 'double_three': 0.0, 'single_two': 0.0,
            'bp_single': 0.0, 'hbp': 0.0, 'double_pull': 0.0, 'single_center': 0.0,
            'double_cf': 0.0, 'flyout_lf_b': 0.0, 'flyout_cf_b': 0.0, 'flyout_rf_b': 0.0
        })

        outcome, location = chart.get_outcome(
            roll=Mock(),
            state=Mock(),
            batter=batter,
            pitcher=pitcher
        )

        assert outcome == PlayOutcome.GROUNDBALL_A
        assert location is not None
        assert location in ['1B', '2B', 'SS', '3B', 'P', 'C']

    def test_get_outcome_raises_error_with_no_cards(self):
        """Test get_outcome() raises error if both players missing cards"""
        chart = PdAutoResultChart()

        batter = Mock(name='Batter', batting_card=None, pitching_card=None)
        pitcher = Mock(name='Pitcher', batting_card=None, pitching_card=None)

        with pytest.raises(ValueError) as exc_info:
            chart.get_outcome(
                roll=Mock(),
                state=Mock(),
                batter=batter,
                pitcher=pitcher
            )

        assert "cannot auto-resolve" in str(exc_info.value).lower()
        assert "missing card data" in str(exc_info.value).lower()

    def test_outcome_distribution_matches_ratings(self):
        """Test outcome distribution roughly matches rating percentages"""
        chart = PdAutoResultChart()

        # Create player with known distribution
        batter = self.create_mock_player('Batter', 'batting', {
            'strikeout': 50.0,
            'walk': 50.0,
            'homerun': 0.0,
            'triple': 0.0,
            'double_two': 0.0,
            'single_one': 0.0,
            'groundout_a': 0.0,
            'groundout_b': 0.0,
            'groundout_c': 0.0,
            'bp_homerun': 0.0,
            'double_three': 0.0,
            'single_two': 0.0,
            'bp_single': 0.0,
            'hbp': 0.0,
            'lineout': 0.0,
            'popout': 0.0,
            'flyout_a': 0.0,
            'flyout_bq': 0.0,
            'flyout_lf_b': 0.0,
            'flyout_rf_b': 0.0,
            'double_pull': 0.0,
            'single_center': 0.0,
        })
        pitcher = self.create_mock_player('Pitcher', 'pitching', {'homerun': 0.0})

        # Force batting card every time for consistent test
        original_select = chart._select_card
        chart._select_card = lambda b, p: (batter.batting_card.ratings['R'], True)

        try:
            # Run 1000 trials
            outcomes = {'strikeout': 0, 'walk': 0}
            for _ in range(1000):
                outcome, _ = chart.get_outcome(
                    roll=Mock(),
                    state=Mock(),
                    batter=batter,
                    pitcher=pitcher
                )
                if outcome == PlayOutcome.STRIKEOUT:
                    outcomes['strikeout'] += 1
                elif outcome == PlayOutcome.WALK:
                    outcomes['walk'] += 1

            # Should be approximately 50/50 (allow 10% variance)
            assert 400 <= outcomes['strikeout'] <= 600
            assert 400 <= outcomes['walk'] <= 600
        finally:
            chart._select_card = original_select


# ============================================================================
# MANUAL OUTCOME SUBMISSION TESTS
# ============================================================================

class TestManualOutcomeSubmission:
    """Test ManualOutcomeSubmission model validation"""

    def test_valid_outcome_with_location(self):
        """Test valid submission with outcome and location"""
        submission = ManualOutcomeSubmission(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='SS'
        )

        assert submission.outcome == PlayOutcome.GROUNDBALL_C
        assert submission.hit_location == 'SS'

    def test_valid_outcome_without_location(self):
        """Test valid submission without location"""
        submission = ManualOutcomeSubmission(
            outcome=PlayOutcome.STRIKEOUT
        )

        assert submission.outcome == PlayOutcome.STRIKEOUT
        assert submission.hit_location is None

    def test_valid_outcome_from_string(self):
        """Test Pydantic converts string to enum automatically"""
        submission = ManualOutcomeSubmission(
            outcome='groundball_c',
            hit_location='SS'
        )

        # Pydantic converts string to enum
        assert submission.outcome == PlayOutcome.GROUNDBALL_C
        assert submission.hit_location == 'SS'

    def test_invalid_outcome_raises_error(self):
        """Test invalid outcome value raises ValidationError"""
        with pytest.raises(ValidationError) as exc_info:
            ManualOutcomeSubmission(outcome='invalid_outcome')

        assert 'outcome' in str(exc_info.value).lower()

    def test_invalid_hit_location_raises_error(self):
        """Test invalid hit location raises ValidationError"""
        with pytest.raises(ValidationError) as exc_info:
            ManualOutcomeSubmission(
                outcome=PlayOutcome.GROUNDBALL_C,
                hit_location='INVALID'
            )

        assert 'hit_location' in str(exc_info.value).lower()

    def test_all_valid_hit_locations_accepted(self):
        """Test all valid hit locations are accepted"""
        valid_locations = ['1B', '2B', 'SS', '3B', 'LF', 'CF', 'RF', 'P', 'C']

        for location in valid_locations:
            submission = ManualOutcomeSubmission(
                outcome=PlayOutcome.GROUNDBALL_C,
                hit_location=location
            )
            assert submission.hit_location == location

    def test_none_hit_location_is_valid(self):
        """Test None for hit_location is valid"""
        submission = ManualOutcomeSubmission(
            outcome=PlayOutcome.STRIKEOUT,
            hit_location=None
        )

        assert submission.hit_location is None
