"""
Shared fixtures for WebSocket handler tests.

Provides reusable mock objects and test utilities for all WebSocket test files.
Follows the pattern established in test_manual_outcome_handlers.py.

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

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

import pendulum

from app.models.game_models import (
    GameState,
    LineupPlayerState,
    TeamLineupState,
    ManualOutcomeSubmission,
)
from app.config.result_charts import PlayOutcome
from app.core.roll_types import AbRoll, RollType
from app.core.play_resolver import PlayResult
from app.core.substitution_manager import SubstitutionResult


# ============================================================================
# CONNECTION MANAGER FIXTURES
# ============================================================================


@pytest.fixture
def mock_manager():
    """
    Mock ConnectionManager with AsyncMock methods for event emission.

    Tracks calls to emit_to_user and broadcast_to_game for assertion.
    """
    manager = MagicMock()
    manager.emit_to_user = AsyncMock()
    manager.broadcast_to_game = AsyncMock()
    manager.join_game = AsyncMock()
    manager.leave_game = AsyncMock()
    manager.connect = AsyncMock()
    manager.disconnect = AsyncMock()
    manager.update_activity = AsyncMock()  # Session activity tracking
    # Tracking dictionaries
    manager.user_sessions = {}
    manager.game_rooms = {}
    return manager


# ============================================================================
# STATE MANAGER FIXTURES
# ============================================================================


@pytest.fixture
def mock_state_manager():
    """
    Mock state_manager singleton with game_lock context manager.

    Provides:
    - get_state() returning mock game state
    - update_state() tracking calls
    - game_lock() as async context manager
    - get_lineup() returning mock lineup
    - recover_game() for state recovery
    """
    state_mgr = MagicMock()
    state_mgr.get_state = MagicMock(return_value=None)
    state_mgr.update_state = MagicMock()
    state_mgr.get_lineup = MagicMock(return_value=None)
    state_mgr.set_lineup = MagicMock()
    state_mgr.recover_game = AsyncMock(return_value=None)

    # Create a proper async context manager for game_lock
    @asynccontextmanager
    async def mock_game_lock(game_id, timeout=30.0):
        yield

    state_mgr.game_lock = mock_game_lock
    return state_mgr


@pytest.fixture
def mock_state_manager_with_timeout():
    """
    Mock state_manager that raises TimeoutError on game_lock.

    Use this to test timeout handling in handlers.
    """
    state_mgr = MagicMock()
    state_mgr.get_state = MagicMock(return_value=None)
    state_mgr.update_state = MagicMock()

    @asynccontextmanager
    async def timeout_lock(game_id, timeout=30.0):
        raise asyncio.TimeoutError(f"Lock timeout for game {game_id}")
        yield  # Never reached, but needed for syntax

    state_mgr.game_lock = timeout_lock
    return state_mgr


# ============================================================================
# GAME ENGINE FIXTURES
# ============================================================================


@pytest.fixture
def mock_game_engine():
    """
    Mock game_engine singleton.

    Provides:
    - resolve_manual_play() for outcome resolution
    - submit_defensive_decision() for defense strategy
    - submit_offensive_decision() for offense strategy
    """
    engine = MagicMock()
    engine.resolve_manual_play = AsyncMock(return_value=None)
    engine.submit_defensive_decision = AsyncMock(return_value=None)
    engine.submit_offensive_decision = AsyncMock(return_value=None)
    engine.start_game = AsyncMock(return_value=None)
    return engine


# ============================================================================
# DICE SYSTEM FIXTURES
# ============================================================================


@pytest.fixture
def mock_dice_system(mock_ab_roll):
    """
    Mock dice_system singleton.

    Returns mock_ab_roll from roll_ab().
    """
    dice = MagicMock()
    dice.roll_ab = MagicMock(return_value=mock_ab_roll)
    return dice


# ============================================================================
# GAME STATE FIXTURES
# ============================================================================


@pytest.fixture
def sample_game_id():
    """Generate a unique game ID for each test."""
    return uuid4()


@pytest.fixture
def mock_game_state(sample_game_id):
    """
    Create a mock active game state with current batter set.

    Returns a GameState configured for:
    - SBA league
    - Active status
    - Inning 1, top half
    - 0 outs, 0-0 score
    - Current batter assigned
    """
    return GameState(
        game_id=sample_game_id,
        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,
        ),
        status="active",
        inning=1,
        half="top",
        outs=0,
        home_score=0,
        away_score=0,
    )


@pytest.fixture
def mock_game_state_with_runners(mock_game_state):
    """
    Create game state with runners on base.

    Runner on first and third (corners situation).
    """
    mock_game_state.runner_on_first = LineupPlayerState(
        lineup_id=2, card_id=200, position="LF", batting_order=2
    )
    mock_game_state.runner_on_third = LineupPlayerState(
        lineup_id=3, card_id=300, position="RF", batting_order=3
    )
    return mock_game_state


# ============================================================================
# ROLL FIXTURES
# ============================================================================


@pytest.fixture
def mock_ab_roll(sample_game_id):
    """
    Create a mock AB roll for dice operations.

    Standard roll values for predictable test behavior.
    """
    return AbRoll(
        roll_id="test_roll_123",
        roll_type=RollType.AB,
        league_id="sba",
        timestamp=pendulum.now("UTC"),
        game_id=sample_game_id,
        d6_one=3,
        d6_two_a=4,
        d6_two_b=3,
        chaos_d20=10,
        resolution_d20=12,
        d6_two_total=7,
        check_wild_pitch=False,
        check_passed_ball=False,
    )


# ============================================================================
# PLAY RESULT FIXTURES
# ============================================================================


@pytest.fixture
def mock_play_result():
    """
    Create a mock play result for outcome resolution.

    Standard groundball out result.
    """
    return PlayResult(
        outcome=PlayOutcome.GROUNDBALL_C,
        outs_recorded=1,
        runs_scored=0,
        batter_result=None,
        runners_advanced=[],
        description="Groundball to shortstop",
        ab_roll=None,
        is_hit=False,
        is_out=True,
        is_walk=False,
    )


@pytest.fixture
def mock_play_result_hit():
    """Create a mock play result for a hit."""
    return PlayResult(
        outcome=PlayOutcome.SINGLE,
        outs_recorded=0,
        runs_scored=0,
        batter_result="1B",
        runners_advanced=[("B", "1B")],
        description="Single to left field",
        ab_roll=None,
        is_hit=True,
        is_out=False,
        is_walk=False,
    )


# ============================================================================
# SUBSTITUTION FIXTURES
# ============================================================================


@pytest.fixture
def mock_substitution_result_success():
    """Create a successful substitution result."""
    return SubstitutionResult(
        success=True,
        player_out_lineup_id=1,
        player_in_card_id=999,
        new_lineup_id=100,
        new_position="CF",
        new_batting_order=1,
        error_message=None,
        error_code=None,
    )


@pytest.fixture
def mock_substitution_result_failure():
    """Create a failed substitution result."""
    return SubstitutionResult(
        success=False,
        player_out_lineup_id=1,
        player_in_card_id=999,
        new_lineup_id=None,
        new_position=None,
        new_batting_order=None,
        error_message="Player not found in roster",
        error_code="PLAYER_NOT_IN_ROSTER",
    )


# ============================================================================
# LINEUP FIXTURES
# ============================================================================


@pytest.fixture
def mock_lineup_state():
    """
    Create a mock team lineup state.

    9 players in standard batting order with positions.
    """
    players = []
    positions = ["C", "1B", "2B", "SS", "3B", "LF", "CF", "RF", "DH"]
    for i in range(9):
        players.append(
            LineupPlayerState(
                lineup_id=i + 1,
                card_id=(i + 1) * 100,
                position=positions[i],
                batting_order=i + 1,
            )
        )
    return TeamLineupState(team_id=1, players=players)


# ============================================================================
# SOCKET.IO SERVER FIXTURES
# ============================================================================


@pytest.fixture
def sio_server():
    """
    Create a bare Socket.io AsyncServer for handler registration.

    Use this when you need to control patching manually.
    """
    from socketio import AsyncServer

    return AsyncServer()


@pytest.fixture
def sio_with_mocks(mock_manager, mock_state_manager, mock_game_engine, mock_dice_system):
    """
    Create Socket.io server with handlers registered and all dependencies mocked.

    This fixture patches all singletons BEFORE registering handlers to ensure
    the handlers capture the mocked versions in their closures.

    Returns a tuple of (sio, patches_dict) where patches_dict contains the
    mock objects for additional configuration in tests.
    """
    from socketio import AsyncServer

    sio = AsyncServer()

    with patch("app.websocket.handlers.state_manager", mock_state_manager), \
         patch("app.websocket.handlers.game_engine", mock_game_engine), \
         patch("app.websocket.handlers.dice_system", mock_dice_system):
        from app.websocket.handlers import register_handlers

        register_handlers(sio, mock_manager)
        yield sio, {
            "manager": mock_manager,
            "state_manager": mock_state_manager,
            "game_engine": mock_game_engine,
            "dice_system": mock_dice_system,
        }


# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================


def get_handler(sio, handler_name: str):
    """
    Get a specific event handler from the Socket.io server.

    Args:
        sio: AsyncServer instance with registered handlers
        handler_name: Name of the event handler (e.g., 'roll_dice')

    Returns:
        The async handler function
    """
    return sio.handlers["/"][handler_name]


def assert_error_emitted(mock_manager, expected_message_substring: str):
    """
    Assert that an error was emitted to the user.

    Args:
        mock_manager: The mock ConnectionManager
        expected_message_substring: Text that should appear in error message
    """
    mock_manager.emit_to_user.assert_called()
    call_args = mock_manager.emit_to_user.call_args
    event_name = call_args[0][1]
    data = call_args[0][2]

    assert event_name in ("error", "substitution_error", "outcome_rejected"), \
        f"Expected error event, got {event_name}"
    assert expected_message_substring in data.get("message", ""), \
        f"Expected '{expected_message_substring}' in message, got: {data}"


def assert_broadcast_sent(mock_manager, event_name: str, game_id: str = None):
    """
    Assert that a broadcast was sent to the game room.

    Args:
        mock_manager: The mock ConnectionManager
        event_name: Expected event name (e.g., 'dice_rolled')
        game_id: Optional game ID to verify destination
    """
    mock_manager.broadcast_to_game.assert_called()
    call_args = mock_manager.broadcast_to_game.call_args
    actual_event = call_args[0][1]

    assert actual_event == event_name, \
        f"Expected broadcast event '{event_name}', got '{actual_event}'"

    if game_id:
        actual_game_id = call_args[0][0]
        assert actual_game_id == game_id, \
            f"Expected game_id '{game_id}', got '{actual_game_id}'"
