"""
Unit tests for X-Check advancement tables.

Tests verify that X-Check defensive play results correctly map to runner
advancement based on:
- Base situation (on_base_code 0-7)
- Defensive positioning (defender_in: True/False)
- Error result (NO, E1, E2, E3, RP)

Test Organization:
1. Groundball Table Lookups (G1, G2, G3)
2. Error Advancement Logic
3. Flyball Advancement
4. X-Check Function Integration
5. Edge Cases and Validation

Author: Claude
Date: 2025-11-02
"""

import pytest
from uuid import uuid4
from app.core.x_check_advancement_tables import (
    get_groundball_advancement,
    build_advancement_from_code,
    build_flyball_advancement_with_error,
    get_hit_advancement_with_error,
    get_error_advancement_bases,
    G1_ADVANCEMENT_TABLE,
    G2_ADVANCEMENT_TABLE,
    G3_ADVANCEMENT_TABLE,
)
from app.core.runner_advancement import (
    GroundballResultType,
    x_check_g1,
    x_check_g2,
    x_check_g3,
    x_check_f1,
    x_check_f2,
    x_check_f3,
)
from app.models.game_models import GameState, DefensiveDecision, LineupPlayerState


# Helper function to create test GameState
def create_test_state():
    """Create a minimal GameState for testing."""
    return GameState(
        game_id=uuid4(),
        league_id='sba',
        home_team_id=1,
        away_team_id=2,
        current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
    )


# Helper function to create test DefensiveDecision
def create_test_defensive_decision():
    """Create a default DefensiveDecision for testing."""
    return DefensiveDecision(
        alignment='normal',
        infield_depth='normal',
        outfield_depth='normal'
    )


# ============================================================================
# SECTION 1: GROUNDBALL TABLE LOOKUPS
# ============================================================================
# Tests verify that table lookups return correct GroundballResultType
# based on base situation, defensive positioning, and error result.


