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

Tests defense range tables, error charts, and helper functions
for X-Check play resolution.

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

import pytest
from app.config.common_x_check_tables import (
    INFIELD_DEFENSE_TABLE,
    OUTFIELD_DEFENSE_TABLE,
    CATCHER_DEFENSE_TABLE,
    LF_RF_ERROR_CHART,
    CF_ERROR_CHART,
    PITCHER_ERROR_CHART,
    CATCHER_ERROR_CHART,
    FIRST_BASE_ERROR_CHART,
    SECOND_BASE_ERROR_CHART,
    THIRD_BASE_ERROR_CHART,
    SHORTSTOP_ERROR_CHART,
    get_fielders_holding_runners,
    get_error_chart_for_position,
)


# ============================================================================
# DEFENSE TABLE TESTS
# ============================================================================


class TestDefenseTables:
    """Test defense range table structure and content."""

    def test_infield_defense_table_dimensions(self):
        """Infield table should be 20 rows × 5 columns."""
        assert len(INFIELD_DEFENSE_TABLE) == 20
        for row in INFIELD_DEFENSE_TABLE:
            assert len(row) == 5

    def test_outfield_defense_table_dimensions(self):
        """Outfield table should be 20 rows × 5 columns."""
        assert len(OUTFIELD_DEFENSE_TABLE) == 20
        for row in OUTFIELD_DEFENSE_TABLE:
            assert len(row) == 5

    def test_catcher_defense_table_dimensions(self):
        """Catcher table should be 20 rows × 5 columns."""
        assert len(CATCHER_DEFENSE_TABLE) == 20
        for row in CATCHER_DEFENSE_TABLE:
            assert len(row) == 5

    def test_infield_defense_table_valid_results(self):
        """All infield results should be valid codes."""
        valid_codes = {'G1', 'G2', 'G2#', 'G3', 'G3#', 'SI1', 'SI2'}

        for row_idx, row in enumerate(INFIELD_DEFENSE_TABLE):
            for col_idx, result in enumerate(row):
                assert result in valid_codes, (
                    f"Invalid infield result '{result}' at row {row_idx + 1}, "
                    f"col {col_idx + 1}"
                )

    def test_outfield_defense_table_valid_results(self):
        """All outfield results should be valid codes."""
        valid_codes = {'F1', 'F2', 'F3', 'SI2', 'DO2', 'DO3', 'TR3'}

        for row_idx, row in enumerate(OUTFIELD_DEFENSE_TABLE):
            for col_idx, result in enumerate(row):
                assert result in valid_codes, (
                    f"Invalid outfield result '{result}' at row {row_idx + 1}, "
                    f"col {col_idx + 1}"
                )

    def test_catcher_defense_table_valid_results(self):
        """All catcher results should be valid codes."""
        valid_codes = {'G1', 'G2', 'G3', 'SI1', 'SPD', 'FO', 'PO'}

        for row_idx, row in enumerate(CATCHER_DEFENSE_TABLE):
            for col_idx, result in enumerate(row):
                assert result in valid_codes, (
                    f"Invalid catcher result '{result}' at row {row_idx + 1}, "
                    f"col {col_idx + 1}"
                )

    def test_best_range_always_best(self):
        """Range 1 (best) should always be equal or better than range 5."""
        # Infield: Lower code number = better (G1 > G2 > G3 > SI)
        assert INFIELD_DEFENSE_TABLE[0][0] in {'G3#', 'G2#', 'G1'}
        assert INFIELD_DEFENSE_TABLE[0][4] in {'SI2', 'SI1'}

        # Outfield: Different codes
        assert OUTFIELD_DEFENSE_TABLE[0][0] == 'TR3'  # Best range
        assert OUTFIELD_DEFENSE_TABLE[0][4] == 'DO3'  # Worst range

    def test_worst_range_consistent(self):
        """Range 5 (worst) should show worst outcomes consistently."""
        # Last row (d20=20) with worst range should still be makeable
        # but harder than best range
        assert INFIELD_DEFENSE_TABLE[19][4] in {'G1', 'G2'}
        assert OUTFIELD_DEFENSE_TABLE[19][4] in {'F2'}


# ============================================================================
# ERROR CHART TESTS
# ============================================================================


