"""
Unit tests for Pydantic game state models.

Tests validation, helper methods, and business logic for all game_models.py classes.

Author: Claude
Date: 2025-10-22
"""

import pytest
from uuid import uuid4
from pydantic import ValidationError

from app.models.game_models import (
    LineupPlayerState,
    TeamLineupState,
    DefensiveDecision,
    OffensiveDecision,
    GameState,
)


# ============================================================================
# TEST FIXTURES
# ============================================================================

@pytest.fixture
def sample_batter():
    """Create a sample batter for testing GameState"""
    return LineupPlayerState(
        lineup_id=1,
        card_id=100,
        position="RF",
        batting_order=3,
        is_active=True
    )


@pytest.fixture
def sample_pitcher():
    """Create a sample pitcher for testing GameState"""
    return LineupPlayerState(
        lineup_id=10,
        card_id=200,
        position="P",
        batting_order=9,
        is_active=True
    )


@pytest.fixture
def sample_catcher():
    """Create a sample catcher for testing GameState"""
    return LineupPlayerState(
        lineup_id=2,
        card_id=101,
        position="C",
        batting_order=2,
        is_active=True
    )


# ============================================================================
# LINEUP TESTS
# ============================================================================

class TestLineupPlayerState:
    """Tests for LineupPlayerState model"""

    def test_create_lineup_player_valid(self):
        """Test creating a valid lineup player"""
        player = LineupPlayerState(
            lineup_id=1,
            card_id=100,
            position="CF",
            batting_order=1,
            is_active=True
        )
        assert player.lineup_id == 1
        assert player.card_id == 100
        assert player.position == "CF"
        assert player.batting_order == 1
        assert player.is_active is True

    def test_lineup_player_valid_positions(self):
        """Test all valid positions"""
        valid_positions = ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF', 'DH']
        for pos in valid_positions:
            player = LineupPlayerState(lineup_id=1, card_id=100, position=pos)
            assert player.position == pos

    def test_lineup_player_invalid_position(self):
        """Test that invalid positions raise error"""
        with pytest.raises(ValidationError) as exc_info:
            LineupPlayerState(lineup_id=1, card_id=100, position="XX")
        assert "Position must be one of" in str(exc_info.value)

    def test_lineup_player_batting_order_range(self):
        """Test valid batting order range (1-9)"""
        for order in range(1, 10):
            player = LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=order)
            assert player.batting_order == order

    def test_lineup_player_batting_order_invalid_zero(self):
        """Test that batting order 0 is invalid"""
        with pytest.raises(ValidationError):
            LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=0)

    def test_lineup_player_batting_order_invalid_ten(self):
        """Test that batting order 10 is invalid"""
        with pytest.raises(ValidationError):
            LineupPlayerState(lineup_id=1, card_id=100, position="CF", batting_order=10)

    def test_lineup_player_no_batting_order(self):
        """Test that batting order can be None (for pitchers in AL)"""
        player = LineupPlayerState(lineup_id=1, card_id=100, position="P", batting_order=None)
        assert player.batting_order is None