class TestGroundballTableLookups:
    """Test groundball advancement table lookups."""

    # ========================================
    # G1 Table Lookups
    # ========================================

    def test_g1_bases_empty_normal_no_error(self):
        """
        Scenario: G1, bases empty, infield normal, no error
        Expected: Result 1 (BATTER_OUT_RUNNERS_HOLD)
        """
        result = get_groundball_advancement('G1', on_base_code=0, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def test_g1_bases_empty_normal_e1(self):
        """
        Scenario: G1, bases empty, infield normal, E1 error
        Expected: SAFE_ALL_ADVANCE_ONE (batter to 1st)
        """
        result = get_groundball_advancement('G1', on_base_code=0, defender_in=False, error_result='E1')
        assert result == GroundballResultType.SAFE_ALL_ADVANCE_ONE

    def test_g1_r1_only_normal_no_error(self):
        """
        Scenario: G1, runner on 1st, infield normal, no error
        Expected: Result 2 (DOUBLE_PLAY_AT_SECOND)
        """
        result = get_groundball_advancement('G1', on_base_code=1, defender_in=False, error_result='NO')
        assert result == GroundballResultType.DOUBLE_PLAY_AT_SECOND

    def test_g1_r1_only_infield_in_no_error(self):
        """
        Scenario: G1, runner on 1st, infield in, no error
        Expected: Result 2 (DOUBLE_PLAY_AT_SECOND)
        Note: Infield in still allows DP on fast grounder
        """
        result = get_groundball_advancement('G1', on_base_code=1, defender_in=True, error_result='NO')
        assert result == GroundballResultType.DOUBLE_PLAY_AT_SECOND

    def test_g1_r2_only_normal_no_error(self):
        """
        Scenario: G1, runner on 2nd, infield normal, no error
        Expected: Result 12 (DECIDE_OPPORTUNITY)
        Note: Runner in scoring position can attempt to advance
        """
        result = get_groundball_advancement('G1', on_base_code=2, defender_in=False, error_result='NO')
        assert result == GroundballResultType.DECIDE_OPPORTUNITY

    def test_g1_r1_r2_normal_no_error(self):
        """
        Scenario: G1, runners on 1st and 2nd, infield normal, no error
        Expected: Result 13 (CONDITIONAL_DOUBLE_PLAY)
        Note: Hit to C/3B = DP at 3rd and 2nd, batter safe. Otherwise = DP at 2nd and 1st
        """
        result = get_groundball_advancement('G1', on_base_code=4, defender_in=False, error_result='NO')
        assert result == GroundballResultType.CONDITIONAL_DOUBLE_PLAY

    def test_g1_r1_r2_infield_in_no_error(self):
        """
        Scenario: G1, runners on 1st and 2nd, infield in, no error
        Expected: Result 2 (DOUBLE_PLAY_AT_SECOND)
        Note: Infield in simplifies to standard DP on fast grounder
        """
        result = get_groundball_advancement('G1', on_base_code=4, defender_in=True, error_result='NO')
        assert result == GroundballResultType.DOUBLE_PLAY_AT_SECOND

    def test_g1_r3_only_normal_no_error(self):
        """
        Scenario: G1, runner on 3rd, infield normal, no error
        Expected: Result 3 (BATTER_OUT_RUNNERS_ADVANCE)
        Note: Batter out, runner on 3rd scores
        """
        result = get_groundball_advancement('G1', on_base_code=3, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE

    def test_g1_r3_only_infield_in_no_error(self):
        """
        Scenario: G1, runner on 3rd, infield in, no error
        Expected: Result 1 (BATTER_OUT_RUNNERS_HOLD)
        Note: Infield in prevents runner from scoring, batter out
        """
        result = get_groundball_advancement('G1', on_base_code=3, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def test_g1_loaded_normal_no_error(self):
        """
        Scenario: G1, bases loaded, infield normal, no error
        Expected: Result 2 (DOUBLE_PLAY_AT_SECOND)
        """
        result = get_groundball_advancement('G1', on_base_code=7, defender_in=False, error_result='NO')
        assert result == GroundballResultType.DOUBLE_PLAY_AT_SECOND

    def test_g1_loaded_infield_in_no_error(self):
        """
        Scenario: G1, bases loaded, infield in, no error
        Expected: Result 10 (DOUBLE_PLAY_HOME_TO_FIRST)
        Note: Attempts DP at home and 1st
        """
        result = get_groundball_advancement('G1', on_base_code=7, defender_in=True, error_result='NO')
        assert result == GroundballResultType.DOUBLE_PLAY_HOME_TO_FIRST

    # ========================================
    # G2 Table Lookups
    # ========================================

    def test_g2_bases_empty_normal_no_error(self):
        """
        Scenario: G2, bases empty, infield normal, no error
        Expected: Result 1 (BATTER_OUT_RUNNERS_HOLD)
        """
        result = get_groundball_advancement('G2', on_base_code=0, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def test_g2_r1_only_normal_no_error(self):
        """
        Scenario: G2, runner on 1st, infield normal, no error
        Expected: Result 4 (BATTER_SAFE_FORCE_OUT_AT_SECOND)
        Note: G2 is slower - batter safe, force out at 2nd
        """
        result = get_groundball_advancement('G2', on_base_code=1, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND

    def test_g2_r1_r2_normal_no_error(self):
        """
        Scenario: G2, runners on 1st and 2nd, infield normal, no error
        Expected: Result 4 (BATTER_SAFE_FORCE_OUT_AT_SECOND)
        Note: G2 is slower - batter safe, runner on 1st forced out at 2nd
        """
        result = get_groundball_advancement('G2', on_base_code=4, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND

    def test_g2_r1_r2_infield_in_no_error(self):
        """
        Scenario: G2, runners on 1st and 2nd, infield in, no error
        Expected: Result 4 (BATTER_SAFE_FORCE_OUT_AT_SECOND)
        """
        result = get_groundball_advancement('G2', on_base_code=4, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND

    def test_g2_r3_only_normal_no_error(self):
        """
        Scenario: G2, runner on 3rd, infield normal, no error
        Expected: Result 5 (CONDITIONAL_ON_MIDDLE_INFIELD)
        """
        result = get_groundball_advancement('G2', on_base_code=3, defender_in=False, error_result='NO')
        assert result == GroundballResultType.CONDITIONAL_ON_MIDDLE_INFIELD

    def test_g2_r3_only_infield_in_no_error(self):
        """
        Scenario: G2, runner on 3rd, infield in, no error
        Expected: Result 1 (BATTER_OUT_RUNNERS_HOLD)
        Note: Infield in prevents runner from scoring, batter out
        """
        result = get_groundball_advancement('G2', on_base_code=3, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def test_g2_loaded_normal_no_error(self):
        """
        Scenario: G2, bases loaded, infield normal, no error
        Expected: Result 4 (BATTER_SAFE_FORCE_OUT_AT_SECOND)
        """
        result = get_groundball_advancement('G2', on_base_code=7, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_FORCE_OUT_AT_SECOND

    def test_g2_loaded_infield_in_no_error(self):
        """
        Scenario: G2, bases loaded, infield in, no error
        Expected: Result 11 (BATTER_SAFE_LEAD_OUT)
        Note: Batter safe at 1st, lead runner (R3) out at home
        """
        result = get_groundball_advancement('G2', on_base_code=7, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_LEAD_OUT

    # ========================================
    # G3 Table Lookups
    # ========================================

    def test_g3_bases_empty_normal_no_error(self):
        """
        Scenario: G3, bases empty, infield normal, no error
        Expected: Result 1 (BATTER_OUT_RUNNERS_HOLD)
        """
        result = get_groundball_advancement('G3', on_base_code=0, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_HOLD

    def test_g3_r1_only_normal_no_error(self):
        """
        Scenario: G3, runner on 1st, infield normal, no error
        Expected: Result 3 (BATTER_OUT_RUNNERS_ADVANCE)
        Note: G3 is slowest - runner advances even with out
        """
        result = get_groundball_advancement('G3', on_base_code=1, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE

    def test_g3_r2_only_normal_no_error(self):
        """
        Scenario: G3, runner on 2nd, infield normal, no error
        Expected: Result 12 (DECIDE_OPPORTUNITY)
        """
        result = get_groundball_advancement('G3', on_base_code=2, defender_in=False, error_result='NO')
        assert result == GroundballResultType.DECIDE_OPPORTUNITY

    def test_g3_r2_only_infield_in_no_error(self):
        """
        Scenario: G3, runner on 2nd, infield in, no error
        Expected: Result 3 (BATTER_OUT_RUNNERS_ADVANCE)
        Note: Infield in changes DECIDE to automatic advance
        """
        result = get_groundball_advancement('G3', on_base_code=2, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE

    def test_g3_r3_only_infield_in_decide(self):
        """
        Scenario: G3, runner on 3rd, infield in, no error
        Expected: Result 12 (DECIDE_OPPORTUNITY)
        Note: DECIDE for R3 attempting to score
        """
        result = get_groundball_advancement('G3', on_base_code=3, defender_in=True, error_result='NO')
        assert result == GroundballResultType.DECIDE_OPPORTUNITY

    def test_g3_r2_r3_infield_in_decide(self):
        """
        Scenario: G3, runners on 2nd and 3rd, infield in, no error
        Expected: Result 12 (DECIDE_OPPORTUNITY)
        Note: DECIDE for lead runner in scoring position
        """
        result = get_groundball_advancement('G3', on_base_code=6, defender_in=True, error_result='NO')
        assert result == GroundballResultType.DECIDE_OPPORTUNITY

    def test_g3_loaded_normal_no_error(self):
        """
        Scenario: G3, bases loaded, infield normal, no error
        Expected: Result 3 (BATTER_OUT_RUNNERS_ADVANCE)
        """
        result = get_groundball_advancement('G3', on_base_code=7, defender_in=False, error_result='NO')
        assert result == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE

    def test_g3_loaded_infield_in_no_error(self):
        """
        Scenario: G3, bases loaded, infield in, no error
        Expected: Result 11 (BATTER_SAFE_LEAD_OUT)
        """
        result = get_groundball_advancement('G3', on_base_code=7, defender_in=True, error_result='NO')
        assert result == GroundballResultType.BATTER_SAFE_LEAD_OUT


# ============================================================================
# SECTION 2: ERROR ADVANCEMENT LOGIC
# ============================================================================
# Tests verify that errors correctly override out results and advance
# all runners (including batter) by E# bases.


class TestErrorAdvancement:
    """Test error advancement logic."""

    def test_e1_bases_empty(self):
        """
        Scenario: E1 error, bases empty
        Expected: Batter to 1st, 0 runs
        """
        result = build_advancement_from_code(
            on_base_code=0,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_ONE,
            result_name="G1"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        # Batter to 1st
        assert result.movements[0].from_base == 0
        assert result.movements[0].to_base == 1
        assert result.movements[0].is_out is False

    def test_e1_runner_on_third(self):
        """
        Scenario: E1 error, runner on 3rd (on_base_code=3)
        Expected: Batter to 1st, R3 scores (1 run)
        """
        result = build_advancement_from_code(
            on_base_code=3,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_ONE,
            result_name="G1"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 1
        assert len(result.movements) == 2
        # Verify R3 scores
        r3_movement = [m for m in result.movements if m.from_base == 3][0]
        assert r3_movement.to_base == 4

    def test_e2_bases_empty(self):
        """
        Scenario: E2 error, bases empty
        Expected: Batter to 2nd, 0 runs
        """
        result = build_advancement_from_code(
            on_base_code=0,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_TWO,
            result_name="G2"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        # Batter to 2nd
        assert result.movements[0].from_base == 0
        assert result.movements[0].to_base == 2

    def test_e2_runner_on_first(self):
        """
        Scenario: E2 error, runner on 1st (on_base_code=1)
        Expected: Batter to 2nd, R1 to 3rd, 0 runs
        """
        result = build_advancement_from_code(
            on_base_code=1,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_TWO,
            result_name="G2"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 0
        assert len(result.movements) == 2
        # Verify R1 to 3rd
        r1_movement = [m for m in result.movements if m.from_base == 1][0]
        assert r1_movement.to_base == 3

    def test_e2_bases_loaded(self):
        """
        Scenario: E2 error, bases loaded (on_base_code=7)
        Expected: Batter to 2nd, R1 to 3rd, R2 scores, R3 scores (2 runs)
        """
        result = build_advancement_from_code(
            on_base_code=7,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_TWO,
            result_name="G2"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 2
        assert len(result.movements) == 4
        # Verify runners score
        r2_movement = [m for m in result.movements if m.from_base == 2][0]
        r3_movement = [m for m in result.movements if m.from_base == 3][0]
        assert r2_movement.to_base == 4
        assert r3_movement.to_base == 4

    def test_e3_bases_empty(self):
        """
        Scenario: E3 error, bases empty
        Expected: Batter to 3rd, 0 runs
        """
        result = build_advancement_from_code(
            on_base_code=0,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_THREE,
            result_name="G3"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        # Batter to 3rd
        assert result.movements[0].from_base == 0
        assert result.movements[0].to_base == 3

    def test_e3_bases_loaded(self):
        """
        Scenario: E3 error, bases loaded (on_base_code=7)
        Expected: Batter to 3rd, all runners score (3 runs)
        """
        result = build_advancement_from_code(
            on_base_code=7,
            gb_type=GroundballResultType.SAFE_ALL_ADVANCE_THREE,
            result_name="G3"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 3
        assert len(result.movements) == 4
        # All runners score
        for movement in result.movements:
            if movement.from_base > 0:  # Not batter
                assert movement.to_base == 4


# ============================================================================
# SECTION 3: FLYBALL ADVANCEMENT
# ============================================================================
# Tests verify flyball advancement with errors.


class TestFlyballAdvancement:
    """Test flyball advancement with errors."""

    def test_flyball_no_error_bases_empty(self):
        """
        Scenario: Flyball, no error, bases empty
        Expected: Batter out, 0 runs
        """
        result = build_flyball_advancement_with_error(
            on_base_code=0,
            error_result='NO',
            flyball_type="F1"
        )
        assert result.outs_recorded == 1
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        assert result.movements[0].is_out is True

    def test_flyball_e1_bases_empty(self):
        """
        Scenario: Flyball + E1, bases empty
        Expected: Out negated, batter to 1st, 0 runs
        """
        result = build_flyball_advancement_with_error(
            on_base_code=0,
            error_result='E1',
            flyball_type="F1"
        )
        assert result.outs_recorded == 0  # Error negates out
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        # Batter to 1st
        assert result.movements[0].from_base == 0
        assert result.movements[0].to_base == 1
        assert result.movements[0].is_out is False

    def test_flyball_e1_runner_on_third(self):
        """
        Scenario: Flyball + E1, runner on 3rd (on_base_code=3)
        Expected: Batter to 1st, R3 scores (1 run)
        """
        result = build_flyball_advancement_with_error(
            on_base_code=3,
            error_result='E1',
            flyball_type="F2"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 1
        assert len(result.movements) == 2
        # R3 scores
        r3_movement = [m for m in result.movements if m.from_base == 3][0]
        assert r3_movement.to_base == 4

    def test_flyball_e2_runner_on_second(self):
        """
        Scenario: Flyball + E2, runner on 2nd (on_base_code=2)
        Expected: Batter to 2nd, R2 scores (1 run)
        """
        result = build_flyball_advancement_with_error(
            on_base_code=2,
            error_result='E2',
            flyball_type="F1"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 1
        assert len(result.movements) == 2
        # Batter to 2nd
        batter_movement = [m for m in result.movements if m.from_base == 0][0]
        assert batter_movement.to_base == 2
        # R2 scores (2 + 2 = 4)
        r2_movement = [m for m in result.movements if m.from_base == 2][0]
        assert r2_movement.to_base == 4

    def test_flyball_e3_bases_loaded(self):
        """
        Scenario: Flyball + E3, bases loaded (on_base_code=7)
        Expected: Batter to 3rd, all runners score (3 runs)
        """
        result = build_flyball_advancement_with_error(
            on_base_code=7,
            error_result='E3',
            flyball_type="F3"
        )
        assert result.outs_recorded == 0
        assert result.runs_scored == 3
        assert len(result.movements) == 4
        # Batter to 3rd
        batter_movement = [m for m in result.movements if m.from_base == 0][0]
        assert batter_movement.to_base == 3
        # All runners score
        for movement in result.movements:
            if movement.from_base > 0:
                assert movement.to_base == 4


# ============================================================================
# SECTION 4: X-CHECK FUNCTION INTEGRATION
# ============================================================================
# Tests verify that x_check_gX and x_check_fX functions correctly integrate
# table lookups with advancement logic.


class TestXCheckFunctionIntegration:
    """Test X-Check function integration."""

    def test_x_check_g1_integration(self):
        """
        Scenario: x_check_g1 with runners on 1st and 2nd, infield in, E1
        Expected: Uses G1 table → SAFE_ALL_ADVANCE_ONE → proper advancement
        """
        state = create_test_state()
        defensive_decision = create_test_defensive_decision()

        result = x_check_g1(
            on_base_code=4,
            defender_in=True,
            error_result='E1',
            state=state,
            hit_location='SS',
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 0
        assert result.runs_scored == 0
        assert len(result.movements) == 3
        # Batter to 1st, R1 to 2nd, R2 to 3rd
        assert any(m.from_base == 0 and m.to_base == 1 for m in result.movements)
        assert any(m.from_base == 1 and m.to_base == 2 for m in result.movements)
        assert any(m.from_base == 2 and m.to_base == 3 for m in result.movements)

    def test_x_check_g2_integration(self):
        """
        Scenario: x_check_g2 with bases loaded, normal, E2
        Expected: Uses G2 table → SAFE_ALL_ADVANCE_TWO → 2 runs score
        """
        state = create_test_state()
        defensive_decision = create_test_defensive_decision()

        result = x_check_g2(
            on_base_code=7,
            defender_in=False,
            error_result='E2',
            state=state,
            hit_location='2B',
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 0
        assert result.runs_scored == 2
        assert len(result.movements) == 4
        # Batter to 2nd
        assert any(m.from_base == 0 and m.to_base == 2 for m in result.movements)

    def test_x_check_g3_integration(self):
        """
        Scenario: x_check_g3 with runner on 3rd, normal, E3
        Expected: Uses G3 table → SAFE_ALL_ADVANCE_THREE → 1 run scores
        """
        state = create_test_state()
        defensive_decision = create_test_defensive_decision()

        result = x_check_g3(
            on_base_code=3,
            defender_in=False,
            error_result='E3',
            state=state,
            hit_location='3B',
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 0
        assert result.runs_scored == 1
        assert len(result.movements) == 2
        # Batter to 3rd, R3 scores
        assert any(m.from_base == 0 and m.to_base == 3 for m in result.movements)
        assert any(m.from_base == 3 and m.to_base == 4 for m in result.movements)

    def test_x_check_f1_integration(self):
        """
        Scenario: x_check_f1 with runner on 2nd, E1 error
        Expected: Flyball + E1 → batter to 1st, R2 to 3rd
        """
        state = create_test_state()

        result = x_check_f1(
            on_base_code=2,
            error_result='E1',
            state=state,
            hit_location='CF'
        )

        assert result.outs_recorded == 0  # Error negates out
        assert result.runs_scored == 0
        assert len(result.movements) == 2
        assert any(m.from_base == 0 and m.to_base == 1 for m in result.movements)
        assert any(m.from_base == 2 and m.to_base == 3 for m in result.movements)

    def test_x_check_f2_integration(self):
        """
        Scenario: x_check_f2 with runners on 1st and 3rd, E2 error
        Expected: Batter to 2nd, R1 to 3rd, R3 scores (1 run)
        """
        state = create_test_state()

        result = x_check_f2(
            on_base_code=5,
            error_result='E2',
            state=state,
            hit_location='RF'
        )

        assert result.outs_recorded == 0
        assert result.runs_scored == 1
        assert len(result.movements) == 3

    def test_x_check_f3_integration(self):
        """
        Scenario: x_check_f3 with bases empty, no error
        Expected: Shallow flyball, batter out, no advancement
        """
        state = create_test_state()

        result = x_check_f3(
            on_base_code=0,
            error_result='NO',
            state=state,
            hit_location='LF'
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0
        assert len(result.movements) == 1
        assert result.movements[0].is_out is True


# ============================================================================
# SECTION 5: EDGE CASES AND VALIDATION
# ============================================================================
# Tests verify error handling, boundary conditions, and input validation.


class TestEdgeCasesAndValidation:
    """Test edge cases and input validation."""

    def test_invalid_groundball_type(self):
        """
        Scenario: Invalid groundball type passed to lookup
        Expected: ValueError raised
        """
        with pytest.raises(ValueError, match="Unknown groundball type"):
            get_groundball_advancement('G4', on_base_code=0, defender_in=False, error_result='NO')

    def test_invalid_on_base_code(self):
        """
        Scenario: Invalid on_base_code (not 0-7)
        Expected: ValueError raised
        """
        with pytest.raises(ValueError, match="not in G1 table"):
            get_groundball_advancement('G1', on_base_code=8, defender_in=False, error_result='NO')

    def test_invalid_error_result(self):
        """
        Scenario: Invalid error result passed to lookup
        Expected: ValueError raised
        """
        with pytest.raises(ValueError, match="not in G1 table"):
            get_groundball_advancement('G1', on_base_code=0, defender_in=False, error_result='E4')

    def test_rare_play_stubbed_as_e1(self):
        """
        Scenario: Rare play (RP) result
        Expected: Currently stubbed as SAFE_ALL_ADVANCE_ONE (E1 equivalent)
        Note: Will be replaced with actual RP logic later
        """
        result = get_groundball_advancement('G1', on_base_code=0, defender_in=False, error_result='RP')
        assert result == GroundballResultType.SAFE_ALL_ADVANCE_ONE

    def test_all_base_codes_covered_g1(self):
        """
        Scenario: Verify all base codes (0-7) covered in G1 table
        Expected: All 8 base codes present
        """
        assert len(G1_ADVANCEMENT_TABLE) == 8
        for base_code in range(8):
            assert base_code in G1_ADVANCEMENT_TABLE

    def test_all_base_codes_covered_g2(self):
        """
        Scenario: Verify all base codes (0-7) covered in G2 table
        Expected: All 8 base codes present
        """
        assert len(G2_ADVANCEMENT_TABLE) == 8
        for base_code in range(8):
            assert base_code in G2_ADVANCEMENT_TABLE

    def test_all_base_codes_covered_g3(self):
        """
        Scenario: Verify all base codes (0-7) covered in G3 table
        Expected: All 8 base codes present
        """
        assert len(G3_ADVANCEMENT_TABLE) == 8
        for base_code in range(8):
            assert base_code in G3_ADVANCEMENT_TABLE

    def test_all_error_types_covered_per_base_code(self):
        """
        Scenario: Verify all error types covered for each base code/position combo
        Expected: Each (defender_in, error_result) key present
        """
        error_types = ['NO', 'E1', 'E2', 'E3', 'RP']
        for base_code in range(8):
            for defender_in in [True, False]:
                for error_result in error_types:
                    key = (defender_in, error_result)
                    # Check G1 table
                    assert key in G1_ADVANCEMENT_TABLE[base_code], \
                        f"G1 missing key {key} for base_code {base_code}"

    def test_hit_advancement_calculation(self):
        """
        Scenario: Test hit advancement formula
        Expected: <Hit Bases> + E# = total bases
        """
        # SI1 + E2 = 1 + 2 = 3 bases
        assert get_hit_advancement_with_error('SI1', 'E2') == 3
        # DO2 + E1 = 2 + 1 = 3 bases
        assert get_hit_advancement_with_error('DO2', 'E1') == 3
        # TR3 + E3 = 3 + 3 = 6 bases (max 4 for scoring)
        assert get_hit_advancement_with_error('TR3', 'E3') == 6

    def test_error_advancement_bases_calculation(self):
        """
        Scenario: Test error advancement base calculation
        Expected: Correct number of bases for each error type
        """
        assert get_error_advancement_bases('NO') == 0
        assert get_error_advancement_bases('E1') == 1
        assert get_error_advancement_bases('E2') == 2
        assert get_error_advancement_bases('E3') == 3
        assert get_error_advancement_bases('RP') == 1  # Stubbed


# ============================================================================
# SECTION 6: COMPREHENSIVE SCENARIO TESTS
# ============================================================================
# Real-world game scenarios combining multiple factors.


class TestComprehensiveScenarios:
    """Test realistic game scenarios."""

    def test_scenario_bases_loaded_infield_in_error(self):
        """
        Real-world scenario: Bases loaded, 1 out, infield in defending against run
        Play: G1 groundball to shortstop, E2 error
        Expected: Error negates DP attempt, batter to 2nd, 2 runs score
        """
        state = create_test_state()
        defensive_decision = create_test_defensive_decision()

        result = x_check_g1(
            on_base_code=7,
            defender_in=True,
            error_result='E2',
            state=state,
            hit_location='SS',
            defensive_decision=defensive_decision
        )

        # Verify result
        assert result.outs_recorded == 0  # Error prevents outs
        assert result.runs_scored == 2  # R2 and R3 score
        assert len(result.movements) == 4  # Batter + 3 runners
        assert "E2" in result.description

    def test_scenario_runner_on_third_two_outs_infield_in(self):
        """
        Real-world scenario: Runner on 3rd, 2 outs, infield in
        Play: G3 slow grounder, no error
        Expected: Table returns DECIDE_OPPORTUNITY, conservative default is batter out
        """
        state = create_test_state()
        state.outs = 2  # Set to 2 outs for this scenario
        defensive_decision = create_test_defensive_decision()

        result = x_check_g3(
            on_base_code=3,
            defender_in=True,
            error_result='NO',
            state=state,
            hit_location='P',
            defensive_decision=defensive_decision
        )

        # Table returns DECIDE_OPPORTUNITY, conservative handling returns batter out
        assert result.result_type == GroundballResultType.DECIDE_OPPORTUNITY
        assert result.outs_recorded == 1  # Conservative: batter out
        assert result.runs_scored == 0  # Conservative: runner holds

    def test_scenario_flyball_to_outfield_runner_tags(self):
        """
        Real-world scenario: Runner on 3rd, 1 out, deep flyball
        Play: F1 to left field, E1 error by outfielder
        Expected: Error allows batter to 1st, runner scores
        """
        state = create_test_state()

        result = x_check_f1(
            on_base_code=3,
            error_result='E1',
            state=state,
            hit_location='LF'
        )

        assert result.outs_recorded == 0  # Error negates out
        assert result.runs_scored == 1  # R3 scores
        # Batter reaches 1st on error
        assert any(m.from_base == 0 and m.to_base == 1 for m in result.movements)

    def test_scenario_double_play_attempt_with_error(self):
        """
        Real-world scenario: Runner on 1st, 0 outs, DP opportunity
        Play: G1 grounder, E1 error
        Expected: Error prevents DP, all safe
        """
        state = create_test_state()
        defensive_decision = create_test_defensive_decision()

        result = x_check_g1(
            on_base_code=1,
            defender_in=False,
            error_result='E1',
            state=state,
            hit_location='2B',
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 0  # No DP, error
        assert result.runs_scored == 0
        # Both batter and R1 safe
        assert len(result.movements) == 2
