"""
Tests for runner advancement logic.

This module tests the complete runner advancement system including:
- Chart lookup (Infield Back vs Infield In)
- All 13 groundball result types
- Double play probability calculations
- Edge cases and special scenarios
"""

import pytest
from unittest.mock import Mock, patch

from app.core.runner_advancement import (
    RunnerAdvancement,
    GroundballResultType,
    RunnerMovement,
    AdvancementResult
)
from app.models.game_models import GameState, DefensiveDecision, LineupPlayerState
from app.config.result_charts import PlayOutcome


# Helper function to create mock runners
def create_runner(lineup_id: int) -> LineupPlayerState:
    """Create a LineupPlayerState for testing."""
    return LineupPlayerState(
        lineup_id=lineup_id,
        card_id=lineup_id * 100,  # Arbitrary card_id
        position="CF",
        batting_order=lineup_id
    )


# ========================================
# Fixtures
# ========================================

@pytest.fixture
def advancement():
    """Create RunnerAdvancement instance."""
    return RunnerAdvancement()


@pytest.fixture
def base_state():
    """Create a base GameState mock for testing."""
    state = Mock(spec=GameState)
    state.outs = 0
    state.current_on_base_code = 0
    # Mock current_batter with lineup_id
    mock_batter = Mock()
    mock_batter.lineup_id = 1
    state.current_batter = mock_batter
    state.on_first = None
    state.on_second = None
    state.on_third = None
    state.is_runner_on_first = Mock(return_value=False)
    state.is_runner_on_second = Mock(return_value=False)
    state.is_runner_on_third = Mock(return_value=False)
    return state


@pytest.fixture
def normal_defense():
    """Create normal defensive decision."""
    return DefensiveDecision(
        alignment="normal",
        infield_depth="normal",
        outfield_depth="normal",
        hold_runners=[]
    )


@pytest.fixture
def infield_in_defense():
    """Create infield in defensive decision."""
    return DefensiveDecision(
        alignment="normal",
        infield_depth="infield_in",
        outfield_depth="normal",
        hold_runners=[]
    )


@pytest.fixture
def corners_in_defense():
    """Create corners in defensive decision."""
    return DefensiveDecision(
        alignment="normal",
        infield_depth="corners_in",
        outfield_depth="normal",
        hold_runners=[]
    )


# ========================================
# Chart Lookup Tests
# ========================================

