"""
Tests for flyball runner advancement logic.

Tests all 4 flyball types:
- FLYOUT_A (Deep): All runners advance
- FLYOUT_B (Medium): R3 scores, R2 DECIDE, R1 holds
- FLYOUT_BQ (Medium-shallow): R3 DECIDE, R2 holds, R1 holds
- FLYOUT_C (Shallow): All runners hold
"""

from uuid import uuid4

import pytest

from app.config import PlayOutcome
from app.core.runner_advancement import RunnerAdvancement
from app.models.game_models import DefensiveDecision, GameState, LineupPlayerState


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


@pytest.fixture
def base_state():
    """Create base GameState for testing."""
    return GameState(
        game_id=uuid4(),
        league_id="sba",
        home_team_id=1,
        away_team_id=2,
        inning=3,
        half="top",
        outs=1,
        current_batter=LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)
    )


@pytest.fixture
def defensive_decision():
    """Create standard defensive decision."""
    return DefensiveDecision(
        alignment="normal",
        infield_depth="normal",
        outfield_depth="normal"
    )


class TestFlyoutA:
    """Tests for FLYOUT_A (Deep flyball - all runners advance)."""

    def test_runner_on_third_scores(self, runner_advancement, base_state, defensive_decision):
        """Runner on third scores on deep flyball."""
        # Setup: Runner on 3rd
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_A,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 1
        assert result.result_type is None  # Flyballs don't use result types

        # Verify movements
        movements = {m.from_base: m for m in result.movements}
        assert movements[0].is_out  # Batter out
        assert movements[3].to_base == 4  # Runner scored
        assert not movements[3].is_out

    def test_all_bases_loaded_all_advance(self, runner_advancement, base_state, defensive_decision):
        """All runners advance on deep flyball with bases loaded."""
        # Setup: Bases loaded
        base_state.on_first = LineupPlayerState(lineup_id=1, card_id=101, position="RF")
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_A,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 1  # Only R3 scores

        # Verify all runners advanced
        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 4  # R3 scores
        assert movements[2].to_base == 3  # R2 to 3rd
        assert movements[1].to_base == 2  # R1 to 2nd

    def test_two_outs_no_advancement(self, runner_advancement, base_state, defensive_decision):
        """With 2 outs, runners don't advance (inning over)."""
        base_state.outs = 2
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_A,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0  # No runs (3rd out ends inning)

        # Only batter movement
        assert len(result.movements) == 1
        assert result.movements[0].is_out

    def test_empty_bases(self, runner_advancement, base_state, defensive_decision):
        """Deep flyball with no runners on base."""
        base_state.current_batter = LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_A,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0
        assert len(result.movements) == 1  # Only batter
        assert result.movements[0].is_out


class TestFlyoutB:
    """Tests for FLYOUT_B (Medium flyball - R3 scores, R2 DECIDE)."""

    def test_runner_on_third_scores(self, runner_advancement, base_state, defensive_decision):
        """Runner on third always scores on FLYOUT_B."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_B,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 4  # R3 scores
        assert not movements[3].is_out

    def test_runner_on_second_holds_by_default(self, runner_advancement, base_state, defensive_decision):
        """Runner on second holds (DECIDE defaults to conservative)."""
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.current_batter = LineupPlayerState(lineup_id=3, card_id=3*100, position="CF", batting_order=3)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_B,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        # R2 holds (no-op movement for state recovery)
        movements = {m.from_base: m for m in result.movements}
        assert movements[2].to_base == 2  # Holds
        assert not movements[2].is_out

    def test_bases_loaded_r3_scores_others_hold(self, runner_advancement, base_state, defensive_decision):
        """With bases loaded, R3 scores, R2 DECIDE (holds), R1 holds."""
        base_state.on_first = LineupPlayerState(lineup_id=1, card_id=101, position="RF")
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_B,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 1  # Only R3 scores

        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 4  # R3 scores
        assert movements[2].to_base == 2  # R2 holds (DECIDE defaults to hold)
        assert movements[1].to_base == 1  # R1 holds

    def test_description_includes_location(self, runner_advancement, base_state, defensive_decision):
        """Description includes hit location and only shows actual runners."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_B,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert "RF" in result.description
        assert "R3 scores" in result.description  # Only R3 exists, so only R3 mentioned


class TestFlyoutBQ:
    """Tests for FLYOUT_BQ (Medium-shallow flyball - R3 DECIDE)."""

    def test_runner_on_third_holds_by_default(self, runner_advancement, base_state, defensive_decision):
        """Runner on third holds (DECIDE defaults to conservative)."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_BQ,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0  # R3 doesn't score (holds by default)

        # R3 holds (no-op movement for state recovery)
        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 3  # Holds
        assert not movements[3].is_out

    def test_all_runners_hold(self, runner_advancement, base_state, defensive_decision):
        """All runners hold on FLYOUT_BQ."""
        base_state.on_first = LineupPlayerState(lineup_id=1, card_id=101, position="RF")
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_BQ,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        # All runners hold
        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 3  # R3 holds (DECIDE defaults to hold)
        assert movements[2].to_base == 2  # R2 holds
        assert movements[1].to_base == 1  # R1 holds

    def test_two_outs_no_movements(self, runner_advancement, base_state, defensive_decision):
        """With 2 outs, no runner movements recorded."""
        base_state.outs = 2
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_BQ,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        # Only batter movement (inning over)
        assert len(result.movements) == 1
        assert result.movements[0].is_out

    def test_description_includes_decide(self, runner_advancement, base_state, defensive_decision):
        """Description mentions DECIDE opportunity."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_BQ,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert "DECIDE" in result.description


