# Phase 3F: Testing & Integration for X-Check System

**Status**: Not Started
**Estimated Effort**: 4-5 hours
**Dependencies**: All previous phases (3A-3E)

## Overview

Comprehensive testing strategy for X-Check system covering:
- Unit tests for all components
- Integration tests for complete flows
- Test fixtures and mock data
- End-to-end scenarios
- Performance validation

## Tasks

### 1. Create Test Fixtures

**File**: `tests/fixtures/x_check_fixtures.py` (NEW FILE)

```python
"""
Test fixtures for X-Check system.

Provides mock players, position ratings, defense tables, etc.

Author: Claude
Date: 2025-11-01
"""
import pytest
from app.models.player_models import PdPlayer, PositionRating, PdBattingCard
from app.models.game_models import GameState, XCheckResult
from app.config.result_charts import PlayOutcome


@pytest.fixture
def mock_position_rating_ss():
    """Mock position rating for shortstop (good defender)."""
    return PositionRating(
        position='SS',
        innings=1200,
        range=2,  # Good range
        error=10,  # Average error rating
        arm=80,
        pb=None,
        overthrow=5,
    )


@pytest.fixture
def mock_position_rating_lf():
    """Mock position rating for left field (average defender)."""
    return PositionRating(
        position='LF',
        innings=800,
        range=3,  # Average range
        error=15,  # Below average error
        arm=70,
        pb=None,
        overthrow=8,
    )


@pytest.fixture
def mock_pd_player_with_positions():
    """Mock PD player with multiple positions cached."""
    from app.models.player_models import PdCardset, PdRarity

    player = PdPlayer(
        id=10932,
        name="Chipper Jones",
        cost=254,
        image="https://pd.manticorum.com/api/v2/players/10932/battingcard",
        cardset=PdCardset(id=21, name="1998 Promos", description="1998", ranked_legal=True),
        set_num=97,
        rarity=PdRarity(id=2, value=3, name="All-Star", color="FFD700"),
        mlbclub="Atlanta Braves",
        franchise="Atlanta Braves",
        pos_1="3B",
        description="April PotM",
        batting_card=PdBattingCard(
            steal_low=1,
            steal_high=12,
            steal_auto=False,
            steal_jump=0.5,
            bunting="C",
            hit_and_run="B",
            running=14,  # Speed for SPD test
            offense_col=1,
            hand="R",
            ratings={},
        ),
    )

    return player


@pytest.fixture
def mock_x_check_result_si2_e1():
    """Mock XCheckResult for SI2 + E1."""
    return XCheckResult(
        position='SS',
        d20_roll=15,
        d6_roll=12,
        defender_range=2,
        defender_error_rating=10,
        defender_id=5001,
        base_result='SI2',
        converted_result='SI2',
        error_result='E1',
        final_outcome=PlayOutcome.SINGLE_2,
        hit_type='si2_plus_error_1',
    )


@pytest.fixture
def mock_x_check_result_g2_no_error():
    """Mock XCheckResult for G2 with no error."""
    return XCheckResult(
        position='2B',
        d20_roll=10,
        d6_roll=8,
        defender_range=3,
        defender_error_rating=12,
        defender_id=5002,
        base_result='G2',
        converted_result='G2',
        error_result='NO',
        final_outcome=PlayOutcome.GROUNDBALL_B,
        hit_type='g2_no_error',
    )


@pytest.fixture
def mock_x_check_result_f2_e3():
    """Mock XCheckResult for F2 + E3 (out becomes error)."""
    return XCheckResult(
        position='LF',
        d20_roll=16,
        d6_roll=17,
        defender_range=4,
        defender_error_rating=18,
        defender_id=5003,
        base_result='F2',
        converted_result='F2',
        error_result='E3',
        final_outcome=PlayOutcome.ERROR,  # Out + error = ERROR
        hit_type='f2_plus_error_3',
    )


@pytest.fixture
def mock_x_check_result_spd_passed():
    """Mock XCheckResult for SPD test (passed)."""
    return XCheckResult(
        position='C',
        d20_roll=12,
        d6_roll=9,
        defender_range=2,
        defender_error_rating=8,
        defender_id=5004,
        base_result='SPD',
        converted_result='SI1',  # Passed speed test
        error_result='NO',
        final_outcome=PlayOutcome.SINGLE_1,
        hit_type='si1_no_error',
        spd_test_roll=13,
        spd_test_target=14,
        spd_test_passed=True,
    )


@pytest.fixture
def mock_game_state_r1():
    """Mock game state with runner on first."""
    # TODO: Create full GameState mock with R1
    pass


@pytest.fixture
def mock_game_state_bases_loaded():
    """Mock game state with bases loaded."""
    # TODO: Create full GameState mock with bases loaded
    pass
```

