# Testing Strategy for Player Models

**Purpose**: Comprehensive test plan for Week 6 player model implementation

---

## Overview

Testing will be organized into three layers:
1. **Unit Tests** - Individual components in isolation
2. **Integration Tests** - Components working together
3. **End-to-End Tests** - Full pipeline from API to game

Target: **90%+ code coverage** on all new modules

---

## Unit Tests

### 1. API Models (`test_api_models.py`)

**File**: `tests/unit/models/test_api_models.py`

**Coverage**: Pydantic model deserialization with real JSON samples

```python
import pytest
from pydantic import ValidationError
from app.models.api_models import (
    SbaPlayerApi,
    PdPlayerApi,
    PdBattingRatingsResponseApi,
    PdPitchingRatingsResponseApi
)

class TestSbaPlayerApi:
    """Test SBA API model deserialization"""

    def test_deserialize_sba_player(self, sba_player_json):
        """Test valid SBA player response"""
        player = SbaPlayerApi(**sba_player_json)

        assert player.id == 12288
        assert player.name == "Ronald Acuna Jr"
        assert player.team.lname == "West Virginia Black Bears"
        assert player.team.manager1.name == "Cal"
        assert player.team.division.division_name == "Big Chungus"

    def test_sba_player_positions(self, sba_player_json):
        """Test position fields"""
        player = SbaPlayerApi(**sba_player_json)

        assert player.pos_1 == "RF"
        assert player.pos_2 is None

    def test_sba_player_missing_required_field(self, sba_player_json):
        """Test validation fails on missing required field"""
        del sba_player_json["name"]

        with pytest.raises(ValidationError) as exc_info:
            SbaPlayerApi(**sba_player_json)

        assert "name" in str(exc_info.value)

class TestPdPlayerApi:
    """Test PD API model deserialization"""

    def test_deserialize_pd_player(self, pd_player_json):
        """Test valid PD player response"""
        player = PdPlayerApi(**pd_player_json)

        assert player.player_id == 11223
        assert player.p_name == "Matt Karchner"
        assert player.cardset.name == "1998 Season"
        assert player.rarity.name == "MVP"
        assert player.mlbplayer.first_name == "Matt"

    def test_pd_player_positions(self, pd_player_json):
        """Test position fields"""
        player = PdPlayerApi(**pd_player_json)

        assert player.pos_1 == "RP"
        assert player.pos_2 == "CP"
        assert player.pos_3 is None

class TestPdBattingRatingsApi:
    """Test PD batting ratings deserialization"""

    def test_deserialize_batting_ratings(self, pd_batting_ratings_json):
        """Test valid batting ratings response"""
        ratings = PdBattingRatingsResponseApi(**pd_batting_ratings_json)

        assert ratings.count == 2
        assert len(ratings.ratings) == 2

        # Check vs L rating
        vs_l = next(r for r in ratings.ratings if r.vs_hand == "L")
        assert vs_l.homerun == 0.0
        assert vs_l.walk == 18.25
        assert vs_l.strikeout == 9.75

        # Check vs R rating
        vs_r = next(r for r in ratings.ratings if r.vs_hand == "R")
        assert vs_r.homerun == 1.05
        assert vs_r.walk == 12.1

    def test_batting_probabilities_sum_to_100(self, pd_batting_ratings_json):
        """Test that all probabilities sum to ~100"""
        ratings = PdBattingRatingsResponseApi(**pd_batting_ratings_json)

        for rating in ratings.ratings:
            total = (
                rating.homerun + rating.bp_homerun +
                rating.triple +
                rating.double_three + rating.double_two + rating.double_pull +
                rating.single_two + rating.single_one + rating.single_center +
                rating.bp_single +
                rating.hbp + rating.walk + rating.strikeout +
                rating.lineout + rating.popout +
                rating.flyout_a + rating.flyout_bq + rating.flyout_lf_b + rating.flyout_rf_b +
                rating.groundout_a + rating.groundout_b + rating.groundout_c
            )

            # Allow small float rounding errors
            assert 99.0 <= total <= 101.0, f"Total probability: {total}"

class TestPdPitchingRatingsApi:
    """Test PD pitching ratings deserialization"""

    def test_deserialize_pitching_ratings(self, pd_pitching_ratings_json):
        """Test valid pitching ratings response"""
        ratings = PdPitchingRatingsResponseApi(**pd_pitching_ratings_json)

        assert ratings.count == 2
        assert len(ratings.ratings) == 2

        # Verify xcheck fields exist
        vs_l = ratings.ratings[0]
        assert vs_l.xcheck_p == 1.0
        assert vs_l.xcheck_ss == 7.0
```

