""" Unit tests for PlayOutcome enum. Tests helper methods and outcome categorization. """ import pytest from app.config import PlayOutcome class TestPlayOutcomeHelpers: """Tests for PlayOutcome helper methods.""" def test_is_hit_for_singles(self): """Single outcomes are hits.""" assert PlayOutcome.SINGLE_1.is_hit() is True assert PlayOutcome.SINGLE_2.is_hit() is True assert PlayOutcome.SINGLE_UNCAPPED.is_hit() is True assert PlayOutcome.BP_SINGLE.is_hit() is True def test_is_hit_for_extra_bases(self): """Extra base hits are hits.""" assert PlayOutcome.DOUBLE_2.is_hit() is True assert PlayOutcome.DOUBLE_3.is_hit() is True assert PlayOutcome.DOUBLE_UNCAPPED.is_hit() is True assert PlayOutcome.TRIPLE.is_hit() is True assert PlayOutcome.HOMERUN.is_hit() is True assert PlayOutcome.BP_HOMERUN.is_hit() is True def test_is_hit_false_for_outs(self): """Outs are not hits.""" assert PlayOutcome.STRIKEOUT.is_hit() is False assert PlayOutcome.GROUNDBALL_A.is_hit() is False assert PlayOutcome.FLYOUT_A.is_hit() is False assert PlayOutcome.BP_FLYOUT.is_hit() is False def test_is_hit_false_for_walks(self): """Walks are not hits.""" assert PlayOutcome.WALK.is_hit() is False assert PlayOutcome.INTENTIONAL_WALK.is_hit() is False def test_is_out_for_standard_outs(self): """Standard outs are outs.""" assert PlayOutcome.STRIKEOUT.is_out() is True assert PlayOutcome.GROUNDBALL_A.is_out() is True assert PlayOutcome.GROUNDBALL_B.is_out() is True assert PlayOutcome.GROUNDBALL_C.is_out() is True assert PlayOutcome.FLYOUT_A.is_out() is True assert PlayOutcome.FLYOUT_B.is_out() is True assert PlayOutcome.FLYOUT_C.is_out() is True assert PlayOutcome.LINEOUT.is_out() is True assert PlayOutcome.POPOUT.is_out() is True def test_is_out_for_baserunning_outs(self): """Baserunning outs are outs.""" assert PlayOutcome.CAUGHT_STEALING.is_out() is True assert PlayOutcome.PICK_OFF.is_out() is True def test_is_out_for_ballpark_outs(self): """Ballpark outs are outs.""" assert PlayOutcome.BP_FLYOUT.is_out() is True assert PlayOutcome.BP_LINEOUT.is_out() is True def test_is_out_false_for_hits(self): """Hits are not outs.""" assert PlayOutcome.SINGLE_1.is_out() is False assert PlayOutcome.HOMERUN.is_out() is False def test_is_walk(self): """Walk outcomes are walks.""" assert PlayOutcome.WALK.is_walk() is True assert PlayOutcome.INTENTIONAL_WALK.is_walk() is True def test_is_walk_false_for_hits(self): """Hits are not walks.""" assert PlayOutcome.SINGLE_1.is_walk() is False assert PlayOutcome.HOMERUN.is_walk() is False def test_is_walk_false_for_hbp(self): """HBP is not a walk (different stat).""" assert PlayOutcome.HIT_BY_PITCH.is_walk() is False def test_is_uncapped(self): """Uncapped outcomes are uncapped.""" assert PlayOutcome.SINGLE_UNCAPPED.is_uncapped() is True assert PlayOutcome.DOUBLE_UNCAPPED.is_uncapped() is True def test_is_uncapped_false_for_normal_hits(self): """Normal hits are not uncapped.""" assert PlayOutcome.SINGLE_1.is_uncapped() is False assert PlayOutcome.DOUBLE_2.is_uncapped() is False assert PlayOutcome.TRIPLE.is_uncapped() is False def test_is_interrupt(self): """Interrupt plays are interrupts.""" assert PlayOutcome.WILD_PITCH.is_interrupt() is True assert PlayOutcome.PASSED_BALL.is_interrupt() is True assert PlayOutcome.STOLEN_BASE.is_interrupt() is True assert PlayOutcome.CAUGHT_STEALING.is_interrupt() is True assert PlayOutcome.BALK.is_interrupt() is True assert PlayOutcome.PICK_OFF.is_interrupt() is True def test_is_interrupt_false_for_normal_plays(self): """Normal plays are not interrupts.""" assert PlayOutcome.SINGLE_1.is_interrupt() is False assert PlayOutcome.STRIKEOUT.is_interrupt() is False assert PlayOutcome.WALK.is_interrupt() is False def test_is_extra_base_hit(self): """Extra base hits are identified correctly.""" assert PlayOutcome.DOUBLE_2.is_extra_base_hit() is True assert PlayOutcome.DOUBLE_3.is_extra_base_hit() is True assert PlayOutcome.DOUBLE_UNCAPPED.is_extra_base_hit() is True assert PlayOutcome.TRIPLE.is_extra_base_hit() is True assert PlayOutcome.HOMERUN.is_extra_base_hit() is True assert PlayOutcome.BP_HOMERUN.is_extra_base_hit() is True def test_is_extra_base_hit_false_for_singles(self): """Singles are not extra base hits.""" assert PlayOutcome.SINGLE_1.is_extra_base_hit() is False assert PlayOutcome.SINGLE_UNCAPPED.is_extra_base_hit() is False assert PlayOutcome.BP_SINGLE.is_extra_base_hit() is False class TestGetBasesAdvanced: """Tests for get_bases_advanced() method.""" def test_singles_advance_one_base(self): """Singles advance one base.""" assert PlayOutcome.SINGLE_1.get_bases_advanced() == 1 assert PlayOutcome.SINGLE_2.get_bases_advanced() == 1 assert PlayOutcome.SINGLE_UNCAPPED.get_bases_advanced() == 1 assert PlayOutcome.BP_SINGLE.get_bases_advanced() == 1 def test_doubles_advance_two_bases(self): """Doubles advance two bases.""" assert PlayOutcome.DOUBLE_2.get_bases_advanced() == 2 assert PlayOutcome.DOUBLE_3.get_bases_advanced() == 2 assert PlayOutcome.DOUBLE_UNCAPPED.get_bases_advanced() == 2 def test_triples_advance_three_bases(self): """Triples advance three bases.""" assert PlayOutcome.TRIPLE.get_bases_advanced() == 3 def test_homeruns_advance_four_bases(self): """Home runs advance four bases.""" assert PlayOutcome.HOMERUN.get_bases_advanced() == 4 assert PlayOutcome.BP_HOMERUN.get_bases_advanced() == 4 def test_outs_advance_zero_bases(self): """Outs advance zero bases.""" assert PlayOutcome.STRIKEOUT.get_bases_advanced() == 0 assert PlayOutcome.GROUNDBALL_A.get_bases_advanced() == 0 assert PlayOutcome.FLYOUT_A.get_bases_advanced() == 0 def test_walks_advance_zero_bases(self): """Walks advance zero bases (forced advancement handled separately).""" assert PlayOutcome.WALK.get_bases_advanced() == 0 assert PlayOutcome.INTENTIONAL_WALK.get_bases_advanced() == 0 def test_interrupts_advance_zero_bases(self): """Interrupts advance zero bases (advancement handled by interrupt logic).""" assert PlayOutcome.WILD_PITCH.get_bases_advanced() == 0 assert PlayOutcome.STOLEN_BASE.get_bases_advanced() == 0 class TestPlayOutcomeValues: """Tests for PlayOutcome string values.""" def test_outcome_string_values(self): """Outcome values match expected strings.""" assert PlayOutcome.STRIKEOUT.value == "strikeout" assert PlayOutcome.SINGLE_1.value == "single_1" assert PlayOutcome.SINGLE_UNCAPPED.value == "single_uncapped" assert PlayOutcome.WILD_PITCH.value == "wild_pitch" assert PlayOutcome.BP_HOMERUN.value == "bp_homerun" def test_can_create_from_string(self): """Can create PlayOutcome from string value.""" outcome = PlayOutcome("strikeout") assert outcome == PlayOutcome.STRIKEOUT def test_uncapped_uses_code_friendly_names(self): """Uncapped outcomes use underscores not parentheses.""" assert "(" not in PlayOutcome.SINGLE_UNCAPPED.value assert "(" not in PlayOutcome.DOUBLE_UNCAPPED.value assert "_" in PlayOutcome.SINGLE_UNCAPPED.value class TestPlayOutcomeCompleteness: """Tests to ensure all outcome categories are covered.""" def test_all_hits_categorized(self): """All hit outcomes are properly categorized.""" hit_outcomes = { PlayOutcome.SINGLE_1, PlayOutcome.SINGLE_2, PlayOutcome.DOUBLE_2, PlayOutcome.DOUBLE_3, PlayOutcome.TRIPLE, PlayOutcome.HOMERUN, PlayOutcome.SINGLE_UNCAPPED, PlayOutcome.DOUBLE_UNCAPPED, PlayOutcome.BP_HOMERUN, PlayOutcome.BP_SINGLE } for outcome in hit_outcomes: assert outcome.is_hit() is True, f"{outcome} should be a hit" def test_all_outs_categorized(self): """All out outcomes are properly categorized.""" out_outcomes = { PlayOutcome.STRIKEOUT, PlayOutcome.GROUNDBALL_A, PlayOutcome.GROUNDBALL_B, PlayOutcome.GROUNDBALL_C, PlayOutcome.FLYOUT_A, PlayOutcome.FLYOUT_B, PlayOutcome.FLYOUT_C, PlayOutcome.LINEOUT, PlayOutcome.POPOUT, PlayOutcome.CAUGHT_STEALING, PlayOutcome.PICK_OFF, PlayOutcome.BP_FLYOUT, PlayOutcome.BP_LINEOUT } for outcome in out_outcomes: assert outcome.is_out() is True, f"{outcome} should be an out" def test_all_interrupts_categorized(self): """All interrupt outcomes are properly categorized.""" interrupt_outcomes = { PlayOutcome.WILD_PITCH, PlayOutcome.PASSED_BALL, PlayOutcome.STOLEN_BASE, PlayOutcome.CAUGHT_STEALING, PlayOutcome.BALK, PlayOutcome.PICK_OFF } for outcome in interrupt_outcomes: assert outcome.is_interrupt() is True, f"{outcome} should be an interrupt"