### 2. Unit Tests for Core Components

**File**: `tests/core/test_x_check_resolution.py`

```python
"""
Unit tests for X-Check resolution logic.

Tests PlayResolver._resolve_x_check() and helper methods.

Author: Claude
Date: 2025-11-01
"""
import pytest
from app.core.play_resolver import PlayResolver
from app.config.result_charts import PlayOutcome


class TestDefenseTableLookup:
    """Test defense table lookups."""

    def test_infield_lookup_best_range(self, play_resolver):
        """Test infield lookup with range 1 (best)."""
        result = play_resolver._lookup_defense_table('SS', d20_roll=1, defense_range=1)
        assert result == 'G3#'

    def test_infield_lookup_worst_range(self, play_resolver):
        """Test infield lookup with range 5 (worst)."""
        result = play_resolver._lookup_defense_table('3B', d20_roll=1, defense_range=5)
        assert result == 'SI2'

    def test_outfield_lookup(self, play_resolver):
        """Test outfield lookup."""
        result = play_resolver._lookup_defense_table('LF', d20_roll=5, defense_range=2)
        assert result == 'DO2'

    def test_catcher_lookup(self, play_resolver):
        """Test catcher-specific table."""
        result = play_resolver._lookup_defense_table('C', d20_roll=10, defense_range=1)
        assert result == 'SPD'


class TestSpdTest:
    """Test SPD (speed test) resolution."""

    def test_spd_pass(self, play_resolver, mock_pd_player_with_positions, mocker):
        """Test passing speed test (roll <= speed)."""
        # Mock dice to roll 12 (player speed = 14)
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=12)

        result, roll, target, passed = play_resolver._resolve_spd_test(
            mock_pd_player_with_positions
        )

        assert result == 'SI1'
        assert roll == 12
        assert target == 14
        assert passed is True

    def test_spd_fail(self, play_resolver, mock_pd_player_with_positions, mocker):
        """Test failing speed test (roll > speed)."""
        # Mock dice to roll 16 (player speed = 14)
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=16)

        result, roll, target, passed = play_resolver._resolve_spd_test(
            mock_pd_player_with_positions
        )

        assert result == 'G3'
        assert roll == 16
        assert target == 14
        assert passed is False


class TestHashConversion:
    """Test G2#/G3# → SI2 conversion logic."""

    def test_conversion_when_playing_in(self, play_resolver, mock_game_state_r1, mock_pd_player_with_positions):
        """Test # conversion when defender playing in."""
        result = play_resolver._apply_hash_conversion(
            result='G2#',
            position='3B',
            adjusted_range=3,  # Was 2, increased to 3 (playing in)
            base_range=2,
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
        )

        assert result == 'SI2'

    def test_conversion_when_holding_runner(self, play_resolver, mock_game_state_r1, mock_pd_player_with_positions, mocker):
        """Test # conversion when holding runner."""
        # Mock holding function to return 1B
        mocker.patch(
            'app.config.common_x_check_tables.get_fielders_holding_runners',
            return_value=['1B']
        )

        result = play_resolver._apply_hash_conversion(
            result='G3#',
            position='1B',
            adjusted_range=2,  # Same as base (not playing in)
            base_range=2,
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
        )

        assert result == 'SI2'

    def test_no_conversion(self, play_resolver, mock_game_state_r1, mock_pd_player_with_positions, mocker):
        """Test no conversion when conditions not met."""
        mocker.patch(
            'app.config.common_x_check_tables.get_fielders_holding_runners',
            return_value=[]
        )

        result = play_resolver._apply_hash_conversion(
            result='G2#',
            position='SS',
            adjusted_range=2,
            base_range=2,
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
        )

        assert result == 'G2'  # # removed, not converted to SI2


class TestErrorChartLookup:
    """Test error chart lookups."""

    def test_no_error(self, play_resolver):
        """Test 3d6 roll with no error."""
        result = play_resolver._lookup_error_chart(
            position='LF',
            error_rating=0,
            d6_roll=6  # Not in any error list for rating 0
        )

        assert result == 'NO'

    def test_error_e1(self, play_resolver):
        """Test 3d6 roll resulting in E1."""
        result = play_resolver._lookup_error_chart(
            position='LF',
            error_rating=1,
            d6_roll=3  # In E1 list for rating 1
        )

        assert result == 'E1'

    def test_rare_play(self, play_resolver):
        """Test 3d6 roll resulting in Rare Play."""
        result = play_resolver._lookup_error_chart(
            position='LF',
            error_rating=10,
            d6_roll=5  # Always RP
        )

        assert result == 'RP'


class TestFinalOutcomeDetermination:
    """Test final outcome and hit_type determination."""

    def test_hit_no_error(self, play_resolver):
        """Test hit with no error."""
        outcome, hit_type = play_resolver._determine_final_x_check_outcome(
            converted_result='SI2',
            error_result='NO'
        )

        assert outcome == PlayOutcome.SINGLE_2
        assert hit_type == 'si2_no_error'

    def test_hit_with_error(self, play_resolver):
        """Test hit with error (keep hit outcome)."""
        outcome, hit_type = play_resolver._determine_final_x_check_outcome(
            converted_result='DO2',
            error_result='E1'
        )

        assert outcome == PlayOutcome.DOUBLE_2
        assert hit_type == 'do2_plus_error_1'

    def test_out_with_error(self, play_resolver):
        """Test out with error (becomes ERROR outcome)."""
        outcome, hit_type = play_resolver._determine_final_x_check_outcome(
            converted_result='F2',
            error_result='E3'
        )

        assert outcome == PlayOutcome.ERROR
        assert hit_type == 'f2_plus_error_3'

    def test_rare_play(self, play_resolver):
        """Test rare play result."""
        outcome, hit_type = play_resolver._determine_final_x_check_outcome(
            converted_result='G1',
            error_result='RP'
        )

        assert outcome == PlayOutcome.ERROR  # RP treated like error
        assert hit_type == 'g1_rare_play'
```