class TestChartLookup:
    """Tests for determining which groundball result applies."""

    def test_empty_bases_always_result_1(self, advancement, base_state, normal_defense):
        """Empty bases always results in batter out, runners hold."""
        base_state.current_on_base_code = 0
        base_state.outs = 0

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        assert result.result_type == GroundballResultType.BATTER_OUT_RUNNERS_HOLD
        assert result.outs_recorded == 1
        assert result.runs_scored == 0

    def test_two_outs_always_result_1(self, advancement, base_state, normal_defense):
        """With 2 outs, always batter out regardless of situation."""
        base_state.current_on_base_code = 7  # Bases loaded
        base_state.outs = 2
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.on_third = create_runner(4)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        assert result.result_type == GroundballResultType.BATTER_OUT_RUNNERS_HOLD
        assert result.outs_recorded == 1

    def test_runner_on_first_gba_infield_back(self, advancement, base_state, normal_defense):
        """Runner on 1st, GBA, Infield Back → Result 2 (DP attempt)."""
        base_state.current_on_base_code = 1
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.is_runner_on_first = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        assert result.result_type == GroundballResultType.DOUBLE_PLAY_AT_SECOND

    def test_runner_on_third_infield_in(self, advancement, base_state, infield_in_defense):
        """Runner on 3rd, GBA, Infield In → Result 7 (forced advancement)."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        assert result.result_type == GroundballResultType.BATTER_OUT_FORCED_ONLY

    def test_bases_loaded_infield_in(self, advancement, base_state, infield_in_defense):
        """Bases loaded, GBA, Infield In → Result 10 (DP home to first)."""
        base_state.current_on_base_code = 7
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.on_third = create_runner(4)
        base_state.is_runner_on_first = Mock(return_value=True)
        base_state.is_runner_on_second = Mock(return_value=True)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        assert result.result_type == GroundballResultType.DOUBLE_PLAY_HOME_TO_FIRST

    def test_corners_in_hit_to_corner(self, advancement, base_state, corners_in_defense):
        """Runner on 3rd, GBA, Corners In, hit to 3B → Uses Infield In rules."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='3B',  # Corner
            state=base_state,
            defensive_decision=corners_in_defense
        )

        # Should use Infield In rules for corners
        assert result.result_type == GroundballResultType.BATTER_OUT_FORCED_ONLY

    def test_corners_in_hit_to_middle(self, advancement, base_state, corners_in_defense):
        """Runner on 3rd, GBA, Corners In, hit to SS → Uses Infield Back rules (Result 5 → Result 3)."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',  # Middle infield
            state=base_state,
            defensive_decision=corners_in_defense
        )

        # Should use Infield Back rules for middle IF (Result 5 hit to MIF = Result 3)
        assert result.result_type == GroundballResultType.BATTER_OUT_RUNNERS_ADVANCE
        assert result.runs_scored == 1  # R3 scores


# ========================================
# Result Handler Tests
# ========================================

class TestResult1:
    """Tests for Result 1: Batter out, runners hold."""

    def test_empty_bases(self, advancement, base_state, normal_defense):
        """Empty bases: only batter out."""
        base_state.current_on_base_code = 0
        base_state.outs = 0

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_B,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

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

    def test_runners_hold(self, advancement, base_state, normal_defense):
        """All runners stay put."""
        base_state.current_on_base_code = 7  # Loaded
        base_state.outs = 2  # Forces result 1
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.on_third = create_runner(4)
        base_state.is_runner_on_first = Mock(return_value=True)
        base_state.is_runner_on_second = Mock(return_value=True)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        # Should have batter + 3 runners
        assert len(result.movements) == 4

        # All runners should stay put
        runner_movements = [m for m in result.movements if m.from_base > 0]
        for movement in runner_movements:
            assert movement.from_base == movement.to_base
            assert movement.is_out is False


class TestResult2:
    """Tests for Result 2: Double play at 2nd and 1st."""

    def test_double_play_executed(self, advancement, base_state, normal_defense):
        """DP is executed when possible (< 2 outs, runner on 1st)."""
        base_state.current_on_base_code = 4  # 1st and 2nd
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.is_runner_on_first = Mock(return_value=True)
        base_state.is_runner_on_second = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        # Should have 2 outs
        assert result.outs_recorded == 2

        # Runner on 1st should be out
        r1_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r1_movement.is_out is True

        # Batter should be out
        batter_movement = next(m for m in result.movements if m.from_base == 0)
        assert batter_movement.is_out is True

        # Runner on 2nd should score
        r2_movement = next(m for m in result.movements if m.lineup_id == 3)
        assert r2_movement.to_base == 4
        assert result.runs_scored == 1

    def test_double_play_not_possible_two_outs(self, advancement, base_state, normal_defense):
        """DP not possible with 2 outs - only batter out."""
        base_state.current_on_base_code = 1  # Runner on 1st
        base_state.outs = 2
        base_state.on_first = create_runner(2)
        base_state.is_runner_on_first = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        # Should only have 1 out (batter)
        assert result.outs_recorded == 1

        # Batter should be out
        batter_movement = next(m for m in result.movements if m.from_base == 0)
        assert batter_movement.is_out is True


class TestResult3:
    """Tests for Result 3: Batter out, runners advance 1 base."""

    def test_runner_on_first(self, advancement, base_state, normal_defense):
        """Runner on 1st advances to 2nd."""
        base_state.current_on_base_code = 1
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.is_runner_on_first = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0

        # Runner advances to 2nd
        r1_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r1_movement.from_base == 1
        assert r1_movement.to_base == 2

    def test_runner_scores_from_third(self, advancement, base_state, normal_defense):
        """Runner on 3rd scores."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='SS',
            state=base_state,
            defensive_decision=normal_defense
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 1

        # Runner scores
        r3_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r3_movement.from_base == 3
        assert r3_movement.to_base == 4


class TestResult5:
    """Tests for Result 5: Conditional on middle infield."""

    def test_hit_to_middle_infield(self, advancement, base_state, normal_defense):
        """Hit to 2B/SS: runners advance."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='2B',  # Middle infield
            state=base_state,
            defensive_decision=normal_defense
        )

        # Should advance runners (Result 3)
        assert result.runs_scored == 1

    def test_hit_to_corner(self, advancement, base_state, normal_defense):
        """Hit to corner: runners hold."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_B,
            hit_location='3B',  # Corner
            state=base_state,
            defensive_decision=normal_defense
        )

        # Should hold runners (Result 1)
        assert result.runs_scored == 0
        r3_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r3_movement.to_base == 3  # Holds


