""" Integration test for X-Check WebSocket flow. Tests the complete flow from dice roll to X-Check result broadcast. Author: Claude Date: 2025-11-03 Phase: 3E-Final """ import pytest import json from uuid import uuid4 from unittest.mock import Mock, patch, AsyncMock import pendulum from app.websocket.handlers import register_handlers from app.websocket.connection_manager import ConnectionManager from app.core.state_manager import state_manager from app.core.game_engine import game_engine from app.models.game_models import GameState, LineupPlayerState from app.models.player_models import PositionRating # Import for forward reference resolution from app.config import PlayOutcome # Rebuild GameState model to resolve forward references GameState.model_rebuild() @pytest.mark.integration class TestXCheckWebSocket: """Test X-Check integration with WebSocket handlers""" @pytest.fixture def mock_sio(self): """Mock Socket.io server""" sio = Mock() sio.emit = AsyncMock() return sio @pytest.fixture def mock_manager(self): """Mock ConnectionManager""" manager = Mock(spec=ConnectionManager) manager.user_sessions = {} manager.game_rooms = {} manager.emit_to_user = AsyncMock() manager.broadcast_to_game = AsyncMock() return manager @pytest.fixture def game_state(self): """Create test game state""" game_id = uuid4() # Create batter batter = LineupPlayerState( lineup_id=10, card_id=123, position="RF", batting_order=3 ) # Create pitcher pitcher = LineupPlayerState( lineup_id=20, card_id=456, position="P", batting_order=9 ) # Create catcher catcher = LineupPlayerState( lineup_id=21, card_id=789, position="C", batting_order=2 ) state = GameState( game_id=game_id, league_id="pd", # PD league has position ratings home_team_id=1, away_team_id=2, current_batter=batter, current_pitcher=pitcher, current_catcher=catcher ) # Clear bases state.on_first = None state.on_second = None state.on_third = None # Register state in state manager state_manager._states[game_id] = state return state async def test_submit_manual_xcheck_outcome(self, mock_sio, mock_manager, game_state): """ Test submitting manual X-Check outcome via WebSocket. Flow: 1. Create game with position ratings loaded 2. Roll dice (stores pending_manual_roll) 3. Submit X-Check outcome 4. Verify play_resolved event includes x_check_details """ game_id = game_state.game_id # Register handlers register_handlers(mock_sio, mock_manager) # Get the submit_manual_outcome handler # It's registered as the 5th event (after connect, disconnect, join_game, leave_game, heartbeat, roll_dice) handler_calls = [call for call in mock_sio.event.call_args_list] submit_handler = None for call in handler_calls: if len(call[0]) > 0 and hasattr(call[0][0], '__name__'): if call[0][0].__name__ == 'submit_manual_outcome': submit_handler = call[0][0] break assert submit_handler is not None, "submit_manual_outcome handler not found" # Mock pending roll (simulate roll_dice was called) from app.core.roll_types import AbRoll ab_roll = AbRoll( roll_id="test-roll-123", roll_type="AB", league_id="pd", game_id=game_id, d6_one=4, d6_two_a=3, d6_two_b=4, chaos_d20=12, resolution_d20=8, timestamp=pendulum.now('UTC') ) game_state.pending_manual_roll = ab_roll state_manager.update_state(game_id, game_state) # Mock session sid = "test-session-123" mock_manager.user_sessions[sid] = "test-user" # Submit X-Check outcome data = { "game_id": str(game_id), "outcome": "x_check", "hit_location": "SS" } # Mock game engine resolve_manual_play to return X-Check result with patch('app.websocket.handlers.game_engine.resolve_manual_play') as mock_resolve: from app.models.game_models import XCheckResult from app.core.play_resolver import PlayResult # Create mock X-Check result xcheck_result = XCheckResult( position="SS", d20_roll=12, d6_roll=10, defender_range=4, defender_error_rating=12, defender_id=25, base_result="G2", converted_result="G2", error_result="NO", final_outcome=PlayOutcome.GROUNDBALL_B, hit_type="g2_no_error" ) play_result = PlayResult( outcome=PlayOutcome.GROUNDBALL_B, outs_recorded=1, runs_scored=0, batter_result=None, runners_advanced=[], description="X-Check SS: G2 → G2 + NO = groundball_b", ab_roll=ab_roll, hit_location="SS", is_hit=False, is_out=True, x_check_details=xcheck_result ) mock_resolve.return_value = play_result # Call handler await submit_handler(sid, data) # Verify outcome_accepted was emitted to user assert mock_manager.emit_to_user.called emit_calls = mock_manager.emit_to_user.call_args_list accepted_call = None for call in emit_calls: if call[0][1] == "outcome_accepted": accepted_call = call break assert accepted_call is not None, "outcome_accepted not emitted" # Verify play_resolved was broadcast with X-Check details assert mock_manager.broadcast_to_game.called broadcast_calls = mock_manager.broadcast_to_game.call_args_list play_resolved_call = None for call in broadcast_calls: if call[0][1] == "play_resolved": play_resolved_call = call break assert play_resolved_call is not None, "play_resolved not broadcast" # Extract broadcast data broadcast_data = play_resolved_call[0][2] # Verify standard play data assert broadcast_data["outcome"] == "groundball_b" assert broadcast_data["hit_location"] == "SS" assert broadcast_data["outs_recorded"] == 1 assert broadcast_data["runs_scored"] == 0 assert broadcast_data["is_out"] is True # Verify X-Check details are included assert "x_check_details" in broadcast_data, "x_check_details missing from broadcast" xcheck_data = broadcast_data["x_check_details"] # Verify X-Check structure assert xcheck_data["position"] == "SS" assert xcheck_data["d20_roll"] == 12 assert xcheck_data["d6_roll"] == 10 assert xcheck_data["defender_range"] == 4 assert xcheck_data["defender_error_rating"] == 12 assert xcheck_data["defender_id"] == 25 assert xcheck_data["base_result"] == "G2" assert xcheck_data["converted_result"] == "G2" assert xcheck_data["error_result"] == "NO" assert xcheck_data["final_outcome"] == "groundball_b" assert xcheck_data["hit_type"] == "g2_no_error" # Verify optional SPD test fields assert xcheck_data["spd_test_roll"] is None assert xcheck_data["spd_test_target"] is None assert xcheck_data["spd_test_passed"] is None async def test_non_xcheck_play_has_no_xcheck_details(self, mock_sio, mock_manager, game_state): """ Test that non-X-Check plays don't include x_check_details. """ game_id = game_state.game_id # Register handlers register_handlers(mock_sio, mock_manager) # Get the submit_manual_outcome handler handler_calls = [call for call in mock_sio.event.call_args_list] submit_handler = None for call in handler_calls: if len(call[0]) > 0 and hasattr(call[0][0], '__name__'): if call[0][0].__name__ == 'submit_manual_outcome': submit_handler = call[0][0] break assert submit_handler is not None # Mock pending roll from app.core.roll_types import AbRoll ab_roll = AbRoll( roll_id="test-roll-124", roll_type="AB", league_id="pd", game_id=game_id, d6_one=5, d6_two_a=6, d6_two_b=6, chaos_d20=20, resolution_d20=20, timestamp=pendulum.now('UTC') ) game_state.pending_manual_roll = ab_roll state_manager.update_state(game_id, game_state) # Mock session sid = "test-session-124" mock_manager.user_sessions[sid] = "test-user" # Submit strikeout (not X-Check) data = { "game_id": str(game_id), "outcome": "strikeout", "hit_location": None } # Mock game engine resolve_manual_play to return strikeout result with patch('app.websocket.handlers.game_engine.resolve_manual_play') as mock_resolve: from app.core.play_resolver import PlayResult play_result = PlayResult( outcome=PlayOutcome.STRIKEOUT, outs_recorded=1, runs_scored=0, batter_result=None, runners_advanced=[], description="Strikeout looking", ab_roll=ab_roll, hit_location=None, is_hit=False, is_out=True, x_check_details=None # No X-Check for strikeout ) mock_resolve.return_value = play_result # Call handler await submit_handler(sid, data) # Verify play_resolved was broadcast assert mock_manager.broadcast_to_game.called broadcast_calls = mock_manager.broadcast_to_game.call_args_list play_resolved_call = None for call in broadcast_calls: if call[0][1] == "play_resolved": play_resolved_call = call break assert play_resolved_call is not None # Extract broadcast data broadcast_data = play_resolved_call[0][2] # Verify X-Check details are NOT included for non-X-Check plays assert "x_check_details" not in broadcast_data, \ "x_check_details should not be present for non-X-Check plays"