### 3. Integration Tests for Complete Flows

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

```python
"""
Integration tests for complete X-Check flows.

Tests end-to-end resolution from outcome to Play record.

Author: Claude
Date: 2025-11-01
"""
import pytest
from app.core.play_resolver import PlayResolver
from app.config.result_charts import PlayOutcome


class TestXCheckInfieldFlow:
    """Test complete X-Check flow for infield positions."""

    @pytest.mark.asyncio
    async def test_infield_groundball_no_error(
        self,
        play_resolver,
        mock_game_state_r1,
        mock_pd_player_with_positions,
        mocker
    ):
        """Test infield X-Check resulting in groundout."""
        # Mock dice rolls
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=15)
        mocker.patch.object(play_resolver.dice, 'roll_3d6', return_value=8)

        # Mock defender with good range
        defender = mock_pd_player_with_positions
        defender.active_position_rating = pytest.fixtures.mock_position_rating_ss()

        result = await play_resolver._resolve_x_check(
            position='SS',
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,  # Reuse for simplicity
        )

        # Verify result
        assert result.x_check_details is not None
        assert result.x_check_details.position == 'SS'
        assert result.x_check_details.error_result == 'NO'
        assert result.outcome.is_out()

    @pytest.mark.asyncio
    async def test_infield_with_error(
        self,
        play_resolver,
        mock_game_state_r1,
        mock_pd_player_with_positions,
        mocker
    ):
        """Test infield X-Check with error."""
        # Mock dice rolls that produce error
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=10)
        mocker.patch.object(play_resolver.dice, 'roll_3d6', return_value=3)  # E1

        result = await play_resolver._resolve_x_check(
            position='2B',
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,
        )

        # Verify error applied
        assert result.x_check_details.error_result == 'E1'
        assert result.outcome == PlayOutcome.ERROR or result.outcome.is_hit()


class TestXCheckOutfieldFlow:
    """Test complete X-Check flow for outfield positions."""

    @pytest.mark.asyncio
    async def test_outfield_flyball_deep(
        self,
        play_resolver,
        mock_game_state_bases_loaded,
        mock_pd_player_with_positions,
        mocker
    ):
        """Test deep flyball (F1) to outfield."""
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=8)
        mocker.patch.object(play_resolver.dice, 'roll_3d6', return_value=10)

        result = await play_resolver._resolve_x_check(
            position='CF',
            state=mock_game_state_bases_loaded,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,
        )

        # F1 should be deep fly with runner advancement
        assert result.x_check_details.converted_result == 'F1'
        assert result.advancement is not None


class TestXCheckCatcherSpdFlow:
    """Test X-Check flow for catcher with SPD test."""

    @pytest.mark.asyncio
    async def test_catcher_spd_pass(
        self,
        play_resolver,
        mock_game_state_r1,
        mock_pd_player_with_positions,
        mocker
    ):
        """Test catcher SPD test with pass."""
        # Roll SPD result
        mocker.patch.object(play_resolver.dice, 'roll_d20', side_effect=[10, 12])  # Table, then SPD
        mocker.patch.object(play_resolver.dice, 'roll_3d6', return_value=9)

        result = await play_resolver._resolve_x_check(
            position='C',
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,
        )

        # Verify SPD test recorded
        assert result.x_check_details.base_result == 'SPD'
        assert result.x_check_details.spd_test_passed is not None
        assert result.x_check_details.converted_result in ['SI1', 'G3']


class TestXCheckHashConversion:
    """Test G2#/G3# conversion scenarios."""

    @pytest.mark.asyncio
    async def test_hash_conversion_playing_in(
        self,
        play_resolver,
        mock_game_state_r1,
        mock_pd_player_with_positions,
        mocker
    ):
        """Test # conversion when infield playing in."""
        # Mock state with infield_in decision
        mock_game_state_r1.current_defensive_decision.infield_in = True

        # Mock rolls to produce G2#
        mocker.patch.object(play_resolver.dice, 'roll_d20', return_value=2)
        mocker.patch.object(play_resolver.dice, 'roll_3d6', return_value=10)

        result = await play_resolver._resolve_x_check(
            position='2B',
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,
        )

        # Should convert to SI2
        assert result.x_check_details.base_result == 'G2#'
        assert result.x_check_details.converted_result == 'SI2'
        assert result.outcome == PlayOutcome.SINGLE_2
```

