"""
Tests for WebSocket query and decision handlers.

Verifies lineup retrieval (get_lineup), box score (get_box_score),
and strategic decision submission handlers (submit_defensive_decision,
submit_offensive_decision).

Author: Claude
Date: 2025-01-27
"""

import pytest
from uuid import uuid4
from unittest.mock import AsyncMock, MagicMock, patch


# ============================================================================
# GET LINEUP HANDLER TESTS
# ============================================================================


class TestGetLineupHandler:
    """Tests for the get_lineup event handler."""

    @pytest.mark.asyncio
    async def test_get_lineup_from_cache(self, mock_manager, mock_lineup_state):
        """
        Verify get_lineup() returns cached lineup when available.

        StateManager caches lineups for fast O(1) lookup. When lineup is
        in cache, handler should return it without hitting database.
        """
        from socketio import AsyncServer

        sio = AsyncServer()
        game_id = uuid4()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
            mock_state_mgr.get_lineup.return_value = mock_lineup_state

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["get_lineup"]

            await handler("test_sid", {"game_id": str(game_id), "team_id": 1})

            mock_state_mgr.get_lineup.assert_called_once_with(game_id, 1)
            mock_manager.emit_to_user.assert_called_once()

            call_args = mock_manager.emit_to_user.call_args
            assert call_args[0][1] == "lineup_data"
            assert call_args[0][2]["team_id"] == 1

    @pytest.mark.asyncio
    async def test_get_lineup_from_db_when_not_cached(
        self, mock_manager, mock_lineup_state, mock_game_state
    ):
        """
        Verify get_lineup() loads from database when not in cache.

        When lineup is not cached, handler should load from database
        via lineup_service and cache the result.
        """
        from socketio import AsyncServer

        sio = AsyncServer()
        game_id = uuid4()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
             patch("app.websocket.handlers.lineup_service") as mock_lineup_svc:
            mock_state_mgr.get_lineup.return_value = None  # Not cached
            mock_state_mgr.get_state.return_value = mock_game_state
            mock_lineup_svc.load_team_lineup_with_player_data = AsyncMock(
                return_value=mock_lineup_state
            )

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["get_lineup"]

            await handler("test_sid", {"game_id": str(game_id), "team_id": 1})

            mock_lineup_svc.load_team_lineup_with_player_data.assert_called_once()
            mock_state_mgr.set_lineup.assert_called_once()  # Cache it

    @pytest.mark.asyncio
    async def test_get_lineup_missing_game_id(self, mock_manager):
        """
        Verify get_lineup() returns error when game_id missing.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        from app.websocket.handlers import register_handlers

        register_handlers(sio, mock_manager)
        handler = sio.handlers["/"]["get_lineup"]

        await handler("test_sid", {"team_id": 1})

        mock_manager.emit_to_user.assert_called_once()
        call_args = mock_manager.emit_to_user.call_args
        assert call_args[0][1] == "error"
        assert "game_id" in call_args[0][2]["message"].lower()

    @pytest.mark.asyncio
    async def test_get_lineup_missing_team_id(self, mock_manager):
        """
        Verify get_lineup() returns error when team_id missing.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        from app.websocket.handlers import register_handlers

        register_handlers(sio, mock_manager)
        handler = sio.handlers["/"]["get_lineup"]

        await handler("test_sid", {"game_id": str(uuid4())})

        mock_manager.emit_to_user.assert_called_once()
        call_args = mock_manager.emit_to_user.call_args
        assert call_args[0][1] == "error"
        assert "team_id" in call_args[0][2]["message"].lower()


# ============================================================================
# GET BOX SCORE HANDLER TESTS
# ============================================================================