---

### 2. Game Models (`test_player_models.py`)

**File**: `tests/unit/models/test_player_models.py`

**Coverage**: Game model creation and methods

```python
import pytest
from pydantic import ValidationError
from app.models.player_models import (
    SbaPlayer,
    PdPlayer,
    PdBattingRating,
    PdPitchingRating
)

class TestSbaPlayer:
    """Test SBA game player model"""

    def test_create_sba_player(self):
        """Test creating SBA player"""
        player = SbaPlayer(
            player_id=12288,
            name="Ronald Acuna Jr",
            positions=["RF", "CF"],
            image_url="https://example.com/image.png",
            team_name="West Virginia Black Bears",
            team_abbrev="WV"
        )

        assert player.player_id == 12288
        assert player.get_primary_position() == "RF"
        assert player.can_play_position("CF")
        assert not player.can_play_position("P")

    def test_sba_player_display_name(self):
        """Test display name formatting"""
        player = SbaPlayer(
            player_id=1,
            name="Test Player",
            positions=["SS"],
            image_url="https://example.com/image.png",
            team_name="Test Team",
            team_abbrev="TT"
        )

        assert player.get_display_name() == "Test Player (SS)"

    def test_sba_player_requires_positions(self):
        """Test validation fails without positions"""
        # Empty positions list should be invalid
        with pytest.raises(ValidationError):
            SbaPlayer(
                player_id=1,
                name="Test",
                positions=[],
                image_url="url",
                team_name="Team",
                team_abbrev="TM"
            )

class TestPdPlayer:
    """Test PD game player model"""

    def test_create_pd_player_with_batting(self):
        """Test creating PD position player"""
        batting_vs_lhp = PdBattingRating(
            vs_hand="L",
            homerun=2.0,
            triple=1.0,
            double=10.0,
            single=15.0,
            walk=12.0,
            hbp=2.0,
            strikeout=10.0,
            flyout=5.0,
            lineout=8.0,
            popout=15.0,
            groundout=20.0,
            bp_homerun=3.0,
            bp_single=5.0,
            avg=0.250,
            obp=0.350,
            slg=0.450
        )

        player = PdPlayer(
            player_id=10633,
            name="Chuck Knoblauch",
            positions=["2B"],
            image_url="https://example.com/image.png",
            team_name="New York Yankees",
            cardset_id=20,
            cardset_name="1998 Season",
            is_pitcher=False,
            hand="R",
            batting_vs_lhp=batting_vs_lhp,
            batting_vs_rhp=None
        )

        assert player.player_id == 10633
        assert not player.is_pitcher
        assert player.get_batting_rating("L") == batting_vs_lhp
        assert player.get_pitching_rating("L") is None

    def test_pd_player_display_name_with_handedness(self):
        """Test display name includes handedness symbol"""
        player = PdPlayer(
            player_id=1,
            name="Test Player",
            positions=["2B"],
            image_url="url",
            team_name="Team",
            cardset_id=1,
            cardset_name="2021",
            is_pitcher=False,
            hand="L"
        )

        display = player.get_display_name()
        assert "⟨" in display  # Left-handed symbol
        assert "(2B)" in display

class TestPdBattingRating:
    """Test PD batting rating model"""

    def test_total_probability_calculation(self):
        """Test probability sum calculation"""
        rating = PdBattingRating(
            vs_hand="L",
            homerun=2.0,
            triple=1.0,
            double=10.0,
            single=15.0,
            walk=12.0,
            hbp=2.0,
            strikeout=10.0,
            flyout=5.0,
            lineout=8.0,
            popout=15.0,
            groundout=20.0,
            bp_homerun=3.0,
            bp_single=5.0,
            avg=0.250,
            obp=0.350,
            slg=0.450
        )

        total = rating.total_probability()
        assert total == 108.0  # Sum of all outcomes
```

