""" Unit tests for player models. Tests polymorphic player model system (BasePlayer, SbaPlayer, PdPlayer) with factory methods and API response parsing. Author: Claude Date: 2025-10-28 """ import pytest from abc import ABC from app.models.player_models import ( BasePlayer, SbaPlayer, PdPlayer, PdCardset, PdRarity, PdBattingRating, PdPitchingRating, PdBattingCard, PdPitchingCard, ) # ==================== BasePlayer Tests ==================== class TestBasePlayer: """Test BasePlayer abstract base class.""" def test_cannot_instantiate_base_player(self): """BasePlayer is abstract and cannot be instantiated directly.""" with pytest.raises(TypeError, match="Can't instantiate abstract class"): BasePlayer(id=1, name="Test") def test_base_player_is_abc(self): """BasePlayer inherits from ABC.""" assert issubclass(BasePlayer, ABC) def test_required_abstract_methods(self): """BasePlayer defines required abstract methods.""" abstract_methods = BasePlayer.__abstractmethods__ assert "get_image_url" in abstract_methods assert "get_positions" in abstract_methods assert "get_display_name" in abstract_methods # ==================== SbaPlayer Tests ==================== class TestSbaPlayer: """Test SbaPlayer model and factory methods.""" @pytest.fixture def minimal_sba_data(self): """Minimal valid SBA API response.""" return { "id": 12288, "name": "Ronald Acuna Jr", "wara": 0.0, } @pytest.fixture def full_sba_data(self): """Full SBA API response with all fields.""" return { "id": 12288, "name": "Ronald Acuna Jr", "wara": 5.2, "image": "https://example.com/acuna.png", "image2": "https://example.com/acuna2.png", "team": { "id": 499, "abbrev": "WV", "lname": "West Virginia Black Bears", }, "season": 12, "pos_1": "RF", "pos_2": "CF", "pos_3": "LF", "headshot": "https://example.com/headshot.jpg", "vanity_card": "https://example.com/vanity.png", "strat_code": "Acuna,R", "bbref_id": "acunaro01", "injury_rating": "5p30", } def test_create_from_minimal_api_response(self, minimal_sba_data): """Can create SbaPlayer with minimal required fields.""" player = SbaPlayer.from_api_response(minimal_sba_data) assert player.id == 12288 assert player.name == "Ronald Acuna Jr" assert player.wara == 0.0 assert player.image is None assert player.team_id is None def test_create_from_full_api_response(self, full_sba_data): """Can create SbaPlayer with all fields populated.""" player = SbaPlayer.from_api_response(full_sba_data) assert player.id == 12288 assert player.name == "Ronald Acuna Jr" assert player.wara == 5.2 assert player.image == "https://example.com/acuna.png" assert player.image2 == "https://example.com/acuna2.png" assert player.team_id == 499 assert player.team_name == "West Virginia Black Bears" assert player.season == 12 assert player.pos_1 == "RF" assert player.pos_2 == "CF" assert player.pos_3 == "LF" assert player.headshot == "https://example.com/headshot.jpg" assert player.strat_code == "Acuna,R" assert player.bbref_id == "acunaro01" def test_get_positions_filters_none(self, full_sba_data): """get_positions() returns only non-None positions.""" player = SbaPlayer.from_api_response(full_sba_data) positions = player.get_positions() assert positions == ["RF", "CF", "LF"] assert None not in positions def test_get_positions_empty_when_no_positions(self, minimal_sba_data): """get_positions() returns empty list when no positions set.""" player = SbaPlayer.from_api_response(minimal_sba_data) positions = player.get_positions() assert positions == [] def test_get_image_url_primary(self, full_sba_data): """get_image_url() returns primary image when available.""" player = SbaPlayer.from_api_response(full_sba_data) assert player.get_image_url() == "https://example.com/acuna.png" def test_get_image_url_fallback_to_image2(self): """get_image_url() falls back to image2 when primary missing.""" data = { "id": 1, "name": "Test", "wara": 0.0, "image2": "https://example.com/image2.png", } player = SbaPlayer.from_api_response(data) assert player.get_image_url() == "https://example.com/image2.png" def test_get_image_url_fallback_to_headshot(self): """get_image_url() falls back to headshot when others missing.""" data = { "id": 1, "name": "Test", "wara": 0.0, "headshot": "https://example.com/headshot.jpg", } player = SbaPlayer.from_api_response(data) assert player.get_image_url() == "https://example.com/headshot.jpg" def test_get_image_url_empty_when_none(self, minimal_sba_data): """get_image_url() returns empty string when no images.""" player = SbaPlayer.from_api_response(minimal_sba_data) assert player.get_image_url() == "" def test_get_display_name(self, full_sba_data): """get_display_name() returns player name.""" player = SbaPlayer.from_api_response(full_sba_data) assert player.get_display_name() == "Ronald Acuna Jr" def test_team_extraction_from_nested_object(self, full_sba_data): """Team ID and name extracted from nested 'team' object.""" player = SbaPlayer.from_api_response(full_sba_data) assert player.team_id == 499 assert player.team_name == "West Virginia Black Bears" def test_team_none_when_missing(self, minimal_sba_data): """Team fields are None when team object missing.""" player = SbaPlayer.from_api_response(minimal_sba_data) assert player.team_id is None assert player.team_name is None # ==================== PdPlayer Tests ==================== class TestPdPlayer: """Test PdPlayer model and factory methods.""" @pytest.fixture def pd_player_data(self): """PD player API response (without scouting data).""" return { "player_id": 10633, "p_name": "Chuck Knoblauch", "cost": 77, "image": "https://pd.example.com/players/10633/battingcard", "cardset": { "id": 20, "name": "1998 Season", "description": "Cards based on the 1998 MLB season", "ranked_legal": True, }, "set_num": 609, "rarity": { "id": 3, "value": 2, "name": "Starter", "color": "C0C0C0", }, "mlbclub": "New York Yankees", "franchise": "New York Yankees", "pos_1": "2B", "pos_2": "SS", "headshot": "https://example.com/headshot.jpg", "strat_code": None, "bbref_id": "knoblch01", "fangr_id": "609", "description": "1998 Season", "quantity": 999, } @pytest.fixture def pd_batting_data(self): """PD batting card API response.""" return { "count": 2, "ratings": [ { "battingcard": { "steal_low": 8, "steal_high": 11, "steal_auto": True, "steal_jump": 0.25, "bunting": "C", "hit_and_run": "D", "running": 13, "offense_col": 1, "hand": "R", }, "vs_hand": "L", "pull_rate": 0.29379, "center_rate": 0.41243, "slap_rate": 0.29378, "homerun": 0.0, "bp_homerun": 2.0, "triple": 1.4, "double_three": 0.0, "double_two": 5.1, "double_pull": 5.1, "single_two": 3.5, "single_one": 4.5, "single_center": 1.35, "bp_single": 5.0, "hbp": 2.0, "walk": 18.25, "strikeout": 9.75, "lineout": 9.0, "popout": 16.0, "flyout_a": 0.0, "flyout_bq": 1.65, "flyout_lf_b": 1.9, "flyout_rf_b": 2.0, "groundout_a": 7.0, "groundout_b": 10.5, "groundout_c": 2.0, "avg": 0.2263888888888889, "obp": 0.41388888888888886, "slg": 0.37453703703703706, }, { "battingcard": { "steal_low": 8, "steal_high": 11, "steal_auto": True, "steal_jump": 0.25, "bunting": "C", "hit_and_run": "D", "running": 13, "offense_col": 1, "hand": "R", }, "vs_hand": "R", "pull_rate": 0.25824, "center_rate": 0.1337, "slap_rate": 0.60806, "homerun": 1.05, "bp_homerun": 3.0, "triple": 1.2, "double_three": 0.0, "double_two": 3.5, "double_pull": 3.5, "single_two": 4.3, "single_one": 6.4, "single_center": 2.4, "bp_single": 5.0, "hbp": 3.0, "walk": 12.1, "strikeout": 9.9, "lineout": 11.0, "popout": 13.0, "flyout_a": 0.0, "flyout_bq": 1.6, "flyout_lf_b": 1.5, "flyout_rf_b": 1.95, "groundout_a": 12.0, "groundout_b": 11.6, "groundout_c": 0.0, "avg": 0.2439814814814815, "obp": 0.3837962962962963, "slg": 0.40185185185185185, }, ], } @pytest.fixture def pd_pitching_data(self): """PD pitching card API response.""" return { "count": 2, "ratings": [ { "pitchingcard": { "balk": 0, "wild_pitch": 20, "hold": 9, "starter_rating": 1, "relief_rating": 2, "closer_rating": None, "batting": "#1WR-C", "offense_col": 1, "hand": "R", }, "vs_hand": "L", "homerun": 2.6, "bp_homerun": 6.0, "triple": 2.1, "double_three": 0.0, "double_two": 7.1, "double_cf": 0.0, "single_two": 1.0, "single_one": 1.0, "single_center": 0.0, "bp_single": 5.0, "hbp": 6.0, "walk": 17.6, "strikeout": 11.4, "flyout_lf_b": 0.0, "flyout_cf_b": 7.75, "flyout_rf_b": 3.6, "groundout_a": 1.75, "groundout_b": 6.1, "xcheck_p": 1.0, "xcheck_c": 3.0, "xcheck_1b": 2.0, "xcheck_2b": 6.0, "xcheck_3b": 3.0, "xcheck_ss": 7.0, "xcheck_lf": 2.0, "xcheck_cf": 3.0, "xcheck_rf": 2.0, "avg": 0.17870370370370367, "obp": 0.3972222222222222, "slg": 0.4388888888888889, }, { "pitchingcard": { "balk": 0, "wild_pitch": 20, "hold": 9, "starter_rating": 1, "relief_rating": 2, "closer_rating": None, "batting": "#1WR-C", "offense_col": 1, "hand": "R", }, "vs_hand": "R", "homerun": 5.0, "bp_homerun": 2.0, "triple": 1.0, "double_three": 0.0, "double_two": 0.0, "double_cf": 6.15, "single_two": 6.5, "single_one": 0.0, "single_center": 6.5, "bp_single": 5.0, "hbp": 2.0, "walk": 10.2, "strikeout": 26.65, "flyout_lf_b": 0.0, "flyout_cf_b": 0.0, "flyout_rf_b": 0.0, "groundout_a": 6.0, "groundout_b": 2.0, "xcheck_p": 1.0, "xcheck_c": 3.0, "xcheck_1b": 2.0, "xcheck_2b": 6.0, "xcheck_3b": 3.0, "xcheck_ss": 7.0, "xcheck_lf": 2.0, "xcheck_cf": 3.0, "xcheck_rf": 2.0, "avg": 0.2652777777777778, "obp": 0.37824074074074077, "slg": 0.5074074074074074, }, ], } def test_create_without_scouting_data(self, pd_player_data): """Can create PdPlayer without scouting data.""" player = PdPlayer.from_api_response(pd_player_data) assert player.player_id == 10633 assert player.name == "Chuck Knoblauch" assert player.cost == 77 assert player.pos_1 == "2B" assert player.pos_2 == "SS" assert player.batting_card is None assert player.pitching_card is None def test_create_with_batting_data(self, pd_player_data, pd_batting_data): """Can create PdPlayer with batting scouting data.""" player = PdPlayer.from_api_response( pd_player_data, batting_data=pd_batting_data ) assert player.batting_card is not None assert player.batting_card.hand == "R" assert player.batting_card.steal_low == 8 assert player.batting_card.steal_high == 11 assert player.batting_card.bunting == "C" assert "L" in player.batting_card.ratings assert "R" in player.batting_card.ratings assert player.pitching_card is None def test_create_with_pitching_data(self, pd_player_data, pd_pitching_data): """Can create PdPlayer with pitching scouting data.""" player = PdPlayer.from_api_response( pd_player_data, pitching_data=pd_pitching_data ) assert player.pitching_card is not None assert player.pitching_card.hand == "R" assert player.pitching_card.balk == 0 assert player.pitching_card.wild_pitch == 20 assert player.pitching_card.starter_rating == 1 assert "L" in player.pitching_card.ratings assert "R" in player.pitching_card.ratings assert player.batting_card is None def test_create_with_full_scouting_data( self, pd_player_data, pd_batting_data, pd_pitching_data ): """Can create PdPlayer with both batting and pitching data.""" player = PdPlayer.from_api_response( pd_player_data, batting_data=pd_batting_data, pitching_data=pd_pitching_data ) assert player.batting_card is not None assert player.pitching_card is not None def test_get_batting_rating_vs_lefties(self, pd_player_data, pd_batting_data): """get_batting_rating('L') returns rating vs left-handed pitchers.""" player = PdPlayer.from_api_response( pd_player_data, batting_data=pd_batting_data ) rating = player.get_batting_rating('L') assert rating is not None assert rating.vs_hand == 'L' assert rating.homerun == 0.0 assert rating.walk == 18.25 assert rating.strikeout == 9.75 assert rating.avg == pytest.approx(0.2263888888888889) def test_get_batting_rating_vs_righties(self, pd_player_data, pd_batting_data): """get_batting_rating('R') returns rating vs right-handed pitchers.""" player = PdPlayer.from_api_response( pd_player_data, batting_data=pd_batting_data ) rating = player.get_batting_rating('R') assert rating is not None assert rating.vs_hand == 'R' assert rating.homerun == 1.05 assert rating.walk == 12.1 assert rating.strikeout == 9.9 def test_get_batting_rating_none_when_no_data(self, pd_player_data): """get_batting_rating() returns None when no batting data.""" player = PdPlayer.from_api_response(pd_player_data) assert player.get_batting_rating('L') is None assert player.get_batting_rating('R') is None def test_get_pitching_rating_vs_lefties(self, pd_player_data, pd_pitching_data): """get_pitching_rating('L') returns rating vs left-handed batters.""" player = PdPlayer.from_api_response( pd_player_data, pitching_data=pd_pitching_data ) rating = player.get_pitching_rating('L') assert rating is not None assert rating.vs_hand == 'L' assert rating.homerun == 2.6 assert rating.walk == 17.6 assert rating.strikeout == 11.4 assert rating.xcheck_ss == 7.0 def test_get_pitching_rating_vs_righties(self, pd_player_data, pd_pitching_data): """get_pitching_rating('R') returns rating vs right-handed batters.""" player = PdPlayer.from_api_response( pd_player_data, pitching_data=pd_pitching_data ) rating = player.get_pitching_rating('R') assert rating is not None assert rating.vs_hand == 'R' assert rating.homerun == 5.0 assert rating.walk == 10.2 assert rating.strikeout == 26.65 def test_get_pitching_rating_none_when_no_data(self, pd_player_data): """get_pitching_rating() returns None when no pitching data.""" player = PdPlayer.from_api_response(pd_player_data) assert player.get_pitching_rating('L') is None assert player.get_pitching_rating('R') is None def test_get_positions(self, pd_player_data): """get_positions() returns non-None positions.""" player = PdPlayer.from_api_response(pd_player_data) positions = player.get_positions() assert positions == ["2B", "SS"] def test_get_display_name_with_description(self, pd_player_data): """get_display_name() includes description in parentheses.""" player = PdPlayer.from_api_response(pd_player_data) assert player.get_display_name() == "Chuck Knoblauch (1998 Season)" def test_get_image_url_primary(self, pd_player_data): """get_image_url() returns primary image.""" player = PdPlayer.from_api_response(pd_player_data) assert player.get_image_url() == "https://pd.example.com/players/10633/battingcard" def test_get_image_url_fallback_to_headshot(self): """get_image_url() falls back to headshot when primary missing.""" data = { "player_id": 1, "p_name": "Test", "cost": 1, "cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True}, "set_num": 1, "rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"}, "mlbclub": "Test", "franchise": "Test", "description": "Test", "headshot": "https://example.com/headshot.jpg", } player = PdPlayer.from_api_response(data) assert player.get_image_url() == "https://example.com/headshot.jpg" def test_cardset_parsing(self, pd_player_data): """Cardset is properly parsed into PdCardset model.""" player = PdPlayer.from_api_response(pd_player_data) assert isinstance(player.cardset, PdCardset) assert player.cardset.id == 20 assert player.cardset.name == "1998 Season" assert player.cardset.ranked_legal is True def test_rarity_parsing(self, pd_player_data): """Rarity is properly parsed into PdRarity model.""" player = PdPlayer.from_api_response(pd_player_data) assert isinstance(player.rarity, PdRarity) assert player.rarity.id == 3 assert player.rarity.value == 2 assert player.rarity.name == "Starter" assert player.rarity.color == "C0C0C0" # ==================== Polymorphism Tests ==================== class TestPolymorphism: """Test polymorphic behavior across player types.""" def test_both_implement_base_player_interface(self): """Both SbaPlayer and PdPlayer implement BasePlayer interface.""" sba_data = {"id": 1, "name": "SBA Player", "wara": 0.0, "pos_1": "1B"} pd_data = { "player_id": 1, "p_name": "PD Player", "cost": 1, "cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True}, "set_num": 1, "rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"}, "mlbclub": "Test", "franchise": "Test", "description": "Test", "pos_1": "1B", } sba_player = SbaPlayer.from_api_response(sba_data) pd_player = PdPlayer.from_api_response(pd_data) # Both are BasePlayer instances assert isinstance(sba_player, BasePlayer) assert isinstance(pd_player, BasePlayer) def test_polymorphic_function_works_with_both(self): """Function accepting BasePlayer works with both implementations.""" def process_player(player: BasePlayer) -> dict: """Example function that works with any BasePlayer.""" return { "name": player.get_display_name(), "positions": player.get_positions(), "image": player.get_image_url(), } sba_data = { "id": 1, "name": "SBA Player", "wara": 0.0, "pos_1": "1B", "image": "https://example.com/sba.png", } pd_data = { "player_id": 1, "p_name": "PD Player", "cost": 1, "cardset": {"id": 1, "name": "Test", "description": "Test", "ranked_legal": True}, "set_num": 1, "rarity": {"id": 1, "value": 1, "name": "Test", "color": "FFF"}, "mlbclub": "Test", "franchise": "Test", "description": "2024", "pos_1": "1B", "image": "https://example.com/pd.png", } sba_result = process_player(SbaPlayer.from_api_response(sba_data)) pd_result = process_player(PdPlayer.from_api_response(pd_data)) assert sba_result["name"] == "SBA Player" assert sba_result["positions"] == ["1B"] assert sba_result["image"] == "https://example.com/sba.png" assert pd_result["name"] == "PD Player (2024)" assert pd_result["positions"] == ["1B"] assert pd_result["image"] == "https://example.com/pd.png"