""" AIService Integration Tests Tests AIService with real database interactions, complex game scenarios, and end-to-end business logic flows that aren't covered by unit tests. These tests focus on: 1. Real database queries and relationships 2. Complex conditional logic branches 3. Error handling with actual data 4. Cross-service interactions 5. Edge cases with realistic game states """ import pytest from sqlmodel import select from app.models.ai_responses import JumpResponse, TagResponse, ThrowResponse, DefenseResponse from app.models.manager_ai import ManagerAi from app.models.player import Player from app.models.position_rating import PositionRating from app.services.ai_service import AIService from tests.factories.manager_ai_factory import ManagerAiFactory from tests.factories.player_factory import PlayerFactory class TestAIServiceDatabaseIntegration: """Test AIService with real database interactions.""" def test_check_steal_opportunity_with_real_database_queries(self, db_session): """Test steal decisions with actual database queries for catcher defense.""" # Create real game scenario with catcher and pitcher catcher = PlayerFactory.create_catcher(db_session, name="Real Catcher") # Create actual position rating for catcher catcher_defense = PositionRating( player_id=catcher.id, variant=0, position='C', arm=8, # Strong arm range=6, error=2 ) db_session.add(catcher_defense) db_session.commit() # Create manager AI and mock game with realistic setup aggressive_ai = ManagerAiFactory.create(db_session, steal=9, running=8) mock_game = self._create_realistic_game_mock(catcher, db_session) ai_service = AIService(db_session) # Test steal to second with real database query result = ai_service.check_steal_opportunity(aggressive_ai, mock_game, 2) assert isinstance(result, JumpResponse) assert result.min_safe is not None # Should factor in real catcher arm strength (8) assert hasattr(result, 'run_if_auto_jump') def test_steal_opportunity_complex_conditional_branches(self, db_session): """Test uncovered conditional branches in steal logic.""" # Test case: steal > 8 and run_diff <= 5 (line 91) high_steal_ai = ManagerAiFactory.create(db_session, steal=9) catcher = PlayerFactory.create_catcher(db_session) self._create_catcher_defense(catcher, db_session, arm=5) mock_game = self._create_realistic_game_mock(catcher, db_session) mock_game.current_play_or_none.return_value.ai_run_diff = 2 # <= 5 mock_game.current_play_or_none.return_value.outs = 1 ai_service = AIService(db_session) result = ai_service.check_steal_opportunity(high_steal_ai, mock_game, 2) # Should hit line 91: min_safe = 13 + num_outs = 14 assert result.min_safe == 14 def test_steal_opportunity_edge_case_branches(self, db_session): """Test edge case branches that are uncovered.""" # Test case: steal > 6 and run_diff <= 5 (line 93) medium_steal_ai = ManagerAiFactory.create(db_session, steal=7) catcher = PlayerFactory.create_catcher(db_session) self._create_catcher_defense(catcher, db_session, arm=4) mock_game = self._create_realistic_game_mock(catcher, db_session) mock_game.current_play_or_none.return_value.ai_run_diff = 3 mock_game.current_play_or_none.return_value.outs = 0 ai_service = AIService(db_session) result = ai_service.check_steal_opportunity(medium_steal_ai, mock_game, 2) # Should hit line 93: min_safe = 14 + num_outs = 14 assert result.min_safe == 14 def test_steal_to_home_scenario(self, db_session): """Test steal to home logic with real runner setup.""" # Test uncovered steal to home scenario (lines 151-171) balanced_ai = ManagerAiFactory.create(db_session, steal=6) # Create runner on third with real batting card runner_on_third = PlayerFactory.create(db_session, name="Fast Runner") catcher = PlayerFactory.create_catcher(db_session) self._create_catcher_defense(catcher, db_session, arm=6) mock_game = self._create_steal_home_game_mock(runner_on_third, catcher, db_session) ai_service = AIService(db_session) result = ai_service.check_steal_opportunity(balanced_ai, mock_game, 4) # Home = 4 assert isinstance(result, JumpResponse) # Should hit steal > 5 branch (line 160): min_safe = 7 assert result.min_safe == 7 def test_pitcher_replacement_with_real_database_queries(self, db_session): """Test pitcher replacement with actual game stats queries.""" # Create realistic pitcher replacement scenario balanced_ai = ManagerAiFactory.create(db_session, behind_aggression=5, ahead_aggression=5) # Mock game will need to return data for real SQL queries mock_game = self._create_pitcher_replacement_mock(db_session) ai_service = AIService(db_session) # Mock the session.exec calls to return realistic data ai_service.session.exec = self._mock_pitcher_stats_queries result = ai_service.should_replace_pitcher(balanced_ai, mock_game) assert isinstance(result, bool) # Test that complex pitcher logic executes without errors def test_defensive_alignment_with_real_position_ratings(self, db_session): """Test defensive alignment with actual position rating queries.""" balanced_ai = ManagerAiFactory.create(db_session, defense=6) catcher = PlayerFactory.create_catcher(db_session, name="Defensive Catcher") # Create real position rating self._create_catcher_defense(catcher, db_session, arm=9, range=7) mock_game = self._create_defense_alignment_mock(catcher, db_session) ai_service = AIService(db_session) result = ai_service.set_defensive_alignment(balanced_ai, mock_game) assert isinstance(result, DefenseResponse) assert result.ai_note is not None def test_tag_decisions_complex_scenarios(self, db_session): """Test tag decision logic with realistic game states.""" # Test tag from second with complex aggression calculations aggressive_ai = ManagerAiFactory.create( db_session, running=8, ahead_aggression=7, behind_aggression=3 ) mock_game = self._create_tag_scenario_mock(db_session) ai_service = AIService(db_session) result = ai_service.check_tag_from_second(aggressive_ai, mock_game) assert isinstance(result, TagResponse) assert result.min_safe is not None # Helper methods for creating realistic mocks and database data def _create_realistic_game_mock(self, catcher, db_session): """Create a realistic game mock with proper runner and catcher setup.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() # Create realistic runner on first runner = PlayerFactory.create(db_session, name="Base Runner") mock_runner = Mock() mock_runner.player.name = runner.name mock_runner.card.batterscouting.battingcard.steal_auto = False mock_runner.card.batterscouting.battingcard.steal_high = 14 mock_runner.card.batterscouting.battingcard.steal_low = 11 mock_play.on_first = mock_runner mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 5 mock_play.outs = 1 mock_play.ai_run_diff = 2 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 12345 return mock_game def _create_catcher_defense(self, catcher, db_session, arm=5, range=5): """Create position rating for catcher.""" defense = PositionRating( player_id=catcher.id, variant=0, position='C', arm=arm, range=range, error=1 ) db_session.add(defense) db_session.commit() return defense def _create_steal_home_game_mock(self, runner, catcher, db_session): """Create game mock for steal to home scenario.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_runner = Mock() mock_runner.player.name = runner.name mock_runner.card.batterscouting.battingcard.steal_low = 12 mock_play.on_third = mock_runner mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 4 mock_play.ai_run_diff = 0 # Tied game mock_play.inning_num = 8 # Late inning mock_game.current_play_or_none.return_value = mock_play mock_game.id = 67890 return mock_game def _create_pitcher_replacement_mock(self, db_session): """Create mock for pitcher replacement testing.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_pitcher = Mock() mock_pitcher.replacing_id = None # Starter mock_pitcher.is_fatigued = True mock_pitcher.card.pitcherscouting.pitchingcard.starter_rating = 6 mock_pitcher.card.player.name_with_desc = "Test Starter" mock_play.pitcher = mock_pitcher mock_play.on_base_code = 3 # Runners on base mock_play.ai_run_diff = -1 # Behind by 1 mock_play.inning_num = 6 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 11111 return mock_game def _create_defense_alignment_mock(self, catcher, db_session): """Create mock for defensive alignment testing.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_play.on_third = Mock() mock_play.on_third.player.name = "Runner on Third" mock_play.could_walkoff = True mock_play.starting_outs = 1 mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 22222 return mock_game def _create_tag_scenario_mock(self, db_session): """Create mock for tag decision testing.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_play.starting_outs = 1 mock_play.ai_run_diff = 3 # Ahead by 3 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 33333 return mock_game def _mock_pitcher_stats_queries(self, query): """Mock database queries for pitcher stats.""" from unittest.mock import Mock mock_result = Mock() mock_result.one.side_effect = [21, 6] # 21 outs, 6 allowed runners return mock_result class TestAIServiceErrorHandling: """Test error handling scenarios that aren't covered.""" def test_check_steal_missing_catcher_defense(self, db_session): """Test error when catcher has no defensive rating.""" ai = ManagerAiFactory.create(db_session, steal=5) catcher = PlayerFactory.create_catcher(db_session) # Don't create position rating - should cause database error mock_game = self._create_basic_game_mock(catcher, db_session) ai_service = AIService(db_session) # Should handle missing position rating gracefully with pytest.raises(Exception): # Database error for missing rating ai_service.check_steal_opportunity(ai, mock_game, 2) def test_steal_opportunity_missing_runner(self, db_session): """Test error handling when expected runner is missing.""" ai = ManagerAiFactory.create(db_session, steal=5) catcher = PlayerFactory.create_catcher(db_session) mock_game = self._create_basic_game_mock(catcher, db_session) mock_game.current_play_or_none.return_value.on_first = None # No runner ai_service = AIService(db_session) with pytest.raises(ValueError, match="no runner found on first"): ai_service.check_steal_opportunity(ai, mock_game, 2) def test_steal_to_home_missing_runner_on_third(self, db_session): """Test error handling for steal to home with no runner on third.""" ai = ManagerAiFactory.create(db_session, steal=5) catcher = PlayerFactory.create_catcher(db_session) mock_game = self._create_basic_game_mock(catcher, db_session) mock_game.current_play_or_none.return_value.ai_run_diff = 0 # Trigger home steal logic mock_game.current_play_or_none.return_value.on_third = None # No runner ai_service = AIService(db_session) with pytest.raises(ValueError, match="no runner found on third"): ai_service.check_steal_opportunity(ai, mock_game, 4) def _create_basic_game_mock(self, catcher, db_session): """Create basic game mock for error testing.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_runner = Mock() mock_runner.player.name = "Test Runner" mock_runner.card.batterscouting.battingcard.steal_auto = False mock_play.on_first = mock_runner mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 5 mock_play.outs = 1 mock_play.ai_run_diff = 2 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 99999 return mock_game class TestAIServiceCrossServiceIntegration: """Test AIService interactions with other services and complex game flows.""" def test_ai_service_with_player_service_integration(self, db_session): """Test AIService using PlayerService for player descriptions.""" from app.services.player_service import PlayerService # Create realistic game scenario ai = ManagerAiFactory.create(db_session, steal=7) runner = PlayerFactory.create(db_session, name="Speed Demon", description="2023 Rookie") catcher = PlayerFactory.create_catcher(db_session) self._create_catcher_position(catcher, db_session) mock_game = self._create_runner_game_mock(runner, catcher, db_session) ai_service = AIService(db_session) player_service = PlayerService(db_session) # Test that AI notes could use PlayerService formatting result = ai_service.check_steal_opportunity(ai, mock_game, 2) # Verify PlayerService can format the runner name consistently formatted_name = player_service.get_formatted_name_with_description(runner) assert "2023 Rookie Speed Demon" == formatted_name # AI service should work independently but could integrate with PlayerService assert isinstance(result, JumpResponse) def test_complex_game_state_decision_chain(self, db_session): """Test complex decision chain covering multiple AI methods.""" # Create comprehensive game scenario ai = ManagerAiFactory.create( db_session, steal=8, running=7, defense=6, ahead_aggression=8, behind_aggression=4 ) catcher = PlayerFactory.create_catcher(db_session, name="Elite Catcher") runner = PlayerFactory.create(db_session, name="Speedster") self._create_catcher_position(catcher, db_session, arm=9) mock_game = self._create_complex_game_mock(runner, catcher, db_session) ai_service = AIService(db_session) # Test multiple AI decisions in sequence steal_result = ai_service.check_steal_opportunity(ai, mock_game, 2) tag_result = ai_service.check_tag_from_second(ai, mock_game) defense_result = ai_service.set_defensive_alignment(ai, mock_game) # All should execute without errors and return proper responses assert isinstance(steal_result, JumpResponse) assert isinstance(tag_result, TagResponse) assert isinstance(defense_result, DefenseResponse) def _create_catcher_position(self, catcher, db_session, arm=7): """Helper to create catcher position rating.""" rating = PositionRating( player_id=catcher.id, variant=0, position='C', arm=arm, range=6, error=1 ) db_session.add(rating) db_session.commit() def _create_runner_game_mock(self, runner, catcher, db_session): """Create game mock with specific runner.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() mock_runner = Mock() mock_runner.player.name = runner.name mock_runner.card.batterscouting.battingcard.steal_auto = True mock_runner.card.batterscouting.battingcard.steal_high = 16 mock_play.on_first = mock_runner mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 4 mock_play.outs = 0 mock_play.ai_run_diff = 1 mock_game.current_play_or_none.return_value = mock_play mock_game.id = 55555 return mock_game def _create_complex_game_mock(self, runner, catcher, db_session): """Create complex game scenario for testing multiple decisions.""" from unittest.mock import Mock mock_game = Mock() mock_play = Mock() # Setup for steal decision mock_runner = Mock() mock_runner.player.name = runner.name mock_runner.card.batterscouting.battingcard.steal_auto = False mock_runner.card.batterscouting.battingcard.steal_high = 15 mock_runner.card.batterscouting.battingcard.steal_low = 12 # Setup for tag decision mock_play.on_first = mock_runner mock_play.on_second = mock_runner # Same runner for tag test mock_play.starting_outs = 1 # Setup for defense decision mock_play.on_third = mock_runner mock_play.could_walkoff = False # Common setup mock_play.catcher = Mock() mock_play.catcher.player_id = catcher.id mock_play.catcher.card.variant = 0 mock_play.pitcher.card.pitcherscouting.pitchingcard.hold = 5 mock_play.outs = 1 mock_play.ai_run_diff = 2 mock_play.on_base_code = 7 # Runners on 1st, 2nd, 3rd mock_game.current_play_or_none.return_value = mock_play mock_game.id = 77777 return mock_game