---

### 3. Mappers (`test_mappers.py`)

**File**: `tests/unit/models/test_mappers.py`

**Coverage**: API → Game model transformation

```python
import pytest
from app.models.api_models import SbaPlayerApi, PdPlayerApi
from app.models.player_models import PlayerMapper

class TestPlayerMapper:
    """Test player model mapping"""

    def test_extract_positions_multiple(self):
        """Test extracting multiple positions"""
        positions = PlayerMapper.extract_positions(
            "RF", "CF", "DH", None, None, None, None, None
        )
        assert positions == ["RF", "CF", "DH"]

    def test_extract_positions_single(self):
        """Test extracting single position"""
        positions = PlayerMapper.extract_positions(
            "P", None, None, None, None, None, None, None
        )
        assert positions == ["P"]

    def test_extract_positions_all_none(self):
        """Test with all None (edge case)"""
        positions = PlayerMapper.extract_positions(
            None, None, None, None, None, None, None, None
        )
        assert positions == []

    def test_map_sba_player(self, sba_player_api):
        """Test SBA player mapping"""
        game_player = PlayerMapper.map_sba_player(sba_player_api)

        assert game_player.player_id == sba_player_api.id
        assert game_player.name == sba_player_api.name
        assert game_player.team_name == sba_player_api.team.lname
        assert game_player.team_abbrev == sba_player_api.team.abbrev
        assert len(game_player.positions) > 0

    def test_map_pd_player_position_player(
        self,
        pd_player_api,
        pd_batting_ratings_api
    ):
        """Test PD position player mapping"""
        game_player = PlayerMapper.map_pd_player(
            pd_player_api,
            pd_batting_ratings_api,
            None  # No pitching ratings
        )

        assert game_player.player_id == pd_player_api.player_id
        assert game_player.name == pd_player_api.p_name
        assert not game_player.is_pitcher
        assert game_player.batting_vs_lhp is not None
        assert game_player.batting_vs_rhp is not None
        assert game_player.pitching_vs_lhb is None

    def test_flatten_batting_rating_combines_doubles(
        self,
        pd_batting_rating_api
    ):
        """Test that double outcomes are combined"""
        # Assume API has: double_three=1.0, double_two=2.0, double_pull=3.0
        game_rating = PlayerMapper._flatten_batting_rating(pd_batting_rating_api)

        # Should be combined to single "double" field
        assert game_rating.double == 6.0  # 1.0 + 2.0 + 3.0
```

---

## Integration Tests

### 1. API Client (`test_api_client.py`)

**File**: `tests/integration/data/test_api_client.py`

**Coverage**: HTTP calls with mocked responses

```python
import pytest
from unittest.mock import AsyncMock, patch
from app.data.api_client import LeagueApiClient

@pytest.mark.integration
@pytest.mark.asyncio
class TestLeagueApiClient:
    """Test API client with mocked HTTP responses"""

    async def test_fetch_sba_player_success(self, mock_httpx_response, sba_player_json):
        """Test successful SBA player fetch"""
        with patch('httpx.AsyncClient') as mock_client:
            mock_client.return_value.get = AsyncMock(
                return_value=mock_httpx_response(sba_player_json)
            )

            async with LeagueApiClient("sba") as client:
                player = await client.get_player(12288)

                assert player.name == "Ronald Acuna Jr"

    async def test_fetch_pd_player_with_ratings(
        self,
        mock_httpx_response,
        pd_player_json,
        pd_batting_ratings_json
    ):
        """Test fetching PD player with ratings"""
        with patch('httpx.AsyncClient') as mock_client:
            # Mock responses for 3 API calls
            mock_client.return_value.get = AsyncMock(
                side_effect=[
                    mock_httpx_response(pd_player_json),
                    mock_httpx_response(pd_batting_ratings_json),
                    httpx.HTTPStatusError("Not a pitcher")
                ]
            )

            async with LeagueApiClient("pd") as client:
                player = await client.get_player(10633)
                batting = await client.get_batting_ratings(10633)

                with pytest.raises(httpx.HTTPStatusError):
                    await client.get_pitching_ratings(10633)

    async def test_api_client_context_manager_closes(self):
        """Test that context manager closes client"""
        client = LeagueApiClient("sba")
        assert client.client is not None

        async with client:
            pass  # Use context manager

        # Client should be closed (aclose called)
        # Verify by checking if subsequent calls fail
```