class TestGetBoxScoreHandler:
    """Tests for the get_box_score event handler."""

    @pytest.mark.asyncio
    async def test_get_box_score_success(self, mock_manager):
        """
        Verify get_box_score() returns box score data from service.

        Handler should fetch box score from materialized views via
        box_score_service and emit to requester.
        """
        from socketio import AsyncServer

        sio = AsyncServer()
        game_id = uuid4()
        mock_box_score = {
            "home_team": {"runs": 5, "hits": 10, "errors": 1},
            "away_team": {"runs": 3, "hits": 7, "errors": 2},
        }

        # box_score_service is imported inside the handler via:
        # from app.services import box_score_service
        with patch("app.services.box_score_service") as mock_service:
            mock_service.get_box_score = AsyncMock(return_value=mock_box_score)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["get_box_score"]

            await handler("test_sid", {"game_id": str(game_id)})

            mock_service.get_box_score.assert_called_once_with(game_id)
            mock_manager.emit_to_user.assert_called_once()

            call_args = mock_manager.emit_to_user.call_args
            assert call_args[0][1] == "box_score_data"
            assert call_args[0][2]["box_score"] == mock_box_score

    @pytest.mark.asyncio
    async def test_get_box_score_not_found(self, mock_manager):
        """
        Verify get_box_score() returns error when no data found.

        When materialized views return no data (e.g., game hasn't started),
        handler should emit error with helpful hint about migrations.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        # box_score_service is imported inside the handler
        with patch("app.services.box_score_service") as mock_service:
            mock_service.get_box_score = AsyncMock(return_value=None)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["get_box_score"]

            await handler("test_sid", {"game_id": str(uuid4())})

            mock_manager.emit_to_user.assert_called_once()
            call_args = mock_manager.emit_to_user.call_args
            assert call_args[0][1] == "error"

    @pytest.mark.asyncio
    async def test_get_box_score_missing_game_id(self, mock_manager):
        """
        Verify get_box_score() returns error when game_id missing.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        from app.websocket.handlers import register_handlers

        register_handlers(sio, mock_manager)
        handler = sio.handlers["/"]["get_box_score"]

        await handler("test_sid", {})

        mock_manager.emit_to_user.assert_called_once()
        call_args = mock_manager.emit_to_user.call_args
        assert call_args[0][1] == "error"


# ============================================================================
# SUBMIT DEFENSIVE DECISION HANDLER TESTS
# ============================================================================


