"""
Tests for manual outcome WebSocket handlers.

Tests the complete manual outcome flow:
1. roll_dice - Server rolls and broadcasts
2. submit_manual_outcome - Players submit outcomes from physical cards

Author: Claude
Date: 2025-10-30
"""
import pytest
from uuid import uuid4
from unittest.mock import AsyncMock, MagicMock, patch
from pydantic import ValidationError

from app.models.game_models import GameState, ManualOutcomeSubmission, LineupPlayerState
from app.config.result_charts import PlayOutcome
from app.core.roll_types import AbRoll, RollType
from app.core.play_resolver import PlayResult
import pendulum


# ============================================================================
# FIXTURES
# ============================================================================

@pytest.fixture
def mock_manager():
    """Mock ConnectionManager"""
    manager = MagicMock()
    manager.emit_to_user = AsyncMock()
    manager.broadcast_to_game = AsyncMock()
    manager.update_activity = AsyncMock()
    return manager


@pytest.fixture
def mock_game_state():
    """Create a mock active game state"""
    return GameState(
        game_id=uuid4(),
        league_id="sba",
        home_team_id=1,
        away_team_id=2,
        current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1),
        status="active",
        inning=1,
        half="top",
        outs=0,
        home_score=0,
        away_score=0
    )


@pytest.fixture
def mock_ab_roll():
    """Create a mock AB roll"""
    return AbRoll(
        roll_id="test_roll_123",
        roll_type=RollType.AB,
        league_id="sba",
        timestamp=pendulum.now('UTC'),
        game_id=uuid4(),
        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,
        chaos_check_skipped=True,  # No runners on base in mock_game_state
    )


@pytest.fixture
def mock_play_result():
    """Create a mock play 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,  # Will be filled in
        is_hit=False,
        is_out=True,
        is_walk=False
    )


# ============================================================================
# ROLL_DICE HANDLER TESTS
# ============================================================================

@pytest.mark.asyncio
async def test_roll_dice_success(mock_manager, mock_game_state, mock_ab_roll):
    """Test successful dice roll"""
    from socketio import AsyncServer

    sio = AsyncServer()

    # Patch BEFORE registering handlers (handlers are closures)
    with patch('app.websocket.handlers.state_manager') as mock_state_mgr, \
         patch('app.websocket.handlers.dice_system') as mock_dice:

        mock_state_mgr.get_state.return_value = mock_game_state
        mock_state_mgr.update_state = MagicMock()
        mock_dice.roll_ab.return_value = mock_ab_roll

        # Register handlers AFTER patching
        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)

        # Get the roll_dice handler
        roll_dice_handler = sio.handlers['/']['roll_dice']

        # Call handler
        await roll_dice_handler('test_sid', {"game_id": str(mock_game_state.game_id)})

        # Verify dice rolled (runners_on_base=False since mock_game_state has no runners)
        mock_dice.roll_ab.assert_called_once_with(
            league_id="sba",
            game_id=mock_game_state.game_id,
            runners_on_base=False,
        )

        # Verify state updated with roll
        mock_state_mgr.update_state.assert_called()

        # Verify broadcast
        mock_manager.broadcast_to_game.assert_called_once()
        call_args = mock_manager.broadcast_to_game.call_args
        assert call_args[0][0] == str(mock_game_state.game_id)
        assert call_args[0][1] == "dice_rolled"
        data = call_args[0][2]
        assert data["roll_id"] == "test_roll_123"
        assert data["d6_one"] == 3
        assert data["d6_two_total"] == 7
        assert data["chaos_d20"] == 10


@pytest.mark.asyncio
async def test_roll_dice_missing_game_id(mock_manager):
    """Test roll_dice with missing game_id"""
    from socketio import AsyncServer
    from app.websocket.handlers import register_handlers

    sio = AsyncServer()
    register_handlers(sio, mock_manager)

    roll_dice_handler = sio.handlers['/']['roll_dice']

    # Call without game_id
    await roll_dice_handler('test_sid', {})

    # Verify error emitted
    mock_manager.emit_to_user.assert_called_once()
    call_args = mock_manager.emit_to_user.call_args
    assert call_args[0][0] == 'test_sid'
    assert call_args[0][1] == 'error'
    assert "Missing game_id" in call_args[0][2]["message"]


@pytest.mark.asyncio
async def test_roll_dice_invalid_game_id(mock_manager):
    """Test roll_dice with invalid UUID format"""
    from socketio import AsyncServer
    from app.websocket.handlers import register_handlers

    sio = AsyncServer()
    register_handlers(sio, mock_manager)

    roll_dice_handler = sio.handlers['/']['roll_dice']

    # Call with invalid UUID
    await roll_dice_handler('test_sid', {"game_id": "not-a-uuid"})

    # Verify error emitted
    mock_manager.emit_to_user.assert_called_once()
    assert "Invalid game_id format" in mock_manager.emit_to_user.call_args[0][2]["message"]


@pytest.mark.asyncio
async def test_roll_dice_game_not_found(mock_manager):
    """Test roll_dice when game doesn't exist"""
    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)
        roll_dice_handler = sio.handlers['/']['roll_dice']

        # Call with non-existent game
        await roll_dice_handler('test_sid', {"game_id": str(uuid4())})

        # Verify error emitted
        mock_manager.emit_to_user.assert_called_once()
        assert "not found" in mock_manager.emit_to_user.call_args[0][2]["message"]


