"""
Unit tests for StateManager.

Tests in-memory state management, lineup operations, and game lifecycle.

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

import pytest
from uuid import uuid4
import pendulum

from app.core.state_manager import StateManager
from app.models.game_models import GameState, TeamLineupState, LineupPlayerState


@pytest.fixture
def state_manager():
    """Create a fresh StateManager for each test"""
    return StateManager()


@pytest.fixture
def sample_game_id():
    """Generate a sample game UUID"""
    return uuid4()


@pytest.fixture
def sample_lineup():
    """Create a sample team lineup"""
    players = [
        LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1),
        LineupPlayerState(lineup_id=2, card_id=102, position="SS", batting_order=2),
        LineupPlayerState(lineup_id=3, card_id=103, position="1B", batting_order=3),
    ]
    return TeamLineupState(team_id=1, players=players)


class TestStateManagerCreate:
    """Tests for creating game states"""

    @pytest.mark.asyncio
    async def test_create_game_sba(self, state_manager, sample_game_id):
        """Test creating an SBA game"""
        state = await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        assert state.game_id == sample_game_id
        assert state.league_id == "sba"
        assert state.home_team_id == 1
        assert state.away_team_id == 2
        assert state.home_team_is_ai is False
        assert state.away_team_is_ai is False
        assert state.status == "pending"

    @pytest.mark.asyncio
    async def test_create_game_pd(self, state_manager, sample_game_id):
        """Test creating a PD game"""
        state = await state_manager.create_game(
            game_id=sample_game_id,
            league_id="pd",
            home_team_id=10,
            away_team_id=20
        )

        assert state.league_id == "pd"
        assert state.home_team_id == 10
        assert state.away_team_id == 20

    @pytest.mark.asyncio
    async def test_create_game_with_ai(self, state_manager, sample_game_id):
        """Test creating game with AI opponent"""
        state = await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            home_team_is_ai=True,
            away_team_is_ai=False
        )

        assert state.home_team_is_ai is True
        assert state.away_team_is_ai is False

    @pytest.mark.asyncio
    async def test_create_game_duplicate_id_raises_error(self, state_manager, sample_game_id):
        """Test that creating duplicate game ID raises error"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        with pytest.raises(ValueError, match="already exists"):
            await state_manager.create_game(
                game_id=sample_game_id,
                league_id="sba",
                home_team_id=3,
                away_team_id=4
            )

    @pytest.mark.asyncio
    async def test_create_game_tracked_in_stats(self, state_manager, sample_game_id):
        """Test that created game appears in stats"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        stats = state_manager.get_stats()
        assert stats["active_games"] == 1
        assert stats["games_by_league"]["sba"] == 1
        assert stats["games_by_status"]["pending"] == 1


class TestStateManagerGetUpdate:
    """Tests for getting and updating game states"""

    @pytest.mark.asyncio
    async def test_get_state_existing(self, state_manager, sample_game_id):
        """Test getting an existing game state"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        state = state_manager.get_state(sample_game_id)
        assert state is not None
        assert state.game_id == sample_game_id

    def test_get_state_nonexistent(self, state_manager):
        """Test getting a nonexistent game returns None"""
        fake_id = uuid4()
        state = state_manager.get_state(fake_id)
        assert state is None

    @pytest.mark.asyncio
    async def test_get_state_updates_access_time(self, state_manager, sample_game_id):
        """Test that getting state updates last access time"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        # Get initial access time
        first_access = state_manager._last_access[sample_game_id]

        # Wait a tiny bit (pendulum has microsecond precision)
        import time
        time.sleep(0.001)

        # Access again
        state_manager.get_state(sample_game_id)
        second_access = state_manager._last_access[sample_game_id]

        assert second_access > first_access

    @pytest.mark.asyncio
    async def test_update_state(self, state_manager, sample_game_id):
        """Test updating game state"""
        original_state = await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        # Modify state
        original_state.inning = 5
        original_state.half = "bottom"
        original_state.home_score = 3
        original_state.away_score = 2

        # Update
        state_manager.update_state(sample_game_id, original_state)

        # Verify changes persisted
        retrieved_state = state_manager.get_state(sample_game_id)
        assert retrieved_state.inning == 5
        assert retrieved_state.half == "bottom"
        assert retrieved_state.home_score == 3
        assert retrieved_state.away_score == 2

    def test_update_state_nonexistent_raises_error(self, state_manager):
        """Test updating nonexistent game raises error"""
        from app.models.game_models import LineupPlayerState

        fake_id = uuid4()
        fake_batter = LineupPlayerState(
            lineup_id=1,
            card_id=100,
            position='CF',
            batting_order=1
        )
        fake_state = GameState(
            game_id=fake_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2,
            current_batter=fake_batter
        )

        with pytest.raises(ValueError, match="not found"):
            state_manager.update_state(fake_id, fake_state)


class TestStateManagerLineups:
    """Tests for lineup management"""

    @pytest.mark.asyncio
    async def test_set_lineup(self, state_manager, sample_game_id, sample_lineup):
        """Test setting team lineup"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        state_manager.set_lineup(sample_game_id, team_id=1, lineup=sample_lineup)

        retrieved_lineup = state_manager.get_lineup(sample_game_id, team_id=1)
        assert retrieved_lineup is not None
        assert retrieved_lineup.team_id == 1
        assert len(retrieved_lineup.players) == 3

    def test_set_lineup_nonexistent_game_raises_error(self, state_manager, sample_lineup):
        """Test setting lineup for nonexistent game raises error"""
        fake_id = uuid4()

        with pytest.raises(ValueError, match="not found"):
            state_manager.set_lineup(fake_id, team_id=1, lineup=sample_lineup)

    @pytest.mark.asyncio
    async def test_get_lineup_nonexistent_team(self, state_manager, sample_game_id):
        """Test getting lineup for nonexistent team returns None"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        lineup = state_manager.get_lineup(sample_game_id, team_id=999)
        assert lineup is None

    @pytest.mark.asyncio
    async def test_set_lineup_multiple_teams(self, state_manager, sample_game_id):
        """Test setting lineups for both teams"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        home_lineup = TeamLineupState(
            team_id=1,
            players=[LineupPlayerState(lineup_id=1, card_id=101, position="CF", batting_order=1)]
        )
        away_lineup = TeamLineupState(
            team_id=2,
            players=[LineupPlayerState(lineup_id=10, card_id=201, position="SS", batting_order=1)]
        )

        state_manager.set_lineup(sample_game_id, team_id=1, lineup=home_lineup)
        state_manager.set_lineup(sample_game_id, team_id=2, lineup=away_lineup)

        home_retrieved = state_manager.get_lineup(sample_game_id, team_id=1)
        away_retrieved = state_manager.get_lineup(sample_game_id, team_id=2)

        assert home_retrieved.team_id == 1
        assert away_retrieved.team_id == 2
        assert home_retrieved.players[0].card_id == 101
        assert away_retrieved.players[0].card_id == 201