class TestFlyoutC:
    """Tests for FLYOUT_C (Shallow flyball - all runners hold)."""

    def test_all_runners_hold(self, runner_advancement, base_state, defensive_decision):
        """All runners hold on shallow flyball."""
        base_state.on_first = LineupPlayerState(lineup_id=1, card_id=101, position="RF")
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_C,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        # All runners hold (no-op movements for state recovery)
        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 3
        assert movements[2].to_base == 2
        assert movements[1].to_base == 1
        assert not any(m.is_out for m in result.movements if m.from_base > 0)

    def test_empty_bases(self, runner_advancement, base_state, defensive_decision):
        """Shallow flyball with no runners."""
        base_state.current_batter = LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_C,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        assert result.outs_recorded == 1
        assert result.runs_scored == 0
        assert len(result.movements) == 1  # Only batter
        assert result.movements[0].is_out

    def test_runner_on_third_does_not_score(self, runner_advancement, base_state, defensive_decision):
        """Runner on third does not score on shallow flyball."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_C,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

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

        movements = {m.from_base: m for m in result.movements}
        assert movements[3].to_base == 3  # Holds
        assert not movements[3].is_out


class TestFlyballEdgeCases:
    """Edge case tests for flyball advancement."""

    def test_invalid_flyball_raises_error(self, runner_advancement, base_state, defensive_decision):
        """Non-flyball outcome raises ValueError."""
        base_state.current_batter = LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)

        with pytest.raises(ValueError, match="only handles groundballs and flyballs"):
            runner_advancement.advance_runners(
                outcome=PlayOutcome.STRIKEOUT,
                hit_location="CF",
                state=base_state,
                defensive_decision=defensive_decision
            )

    def test_all_flyball_types_supported(self, runner_advancement, base_state, defensive_decision):
        """All 4 flyball types are supported."""
        base_state.current_batter = LineupPlayerState(lineup_id=1, card_id=1*100, position="CF", batting_order=1)

        for outcome in [PlayOutcome.FLYOUT_A, PlayOutcome.FLYOUT_B, PlayOutcome.FLYOUT_BQ, PlayOutcome.FLYOUT_C]:
            result = runner_advancement.advance_runners(
                outcome=outcome,
                hit_location="CF",
                state=base_state,
                defensive_decision=defensive_decision
            )
            assert result.outs_recorded == 1
            assert result.result_type is None  # Flyballs don't use result types

    def test_all_outfield_locations_supported(self, runner_advancement, base_state, defensive_decision):
        """All outfield locations (LF, CF, RF) are supported."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        for location in ["LF", "CF", "RF"]:
            result = runner_advancement.advance_runners(
                outcome=PlayOutcome.FLYOUT_B,
                hit_location=location,
                state=base_state,
                defensive_decision=defensive_decision
            )
            assert location in result.description


class TestNoOpMovements:
    """Tests verifying no-op movements for state recovery."""

    def test_flyout_c_records_hold_movements(self, runner_advancement, base_state, defensive_decision):
        """FLYOUT_C records hold movements (critical for state recovery)."""
        base_state.on_first = LineupPlayerState(lineup_id=1, card_id=101, position="RF")
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_C,
            hit_location="LF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        # Should have batter movement + 2 runner hold movements
        assert len(result.movements) == 3

        movements = {m.from_base: m for m in result.movements}
        # R1 holds: from_base=1, to_base=1
        assert movements[1].from_base == 1
        assert movements[1].to_base == 1
        # R3 holds: from_base=3, to_base=3
        assert movements[3].from_base == 3
        assert movements[3].to_base == 3

    def test_flyout_bq_r3_hold_recorded(self, runner_advancement, base_state, defensive_decision):
        """FLYOUT_BQ records R3 hold movement (DECIDE defaults to hold)."""
        base_state.on_third = LineupPlayerState(lineup_id=3, card_id=103, position="CF")
        base_state.current_batter = LineupPlayerState(lineup_id=4, card_id=4*100, position="CF", batting_order=4)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_BQ,
            hit_location="CF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        movements = {m.from_base: m for m in result.movements}
        # R3 DECIDE defaults to hold - must be recorded
        assert movements[3].from_base == 3
        assert movements[3].to_base == 3
        assert not movements[3].is_out

    def test_flyout_b_r2_hold_recorded(self, runner_advancement, base_state, defensive_decision):
        """FLYOUT_B records R2 hold movement (DECIDE defaults to hold)."""
        base_state.on_second = LineupPlayerState(lineup_id=2, card_id=102, position="SS")
        base_state.current_batter = LineupPlayerState(lineup_id=3, card_id=3*100, position="CF", batting_order=3)

        result = runner_advancement.advance_runners(
            outcome=PlayOutcome.FLYOUT_B,
            hit_location="RF",
            state=base_state,
            defensive_decision=defensive_decision
        )

        movements = {m.from_base: m for m in result.movements}
        # R2 DECIDE defaults to hold - must be recorded
        assert movements[2].from_base == 2
        assert movements[2].to_base == 2
        assert not movements[2].is_out
