""" Integration Tests for GameEngine Tests complete game flow including: - Single at-bat execution - Full half-inning (3 outs) - Lineup validation - Snapshot tracking - Batting order cycling These tests require database access (marked with @pytest.mark.integration). """ import pytest from uuid import uuid4 from app.core.state_manager import state_manager from app.core.game_engine import game_engine from app.core.validators import ValidationError from app.database.operations import DatabaseOperations from app.models.game_models import DefensiveDecision, OffensiveDecision @pytest.mark.integration @pytest.mark.asyncio class TestSingleAtBat: """Test single at-bat execution""" async def test_complete_at_bat_flow(self): """Test complete at-bat flow: create → start → decisions → resolve""" # Create game game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create dummy lineups positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] # Away team lineup for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) # Home team lineup for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) # Create in state manager state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) assert state.status == "pending" # Start game state = await game_engine.start_game(game_id) assert state.status == "active" assert state.inning == 1 assert state.half == "top" assert state.outs == 0 # Submit defensive decision def_decision = DefensiveDecision( alignment="normal", infield_depth="normal", outfield_depth="normal" ) state = await game_engine.submit_defensive_decision(game_id, def_decision) assert state.pending_decision == "offensive" # Submit offensive decision off_decision = OffensiveDecision(approach="normal") state = await game_engine.submit_offensive_decision(game_id, off_decision) assert state.pending_decision == "resolution" # Resolve play result = await game_engine.resolve_play(game_id) assert result is not None assert result.outcome is not None assert result.ab_roll is not None # Verify state updated final_state = await game_engine.get_game_state(game_id) assert final_state.play_count == 1 assert final_state.last_play_result is not None @pytest.mark.integration @pytest.mark.asyncio class TestFullInning: """Test complete half-inning execution""" async def test_full_half_inning_three_outs(self): """Test playing until 3 outs completes half-inning""" # Create and start game game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create dummy lineups positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) await game_engine.start_game(game_id) at_bat_count = 0 initial_inning = 1 initial_half = "top" # Play until 3 outs while True: state = await game_engine.get_game_state(game_id) # Check if inning changed if state.inning != initial_inning or state.half != initial_half: break # Safety check if at_bat_count > 50: pytest.fail("Safety limit reached - something wrong with inning advancement") # Submit decisions await game_engine.submit_defensive_decision(game_id, DefensiveDecision(alignment="normal")) await game_engine.submit_offensive_decision(game_id, OffensiveDecision(approach="normal")) # Resolve result = await game_engine.resolve_play(game_id) at_bat_count += 1 # Verify inning advanced final_state = await game_engine.get_game_state(game_id) assert final_state.inning == 1 assert final_state.half == "bottom" assert final_state.outs == 0 # Reset after inning change assert at_bat_count >= 3 # At least 3 at-bats for 3 outs @pytest.mark.integration @pytest.mark.asyncio class TestLineupValidation: """Test lineup validation at game start""" async def test_start_game_fails_with_no_lineups(self): """Test starting game with no lineups fails""" game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) # Should raise ValidationError with pytest.raises(ValidationError) as exc_info: await game_engine.start_game(game_id) assert "lineup incomplete" in str(exc_info.value).lower() async def test_start_game_fails_with_incomplete_lineup(self): """Test starting game with incomplete lineup (< 9 players) fails""" game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Add only 5 players per team for i in range(1, 6): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=["P", "C", "1B", "2B", "3B"][i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=["P", "C", "1B", "2B", "3B"][i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) # Should raise ValidationError with pytest.raises(ValidationError) as exc_info: await game_engine.start_game(game_id) assert "lineup incomplete" in str(exc_info.value).lower() assert "5 players" in str(exc_info.value) # Shows actual count async def test_start_game_fails_with_missing_positions(self): """Test starting game with missing defensive positions fails""" game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) # Create 9 players but missing SS positions = ["P", "C", "1B", "2B", "3B", "LF", "CF", "RF", "DH"] # Missing SS, has DH for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) # Should raise ValidationError with pytest.raises(ValidationError) as exc_info: await game_engine.start_game(game_id) assert "missing active player at ss" in str(exc_info.value).lower() @pytest.mark.integration @pytest.mark.asyncio class TestSnapshotTracking: """Test snapshot tracking in GameState""" async def test_snapshot_fields_populated(self): """Test that snapshot fields are populated in GameState""" # Create game with lineups game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) await game_engine.start_game(game_id) # Check snapshot fields after game start state = await game_engine.get_game_state(game_id) assert state.current_batter is not None assert state.current_pitcher is not None assert state.current_catcher is not None assert state.current_on_base_code == 0 # Empty bases async def test_on_base_code_calculation(self): """Test on_base_code matches runners""" # Create game and play until we have runners game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) await game_engine.start_game(game_id) # Play until we get runners on base for _ in range(20): state = await game_engine.get_game_state(game_id) await game_engine.submit_defensive_decision(game_id, DefensiveDecision()) await game_engine.submit_offensive_decision(game_id, OffensiveDecision()) await game_engine.resolve_play(game_id) state = await game_engine.get_game_state(game_id) all_runners = state.get_all_runners() if all_runners: # Verify on_base_code matches runners expected_code = 0 for base, runner in all_runners: if base == 1: expected_code |= 1 elif base == 2: expected_code |= 2 elif base == 3: expected_code |= 4 assert state.current_on_base_code == expected_code break @pytest.mark.integration @pytest.mark.asyncio class TestBattingOrderCycling: """Test batting order cycles independently per team""" async def test_independent_batting_order_indices(self): """Test that each team tracks batting order independently""" # Create game game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) state = await game_engine.start_game(game_id) # Check initial indices assert state.away_team_batter_idx == 1 # Advanced from 0 during start_game assert state.home_team_batter_idx == 0 # Not advanced yet (top of 1st) # After first play, away_team should advance to 2 await game_engine.submit_defensive_decision(game_id, DefensiveDecision()) await game_engine.submit_offensive_decision(game_id, OffensiveDecision()) await game_engine.resolve_play(game_id) state = await game_engine.get_game_state(game_id) # After first play, away idx advances again (now at 2) # Home idx still at 0 (bottom hasn't started) assert state.away_team_batter_idx == 2 assert state.home_team_batter_idx == 0 async def test_batting_order_wraps_at_nine(self): """Test batting order wraps from 8 back to 0""" # Create game game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) # Manually set away_team_batter_idx to 8 state.away_team_batter_idx = 8 state_manager.update_state(game_id, state) await game_engine.start_game(game_id) # After start_game, idx should wrap from 8 to 0 (8+1 % 9 = 0) state = await game_engine.get_game_state(game_id) assert state.away_team_batter_idx == 0 # Wrapped around @pytest.mark.integration @pytest.mark.asyncio class TestGameCompletion: """Test game completion conditions""" async def test_game_completes_after_9_innings(self): """Test game status changes to completed when game ends""" # This is a longer test - we'll fast-forward to end game_id = uuid4() db_ops = DatabaseOperations() await db_ops.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2, game_mode="friendly", visibility="public" ) positions = ["P", "C", "1B", "2B", "3B", "SS", "LF", "CF", "RF"] for i in range(1, 10): await db_ops.add_sba_lineup_player( game_id=game_id, team_id=2, player_id=100 + i, position=positions[i-1], batting_order=i ) await db_ops.add_sba_lineup_player( game_id=game_id, team_id=1, player_id=200 + i, position=positions[i-1], batting_order=i ) state = await state_manager.create_game( game_id=game_id, league_id="sba", home_team_id=1, away_team_id=2 ) await game_engine.start_game(game_id) # Fast-forward: Manually set to bottom 9th with home team ahead state = await game_engine.get_game_state(game_id) state.inning = 9 state.half = "bottom" state.home_score = 5 state.away_score = 2 state.outs = 2 # 2 outs state_manager.update_state(game_id, state) # Play one more out (should end game) await game_engine.submit_defensive_decision(game_id, DefensiveDecision()) await game_engine.submit_offensive_decision(game_id, OffensiveDecision()) await game_engine.resolve_play(game_id) # Game should be completed final_state = await game_engine.get_game_state(game_id) assert final_state.status == "completed"