class TestStateManagerRemove:
    """Tests for removing games"""

    @pytest.mark.asyncio
    async def test_remove_game(self, state_manager, sample_game_id):
        """Test removing a game from memory"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )

        assert state_manager.exists(sample_game_id)

        state_manager.remove_game(sample_game_id)

        assert not state_manager.exists(sample_game_id)
        assert state_manager.get_state(sample_game_id) is None

    @pytest.mark.asyncio
    async def test_remove_game_with_lineups(self, state_manager, sample_game_id, sample_lineup):
        """Test removing game also removes lineups"""
        await state_manager.create_game(
            game_id=sample_game_id,
            league_id="sba",
            home_team_id=1,
            away_team_id=2
        )
        state_manager.set_lineup(sample_game_id, team_id=1, lineup=sample_lineup)

        state_manager.remove_game(sample_game_id)

        assert state_manager.get_lineup(sample_game_id, team_id=1) is None

    def test_remove_nonexistent_game_no_error(self, state_manager):
        """Test removing nonexistent game doesn't raise error"""
        fake_id = uuid4()
        # Should not raise
        state_manager.remove_game(fake_id)


class TestStateManagerEviction:
    """Tests for idle game eviction"""

    @pytest.mark.asyncio
    async def test_evict_idle_games(self, state_manager):
        """
        Test evicting idle games based on configured timeout.

        Games that haven't been accessed beyond the idle timeout should be evicted.
        """
        from unittest.mock import patch, MagicMock

        # Create two games
        game1 = uuid4()
        game2 = uuid4()

        await state_manager.create_game(game1, "sba", 1, 2)
        await state_manager.create_game(game2, "sba", 3, 4)

        # Manually set one game's access time to be old (25 hours ago)
        old_time = pendulum.now('UTC').subtract(hours=25)
        state_manager._last_access[game1] = old_time

        # Mock settings with 24 hour timeout
        with patch("app.config.get_settings") as mock_settings:
            settings = MagicMock()
            settings.game_idle_timeout_hours = 24
            mock_settings.return_value = settings

            # Evict idle games
            evicted = await state_manager.evict_idle_games()

            assert len(evicted) == 1
            assert game1 in evicted
            assert not state_manager.exists(game1)
            assert state_manager.exists(game2)

    @pytest.mark.asyncio
    async def test_evict_no_idle_games(self, state_manager, sample_game_id):
        """
        Test eviction when no games are idle.

        Recently accessed games should not be evicted.
        """
        from unittest.mock import patch, MagicMock

        await state_manager.create_game(sample_game_id, "sba", 1, 2)

        # Mock settings with 24 hour timeout
        with patch("app.config.get_settings") as mock_settings:
            settings = MagicMock()
            settings.game_idle_timeout_hours = 24
            mock_settings.return_value = settings

            evicted = await state_manager.evict_idle_games()

            assert len(evicted) == 0
            assert state_manager.exists(sample_game_id)