# ============================================================================
# SUBMIT_MANUAL_OUTCOME HANDLER TESTS
# ============================================================================

@pytest.mark.asyncio
async def test_submit_manual_outcome_success(mock_manager, mock_game_state, mock_ab_roll, mock_play_result):
    """Test successful manual outcome submission"""
    from socketio import AsyncServer

    sio = AsyncServer()

    # Add pending roll to state
    mock_game_state.pending_manual_roll = mock_ab_roll

    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
        mock_state_mgr.update_state = MagicMock()
        mock_engine.resolve_manual_play = AsyncMock(return_value=mock_play_result)

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        # Submit outcome
        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "groundball_c",
            "hit_location": "SS"
        })

        # Verify acceptance emitted
        assert mock_manager.emit_to_user.call_count == 1
        accept_call = mock_manager.emit_to_user.call_args_list[0]
        assert accept_call[0][1] == "outcome_accepted"

        # Verify game engine called
        mock_engine.resolve_manual_play.assert_called_once()
        call_args = mock_engine.resolve_manual_play.call_args
        assert call_args[1]["game_id"] == mock_game_state.game_id
        assert call_args[1]["ab_roll"] == mock_ab_roll
        assert call_args[1]["outcome"] == PlayOutcome.GROUNDBALL_C
        assert call_args[1]["hit_location"] == "SS"

        # Verify broadcasts (play_resolved and game_state_update)
        assert mock_manager.broadcast_to_game.call_count == 2
        # Check that play_resolved was broadcasted
        calls = mock_manager.broadcast_to_game.call_args_list
        assert any(call[0][1] == "play_resolved" for call in calls)
        assert any(call[0][1] == "game_state_update" for call in calls)


@pytest.mark.asyncio
async def test_submit_manual_outcome_missing_game_id(mock_manager):
    """Test submit with missing game_id"""
    from socketio import AsyncServer
    from app.websocket.handlers import register_handlers

    sio = AsyncServer()
    register_handlers(sio, mock_manager)
    submit_handler = sio.handlers['/']['submit_manual_outcome']

    await submit_handler('test_sid', {"outcome": "groundball_c"})

    # Verify rejection
    mock_manager.emit_to_user.assert_called_once()
    call_args = mock_manager.emit_to_user.call_args
    assert call_args[0][1] == "outcome_rejected"
    assert "Missing game_id" in call_args[0][2]["message"]


@pytest.mark.asyncio
async def test_submit_manual_outcome_missing_outcome(mock_manager, mock_game_state):
    """Test submit with missing outcome"""
    from socketio import AsyncServer

    sio = AsyncServer()

    with patch('app.websocket.handlers.state_manager') as mock_state_mgr:
        mock_state_mgr.get_state.return_value = mock_game_state

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

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

        # Verify rejection
        mock_manager.emit_to_user.assert_called_once()
        assert "Missing outcome" in mock_manager.emit_to_user.call_args[0][2]["message"]


