"""Tests for GameStateManager.

This module tests the game state persistence layer that manages
the Redis-primary, Postgres-backup storage strategy.
"""

from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch
from uuid import UUID, uuid4

import pytest

from app.core.enums import TurnPhase
from app.core.models.game_state import GameState, PlayerState
from app.db.models.game import EndReason, GameType
from app.services.game_state_manager import ArchiveResult, GameStateManager


@pytest.fixture
def mock_redis() -> AsyncMock:
    """Create a mock RedisHelper.

    The RedisHelper handles cache operations for game state.
    """
    redis = AsyncMock()
    redis.set_json = AsyncMock()
    redis.get_json = AsyncMock(return_value=None)
    redis.delete = AsyncMock(return_value=True)
    redis.exists = AsyncMock(return_value=False)
    return redis


@pytest.fixture
def game_state_manager(mock_redis: AsyncMock) -> GameStateManager:
    """Create a GameStateManager with mock Redis."""
    return GameStateManager(redis=mock_redis)


@pytest.fixture
def sample_game_state() -> GameState:
    """Create a sample game state for testing."""
    player1 = PlayerState(player_id="player-1")
    player2 = PlayerState(player_id="player-2")

    return GameState(
        game_id="game-123",
        players={"player-1": player1, "player-2": player2},
        current_player_id="player-1",
        turn_number=10,
        phase=TurnPhase.MAIN,
        action_log=[{"type": "attack", "index": 0}],
    )