class TestSubmitDefensiveDecisionHandler:
    """Tests for the submit_defensive_decision event handler."""

    @pytest.mark.asyncio
    async def test_submit_defensive_decision_success(
        self, mock_manager, mock_game_state
    ):
        """
        Verify submit_defensive_decision() processes and broadcasts decision.

        Handler should create DefensiveDecision, submit to game engine,
        and broadcast to game room so both teams see the defense strategy.

        Valid values:
        - infield_depth: infield_in, normal, corners_in
        - outfield_depth: normal, shallow
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
             patch("app.websocket.handlers.game_engine") as mock_engine:
            mock_state_mgr.get_state.return_value = mock_game_state
            updated_state = MagicMock()
            updated_state.pending_decision = None
            mock_engine.submit_defensive_decision = AsyncMock(return_value=updated_state)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_defensive_decision"]

            await handler(
                "test_sid",
                {
                    "game_id": str(mock_game_state.game_id),
                    "infield_depth": "infield_in",  # Valid: infield_in, normal, corners_in
                    "outfield_depth": "shallow",     # Valid: normal, shallow
                    "hold_runners": [1],
                },
            )

            mock_engine.submit_defensive_decision.assert_called_once()
            mock_manager.broadcast_to_game.assert_called_once()

            call_args = mock_manager.broadcast_to_game.call_args
            assert call_args[0][1] == "defensive_decision_submitted"
            assert call_args[0][2]["decision"]["infield_depth"] == "infield_in"

    @pytest.mark.asyncio
    async def test_submit_defensive_decision_uses_defaults(
        self, mock_manager, mock_game_state
    ):
        """
        Verify submit_defensive_decision() uses default values when not provided.

        Handler should default to normal alignment, normal depth, no hold runners
        when these fields are omitted from the request.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
             patch("app.websocket.handlers.game_engine") as mock_engine:
            mock_state_mgr.get_state.return_value = mock_game_state
            updated_state = MagicMock()
            updated_state.pending_decision = None
            mock_engine.submit_defensive_decision = AsyncMock(return_value=updated_state)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_defensive_decision"]

            await handler("test_sid", {"game_id": str(mock_game_state.game_id)})

            call_args = mock_manager.broadcast_to_game.call_args
            decision = call_args[0][2]["decision"]
            assert decision["alignment"] == "normal"
            assert decision["infield_depth"] == "normal"
            assert decision["outfield_depth"] == "normal"
            assert decision["hold_runners"] == []

    @pytest.mark.asyncio
    async def test_submit_defensive_decision_game_not_found(self, mock_manager):
        """
        Verify submit_defensive_decision() returns error when game not found.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
            mock_state_mgr.get_state.return_value = None

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_defensive_decision"]

            await handler("test_sid", {"game_id": str(uuid4())})

            mock_manager.emit_to_user.assert_called_once()
            call_args = mock_manager.emit_to_user.call_args
            assert call_args[0][1] == "error"
            assert "not found" in call_args[0][2]["message"].lower()


# ============================================================================
# SUBMIT OFFENSIVE DECISION HANDLER TESTS
# ============================================================================


class TestSubmitOffensiveDecisionHandler:
    """Tests for the submit_offensive_decision event handler."""

    @pytest.mark.asyncio
    async def test_submit_offensive_decision_success(
        self, mock_manager, mock_game_state
    ):
        """
        Verify submit_offensive_decision() processes and broadcasts decision.

        Handler should create OffensiveDecision with action and steal attempts,
        submit to game engine, and broadcast to game room.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
             patch("app.websocket.handlers.game_engine") as mock_engine:
            mock_state_mgr.get_state.return_value = mock_game_state
            updated_state = MagicMock()
            updated_state.pending_decision = None
            mock_engine.submit_offensive_decision = AsyncMock(return_value=updated_state)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_offensive_decision"]

            await handler(
                "test_sid",
                {
                    "game_id": str(mock_game_state.game_id),
                    "action": "steal",
                    "steal_attempts": [2],
                },
            )

            mock_engine.submit_offensive_decision.assert_called_once()
            mock_manager.broadcast_to_game.assert_called_once()

            call_args = mock_manager.broadcast_to_game.call_args
            assert call_args[0][1] == "offensive_decision_submitted"
            assert call_args[0][2]["decision"]["action"] == "steal"
            assert call_args[0][2]["decision"]["steal_attempts"] == [2]

    @pytest.mark.asyncio
    async def test_submit_offensive_decision_defaults_to_swing_away(
        self, mock_manager, mock_game_state
    ):
        """
        Verify submit_offensive_decision() defaults to swing_away action.

        When no action is provided, handler should default to swing_away
        which is the most common offensive approach.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr, \
             patch("app.websocket.handlers.game_engine") as mock_engine:
            mock_state_mgr.get_state.return_value = mock_game_state
            updated_state = MagicMock()
            updated_state.pending_decision = None
            mock_engine.submit_offensive_decision = AsyncMock(return_value=updated_state)

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_offensive_decision"]

            await handler("test_sid", {"game_id": str(mock_game_state.game_id)})

            call_args = mock_manager.broadcast_to_game.call_args
            decision = call_args[0][2]["decision"]
            assert decision["action"] == "swing_away"
            assert decision["steal_attempts"] == []

    @pytest.mark.asyncio
    async def test_submit_offensive_decision_game_not_found(self, mock_manager):
        """
        Verify submit_offensive_decision() returns error when game not found.
        """
        from socketio import AsyncServer

        sio = AsyncServer()

        with patch("app.websocket.handlers.state_manager") as mock_state_mgr:
            mock_state_mgr.get_state.return_value = None

            from app.websocket.handlers import register_handlers

            register_handlers(sio, mock_manager)
            handler = sio.handlers["/"]["submit_offensive_decision"]

            await handler("test_sid", {"game_id": str(uuid4())})

            mock_manager.emit_to_user.assert_called_once()
            call_args = mock_manager.emit_to_user.call_args
            assert call_args[0][1] == "error"
            assert "not found" in call_args[0][2]["message"].lower()