class TestResult7:
    """Tests for Result 7: Forced advancement only."""

    def test_bases_loaded_forced_advancement(self, advancement, base_state, infield_in_defense):
        """Bases loaded, Infield In, GBC: Result 7/8 - forced advancement only."""
        base_state.current_on_base_code = 7  # Bases loaded
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.on_third = create_runner(4)
        base_state.is_runner_on_first = Mock(return_value=True)
        base_state.is_runner_on_second = Mock(return_value=True)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,  # GBC
            hit_location='P',  # Not 1B/2B (would be Result 12)
            state=base_state,
            defensive_decision=infield_in_defense
        )

        # Result 11: Batter safe, lead runner out
        assert result.result_type == GroundballResultType.BATTER_SAFE_LEAD_OUT
        assert result.outs_recorded == 1

    def test_runner_on_third_only_holds(self, advancement, base_state, infield_in_defense):
        """Runner on 3rd only (not forced): holds."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        assert result.runs_scored == 0

        # R3 should hold (not forced)
        r3_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r3_movement.to_base == 3


class TestResult10:
    """Tests for Result 10: Double play home to first."""

    def test_successful_dp_home_to_first(self, advancement, base_state, infield_in_defense):
        """Successful DP: R3 out at home, batter out."""
        base_state.current_on_base_code = 7
        base_state.outs = 0
        base_state.on_first = create_runner(2)
        base_state.on_second = create_runner(3)
        base_state.on_third = create_runner(4)
        base_state.is_runner_on_first = Mock(return_value=True)
        base_state.is_runner_on_second = Mock(return_value=True)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_A,
            hit_location='SS',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        assert result.outs_recorded == 2
        assert result.runs_scored == 0

        # R3 should be out
        r3_movement = next(m for m in result.movements if m.lineup_id == 4)
        assert r3_movement.is_out is True

        # Batter should be out
        batter_movement = next(m for m in result.movements if m.from_base == 0)
        assert batter_movement.is_out is True

        # Other runners should advance
        r2_movement = next(m for m in result.movements if m.lineup_id == 3)
        assert r2_movement.to_base == 3

        r1_movement = next(m for m in result.movements if m.lineup_id == 2)
        assert r1_movement.to_base == 2


class TestResult12:
    """Tests for Result 12: DECIDE opportunity."""

    def test_hit_to_1b_simple_advancement(self, advancement, base_state, infield_in_defense):
        """Hit to 1B: Simple advancement (Result 3)."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='1B',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        # Should be Result 3 behavior
        assert result.runs_scored == 1

    def test_hit_to_3b_runners_hold(self, advancement, base_state, infield_in_defense):
        """Hit to 3B: Runners hold (Result 1)."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='3B',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        # Should be Result 1 behavior
        assert result.runs_scored == 0

    def test_hit_to_ss_decide_opportunity(self, advancement, base_state, infield_in_defense):
        """Hit to SS/P/C: DECIDE opportunity (conservative default)."""
        base_state.current_on_base_code = 3
        base_state.outs = 0
        base_state.on_third = create_runner(2)
        base_state.is_runner_on_third = Mock(return_value=True)

        result = advancement.advance_runners(
            outcome=PlayOutcome.GROUNDBALL_C,
            hit_location='SS',
            state=base_state,
            defensive_decision=infield_in_defense
        )

        # Conservative default: runners hold
        assert result.result_type == GroundballResultType.DECIDE_OPPORTUNITY
        assert result.runs_scored == 0


# ========================================
# Edge Cases
# ========================================

class TestEdgeCases:
    """Tests for edge cases and special scenarios."""

    def test_invalid_outcome_raises_error(self, advancement, base_state, normal_defense):
        """Non-groundball outcomes raise ValueError."""
        with pytest.raises(ValueError, match="only handles groundballs"):
            advancement.advance_runners(
                outcome=PlayOutcome.HOMERUN,
                hit_location='CF',
                state=base_state,
                defensive_decision=normal_defense
            )

    def test_all_groundball_types_supported(self, advancement, base_state, normal_defense):
        """All three groundball types work."""
        base_state.current_on_base_code = 0
        base_state.outs = 0

        for outcome in [PlayOutcome.GROUNDBALL_A, PlayOutcome.GROUNDBALL_B, PlayOutcome.GROUNDBALL_C]:
            result = advancement.advance_runners(
                outcome=outcome,
                hit_location='SS',
                state=base_state,
                defensive_decision=normal_defense
            )
            assert result is not None
            assert isinstance(result, AdvancementResult)

    def test_all_hit_locations_supported(self, advancement, base_state, normal_defense):
        """All hit locations work."""
        base_state.current_on_base_code = 0
        base_state.outs = 0

        locations = ['1B', '2B', 'SS', '3B', 'P', 'C']
        for location in locations:
            result = advancement.advance_runners(
                outcome=PlayOutcome.GROUNDBALL_A,
                hit_location=location,
                state=base_state,
                defensive_decision=normal_defense
            )
            assert result is not None

    def test_all_on_base_codes_supported(self, advancement, base_state, normal_defense):
        """All on-base codes (0-7) work."""
        for code in range(8):
            base_state.current_on_base_code = code
            base_state.outs = 0

            # Set up runner mocks based on code
            base_state.is_runner_on_first = Mock(return_value=code in [1, 4, 5, 7])
            base_state.is_runner_on_second = Mock(return_value=code in [2, 4, 6, 7])
            base_state.is_runner_on_third = Mock(return_value=code in [3, 5, 6, 7])

            if code in [1, 4, 5, 7]:
                base_state.on_first = create_runner(2)
            if code in [2, 4, 6, 7]:
                base_state.on_second = create_runner(3)
            if code in [3, 5, 6, 7]:
                base_state.on_third = create_runner(4)

            result = advancement.advance_runners(
                outcome=PlayOutcome.GROUNDBALL_A,
                hit_location='SS',
                state=base_state,
                defensive_decision=normal_defense
            )
            assert result is not None


# ============================================================================
# X-CHECK PLACEHOLDER FUNCTION TESTS
# ============================================================================


class TestXCheckPlaceholders:
    """Test X-Check advancement functions (Phase 3D implementation)."""

    def test_x_check_g1_returns_valid_result(self):
        """x_check_g1 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_g1, GroundballResultType
        from uuid import uuid4

        # Create GameState (bases empty, 0 outs)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )
        state.outs = 0

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        result = x_check_g1(
            on_base_code=0,
            defender_in=False,
            error_result='NO',
            state=state,
            hit_location='SS',
            defensive_decision=defensive_decision
        )

        assert isinstance(result, AdvancementResult)
        # With no error on bases empty, should be batter out
        assert result.result_type == GroundballResultType.BATTER_OUT_RUNNERS_HOLD
        assert len(result.movements) == 1  # Batter out
        assert result.outs_recorded == 1

    def test_x_check_g2_returns_valid_result(self):
        """x_check_g2 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_g2, GroundballResultType
        from uuid import uuid4

        # Create GameState (not used for error case, but required by signature)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        result = x_check_g2(
            on_base_code=5,  # R1 + R3
            defender_in=True,
            error_result='E1',
            state=state,
            hit_location='2B',
            defensive_decision=defensive_decision
        )

        assert isinstance(result, AdvancementResult)
        # With E1 error, should advance all 1 base
        assert result.result_type == GroundballResultType.SAFE_ALL_ADVANCE_ONE
        assert result.outs_recorded == 0  # Error negates out
        # R1 + R3 + Batter = 3 movements
        assert len(result.movements) == 3

    def test_x_check_g3_returns_valid_result(self):
        """x_check_g3 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_g3, GroundballResultType
        from uuid import uuid4

        # Create GameState (not used for error case, but required by signature)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        result = x_check_g3(
            on_base_code=7,  # Bases loaded
            defender_in=False,
            error_result='E2',
            state=state,
            hit_location='3B',
            defensive_decision=defensive_decision
        )

        assert isinstance(result, AdvancementResult)
        # With E2 error on bases loaded, should advance all 2 bases
        assert result.result_type == GroundballResultType.SAFE_ALL_ADVANCE_TWO
        assert result.outs_recorded == 0
        assert result.runs_scored == 2  # R2 and R3 score
        assert 'E2' in result.description

    def test_x_check_f1_returns_valid_result(self):
        """x_check_f1 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_f1
        from uuid import uuid4

        # Create GameState (bases empty, 0 outs)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )
        state.outs = 0

        result = x_check_f1(
            on_base_code=0,
            error_result='NO',
            state=state,
            hit_location='CF'
        )

        assert isinstance(result, AdvancementResult)
        assert result.outs_recorded == 1  # F1 is a flyout, should record out
        assert result.runs_scored == 0
        # F1 with no error delegates to FLYOUT_A logic
        assert 'flyball' in result.description.lower() or 'Deep' in result.description

    def test_x_check_f2_returns_valid_result(self):
        """x_check_f2 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_f2
        from uuid import uuid4

        # Create GameState (not used for error case, but required by signature)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )

        result = x_check_f2(
            on_base_code=3,  # Code 3 = R3 only (sequential mapping, not bit field)
            error_result='E3',
            state=state,
            hit_location='RF'
        )

        assert isinstance(result, AdvancementResult)
        # With E3 error, out is negated, all advance 3 bases
        assert result.outs_recorded == 0  # Error negates out
        assert result.runs_scored == 1  # R3 scores (Code 3 = R3 only)
        assert 'E3' in result.description

    def test_x_check_f3_returns_valid_result(self):
        """x_check_f3 should return valid AdvancementResult (Phase 3D: Now implemented)."""
        from app.core.runner_advancement import x_check_f3
        from uuid import uuid4

        # Create GameState (not used for error case, but required by signature)
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )

        result = x_check_f3(
            on_base_code=5,  # R1 + R3
            error_result='RP',
            state=state,
            hit_location='LF'
        )

        assert isinstance(result, AdvancementResult)
        # With RP error (stubbed as E1), out is negated
        assert result.outs_recorded == 0  # Error negates out
        assert result.runs_scored == 1  # R3 scores (3 + 1 = 4)
        assert 'RP' in result.description or 'advance 1' in result.description

    def test_x_check_functions_accept_all_error_types(self):
        """X-Check functions should accept all error result types."""
        from app.core.runner_advancement import x_check_g1, x_check_f1
        from uuid import uuid4

        # Create GameState
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )
        state.outs = 0

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        error_types = ['NO', 'E1', 'E2', 'E3', 'RP']

        for error_type in error_types:
            # Test groundball function
            result_g = x_check_g1(
                on_base_code=0,
                defender_in=False,
                error_result=error_type,
                state=state,
                hit_location='SS',
                defensive_decision=defensive_decision
            )
            assert isinstance(result_g, AdvancementResult)

            # Test flyball function
            result_f = x_check_f1(
                on_base_code=0,
                error_result=error_type,
                state=state,
                hit_location='CF'
            )
            assert isinstance(result_f, AdvancementResult)

    def test_x_check_g_functions_accept_all_on_base_codes(self):
        """X-Check G functions should accept all on-base codes 0-7."""
        from app.core.runner_advancement import x_check_g1
        from uuid import uuid4

        # Create GameState
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )
        state.outs = 0

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        for on_base_code in range(8):
            result = x_check_g1(
                on_base_code=on_base_code,
                defender_in=False,
                error_result='NO',
                state=state,
                hit_location='2B',
                defensive_decision=defensive_decision
            )
            assert isinstance(result, AdvancementResult)

    def test_x_check_g_functions_accept_defender_in_flags(self):
        """X-Check G functions should accept both defender_in values."""
        from app.core.runner_advancement import x_check_g1
        from uuid import uuid4

        # Create GameState
        from app.models.game_models import LineupPlayerState

        mock_batter = LineupPlayerState(lineup_id=1, card_id=100, position='CF', batting_order=1)

        state = GameState(
            game_id=uuid4(),
            league_id='sba',
            home_team_id=1,
            away_team_id=2,

            current_batter=mock_batter

        )
        state.outs = 0

        defensive_decision = DefensiveDecision(
            alignment='normal',
            infield_depth='normal',
            outfield_depth='normal'
        )

        for defender_in in [True, False]:
            result = x_check_g1(
                on_base_code=0,
                defender_in=defender_in,
                error_result='NO',
                state=state,
                hit_location='1B',
                defensive_decision=defensive_decision
            )
            assert isinstance(result, AdvancementResult)