class TestTeamLineupState:
    """Tests for TeamLineupState model"""

    def test_create_empty_lineup(self):
        """Test creating an empty lineup"""
        lineup = TeamLineupState(team_id=1, players=[])
        assert lineup.team_id == 1
        assert len(lineup.players) == 0

    def test_get_batting_order(self):
        """Test getting players in batting order"""
        players = [
            LineupPlayerState(lineup_id=3, card_id=103, position="CF", batting_order=3),
            LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
            LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2),
            LineupPlayerState(lineup_id=10, card_id=110, position="P", batting_order=None),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        ordered = lineup.get_batting_order()
        assert len(ordered) == 3  # Pitcher excluded
        assert ordered[0].batting_order == 1
        assert ordered[1].batting_order == 2
        assert ordered[2].batting_order == 3

    def test_get_pitcher(self):
        """Test getting the active pitcher"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            LineupPlayerState(lineup_id=10, card_id=110, position="P", is_active=True),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        pitcher = lineup.get_pitcher()
        assert pitcher is not None
        assert pitcher.position == "P"
        assert pitcher.card_id == 110

    def test_get_pitcher_none_when_inactive(self):
        """Test that inactive pitcher is not returned"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            LineupPlayerState(lineup_id=10, card_id=110, position="P", is_active=False),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        pitcher = lineup.get_pitcher()
        assert pitcher is None

    def test_get_player_by_lineup_id(self):
        """Test getting player by lineup ID"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            LineupPlayerState(lineup_id=5, card_id=105, position="SS", batting_order=5),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        player = lineup.get_player_by_lineup_id(5)
        assert player is not None
        assert player.card_id == 105
        assert player.position == "SS"

    def test_get_player_by_lineup_id_not_found(self):
        """Test that None is returned for non-existent lineup ID"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        player = lineup.get_player_by_lineup_id(99)
        assert player is None

    def test_get_batter(self):
        """Test getting batter by batting order index"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
            LineupPlayerState(lineup_id=2, card_id=102, position="CF", batting_order=2),
            LineupPlayerState(lineup_id=3, card_id=103, position="RF", batting_order=3),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        batter = lineup.get_batter(0)  # First batter (index 0)
        assert batter is not None
        assert batter.batting_order == 1

        batter = lineup.get_batter(2)  # Third batter (index 2)
        assert batter is not None
        assert batter.batting_order == 3

    def test_get_batter_out_of_range(self):
        """Test that None is returned for out-of-range index"""
        players = [
            LineupPlayerState(lineup_id=1, card_id=101, position="LF", batting_order=1),
        ]
        lineup = TeamLineupState(team_id=1, players=players)

        batter = lineup.get_batter(5)
        assert batter is None


# ============================================================================
# DECISION TESTS
# ============================================================================

class TestDefensiveDecision:
    """Tests for DefensiveDecision model"""

    def test_create_defensive_decision_defaults(self):
        """Test creating defensive decision with defaults"""
        decision = DefensiveDecision()
        assert decision.infield_depth == "normal"
        assert decision.outfield_depth == "normal"
        assert decision.hold_runners == []

    def test_defensive_decision_valid_infield_depths(self):
        """Test all valid infield depths"""
        valid = ['infield_in', 'normal', 'corners_in']
        for depth in valid:
            decision = DefensiveDecision(infield_depth=depth)
            assert decision.infield_depth == depth

    def test_defensive_decision_invalid_infield_depth(self):
        """Test that invalid infield depth raises error"""
        with pytest.raises(ValidationError):
            DefensiveDecision(infield_depth="invalid")

    def test_defensive_decision_hold_runners(self):
        """Test holding runners"""
        decision = DefensiveDecision(hold_runners=[1, 3])
        assert decision.hold_runners == [1, 3]


class TestOffensiveDecision:
    """Tests for OffensiveDecision model"""

    def test_create_offensive_decision_defaults(self):
        """Test creating offensive decision with defaults"""
        decision = OffensiveDecision()
        assert decision.action == "swing_away"
        assert decision.steal_attempts == []

    def test_offensive_decision_steal_action(self):
        """Test steal action with steal attempts"""
        decision = OffensiveDecision(action="steal", steal_attempts=[2])
        assert decision.action == "steal"
        assert decision.steal_attempts == [2]

        decision = OffensiveDecision(action="steal", steal_attempts=[2, 3])  # Double steal
        assert decision.steal_attempts == [2, 3]

    def test_offensive_decision_invalid_steal_base(self):
        """Test that invalid steal base raises error"""
        with pytest.raises(ValidationError):
            OffensiveDecision(steal_attempts=[1])  # Can't steal first

    def test_offensive_decision_hit_and_run(self):
        """Test hit and run action"""
        decision = OffensiveDecision(action="hit_and_run")
        assert decision.action == "hit_and_run"

    def test_offensive_decision_bunt(self):
        """Test bunt actions"""
        decision = OffensiveDecision(action="sac_bunt")
        assert decision.action == "sac_bunt"

        decision = OffensiveDecision(action="squeeze_bunt")
        assert decision.action == "squeeze_bunt"


# ============================================================================
# GAMESTATE TESTS
# ============================================================================

class TestGameState:
    """Tests for GameState model"""

    def test_create_game_state_minimal(self, sample_batter):
        """Test creating game state with minimal fields"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=sample_batter
        )
        assert state.game_id == game_id
        assert state.league_id == "sba"
        assert state.home_team_id == 1
        assert state.away_team_id == 2
        assert state.status == "pending"
        assert state.inning == 1
        assert state.half == "top"
        assert state.outs == 0
        assert state.home_score == 0
        assert state.away_score == 0

    def test_game_state_valid_league_ids(self, sample_batter):
        """Test valid league IDs"""
        game_id = uuid4()
        for league in ['sba', 'pd']:
            state = GameState(
                game_id=game_id,
                league_id=league,
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter
            )
            assert state.league_id == league

    def test_game_state_invalid_league_id(self, sample_batter):
        """Test that invalid league ID raises error"""
        game_id = uuid4()
        with pytest.raises(ValidationError):
            GameState(
                game_id=game_id,
                league_id="invalid",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter
            )

    def test_game_state_valid_statuses(self, sample_batter):
        """Test valid game statuses"""
        game_id = uuid4()
        for status in ['pending', 'active', 'paused', 'completed']:
            state = GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                status=status
            )
            assert state.status == status

    def test_game_state_invalid_status(self, sample_batter):
        """Test that invalid status raises error"""
        game_id = uuid4()
        with pytest.raises(ValidationError):
            GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                status="invalid"
            )

    def test_game_state_valid_halves(self, sample_batter):
        """Test valid inning halves"""
        game_id = uuid4()
        for half in ['top', 'bottom']:
            state = GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                half=half
            )
            assert state.half == half

    def test_game_state_invalid_half(self, sample_batter):
        """Test that invalid half raises error"""
        game_id = uuid4()
        with pytest.raises(ValidationError):
            GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                half="middle"
            )

    def test_game_state_outs_validation(self, sample_batter):
        """Test outs validation (0-2)"""
        game_id = uuid4()

        # Valid outs
        for outs in [0, 1, 2]:
            state = GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                outs=outs
            )
            assert state.outs == outs

        # Invalid outs
        with pytest.raises(ValidationError):
            GameState(
                game_id=game_id,
                league_id="sba",
                home_team_id=1,
                away_team_id=2,
                current_batter=sample_batter,
                outs=3
            )

    def test_get_batting_team_id(self, sample_batter):
        """Test getting batting team ID"""
        game_id = uuid4()

        # Top of inning - away team bats
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            half="top"
            )
        assert state.get_batting_team_id() == 2

        # Bottom of inning - home team bats
        state.half = "bottom"
        assert state.get_batting_team_id() == 1

    def test_get_fielding_team_id(self, sample_batter):
        """Test getting fielding team ID"""
        game_id = uuid4()

        # Top of inning - home team fields
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            half="top"
            )
        assert state.get_fielding_team_id() == 1

        # Bottom of inning - away team fields
        state.half = "bottom"
        assert state.get_fielding_team_id() == 2

    def test_is_runner_on_base(self, sample_batter):
        """Test checking for runners on specific bases"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="SS", batting_order=3)
            )

        assert state.is_runner_on_first() is True
        assert state.is_runner_on_second() is False
        assert state.is_runner_on_third() is True

    def test_get_runner_at_base(self, sample_batter):
        """Test getting runner at specific base"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
            )

        runner = state.get_runner_at_base(1)
        assert runner is not None
        assert runner.lineup_id == 1

        runner = state.get_runner_at_base(3)
        assert runner is None

    def test_bases_occupied(self, sample_batter):
        """Test getting list of occupied bases"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_third=LineupPlayerState(lineup_id=3, card_id=103, position="SS", batting_order=3)
            )

        bases = state.bases_occupied()
        assert bases == [1, 3]

    def test_clear_bases(self, sample_batter):
        """Test clearing all runners"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
            on_second=LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
            )

        assert len(state.get_all_runners()) == 2
        state.clear_bases()
        assert len(state.get_all_runners()) == 0
        assert state.on_first is None
        assert state.on_second is None
        assert state.on_third is None

    def test_add_runner(self, sample_batter):
        """Test adding a runner to a base"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter
            )

        player = LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
        state.add_runner(player=player, base=1)
        assert len(state.get_all_runners()) == 1
        assert state.is_runner_on_first() is True

    def test_add_runner_replaces_existing(self, sample_batter):
        """Test that adding runner to occupied base replaces existing"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )

        new_player = LineupPlayerState(lineup_id=2, card_id=102, position="RF", batting_order=2)
        state.add_runner(player=new_player, base=1)
        assert len(state.get_all_runners()) == 1  # Still only 1 runner
        runner = state.get_runner_at_base(1)
        assert runner.lineup_id == 2  # New runner replaced old

    def test_advance_runner_to_base(self, sample_batter):
        """Test advancing runner to another base"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )

        state.advance_runner(from_base=1, to_base=2)
        assert state.is_runner_on_first() is False
        assert state.is_runner_on_second() is True

    def test_advance_runner_to_home(self, sample_batter):
        """Test advancing runner to home (scoring)"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            half="top",
            on_third=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )

        initial_score = state.away_score
        state.advance_runner(from_base=3, to_base=4)
        assert len(state.get_all_runners()) == 0  # Runner removed from bases
        assert state.away_score == initial_score + 1  # Score increased

    def test_advance_runner_scores_correct_team(self, sample_batter):
        """Test that scoring increments correct team's score"""
        game_id = uuid4()

        # Top of inning - away team batting
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            half="top",
            on_third=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )
        state.advance_runner(from_base=3, to_base=4)
        assert state.away_score == 1
        assert state.home_score == 0

        # Bottom of inning - home team batting
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            half="bottom",
            on_third=LineupPlayerState(lineup_id=5, card_id=105, position="RF", batting_order=5)
            )
        state.advance_runner(from_base=3, to_base=4)
        assert state.home_score == 1
        assert state.away_score == 0

    def test_increment_outs(self, sample_batter):
        """Test incrementing outs"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            outs=0
            )

        assert state.increment_outs() is False  # 1 out - not end of inning
        assert state.outs == 1

        assert state.increment_outs() is False  # 2 outs - not end of inning
        assert state.outs == 2

        assert state.increment_outs() is True  # 3 outs - end of inning
        assert state.outs == 3

    def test_end_half_inning_top_to_bottom(self, sample_batter):
        """Test ending top of inning goes to bottom"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=3,
            half="top",
            outs=2,
            on_first=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )

        state.end_half_inning()
        assert state.inning == 3  # Same inning
        assert state.half == "bottom"  # Now bottom
        assert state.outs == 0  # Outs reset
        assert len(state.get_all_runners()) == 0  # Bases cleared

    def test_end_half_inning_bottom_to_next_top(self, sample_batter):
        """Test ending bottom of inning goes to next inning top"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=3,
            half="bottom",
            outs=2,
            on_second=LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)
            )

        state.end_half_inning()
        assert state.inning == 4  # Next inning
        assert state.half == "top"  # Top of next inning
        assert state.outs == 0  # Outs reset
        assert len(state.get_all_runners()) == 0  # Bases cleared

    def test_is_game_over_early_innings(self, sample_batter):
        """Test game is not over in early innings"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=5,
            half="bottom",
            home_score=5,
            away_score=2
            )

        assert state.is_game_over() is False

    def test_is_game_over_bottom_ninth_home_ahead(self, sample_batter):
        """Test game over when home team ahead in bottom 9th"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=9,
            half="bottom",
            home_score=5,
            away_score=2
            )

        assert state.is_game_over() is True

    def test_is_game_over_bottom_ninth_tied(self, sample_batter):
        """Test game continues when tied in bottom 9th"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=9,
            half="bottom",
            home_score=2,
            away_score=2
            )

        assert state.is_game_over() is False

    def test_is_game_over_extra_innings_walkoff(self, sample_batter):
        """Test game over on walk-off in extra innings"""
        game_id = uuid4()
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=10,
            half="bottom",
            home_score=5,
            away_score=4
            )

        assert state.is_game_over() is True

    def test_is_game_over_after_top_ninth_home_ahead(self, sample_batter):
        """Test game over at start of bottom 9th when away team ahead"""
        game_id = uuid4()
        # Bottom of 9th, away team ahead - home team can't catch up
        # Note: This would only happen at START of bottom 9th
        # In reality, game wouldn't start bottom 9th if home is losing
        state = GameState(
            game_id=game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
                current_batter=sample_batter,
            inning=9,
            half="bottom",
            outs=0,
            home_score=2,
            away_score=5
            )

        # Game continues - home team gets chance to catch up
        assert state.is_game_over() is False