class TestErrorCharts:
    """Test error chart structure and content."""

    def test_lf_rf_error_chart_structure(self):
        """LF/RF error chart should have ratings 0-25."""
        assert len(LF_RF_ERROR_CHART) == 26  # 0 through 25

        # Each rating should have 4 error types
        for rating, chart in LF_RF_ERROR_CHART.items():
            assert 'RP' in chart
            assert 'E1' in chart
            assert 'E2' in chart
            assert 'E3' in chart
            # Each type should be a list of 3d6 rolls (3-18)
            for error_type, rolls in chart.items():
                assert isinstance(rolls, list)
                for roll in rolls:
                    assert 3 <= roll <= 18, f"Invalid 3d6 roll: {roll}"

    def test_cf_error_chart_structure(self):
        """CF error chart should have ratings 0-25."""
        assert len(CF_ERROR_CHART) == 26  # 0 through 25

        for rating, chart in CF_ERROR_CHART.items():
            assert 'RP' in chart
            assert 'E1' in chart
            assert 'E2' in chart
            assert 'E3' in chart

    def test_infield_error_charts_complete(self):
        """Infield error charts should now have data (Phase 3B completed)."""
        # Verify all infield charts now have data (not empty placeholders)
        assert len(PITCHER_ERROR_CHART) > 0
        assert len(CATCHER_ERROR_CHART) > 0
        assert len(FIRST_BASE_ERROR_CHART) > 0
        assert len(SECOND_BASE_ERROR_CHART) > 0
        assert len(THIRD_BASE_ERROR_CHART) > 0
        assert len(SHORTSTOP_ERROR_CHART) > 0

        # Verify structure matches outfield charts (has E3 even if empty)
        for rating_dict in CATCHER_ERROR_CHART.values():
            assert 'RP' in rating_dict
            assert 'E1' in rating_dict
            assert 'E2' in rating_dict
            assert 'E3' in rating_dict

    def test_error_rating_0_has_minimal_errors(self):
        """Error rating 0 should have fewest error opportunities."""
        # RP should always be on 5 (snake eyes + 3)
        assert LF_RF_ERROR_CHART[0]['RP'] == [5]
        assert CF_ERROR_CHART[0]['RP'] == [5]

        # E1 should be empty
        assert LF_RF_ERROR_CHART[0]['E1'] == []
        assert CF_ERROR_CHART[0]['E1'] == []

    def test_error_rating_25_has_most_errors(self):
        """Error rating 25 should have most error opportunities."""
        # Should have multiple rolls for each error type
        assert len(LF_RF_ERROR_CHART[25]['E1']) > 0
        assert len(LF_RF_ERROR_CHART[25]['E2']) > 0
        assert len(LF_RF_ERROR_CHART[25]['E3']) > 0

    def test_error_rolls_unique_within_rating(self):
        """Same 3d6 roll shouldn't appear in multiple error types for one rating."""
        for rating, chart in LF_RF_ERROR_CHART.items():
            all_rolls = []
            all_rolls.extend(chart['RP'])
            all_rolls.extend(chart['E1'])
            all_rolls.extend(chart['E2'])
            all_rolls.extend(chart['E3'])

            # Check for duplicates
            assert len(all_rolls) == len(set(all_rolls)), (
                f"Duplicate 3d6 roll in error rating {rating}"
            )


# ============================================================================
# HELPER FUNCTION TESTS
# ============================================================================


class TestGetErrorChartForPosition:
    """Test get_error_chart_for_position() function."""

    def test_get_chart_for_lf(self):
        """LF should return LF/RF chart."""
        chart = get_error_chart_for_position('LF')
        assert chart == LF_RF_ERROR_CHART

    def test_get_chart_for_rf(self):
        """RF should return LF/RF chart (same as LF)."""
        chart = get_error_chart_for_position('RF')
        assert chart == LF_RF_ERROR_CHART

    def test_get_chart_for_cf(self):
        """CF should return CF chart."""
        chart = get_error_chart_for_position('CF')
        assert chart == CF_ERROR_CHART

    def test_get_chart_for_infield_positions(self):
        """Infield positions should return empty placeholders."""
        assert get_error_chart_for_position('P') == PITCHER_ERROR_CHART
        assert get_error_chart_for_position('C') == CATCHER_ERROR_CHART
        assert get_error_chart_for_position('1B') == FIRST_BASE_ERROR_CHART
        assert get_error_chart_for_position('2B') == SECOND_BASE_ERROR_CHART
        assert get_error_chart_for_position('3B') == THIRD_BASE_ERROR_CHART
        assert get_error_chart_for_position('SS') == SHORTSTOP_ERROR_CHART

    def test_invalid_position_raises_error(self):
        """Invalid position should raise ValueError."""
        with pytest.raises(ValueError, match="Unknown position"):
            get_error_chart_for_position('DH')

        with pytest.raises(ValueError, match="Unknown position"):
            get_error_chart_for_position('XX')


