"""
Unit Tests for Game Validators

Tests rule validation, lineup validation, and game state checks.
"""
import pytest
from uuid import uuid4
from dataclasses import dataclass

from app.core.validators import GameValidator, ValidationError
from app.models.game_models import GameState, DefensiveDecision, OffensiveDecision, LineupPlayerState


# Mock LineupPlayerState for lineup validation tests
@dataclass
class MockLineupPlayer:
    """Mock lineup player for testing"""
    id: int
    position: str
    is_active: bool
    batting_order: int


class TestGameStateValidation:
    """Test game state validation"""

    def test_validate_game_active_success(self):
        """Test validating active game"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            status="active"
        )

        # Should not raise
        validator.validate_game_active(state)

    def test_validate_game_active_fails_pending(self):
        """Test validating pending game fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            status="pending"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_game_active(state)

        assert "not active" in str(exc_info.value).lower()
        assert "pending" in str(exc_info.value)

    def test_validate_game_active_fails_completed(self):
        """Test validating completed game fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            status="completed"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_game_active(state)

        assert "not active" in str(exc_info.value).lower()


class TestOutsValidation:
    """Test outs validation"""

    def test_validate_outs_valid_range(self):
        """Test valid outs (0-2)"""
        validator = GameValidator()

        # All valid outs should pass
        for outs in [0, 1, 2]:
            validator.validate_outs(outs)

    def test_validate_outs_negative_fails(self):
        """Test negative outs fails"""
        validator = GameValidator()

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_outs(-1)

        assert "invalid outs" in str(exc_info.value).lower()
        assert "-1" in str(exc_info.value)

    def test_validate_outs_too_high_fails(self):
        """Test outs > 2 fails"""
        validator = GameValidator()

        for invalid_outs in [3, 4, 10]:
            with pytest.raises(ValidationError) as exc_info:
                validator.validate_outs(invalid_outs)

            assert "invalid outs" in str(exc_info.value).lower()


class TestInningValidation:
    """Test inning validation"""

    def test_validate_inning_valid(self):
        """Test valid innings"""
        validator = GameValidator()

        # Valid innings
        validator.validate_inning(1, "top")
        validator.validate_inning(9, "bottom")
        validator.validate_inning(15, "top")  # Extra innings

    def test_validate_inning_zero_fails(self):
        """Test inning 0 fails"""
        validator = GameValidator()

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_inning(0, "top")

        assert "invalid inning" in str(exc_info.value).lower()

    def test_validate_inning_negative_fails(self):
        """Test negative inning fails"""
        validator = GameValidator()

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_inning(-1, "top")

        assert "invalid inning" in str(exc_info.value).lower()

    def test_validate_inning_invalid_half(self):
        """Test invalid half fails"""
        validator = GameValidator()

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_inning(1, "middle")

        assert "invalid half" in str(exc_info.value).lower()

    def test_validate_inning_invalid_half_variants(self):
        """Test various invalid half values"""
        validator = GameValidator()

        for invalid_half in ["top1", "Bottom", "TOP", "bot", ""]:
            with pytest.raises(ValidationError):
                validator.validate_inning(1, invalid_half)


class TestDefensiveDecisionValidation:
    """Test defensive decision validation"""

    def test_validate_defensive_decision_valid(self):
        """Test valid defensive decision"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            runners=[]
        )
        decision = DefensiveDecision(
            alignment="normal",
            infield_depth="normal"
        )

        # Should not raise
        validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_invalid_depth(self):
        """Test invalid infield depth fails at Pydantic validation"""
        from pydantic_core import ValidationError as PydanticValidationError

        # Pydantic catches invalid depth at model creation
        with pytest.raises(PydanticValidationError) as exc_info:
            decision = DefensiveDecision(
                alignment="normal",
                infield_depth="super_deep"
            )

        assert "infield_depth" in str(exc_info.value).lower()

    def test_validate_defensive_decision_hold_runner_valid(self):
        """Test holding runner that exists"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = DefensiveDecision(
            alignment="normal",
            hold_runners=[1]
        )

        # Should not raise
        validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_hold_empty_base_fails(self):
        """Test holding empty base fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = DefensiveDecision(
            alignment="normal",
            hold_runners=[1]
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_decision(decision, state)

        assert "cannot hold runner on base" in str(exc_info.value).lower()
        assert "no runner" in str(exc_info.value).lower()

    def test_validate_defensive_decision_hold_invalid_base_number(self):
        """Test holding invalid base number fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = DefensiveDecision(
            alignment="normal",
            hold_runners=[1, 4]  # 4 is invalid
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_decision(decision, state)

        assert "invalid hold runner base" in str(exc_info.value).lower()
        assert "4" in str(exc_info.value)

    def test_validate_defensive_decision_hold_multiple_runners(self):
        """Test holding multiple runners with bases loaded"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = DefensiveDecision(
            alignment="normal",
            hold_runners=[1, 2, 3]
        )

        # Should not raise
        validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_corners_in_without_runner_on_third_fails(self):
        """Test corners_in depth without runner on third fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = DefensiveDecision(
            infield_depth="corners_in"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_decision(decision, state)

        assert "cannot play corners_in" in str(exc_info.value).lower()
        assert "without a runner on third" in str(exc_info.value).lower()

    def test_validate_defensive_decision_corners_in_with_runner_on_third_succeeds(self):
        """Test corners_in depth with runner on third succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = DefensiveDecision(
            infield_depth="corners_in"
        )

        # Should not raise
        validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_infield_in_without_runner_on_third_fails(self):
        """Test infield_in depth without runner on third fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2)
        )
        decision = DefensiveDecision(
            infield_depth="infield_in"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_decision(decision, state)

        assert "cannot play infield_in" in str(exc_info.value).lower()
        assert "without a runner on third" in str(exc_info.value).lower()

    def test_validate_defensive_decision_infield_in_with_bases_loaded_succeeds(self):
        """Test infield_in depth with bases loaded succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = DefensiveDecision(
            infield_depth="infield_in"
        )

        # Should not raise
        validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_all_valid_alignments(self):
        """Test all valid alignment options"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )

        valid_alignments = ["normal", "shifted_left", "shifted_right", "extreme_shift"]
        for alignment in valid_alignments:
            decision = DefensiveDecision(alignment=alignment)
            # Should not raise
            validator.validate_defensive_decision(decision, state)

    def test_validate_defensive_decision_all_valid_outfield_depths(self):
        """Test all valid outfield depth options"""
        validator = GameValidator()

        # Test normal depth (always valid)
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = DefensiveDecision(outfield_depth="normal")
        validator.validate_defensive_decision(decision, state)

        # Test shallow depth (requires walk-off scenario)
        walkoff_state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=9,
            half="bottom",
            home_score=3,
            away_score=4,
            on_first=LineupPlayerState(lineup_id=2, card_id=201, position="SS", batting_order=2)
        )
        decision_shallow = DefensiveDecision(outfield_depth="shallow")
        validator.validate_defensive_decision(decision_shallow, walkoff_state)

    def test_validate_defensive_decision_shallow_without_walkoff_fails(self):
        """Test shallow outfield fails without walk-off scenario"""
        validator = GameValidator()

        # Not bottom of 9th
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=1),
            inning=5,
            half="bottom",
            home_score=3,
            away_score=4,
            on_first=LineupPlayerState(lineup_id=2, card_id=201, position="SS", batting_order=2)
        )
        decision = DefensiveDecision(outfield_depth="shallow")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_decision(decision, state)
        assert "walk-off" in str(exc_info.value).lower()


class TestOffensiveDecisionValidation:
    """Test offensive decision validation"""

    def test_validate_offensive_decision_valid(self):
        """Test valid offensive decision"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            outs=0
        )
        decision = OffensiveDecision(approach="normal")

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_steal_valid(self):
        """Test valid steal attempt"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(
            approach="normal",
            steal_attempts=[2]
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_steal_without_runner_fails(self):
        """Test stealing without runner fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(
            approach="normal",
            steal_attempts=[2]
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "cannot steal" in str(exc_info.value).lower()
        assert "no runner" in str(exc_info.value).lower()

    def test_validate_offensive_decision_bunt_with_two_outs_fails(self):
        """Test bunting with 2 outs fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            outs=2
        )
        decision = OffensiveDecision(
            action="sac_bunt"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "sacrifice bunt cannot be used with 2 outs" in str(exc_info.value).lower()

    def test_validate_offensive_decision_bunt_with_less_than_two_outs_valid(self):
        """Test bunting with 0-1 outs is valid"""
        validator = GameValidator()

        for outs in [0, 1]:
            state = GameState(
                game_id=uuid4(),
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
                outs=outs
        )
            decision = OffensiveDecision(
                action="sac_bunt"
            )

            # Should not raise
            validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_steal_third_with_runner_on_second(self):
        """Test stealing third with runner on second succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2)
        )
        decision = OffensiveDecision(
            steal_attempts=[3]
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_steal_home_with_runner_on_third(self):
        """Test stealing home with runner on third succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = OffensiveDecision(
            steal_attempts=[4]
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_double_steal(self):
        """Test double steal with runners on first and second"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2)
        )
        decision = OffensiveDecision(
            steal_attempts=[2, 3]
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_steal_invalid_base_fails(self):
        """Test stealing to invalid base number fails at Pydantic validation"""
        from pydantic_core import ValidationError as PydanticValidationError

        # Pydantic catches invalid base at model creation
        with pytest.raises(PydanticValidationError) as exc_info:
            decision = OffensiveDecision(
                steal_attempts=[5]  # 5 is invalid
            )

        assert "steal_attempts" in str(exc_info.value).lower()

    def test_validate_offensive_decision_bunt_and_hit_and_run_fails(self):
        """Test bunting and hit-and-run simultaneously fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        # This test is no longer relevant - actions are mutually exclusive via the field
        # Can't have action="sac_bunt" and action="hit_and_run" at the same time
        pass

    def test_validate_offensive_decision_hit_and_run_without_runners_fails(self):
        """Test hit-and-run without runners on base fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(
            action="hit_and_run"
        )

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "hit and run" in str(exc_info.value).lower()
        assert "runner" in str(exc_info.value).lower()

    def test_validate_offensive_decision_hit_and_run_with_runner_on_first_succeeds(self):
        """Test hit-and-run with runner on first succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(
            action="hit_and_run"
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_hit_and_run_with_runner_on_second_only(self):
        """Test hit-and-run with runner on second only succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2)
        )
        decision = OffensiveDecision(
            hit_and_run=True
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_hit_and_run_with_runner_on_third_only(self):
        """Test hit-and-run with runner on third only succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = OffensiveDecision(
            hit_and_run=True
        )

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_all_valid_options(self):
        """Test all valid offensive decision options"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=2, card_id=201, position="SS", batting_order=2)
        )

        # Test various combinations of offensive options
        decision = OffensiveDecision(hit_and_run=True, steal_attempts=[2])
        validator.validate_offensive_decision(decision, state)

    # Session 2: Tests for new action field
    def test_validate_offensive_decision_action_squeeze_bunt_valid(self):
        """Test squeeze bunt with runner on third succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3),
            outs=1
        )
        decision = OffensiveDecision(action="squeeze_bunt")

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_squeeze_bunt_no_runner_on_third_fails(self):
        """Test squeeze bunt without runner on third fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="squeeze_bunt")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "squeeze bunt requires a runner on third" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_squeeze_bunt_bases_loaded_succeeds(self):
        """Test squeeze bunt with bases loaded succeeds (no restriction on bases loaded)"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3),
            outs=1
        )
        decision = OffensiveDecision(action="squeeze_bunt")

        # Should not raise - squeeze bunt is allowed with bases loaded
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_squeeze_bunt_two_outs_fails(self):
        """Test squeeze bunt with 2 outs fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3),
            outs=2
        )
        decision = OffensiveDecision(action="squeeze_bunt")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "squeeze bunt cannot be used with 2 outs" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_check_jump_valid(self):
        """Test check jump with runner on base succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="check_jump")

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_check_jump_no_runners_fails(self):
        """Test check jump without runners fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="check_jump")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "check jump requires at least one runner" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_check_jump_trail_runner_fails(self):
        """Test check jump with trail runner (R2 when R3 is on base) fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="LF", batting_order=3)
        )
        decision = OffensiveDecision(action="check_jump")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "check jump not allowed for trail runner" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_sac_bunt_two_outs_fails(self):
        """Test sac bunt with 2 outs fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            outs=2
        )
        decision = OffensiveDecision(action="sac_bunt")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "sacrifice bunt cannot be used with 2 outs" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_hit_and_run_valid(self):
        """Test hit_and_run action with runner on base succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="hit_and_run")

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_hit_and_run_no_runners_fails(self):
        """Test hit_and_run action without runners fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="hit_and_run")

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "hit and run action requires at least one runner" in str(exc_info.value).lower()

    def test_validate_offensive_decision_action_swing_away_always_valid(self):
        """Test swing_away action is always valid"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            outs=2  # Even with 2 outs and no runners
        )
        decision = OffensiveDecision(action="swing_away")

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_steal_valid(self):
        """Test steal action with steal_attempts succeeds"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="steal", steal_attempts=[2])

        # Should not raise
        validator.validate_offensive_decision(decision, state)

    def test_validate_offensive_decision_action_steal_no_attempts_fails(self):
        """Test steal action without steal_attempts fails"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        )
        decision = OffensiveDecision(action="steal")  # No steal_attempts

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_offensive_decision(decision, state)

        assert "steal action requires steal_attempts" in str(exc_info.value).lower()


class TestLineupValidation:
    """Test lineup position validation"""

    def test_validate_defensive_lineup_valid(self):
        """Test valid defensive lineup"""
        validator = GameValidator()
        lineup = [
            MockLineupPlayer(id=1, position="P", is_active=True, batting_order=1),
            MockLineupPlayer(id=2, position="C", is_active=True, batting_order=2),
            MockLineupPlayer(id=3, position="1B", is_active=True, batting_order=3),
            MockLineupPlayer(id=4, position="2B", is_active=True, batting_order=4),
            MockLineupPlayer(id=5, position="3B", is_active=True, batting_order=5),
            MockLineupPlayer(id=6, position="SS", is_active=True, batting_order=6),
            MockLineupPlayer(id=7, position="LF", is_active=True, batting_order=7),
            MockLineupPlayer(id=8, position="CF", is_active=True, batting_order=8),
            MockLineupPlayer(id=9, position="RF", is_active=True, batting_order=9),
        ]

        # Should not raise
        validator.validate_defensive_lineup_positions(lineup)

    def test_validate_defensive_lineup_missing_position(self):
        """Test lineup missing required position fails"""
        validator = GameValidator()
        lineup = [
            MockLineupPlayer(id=1, position="P", is_active=True, batting_order=1),
            MockLineupPlayer(id=2, position="C", is_active=True, batting_order=2),
            MockLineupPlayer(id=3, position="1B", is_active=True, batting_order=3),
            MockLineupPlayer(id=4, position="2B", is_active=True, batting_order=4),
            MockLineupPlayer(id=5, position="3B", is_active=True, batting_order=5),
            # Missing SS
            MockLineupPlayer(id=7, position="LF", is_active=True, batting_order=7),
            MockLineupPlayer(id=8, position="CF", is_active=True, batting_order=8),
            MockLineupPlayer(id=9, position="RF", is_active=True, batting_order=9),
        ]

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_lineup_positions(lineup)

        assert "missing active player at ss" in str(exc_info.value).lower()

    def test_validate_defensive_lineup_duplicate_position(self):
        """Test lineup with duplicate position fails"""
        validator = GameValidator()
        lineup = [
            MockLineupPlayer(id=1, position="P", is_active=True, batting_order=1),
            MockLineupPlayer(id=2, position="C", is_active=True, batting_order=2),
            MockLineupPlayer(id=3, position="1B", is_active=True, batting_order=3),
            MockLineupPlayer(id=4, position="2B", is_active=True, batting_order=4),
            MockLineupPlayer(id=5, position="3B", is_active=True, batting_order=5),
            MockLineupPlayer(id=6, position="SS", is_active=True, batting_order=6),
            MockLineupPlayer(id=7, position="LF", is_active=True, batting_order=7),
            MockLineupPlayer(id=8, position="CF", is_active=True, batting_order=8),
            MockLineupPlayer(id=9, position="CF", is_active=True, batting_order=9),  # Duplicate CF
        ]

        with pytest.raises(ValidationError) as exc_info:
            validator.validate_defensive_lineup_positions(lineup)

        assert "multiple active players at cf" in str(exc_info.value).lower()

    def test_validate_defensive_lineup_ignores_inactive_players(self):
        """Test inactive players are ignored in validation"""
        validator = GameValidator()
        lineup = [
            MockLineupPlayer(id=1, position="P", is_active=True, batting_order=1),
            MockLineupPlayer(id=2, position="C", is_active=True, batting_order=2),
            MockLineupPlayer(id=3, position="1B", is_active=True, batting_order=3),
            MockLineupPlayer(id=4, position="2B", is_active=True, batting_order=4),
            MockLineupPlayer(id=5, position="3B", is_active=True, batting_order=5),
            MockLineupPlayer(id=6, position="SS", is_active=True, batting_order=6),
            MockLineupPlayer(id=7, position="LF", is_active=True, batting_order=7),
            MockLineupPlayer(id=8, position="CF", is_active=True, batting_order=8),
            MockLineupPlayer(id=9, position="RF", is_active=True, batting_order=9),
            # Inactive players shouldn't cause duplicate errors
            MockLineupPlayer(id=10, position="P", is_active=False, batting_order=10),
            MockLineupPlayer(id=11, position="C", is_active=False, batting_order=11),
        ]

        # Should not raise
        validator.validate_defensive_lineup_positions(lineup)

    def test_validate_defensive_lineup_allows_dh(self):
        """Test lineup with DH is valid (DH not required)"""
        validator = GameValidator()
        lineup = [
            MockLineupPlayer(id=1, position="P", is_active=True, batting_order=1),
            MockLineupPlayer(id=2, position="C", is_active=True, batting_order=2),
            MockLineupPlayer(id=3, position="1B", is_active=True, batting_order=3),
            MockLineupPlayer(id=4, position="2B", is_active=True, batting_order=4),
            MockLineupPlayer(id=5, position="3B", is_active=True, batting_order=5),
            MockLineupPlayer(id=6, position="SS", is_active=True, batting_order=6),
            MockLineupPlayer(id=7, position="LF", is_active=True, batting_order=7),
            MockLineupPlayer(id=8, position="CF", is_active=True, batting_order=8),
            MockLineupPlayer(id=9, position="RF", is_active=True, batting_order=9),
            MockLineupPlayer(id=10, position="DH", is_active=True, batting_order=10),  # DH is optional
        ]

        # Should not raise
        validator.validate_defensive_lineup_positions(lineup)


class TestGameFlowValidation:
    """Test game flow validation"""

    def test_can_continue_inning_with_less_than_three_outs(self):
        """Test inning can continue with 0-2 outs"""
        validator = GameValidator()

        for outs in [0, 1, 2]:
            state = GameState(
                game_id=uuid4(),
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
                outs=outs
        )
            assert validator.can_continue_inning(state) is True

    def test_cannot_continue_inning_with_three_outs(self):
        """Test inning cannot continue with 3 outs (e.g., after third out recorded)"""
        validator = GameValidator()
        # Create with valid outs, then increment to 3 (simulates GameEngine flow)
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            outs=2
        )
        # Simulate third out being recorded (outs goes to 3 temporarily)
        state.outs += 1  # Now 3 - validate_assignment not enabled, so this works

        assert validator.can_continue_inning(state) is False

    def test_is_game_over_before_9th(self):
        """Test game is not over before 9th inning"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=8,
            half="bottom",
            home_score=5,
            away_score=2
        )

        assert validator.is_game_over(state) is False

    def test_is_game_over_9th_bottom_home_ahead(self):
        """Test game is over in bottom 9th if home team ahead"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=9,
            half="bottom",
            home_score=5,
            away_score=2
        )

        assert validator.is_game_over(state) is True

    def test_is_game_over_9th_bottom_tied(self):
        """Test game is NOT over in bottom 9th if tied"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=9,
            half="bottom",
            home_score=5,
            away_score=5
        )

        assert validator.is_game_over(state) is False

    def test_is_game_over_9th_top(self):
        """Test game is NOT over in top 9th"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=9,
            half="top",
            home_score=5,
            away_score=2
        )

        assert validator.is_game_over(state) is False

    def test_is_game_over_extras_home_ahead(self):
        """Test game is over in extras if home ahead in bottom"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=12,
            half="bottom",
            home_score=8,
            away_score=7
        )

        assert validator.is_game_over(state) is True

    def test_is_game_over_extras_tied(self):
        """Test game continues in extras if tied"""
        validator = GameValidator()
        state = GameState(
            game_id=uuid4(),
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
            inning=15,
            half="bottom",
            home_score=10,
            away_score=10
        )

        assert validator.is_game_over(state) is False


class TestGameValidatorSingleton:
    """Test game_validator singleton"""

    def test_singleton_import(self):
        """Test that game_validator singleton is importable"""
        from app.core.validators import game_validator
        assert game_validator is not None
        assert isinstance(game_validator, GameValidator)
