import pytest import random from unittest.mock import Mock, patch, call import discord from in_game import simulations, data_cache from tests.factory import session_fixture @pytest.fixture def mock_batting_wrapper(): """Create a mock BattingWrapper with test data.""" batting_card = data_cache.BattingCard( player_id=123, variant=0, steal_low=9, steal_high=12, steal_auto=True, steal_jump=0.25, bunting='B', hit_and_run='A', running=13, offense_col=1, hand='R' ) ratings_vl = data_cache.BattingRatings( homerun=5.0, bp_homerun=7.0, triple=2.0, double_three=1.0, double_two=8.0, double_pull=6.0, single_two=4.0, single_one=3.0, single_center=12.0, bp_single=5.0, hbp=2.0, walk=10.0, strikeout=15.0, lineout=3.0, popout=6.0, flyout_a=1.0, flyout_bq=0.5, flyout_lf_b=4.0, flyout_rf_b=8.0, groundout_a=2.0, groundout_b=20.0, groundout_c=15.0, avg=0.285, obp=0.375, slg=0.455, pull_rate=0.42, center_rate=0.33, slap_rate=0.25 ) ratings_vr = data_cache.BattingRatings( homerun=3.0, bp_homerun=6.0, triple=1.0, double_three=0.5, double_two=6.0, double_pull=5.0, single_two=3.5, single_one=2.5, single_center=10.0, bp_single=4.0, hbp=1.5, walk=8.0, strikeout=18.0, lineout=4.0, popout=7.0, flyout_a=1.5, flyout_bq=0.75, flyout_lf_b=5.0, flyout_rf_b=10.0, groundout_a=2.5, groundout_b=22.0, groundout_c=16.0, avg=0.265, obp=0.345, slg=0.425, pull_rate=0.45, center_rate=0.30, slap_rate=0.25 ) return data_cache.BattingWrapper( card=batting_card, ratings_vl=ratings_vl, ratings_vr=ratings_vr ) @pytest.fixture def mock_pitching_wrapper(): """Create a mock PitchingWrapper with test data.""" pitching_card = data_cache.PitchingCard( player_id=456, variant=0, balk=1, wild_pitch=2, hold=8, starter_rating=7, relief_rating=5, batting='B', offense_col=1, hand='L', closer_rating=6 ) ratings_vl = data_cache.PitchingRatings( homerun=3.0, bp_homerun=5.0, triple=1.0, double_three=0.5, double_two=5.0, double_cf=4.0, single_two=3.0, single_one=2.0, single_center=8.0, bp_single=4.0, hbp=2.0, walk=12.0, strikeout=25.0, flyout_lf_b=5.0, flyout_cf_b=6.0, flyout_rf_b=6.0, groundout_a=3.0, groundout_b=18.0, xcheck_p=1.0, xcheck_c=1.0, xcheck_1b=1.0, xcheck_2b=1.0, xcheck_3b=1.0, xcheck_ss=1.0, xcheck_lf=1.0, xcheck_cf=1.0, xcheck_rf=1.0, avg=0.245, obp=0.335, slg=0.385 ) ratings_vr = data_cache.PitchingRatings( homerun=4.0, bp_homerun=6.0, triple=1.5, double_three=0.75, double_two=7.0, double_cf=5.0, single_two=4.0, single_one=3.0, single_center=10.0, bp_single=5.0, hbp=1.5, walk=10.0, strikeout=22.0, flyout_lf_b=6.0, flyout_cf_b=7.0, flyout_rf_b=8.0, groundout_a=4.0, groundout_b=20.0, xcheck_p=1.5, xcheck_c=1.5, xcheck_1b=1.5, xcheck_2b=1.5, xcheck_3b=1.5, xcheck_ss=1.5, xcheck_lf=1.5, xcheck_cf=1.5, xcheck_rf=1.5, avg=0.255, obp=0.345, slg=0.405 ) return data_cache.PitchingWrapper( card=pitching_card, ratings_vl=ratings_vl, ratings_vr=ratings_vr ) @pytest.fixture def mock_player_data(): """Create mock player data for helper function calls.""" return { 'player_id': 123, 'p_name': 'Test Player', 'description': '2024', 'image': 'test_player_pitchingcard', 'image2': 'test_player_battingcard' } class TestGetResult: """Test the get_result function which simulates plate appearance outcomes.""" @patch('random.choice') @patch('random.choices') def test_get_result_pitcher_chosen_left_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper): """Test get_result when pitcher is chosen and batter is left-handed.""" # Make the batter left-handed mock_batting_wrapper.card.hand = 'L' mock_choice.return_value = 'pitcher' mock_choices.return_value = ['strikeout'] result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) mock_choice.assert_called_once_with(['pitcher', 'batter']) assert result == 'strikeout' # Verify the correct ratings were used (pitcher vs left-handed batter) call_args = mock_choices.call_args assert call_args is not None # Should use pitcher's ratings_vl when facing left-handed batter @patch('random.choice') @patch('random.choices') def test_get_result_pitcher_chosen_right_handed_batter(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper): """Test get_result when pitcher is chosen and batter is right-handed.""" # Batter is already right-handed in fixture mock_choice.return_value = 'pitcher' mock_choices.return_value = ['groundout_b'] result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) mock_choice.assert_called_once_with(['pitcher', 'batter']) assert result == 'groundout_b' @patch('random.choice') @patch('random.choices') def test_get_result_batter_chosen_left_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper): """Test get_result when batter is chosen and pitcher is left-handed.""" # Pitcher is already left-handed in fixture mock_choice.return_value = 'batter' mock_choices.return_value = ['homerun'] result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) mock_choice.assert_called_once_with(['pitcher', 'batter']) assert result == 'homerun' @patch('random.choice') @patch('random.choices') def test_get_result_batter_chosen_right_handed_pitcher(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper): """Test get_result when batter is chosen and pitcher is right-handed.""" # Make the pitcher right-handed mock_pitching_wrapper.card.hand = 'R' mock_choice.return_value = 'batter' mock_choices.return_value = ['single_center'] result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) mock_choice.assert_called_once_with(['pitcher', 'batter']) assert result == 'single_center' @patch('random.choice') @patch('random.choices') def test_get_result_unused_fields_filtered_out(self, mock_choices, mock_choice, mock_pitching_wrapper, mock_batting_wrapper): """Test that unused statistical fields are filtered out from the random selection.""" mock_choice.return_value = 'batter' mock_choices.return_value = ['walk'] result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) # Get the call arguments to verify unused fields were removed call_args = mock_choices.call_args results_list = call_args[0][0] # First positional argument (results) probs_list = call_args[0][1] # Second positional argument (probabilities) # Verify unused fields are not in the results unused_fields = ['avg', 'obp', 'slg', 'pull_rate', 'center_rate', 'slap_rate'] for field in unused_fields: assert field not in results_list # Verify we have valid baseball outcomes assert len(results_list) == len(probs_list) assert all(isinstance(prob, (int, float)) for prob in probs_list) assert result == 'walk' class TestGetPosEmbeds: """Test the get_pos_embeds function which creates Discord embeds for matchups.""" @patch('in_game.simulations.get_result') @patch('helpers.player_desc') @patch('helpers.player_pcard') @patch('helpers.player_bcard') @patch('helpers.SBA_COLOR', 'a6ce39') def test_get_pos_embeds_structure(self, mock_bcard, mock_pcard, mock_desc, mock_get_result, mock_pitching_wrapper, mock_batting_wrapper, mock_player_data): """Test that get_pos_embeds returns the correct embed structure.""" # Setup mocks mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter'] mock_pcard.return_value = 'pitcher_card_url' mock_bcard.return_value = 'batter_card_url' mock_get_result.return_value = 'strikeout' pitcher_data = {'p_name': 'Test Pitcher', 'description': '2024'} batter_data = {'p_name': 'Test Batter', 'description': '2024'} embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper) # Verify we get 3 embeds assert len(embeds) == 3 assert all(isinstance(embed, discord.Embed) for embed in embeds) # Check first embed (pitcher) assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter' assert embeds[0].image.url == 'pitcher_card_url' assert embeds[0].color.value == int('a6ce39', 16) # Check second embed (batter) assert embeds[1].image.url == 'batter_card_url' assert embeds[1].color.value == int('a6ce39', 16) # Check third embed (result) assert len(embeds[2].fields) == 3 assert embeds[2].fields[0].name == 'Pitcher' assert embeds[2].fields[0].value == '2024 Test Pitcher' assert embeds[2].fields[1].name == 'Batter' assert embeds[2].fields[1].value == '2024 Test Batter' assert embeds[2].fields[2].name == 'Result' assert embeds[2].fields[2].value == 'strikeout' assert embeds[2].fields[2].inline is False # Verify helper functions were called correctly mock_desc.assert_has_calls([call(pitcher_data), call(batter_data)]) mock_pcard.assert_called_once_with(pitcher_data) mock_bcard.assert_called_once_with(batter_data) mock_get_result.assert_called_once_with(mock_pitching_wrapper, mock_batting_wrapper) @patch('in_game.simulations.get_result') @patch('helpers.player_desc') @patch('helpers.player_pcard') @patch('helpers.player_bcard') @patch('helpers.SBA_COLOR', 'a6ce39') def test_get_pos_embeds_different_results(self, mock_bcard, mock_pcard, mock_desc, mock_get_result, mock_pitching_wrapper, mock_batting_wrapper, mock_player_data): """Test get_pos_embeds with different simulation results.""" mock_desc.side_effect = ['Live Ace Pitcher', 'Live Power Hitter'] mock_pcard.return_value = 'ace_pitcher_card' mock_bcard.return_value = 'power_hitter_card' mock_get_result.return_value = 'homerun' pitcher_data = {'p_name': 'Ace Pitcher', 'description': 'Live'} batter_data = {'p_name': 'Power Hitter', 'description': 'Live'} embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper) assert len(embeds) == 3 assert embeds[0].title == 'Live Ace Pitcher vs. Live Power Hitter' assert embeds[2].fields[2].value == 'homerun' class TestIntegration: """Integration tests that test the functions together with minimal mocking.""" def test_get_result_randomness_distribution(self, mock_pitching_wrapper, mock_batting_wrapper): """Test that get_result produces a reasonable distribution of outcomes over many iterations.""" # Run many simulations to test randomness results = [] for _ in range(1000): result = simulations.get_result(mock_pitching_wrapper, mock_batting_wrapper) results.append(result) # Verify we got various outcomes (not just one result) unique_results = set(results) assert len(unique_results) > 1 # Verify all results are valid baseball outcomes valid_outcomes = { 'homerun', 'triple', 'double_two', 'double_three', 'double_pull', 'double_cf', 'single_center', 'single_one', 'single_two', 'bp_single', 'bp_homerun', 'walk', 'hbp', 'strikeout', 'lineout', 'popout', 'flyout_a', 'flyout_bq', 'flyout_lf_b', 'flyout_cf_b', 'flyout_rf_b', 'groundout_a', 'groundout_b', 'groundout_c', 'xcheck_p', 'xcheck_c', 'xcheck_1b', 'xcheck_2b', 'xcheck_3b', 'xcheck_ss', 'xcheck_lf', 'xcheck_cf', 'xcheck_rf' } for result in unique_results: assert result in valid_outcomes @patch('helpers.SBA_COLOR', 'a6ce39') def test_full_simulation_flow(self, mock_pitching_wrapper, mock_batting_wrapper): """Test the complete flow from player data to Discord embeds.""" pitcher_data = { 'p_name': 'Test Pitcher', 'description': '2024', 'image': 'test_pitcher_pitchingcard', 'image2': None } batter_data = { 'p_name': 'Test Batter', 'description': '2024', 'image': None, 'image2': 'test_batter_battingcard' } # This will call the actual helper functions, so we need valid player data with patch('helpers.player_desc') as mock_desc, \ patch('helpers.player_pcard') as mock_pcard, \ patch('helpers.player_bcard') as mock_bcard: mock_desc.side_effect = ['2024 Test Pitcher', '2024 Test Batter'] mock_pcard.return_value = 'test_pitcher_pitchingcard' mock_bcard.return_value = 'test_batter_battingcard' embeds = simulations.get_pos_embeds(pitcher_data, batter_data, mock_pitching_wrapper, mock_batting_wrapper) # Verify the complete embed structure assert len(embeds) == 3 assert embeds[0].title == '2024 Test Pitcher vs. 2024 Test Batter' assert embeds[0].image.url == 'test_pitcher_pitchingcard' assert embeds[1].image.url == 'test_batter_battingcard' # The result should be one of many possible baseball outcomes result_field = embeds[2].fields[2] assert result_field.name == 'Result' assert isinstance(result_field.value, str) assert len(result_field.value) > 0