### 4. WebSocket Event Tests

**File**: `tests/websocket/test_x_check_events.py`

```python
"""
Integration tests for X-Check WebSocket events.

Author: Claude
Date: 2025-11-01
"""
import pytest
from unittest.mock import AsyncMock


class TestXCheckAutoMode:
    """Test PD auto mode X-Check flow."""

    @pytest.mark.asyncio
    async def test_auto_result_broadcast(self, socket_client, mock_game_state_r1):
        """Test auto-resolved result broadcast."""
        # Trigger X-Check
        await socket_client.emit('action', {
            'game_id': 1,
            'action_type': 'swing',
            # ... other params
        })

        # Should receive x_check_auto_result event
        response = await socket_client.receive()
        assert response['type'] == 'x_check_auto_result'
        assert 'x_check' in response
        assert response['x_check']['position'] in ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF']

    @pytest.mark.asyncio
    async def test_accept_auto_result(self, socket_client):
        """Test player accepting auto result."""
        # Confirm result
        await socket_client.emit('confirm_x_check_result', {
            'game_id': 1,
            'accepted': True,
        })

        # Should receive updated game state
        response = await socket_client.receive()
        assert response['type'] == 'game_update'
        # Play should be recorded

    @pytest.mark.asyncio
    async def test_reject_auto_result(self, socket_client, mocker):
        """Test player rejecting auto result (logs override)."""
        # Mock override logger
        log_mock = mocker.patch('app.websocket.game_handlers.log_x_check_override')

        # Reject result
        await socket_client.emit('confirm_x_check_result', {
            'game_id': 1,
            'accepted': False,
            'override_outcome': 'SI2_E1',
        })

        # Verify override logged
        assert log_mock.called


class TestXCheckManualMode:
    """Test SBA manual mode X-Check flow."""

    @pytest.mark.asyncio
    async def test_manual_options_broadcast(self, socket_client):
        """Test manual mode dice + options broadcast."""
        # Trigger X-Check
        await socket_client.emit('action', {
            'game_id': 1,
            'action_type': 'swing',
            # ... params
        })

        # Should receive manual options
        response = await socket_client.receive()
        assert response['type'] == 'x_check_manual_options'
        assert 'd20' in response
        assert 'd6' in response
        assert 'options' in response
        assert len(response['options']) > 0

    @pytest.mark.asyncio
    async def test_manual_submission(self, socket_client):
        """Test player submitting manual outcome."""
        # Submit choice
        await socket_client.emit('submit_x_check_manual', {
            'game_id': 1,
            'outcome': 'SI2_E1',
        })

        # Should receive updated game state
        response = await socket_client.receive()
        assert response['type'] == 'game_update'
```