---

### 2. Full Pipeline (`test_player_pipeline.py`)

**File**: `tests/integration/test_player_pipeline.py`

**Coverage**: API → Mapper → Game Model

```python
import pytest
from app.models.player_models import PlayerFactory

@pytest.mark.integration
@pytest.mark.asyncio
class TestPlayerPipeline:
    """Test complete pipeline from API to game model"""

    async def test_sba_player_full_pipeline(self, mock_sba_api):
        """Test fetching and mapping SBA player"""
        player = await PlayerFactory.create_player("sba", 12288)

        # Verify game model
        assert player.player_id == 12288
        assert player.name == "Ronald Acuna Jr"
        assert "RF" in player.positions
        assert player.can_play_position("RF")

    async def test_pd_position_player_full_pipeline(self, mock_pd_api):
        """Test fetching and mapping PD position player"""
        player = await PlayerFactory.create_player("pd", 10633)

        # Verify game model
        assert player.player_id == 10633
        assert player.name == "Chuck Knoblauch"
        assert not player.is_pitcher

        # Verify ratings attached
        assert player.batting_vs_lhp is not None
        assert player.batting_vs_rhp is not None
        assert player.pitching_vs_lhb is None

    async def test_pd_pitcher_full_pipeline(self, mock_pd_api):
        """Test fetching and mapping PD pitcher"""
        player = await PlayerFactory.create_player("pd", 11223)

        # Verify game model
        assert player.player_id == 11223
        assert player.is_pitcher

        # Verify both batting and pitching ratings
        assert player.batting_vs_lhp is not None
        assert player.pitching_vs_lhb is not None
        assert player.pitching_vs_rhb is not None
```

---

## Test Fixtures

### Shared Fixtures (`conftest.py`)

```python
import pytest
import json
from pathlib import Path

@pytest.fixture
def sba_player_json():
    """Load SBA player JSON from file"""
    path = Path(__file__).parent / "fixtures" / "sba_player.json"
    with open(path) as f:
        return json.load(f)

@pytest.fixture
def pd_player_json():
    """Load PD player JSON from file"""
    path = Path(__file__).parent / "fixtures" / "pd_player.json"
    with open(path) as f:
        return json.load(f)

@pytest.fixture
def pd_batting_ratings_json():
    """Load PD batting ratings JSON from file"""
    path = Path(__file__).parent / "fixtures" / "pd_batting_ratings.json"
    with open(path) as f:
        return json.load(f)

@pytest.fixture
def pd_pitching_ratings_json():
    """Load PD pitching ratings JSON from file"""
    path = Path(__file__).parent / "fixtures" / "pd_pitching_ratings.json"
    with open(path) as f:
        return json.load(f)
```

### Fixture JSON Files

Store real API responses in `tests/fixtures/`:
- `sba_player.json` - From user's example
- `pd_player.json` - From user's example
- `pd_batting_ratings.json` - From user's example
- `pd_pitching_ratings.json` - From user's example

---

## Coverage Goals

| Module | Target | Priority |
|--------|--------|----------|
| `api_models.py` | 95%+ | High |
| `player_models.py` | 95%+ | High |
| `api_client.py` | 85%+ | Medium |
| Mappers | 95%+ | High |
| Integration | 80%+ | Medium |

---

## Running Tests

```bash
# All tests
pytest tests/ -v

# Only unit tests
pytest tests/unit/ -v

# Only integration tests
pytest tests/integration/ -v -m integration

# With coverage
pytest tests/ --cov=app.models --cov=app.data --cov-report=html

# Specific file
pytest tests/unit/models/test_api_models.py -v
```

---

## Test Markers

```python
# pytest.ini
[pytest]
markers =
    integration: marks tests as integration tests (slow, may require network)
    unit: marks tests as unit tests (fast, no external dependencies)
```

Usage:
```bash
# Run only unit tests
pytest -m unit

# Run only integration tests
pytest -m integration

# Skip integration tests
pytest -m "not integration"
```

---

**Status**: Week 6 testing strategy complete. Ready for implementation!