class TestStateManagerStats:
    """Tests for statistics"""

    @pytest.mark.asyncio
    async def test_get_stats_empty(self, state_manager):
        """Test stats when no games exist"""
        stats = state_manager.get_stats()

        assert stats["active_games"] == 0
        assert stats["total_lineups"] == 0
        assert stats["games_by_league"] == {}
        assert stats["games_by_status"] == {}

    @pytest.mark.asyncio
    async def test_get_stats_multiple_games(self, state_manager):
        """Test stats with multiple games"""
        # Create multiple games
        await state_manager.create_game(uuid4(), "sba", 1, 2)
        await state_manager.create_game(uuid4(), "sba", 3, 4)
        await state_manager.create_game(uuid4(), "pd", 5, 6)

        stats = state_manager.get_stats()

        assert stats["active_games"] == 3
        assert stats["games_by_league"]["sba"] == 2
        assert stats["games_by_league"]["pd"] == 1
        assert stats["games_by_status"]["pending"] == 3

    @pytest.mark.asyncio
    async def test_get_stats_with_lineups(self, state_manager, sample_game_id, sample_lineup):
        """Test stats include lineup counts"""
        await state_manager.create_game(sample_game_id, "sba", 1, 2)
        state_manager.set_lineup(sample_game_id, team_id=1, lineup=sample_lineup)
        state_manager.set_lineup(sample_game_id, team_id=2, lineup=sample_lineup)

        stats = state_manager.get_stats()

        assert stats["total_lineups"] == 2


class TestStateManagerUtilities:
    """Tests for utility methods"""

    @pytest.mark.asyncio
    async def test_exists(self, state_manager, sample_game_id):
        """Test checking if game exists"""
        assert not state_manager.exists(sample_game_id)

        await state_manager.create_game(sample_game_id, "sba", 1, 2)

        assert state_manager.exists(sample_game_id)

    @pytest.mark.asyncio
    async def test_get_all_game_ids(self, state_manager):
        """Test getting all game IDs"""
        game1 = uuid4()
        game2 = uuid4()
        game3 = uuid4()

        await state_manager.create_game(game1, "sba", 1, 2)
        await state_manager.create_game(game2, "sba", 3, 4)
        await state_manager.create_game(game3, "pd", 5, 6)

        all_ids = state_manager.get_all_game_ids()

        assert len(all_ids) == 3
        assert game1 in all_ids
        assert game2 in all_ids
        assert game3 in all_ids

    def test_get_all_game_ids_empty(self, state_manager):
        """Test getting game IDs when no games exist"""
        all_ids = state_manager.get_all_game_ids()

        assert all_ids == []


class TestStateManagerRecovery:
    """Tests for game recovery"""

    @pytest.mark.asyncio
    async def test_recover_game_nonexistent(self, state_manager):
        """Test that recovering nonexistent game returns None"""
        from unittest.mock import AsyncMock

        fake_id = uuid4()

        # Mock the database operation to return None (game not found)
        state_manager.db_ops.load_game_state = AsyncMock(return_value=None)

        recovered = await state_manager.recover_game(fake_id)

        # Returns None for nonexistent game
        assert recovered is None