class TestGetFieldersHoldingRunners:
    """Test get_fielders_holding_runners() function."""

    def test_empty_bases_no_holds(self):
        """No runners means no fielders holding."""
        result = get_fielders_holding_runners([], 'R')
        assert result == []

    def test_runner_on_first_only_rhb(self):
        """R1 only with RHB: 1B and 2B hold."""
        result = get_fielders_holding_runners([1], 'R')
        assert '1B' in result
        assert '2B' in result

    def test_runner_on_first_only_lhb(self):
        """R1 only with LHB: 1B and SS hold."""
        result = get_fielders_holding_runners([1], 'L')
        assert '1B' in result
        assert 'SS' in result

    def test_runner_on_second_only_rhb(self):
        """R2 only with RHB: 2B holds."""
        result = get_fielders_holding_runners([2], 'R')
        assert result == ['2B']

    def test_runner_on_second_only_lhb(self):
        """R2 only with LHB: SS holds."""
        result = get_fielders_holding_runners([2], 'L')
        assert result == ['SS']

    def test_runner_on_third_only(self):
        """R3 only: 3B holds."""
        result = get_fielders_holding_runners([3], 'R')
        assert result == ['3B']

    def test_first_and_third_rhb(self):
        """R1 and R3 with RHB: 1B, 2B, and 3B hold."""
        result = get_fielders_holding_runners([1, 3], 'R')
        assert '1B' in result
        assert '2B' in result
        assert '3B' in result

    def test_first_and_third_lhb(self):
        """R1 and R3 with LHB: 1B, SS, and 3B hold."""
        result = get_fielders_holding_runners([1, 3], 'L')
        assert '1B' in result
        assert 'SS' in result
        assert '3B' in result

    def test_first_and_second_rhb(self):
        """R1 and R2 with RHB: 1B and 2B hold (2B already added for R1)."""
        result = get_fielders_holding_runners([1, 2], 'R')
        assert '1B' in result
        assert '2B' in result

    def test_first_and_second_lhb(self):
        """R1 and R2 with LHB: 1B and SS hold (SS already added for R1)."""
        result = get_fielders_holding_runners([1, 2], 'L')
        assert '1B' in result
        assert 'SS' in result

    def test_bases_loaded_rhb(self):
        """Bases loaded with RHB: 1B, 2B, and 3B hold."""
        result = get_fielders_holding_runners([1, 2, 3], 'R')
        assert '1B' in result
        assert '2B' in result
        assert '3B' in result

    def test_bases_loaded_lhb(self):
        """Bases loaded with LHB: 1B, SS, and 3B hold."""
        result = get_fielders_holding_runners([1, 2, 3], 'L')
        assert '1B' in result
        assert 'SS' in result
        assert '3B' in result

    def test_second_and_third_rhb(self):
        """R2 and R3 with RHB: 2B and 3B hold (no runner on first)."""
        result = get_fielders_holding_runners([2, 3], 'R')
        assert '2B' in result
        assert '3B' in result

    def test_second_and_third_lhb(self):
        """R2 and R3 with LHB: SS and 3B hold (no runner on first)."""
        result = get_fielders_holding_runners([2, 3], 'L')
        assert 'SS' in result
        assert '3B' in result


# ============================================================================
# INTEGRATION TESTS
# ============================================================================


class TestXCheckTablesIntegration:
    """Integration tests combining tables and helper functions."""

    def test_defense_table_lookup(self):
        """Test looking up a defense result."""
        # d20 roll = 10, defense range = 3 (average)
        d20_roll = 10
        defense_range = 3

        # Row index = d20 - 1, col index = range - 1
        result = INFIELD_DEFENSE_TABLE[d20_roll - 1][defense_range - 1]

        # d20=10, range=3 should be G2
        assert result == 'G2'

    def test_error_check_workflow(self):
        """Test complete error check workflow."""
        # Scenario: LF with error rating 10, 3d6 roll = 16

        position = 'LF'
        error_rating = 10
        three_d6_roll = 16

        # Get error chart for position
        chart = get_error_chart_for_position(position)

        # Get error chances for this rating
        error_chances = chart[error_rating]

        # Check each error type
        if three_d6_roll in error_chances['RP']:
            error_result = 'RP'
        elif three_d6_roll in error_chances['E1']:
            error_result = 'E1'
        elif three_d6_roll in error_chances['E2']:
            error_result = 'E2'
        elif three_d6_roll in error_chances['E3']:
            error_result = 'E3'
        else:
            error_result = 'NO'

        # For rating 10, roll 16: E1 = [4, 16]
        assert error_result == 'E1'

    def test_holding_runner_affects_result(self):
        """Test how holding runners might affect play resolution."""
        # Scenario: G2# result with runner on 1st

        # Check if fielders are holding
        fielders_holding = get_fielders_holding_runners([1], 'R')

        # If fielder involved in play is holding, G2# → SI2
        # This logic will be implemented in Phase 3C
        assert '1B' in fielders_holding
        assert '2B' in fielders_holding  # RHB means 2B holds

        # For now, just verify the function returns expected values
        # Full integration will happen in Phase 3C