class TestArchiveToHistory:
    """Tests for the archive_to_history method.

    This method handles the complete game ending process:
    - Creates GameHistory record
    - Deletes ActiveGame record
    - Clears Redis cache
    """

    @pytest.mark.asyncio
    async def test_archive_to_history_returns_result(
        self,
        game_state_manager: GameStateManager,
        sample_game_state: GameState,
    ) -> None:
        """Test that archive_to_history returns ArchiveResult.

        The result should contain game metadata and success status.
        """
        from app.db.models.game import ActiveGame

        game_id = str(uuid4())
        sample_game_state.game_id = game_id
        player1_id = uuid4()
        player2_id = uuid4()

        # Create mock ActiveGame
        mock_active_game = MagicMock(spec=ActiveGame)
        mock_active_game.id = UUID(game_id)
        mock_active_game.game_type = GameType.FREEPLAY
        mock_active_game.player1_id = player1_id
        mock_active_game.player2_id = player2_id
        mock_active_game.npc_id = None
        mock_active_game.started_at = datetime.now(UTC)

        # Mock database session
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = mock_active_game
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.delete = AsyncMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            result = await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=player1_id,
                winner_is_npc=False,
                end_reason=EndReason.PRIZES_TAKEN,
                replay_data={"action_log": sample_game_state.action_log},
            )

        assert isinstance(result, ArchiveResult)
        assert result.success is True
        assert result.game_id == game_id
        assert result.history_id == game_id
        assert result.turn_count == 10
        assert result.game_type == GameType.FREEPLAY

    @pytest.mark.asyncio
    async def test_archive_to_history_creates_game_history(
        self,
        game_state_manager: GameStateManager,
        sample_game_state: GameState,
    ) -> None:
        """Test that archive_to_history creates a GameHistory record.

        The GameHistory record should be added to the session with
        all the required fields populated.
        """
        from app.db.models.game import ActiveGame, GameHistory

        game_id = str(uuid4())
        sample_game_state.game_id = game_id
        player1_id = uuid4()

        # Create mock ActiveGame
        mock_active_game = MagicMock(spec=ActiveGame)
        mock_active_game.id = UUID(game_id)
        mock_active_game.game_type = GameType.CAMPAIGN
        mock_active_game.player1_id = player1_id
        mock_active_game.player2_id = None
        mock_active_game.npc_id = "grass_trainer_1"
        mock_active_game.started_at = datetime.now(UTC)

        # Mock database session
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = mock_active_game
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.delete = AsyncMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=player1_id,
                winner_is_npc=False,
                end_reason=EndReason.RESIGNATION,
                replay_data={"action_log": []},
            )

        # Verify GameHistory was added
        mock_session.add.assert_called_once()
        added_obj = mock_session.add.call_args[0][0]
        assert isinstance(added_obj, GameHistory)
        assert added_obj.id == UUID(game_id)
        assert added_obj.game_type == GameType.CAMPAIGN
        assert added_obj.npc_id == "grass_trainer_1"
        assert added_obj.end_reason == EndReason.RESIGNATION

    @pytest.mark.asyncio
    async def test_archive_to_history_deletes_active_game(
        self,
        game_state_manager: GameStateManager,
        sample_game_state: GameState,
    ) -> None:
        """Test that archive_to_history deletes the ActiveGame record.

        After archiving to history, the game should be removed from
        the active games table.
        """
        from app.db.models.game import ActiveGame

        game_id = str(uuid4())
        sample_game_state.game_id = game_id

        # Create mock ActiveGame
        mock_active_game = MagicMock(spec=ActiveGame)
        mock_active_game.id = UUID(game_id)
        mock_active_game.game_type = GameType.FREEPLAY
        mock_active_game.player1_id = uuid4()
        mock_active_game.player2_id = uuid4()
        mock_active_game.npc_id = None
        mock_active_game.started_at = datetime.now(UTC)

        # Mock database session
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = mock_active_game
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.delete = AsyncMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=uuid4(),
                winner_is_npc=False,
                end_reason=EndReason.PRIZES_TAKEN,
            )

        # Verify ActiveGame was deleted
        mock_session.delete.assert_called_once_with(mock_active_game)

    @pytest.mark.asyncio
    async def test_archive_to_history_deletes_from_cache(
        self,
        game_state_manager: GameStateManager,
        mock_redis: AsyncMock,
        sample_game_state: GameState,
    ) -> None:
        """Test that archive_to_history deletes from Redis cache.

        After archiving, the game state should be removed from the
        Redis cache to free up memory.
        """
        from app.db.models.game import ActiveGame

        game_id = str(uuid4())
        sample_game_state.game_id = game_id

        # Create mock ActiveGame
        mock_active_game = MagicMock(spec=ActiveGame)
        mock_active_game.id = UUID(game_id)
        mock_active_game.game_type = GameType.FREEPLAY
        mock_active_game.player1_id = uuid4()
        mock_active_game.player2_id = uuid4()
        mock_active_game.npc_id = None
        mock_active_game.started_at = datetime.now(UTC)

        # Mock database session
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = mock_active_game
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.delete = AsyncMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=uuid4(),
                winner_is_npc=False,
                end_reason=EndReason.TIMEOUT,
            )

        # Verify Redis cache was deleted
        mock_redis.delete.assert_called_once_with(f"game:{game_id}")

    @pytest.mark.asyncio
    async def test_archive_to_history_handles_missing_active_game(
        self,
        game_state_manager: GameStateManager,
        sample_game_state: GameState,
    ) -> None:
        """Test that archive handles case when ActiveGame is not found.

        This shouldn't normally happen, but the method should handle
        it gracefully by using defaults from the game state.
        """
        game_id = str(uuid4())
        sample_game_state.game_id = game_id

        # Mock database session - ActiveGame not found
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = None  # Not found
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            result = await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=None,
                winner_is_npc=False,
                end_reason=EndReason.DRAW,
            )

        # Should still succeed with defaults
        assert result.success is True
        assert result.game_type == GameType.FREEPLAY  # Default
        assert result.duration_seconds == 0  # Default when no started_at

    @pytest.mark.asyncio
    async def test_archive_to_history_npc_winner(
        self,
        game_state_manager: GameStateManager,
        sample_game_state: GameState,
    ) -> None:
        """Test archiving when NPC is the winner.

        In campaign mode, the NPC can win. The winner_is_npc flag
        should be set correctly in GameHistory.
        """
        from app.db.models.game import ActiveGame, GameHistory

        game_id = str(uuid4())
        sample_game_state.game_id = game_id
        player1_id = uuid4()

        # Create mock ActiveGame for campaign
        mock_active_game = MagicMock(spec=ActiveGame)
        mock_active_game.id = UUID(game_id)
        mock_active_game.game_type = GameType.CAMPAIGN
        mock_active_game.player1_id = player1_id
        mock_active_game.player2_id = None
        mock_active_game.npc_id = "fire_leader"
        mock_active_game.started_at = datetime.now(UTC)

        # Mock database session
        mock_session = AsyncMock()
        mock_result = MagicMock()
        mock_result.scalar_one_or_none.return_value = mock_active_game
        mock_session.execute = AsyncMock(return_value=mock_result)
        mock_session.add = MagicMock()
        mock_session.delete = AsyncMock()
        mock_session.flush = AsyncMock()
        mock_session.commit = AsyncMock()

        with patch("app.services.game_state_manager.get_session") as mock_get_session:
            mock_get_session.return_value.__aenter__ = AsyncMock(return_value=mock_session)
            mock_get_session.return_value.__aexit__ = AsyncMock(return_value=None)

            await game_state_manager.archive_to_history(
                game_id=game_id,
                state=sample_game_state,
                winner_id=None,  # NPC won, so no UUID
                winner_is_npc=True,
                end_reason=EndReason.NO_POKEMON,
            )

        # Verify GameHistory has NPC winner flag
        added_obj = mock_session.add.call_args[0][0]
        assert isinstance(added_obj, GameHistory)
        assert added_obj.winner_is_npc is True
        assert added_obj.winner_id is None
