""" Unit tests for PendingXCheck model. Tests the interactive x-check state model including validation, field constraints, and workflow state tracking. """ import pytest from pydantic import ValidationError from app.models.game_models import PendingXCheck class TestPendingXCheckCreation: """Test PendingXCheck model creation and basic validation.""" def test_create_minimal_pending_x_check(self): """Should create PendingXCheck with required fields only.""" pending = PendingXCheck( position="SS", ab_roll_id="test123", d20_roll=12, d6_individual=[3, 4, 5], d6_total=12, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=42, ) assert pending.position == "SS" assert pending.ab_roll_id == "test123" assert pending.d20_roll == 12 assert pending.d6_individual == [3, 4, 5] assert pending.d6_total == 12 assert pending.chart_row == ["G1", "G2", "G3", "SI1", "SI2"] assert pending.chart_type == "infield" assert pending.defender_lineup_id == 42 # Optional fields should be None assert pending.spd_d20 is None assert pending.selected_result is None assert pending.error_result is None assert pending.decide_runner_base is None assert pending.decide_target_base is None assert pending.decide_advance is None assert pending.decide_throw is None assert pending.decide_d20 is None def test_create_with_spd_d20(self): """Should create PendingXCheck with SPD d20 pre-rolled.""" pending = PendingXCheck( position="C", ab_roll_id="test456", d20_roll=10, d6_individual=[2, 3, 4], d6_total=9, chart_row=["G1", "SPD", "G3", "SI1", "SI2"], chart_type="catcher", spd_d20=15, defender_lineup_id=99, ) assert pending.spd_d20 == 15 assert "SPD" in pending.chart_row def test_create_with_result_selection(self): """Should create PendingXCheck with player selections.""" pending = PendingXCheck( position="LF", ab_roll_id="test789", d20_roll=18, d6_individual=[5, 5, 6], d6_total=16, chart_row=["F1", "F2", "F2", "F3", "F3"], chart_type="outfield", defender_lineup_id=7, selected_result="F2", error_result="E1", ) assert pending.selected_result == "F2" assert pending.error_result == "E1" def test_create_with_decide_data(self): """Should create PendingXCheck with DECIDE workflow data.""" pending = PendingXCheck( position="2B", ab_roll_id="test999", d20_roll=8, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=14, selected_result="G2", error_result="NO", decide_runner_base=2, decide_target_base=3, decide_advance=True, decide_throw="runner", decide_d20=17, ) assert pending.decide_runner_base == 2 assert pending.decide_target_base == 3 assert pending.decide_advance is True assert pending.decide_throw == "runner" assert pending.decide_d20 == 17 class TestPendingXCheckValidation: """Test field validation for PendingXCheck.""" def test_d20_roll_must_be_1_to_20(self): """Should reject d20 values outside 1-20 range.""" with pytest.raises(ValidationError) as exc_info: PendingXCheck( position="SS", ab_roll_id="test", d20_roll=0, # Invalid d6_individual=[1, 2, 3], d6_total=6, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) assert "d20_roll" in str(exc_info.value) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=21, # Invalid d6_individual=[1, 2, 3], d6_total=6, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) def test_d6_individual_must_have_exactly_3_dice(self): """Should reject d6_individual with wrong number of dice.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[1, 2], # Too few d6_total=3, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[1, 2, 3, 4], # Too many d6_total=10, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) def test_d6_individual_values_must_be_1_to_6(self): """Should reject d6 values outside 1-6 range.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[0, 2, 3], # 0 invalid d6_total=5, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[1, 7, 3], # 7 invalid d6_total=11, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) def test_d6_total_must_be_3_to_18(self): """Should reject d6_total outside 3-18 range.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[1, 1, 1], d6_total=2, # Invalid chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[6, 6, 6], d6_total=19, # Invalid chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) def test_chart_row_must_have_exactly_5_columns(self): """Should reject chart_row with wrong number of columns.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3"], # Too few chart_type="infield", defender_lineup_id=1, ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2", "Extra"], # Too many chart_type="infield", defender_lineup_id=1, ) def test_chart_type_must_be_valid(self): """Should reject invalid chart_type values.""" with pytest.raises(ValidationError) as exc_info: PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="invalid", # Must be infield/outfield/catcher defender_lineup_id=1, ) assert "chart_type" in str(exc_info.value) def test_error_result_must_be_valid(self): """Should reject invalid error_result values.""" with pytest.raises(ValidationError) as exc_info: PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, error_result="INVALID", # Must be NO/E1/E2/E3/RP ) assert "error_result" in str(exc_info.value) def test_decide_throw_must_be_valid(self): """Should reject invalid decide_throw values.""" with pytest.raises(ValidationError) as exc_info: PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_throw="invalid", # Must be runner/first ) assert "decide_throw" in str(exc_info.value) def test_decide_runner_base_must_be_1_to_3(self): """Should reject decide_runner_base outside 1-3 range.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_runner_base=0, # Invalid ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_runner_base=4, # Invalid (home is target, not source) ) def test_decide_target_base_must_be_2_to_4(self): """Should reject decide_target_base outside 2-4 range.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_target_base=1, # Invalid (can't advance backwards) ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_target_base=5, # Invalid ) def test_spd_d20_must_be_1_to_20(self): """Should reject spd_d20 outside 1-20 range.""" with pytest.raises(ValidationError): PendingXCheck( position="C", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "SPD", "G3", "SI1", "SI2"], chart_type="catcher", defender_lineup_id=1, spd_d20=0, # Invalid ) with pytest.raises(ValidationError): PendingXCheck( position="C", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "SPD", "G3", "SI1", "SI2"], chart_type="catcher", defender_lineup_id=1, spd_d20=21, # Invalid ) def test_decide_d20_must_be_1_to_20(self): """Should reject decide_d20 outside 1-20 range.""" with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_d20=0, # Invalid ) with pytest.raises(ValidationError): PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, decide_d20=21, # Invalid ) class TestPendingXCheckMutability: """Test that PendingXCheck allows mutation during workflow.""" def test_can_update_selected_result(self): """Should allow updating selected_result after creation.""" pending = PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) assert pending.selected_result is None # Should be able to mutate pending.selected_result = "G2" assert pending.selected_result == "G2" def test_can_update_error_result(self): """Should allow updating error_result after creation.""" pending = PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) pending.error_result = "E1" assert pending.error_result == "E1" def test_can_update_decide_fields(self): """Should allow updating DECIDE fields during workflow.""" pending = PendingXCheck( position="SS", ab_roll_id="test", d20_roll=10, d6_individual=[3, 3, 3], d6_total=9, chart_row=["G1", "G2", "G3", "SI1", "SI2"], chart_type="infield", defender_lineup_id=1, ) # Simulate DECIDE workflow pending.decide_runner_base = 2 pending.decide_target_base = 3 pending.decide_advance = True pending.decide_throw = "first" assert pending.decide_runner_base == 2 assert pending.decide_target_base == 3 assert pending.decide_advance is True assert pending.decide_throw == "first"