"""
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"