### 5. Performance Tests

**File**: `tests/performance/test_x_check_performance.py`

```python
"""
Performance tests for X-Check resolution.

Ensures resolution stays under latency targets.

Author: Claude
Date: 2025-11-01
"""
import pytest
import time


class TestXCheckPerformance:
    """Test X-Check resolution performance."""

    @pytest.mark.asyncio
    async def test_resolution_latency(self, play_resolver, mock_game_state_r1, mock_pd_player_with_positions):
        """Test single X-Check resolution completes under 100ms."""
        start = time.time()

        result = await play_resolver._resolve_x_check(
            position='SS',
            state=mock_game_state_r1,
            batter=mock_pd_player_with_positions,
            pitcher=mock_pd_player_with_positions,
        )

        elapsed = (time.time() - start) * 1000  # Convert to ms

        assert elapsed < 100, f"X-Check resolution took {elapsed}ms (target: <100ms)"

    @pytest.mark.asyncio
    async def test_batch_resolution(self, play_resolver, mock_game_state_r1, mock_pd_player_with_positions):
        """Test 100 X-Check resolutions complete under 5 seconds."""
        start = time.time()

        for _ in range(100):
            await play_resolver._resolve_x_check(
                position='LF',
                state=mock_game_state_r1,
                batter=mock_pd_player_with_positions,
                pitcher=mock_pd_player_with_positions,
            )

        elapsed = time.time() - start

        assert elapsed < 5.0, f"100 resolutions took {elapsed}s (target: <5s)"
```

## Testing Checklist

### Unit Tests
- [ ] Defense table lookup (all positions)
- [ ] SPD test (pass/fail)
- [ ] Hash conversion (playing in, holding runner, none)
- [ ] Error chart lookup (all error types)
- [ ] Final outcome determination (all combinations)
- [ ] Advancement table lookups
- [ ] Option generation

### Integration Tests
- [ ] Complete infield X-Check (no error)
- [ ] Complete infield X-Check (with error)
- [ ] Complete outfield X-Check
- [ ] Complete catcher X-Check with SPD
- [ ] Hash conversion in game context
- [ ] Error overriding outs
- [ ] Rare play handling

### WebSocket Tests
- [ ] Auto mode result broadcast
- [ ] Accept auto result
- [ ] Reject auto result (logs override)
- [ ] Manual mode options broadcast
- [ ] Manual submission

### Performance Tests
- [ ] Single resolution < 100ms
- [ ] Batch resolution (100 plays) < 5s

### Database Tests
- [ ] Play record created with check_pos
- [ ] Play record has correct hit_type
- [ ] Defender_id populated
- [ ] Error and hit flags correct

## Acceptance Criteria

- [ ] All unit tests pass (>95% coverage for X-Check code)
- [ ] All integration tests pass
- [ ] All WebSocket tests pass
- [ ] Performance tests meet targets
- [ ] No regressions in existing tests
- [ ] Test fixtures complete and documented
- [ ] Mock data representative of real scenarios

## Notes

- Use pytest fixtures for reusable test data
- Mock Redis for position rating tests
- Mock dice rolls for deterministic tests
- Test edge cases (range 1, range 5, error 0, error 25)
- Test all position types (P, C, IF, OF)
- Validate WebSocket message formats match frontend expectations

## Final Integration Checklist

After all tests pass:

- [ ] Manual smoke test: Create PD game, trigger X-Check, verify UI
- [ ] Manual smoke test: Create SBA game, trigger X-Check, verify manual flow
- [ ] Verify Redis caching working (position ratings persisted)
- [ ] Verify override logging working (check database)
- [ ] Performance profiling (identify any bottlenecks)
- [ ] Code review: Check all imports present (no NameErrors)
- [ ] Documentation: Update API docs with X-Check events
- [ ] Frontend integration: Verify all event handlers working

## Success Metrics

- **Correctness**: All test scenarios produce expected outcomes
- **Performance**: Sub-100ms resolution time
- **Reliability**: No exceptions in 1000-play test
- **User Experience**: Auto/manual flows work smoothly
- **Debuggability**: Override logs help diagnose issues

---

**END OF PHASE 3F**

Once all phases (3A-3F) are complete, the X-Check system will be fully functional and tested!