@pytest.mark.asyncio
async def test_submit_manual_outcome_invalid_outcome(mock_manager, mock_game_state):
    """Test submit with invalid outcome value"""
    from socketio import AsyncServer

    sio = AsyncServer()

    with patch('app.websocket.handlers.state_manager') as mock_state_mgr:
        mock_state_mgr.get_state.return_value = mock_game_state

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "invalid_outcome"
        })

        # Verify rejection
        mock_manager.emit_to_user.assert_called_once()
        call_args = mock_manager.emit_to_user.call_args
        assert call_args[0][1] == "outcome_rejected"
        assert "outcome" in call_args[0][2]["field"]


@pytest.mark.asyncio
async def test_submit_manual_outcome_invalid_location(mock_manager, mock_game_state):
    """Test submit with invalid hit_location"""
    from socketio import AsyncServer

    sio = AsyncServer()

    with patch('app.websocket.handlers.state_manager') as mock_state_mgr:
        mock_state_mgr.get_state.return_value = mock_game_state

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "groundball_c",
            "hit_location": "INVALID"
        })

        # Verify rejection
        mock_manager.emit_to_user.assert_called_once()
        assert "hit_location" in mock_manager.emit_to_user.call_args[0][2]["field"]


@pytest.mark.asyncio
async def test_submit_manual_outcome_missing_required_location(mock_manager, mock_game_state, mock_ab_roll, mock_play_result):
    """Test submit groundball without hit_location (now optional/accepted)"""
    from socketio import AsyncServer

    sio = AsyncServer()

    # Add pending roll to state
    mock_game_state.pending_manual_roll = mock_ab_roll

    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
        mock_state_mgr.update_state = MagicMock()
        mock_engine.resolve_manual_play = AsyncMock(return_value=mock_play_result)

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        # Submit groundball without location (hit_location is now optional)
        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "groundball_c"
            # No hit_location - validation changed to make it optional
        })

        # Verify acceptance (behavior changed - hit_location no longer required)
        assert mock_manager.emit_to_user.call_count >= 1
        accept_call = mock_manager.emit_to_user.call_args_list[0]
        assert accept_call[0][1] == "outcome_accepted"


@pytest.mark.asyncio
async def test_submit_manual_outcome_no_pending_roll(mock_manager, mock_game_state):
    """Test submit when no dice have been rolled"""
    from socketio import AsyncServer

    sio = AsyncServer()

    # State without pending roll
    mock_game_state.pending_manual_roll = None

    with patch('app.websocket.handlers.state_manager') as mock_state_mgr:
        mock_state_mgr.get_state.return_value = mock_game_state

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "groundball_c",
            "hit_location": "SS"
        })

        # Verify rejection
        mock_manager.emit_to_user.assert_called_once()
        assert "No pending dice roll" in mock_manager.emit_to_user.call_args[0][2]["message"]


@pytest.mark.asyncio
async def test_submit_manual_outcome_walk_no_location(mock_manager, mock_game_state, mock_ab_roll, mock_play_result):
    """Test submitting walk (doesn't require hit_location)"""
    from socketio import AsyncServer

    sio = AsyncServer()

    mock_game_state.pending_manual_roll = mock_ab_roll
    mock_play_result.outcome = PlayOutcome.WALK

    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
        mock_state_mgr.update_state = MagicMock()
        mock_engine.resolve_manual_play = AsyncMock(return_value=mock_play_result)

        from app.websocket.handlers import register_handlers
        register_handlers(sio, mock_manager)
        submit_handler = sio.handlers['/']['submit_manual_outcome']

        # Submit walk without location (valid)
        await submit_handler('test_sid', {
            "game_id": str(mock_game_state.game_id),
            "outcome": "walk"
        })

        # Should succeed
        assert mock_manager.emit_to_user.call_args[0][1] == "outcome_accepted"
        mock_engine.resolve_manual_play.assert_called_once()


# ============================================================================
# SUMMARY
# ============================================================================

"""
Test Coverage:

roll_dice (5 tests):
✅ Successful dice roll and broadcast
✅ Missing game_id
✅ Invalid game_id format
✅ Game not found
✅ Dice roll storage in game state

submit_manual_outcome (10 tests):
✅ Successful outcome submission and play resolution
✅ Missing game_id
✅ Missing outcome
✅ Invalid outcome value
✅ Invalid hit_location value
✅ Missing required hit_location (groundballs)
✅ No pending dice roll
✅ Walk without location (valid)
✅ Outcome acceptance broadcast
✅ Play result broadcast

Total: 15 tests covering all major paths and edge cases
"""
