import pytest from app.playoff import ( is_pinned, is_playoff_game, is_playoff_ot, ot_label, series_badges, series_blurb, series_state, series_summary, today_meta, ) from tests.conftest import make_game, make_playoff_game class TestSeriesState: def test_empty_returns_defaults(self): state = series_state({}) assert state["is_opener"] is True assert state["game_number"] == 1 assert state["round"] == 1 assert state["leader"] is None @pytest.mark.parametrize( "top,bot,expected_game", [(0, 0, 1), (1, 0, 2), (1, 1, 3), (2, 1, 4), (2, 2, 5), (3, 2, 6), (3, 3, 7)], ) def test_game_number_computation(self, top, bot, expected_game): state = series_state({"round": 1, "topSeedWins": top, "bottomSeedWins": bot}) assert state["game_number"] == expected_game def test_game7_predicate(self): state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 3}) assert state["is_game7"] is True assert state["is_clincher"] is False assert state["is_pivotal"] is False def test_clincher_predicate(self): state = series_state({"round": 1, "topSeedWins": 3, "bottomSeedWins": 1}) assert state["is_clincher"] is True assert state["is_elimination"] is True assert state["is_game7"] is False def test_pivotal_predicate(self): state = series_state({"round": 2, "topSeedWins": 2, "bottomSeedWins": 2}) assert state["is_pivotal"] is True assert state["is_game7"] is False assert state["is_clincher"] is False def test_opener_predicate(self): state = series_state({"round": 1, "topSeedWins": 0, "bottomSeedWins": 0}) assert state["is_opener"] is True def test_leader_top(self): state = series_state({"round": 1, "topSeedWins": 2, "bottomSeedWins": 1}) assert state["leader"] == "top" assert state["hi"] == 2 and state["lo"] == 1 def test_leader_bottom(self): state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 2}) assert state["leader"] == "bottom" def test_no_leader_when_tied(self): state = series_state({"round": 1, "topSeedWins": 1, "bottomSeedWins": 1}) assert state["leader"] is None class TestSeriesBlurb: def test_opener_blurb(self): game = make_playoff_game(top_wins=0, bottom_wins=0) assert series_blurb(game) == "Series opener" def test_game7_blurb(self): game = make_playoff_game(top_wins=3, bottom_wins=3) assert series_blurb(game) == "Win-or-go-home" def test_clincher_blurb_names_leader(self): game = make_playoff_game( top_wins=3, bottom_wins=1, top_abbrev="TOR", bottom_abbrev="BOS", top_is_home=True, ) blurb = series_blurb(game) assert "Top Seeds" in blurb assert "close it out" in blurb def test_pivotal_blurb(self): game = make_playoff_game(top_wins=2, bottom_wins=2) assert "pivotal" in series_blurb(game).lower() def test_leader_trailer_blurb(self): game = make_playoff_game(top_wins=2, bottom_wins=1) blurb = series_blurb(game) assert "lead" in blurb assert "2\u20111" in blurb def test_tied_mid_series_blurb(self): game = make_playoff_game(top_wins=1, bottom_wins=1) blurb = series_blurb(game) assert "1‑1" in blurb def test_final_clincher_falls_through_to_leader_blurb(self): # Post-game seriesStatus (3-0) would trigger the clincher branch, but # the FINAL card is already decided — that stake belongs to Game 4. game = make_playoff_game( top_wins=3, bottom_wins=0, top_abbrev="PHI", bottom_abbrev="PIT", top_is_home=True, game_state="OFF", ) blurb = series_blurb(game) assert "close it out" not in blurb assert "lead" in blurb assert "3‑0" in blurb class TestSeriesBadges: def test_round_1_always_first(self): game = make_playoff_game(round_num=1, top_wins=1, bottom_wins=0) assert series_badges(game)[0] == "R1" def test_cup_final_label(self): game = make_playoff_game(round_num=4, top_wins=0, bottom_wins=0) assert series_badges(game)[0] == "CUP FINAL" def test_conf_final_label(self): game = make_playoff_game(round_num=3, top_wins=0, bottom_wins=0) assert series_badges(game)[0] == "CONF FINAL" def test_game7_badge(self): game = make_playoff_game(top_wins=3, bottom_wins=3) assert "GAME 7" in series_badges(game) def test_clincher_badge(self): game = make_playoff_game(top_wins=3, bottom_wins=1) assert "CLINCHER" in series_badges(game) def test_pivotal_badge(self): game = make_playoff_game(top_wins=2, bottom_wins=2) assert "PIVOTAL" in series_badges(game) def test_opener_has_no_stake_badge(self): badges = series_badges(make_playoff_game(top_wins=0, bottom_wins=0)) assert badges == ["R1"] def test_no_stake_badge_on_final(self): # Post-game seriesStatus shows is_clincher true, but CLINCHER refers to # the upcoming Game 4, not the completed card. game = make_playoff_game(top_wins=3, bottom_wins=0, game_state="OFF") assert series_badges(game) == ["R1"] class TestSeriesSummary: def test_opener_summary(self): game = make_playoff_game(top_wins=0, bottom_wins=0, round_num=1) assert series_summary(game) == "Game 1 of 7" def test_leader_summary(self): game = make_playoff_game(top_wins=2, bottom_wins=1) assert series_summary(game) == "Game 4 of 7" def test_tied_mid_series_summary(self): game = make_playoff_game(top_wins=1, bottom_wins=1) assert series_summary(game) == "Game 3 of 7" def test_finished_game_uses_pre_advance_number(self): # Scoreboard payloads don't carry gameNumber. Once a game goes FINAL, # seriesStatus already includes this game's result, so the card's game # number is hi+lo, not hi+lo+1. game = make_playoff_game(top_wins=1, bottom_wins=0, game_state="FINAL") assert series_summary(game) == "Game 1 of 7" def test_finished_game_honors_explicit_game_number(self): game = make_playoff_game( top_wins=2, bottom_wins=0, game_state="FINAL", game_number=2 ) assert series_summary(game) == "Game 2 of 7" def test_fut_game_uses_explicit_game_number(self): game = make_playoff_game( top_wins=1, bottom_wins=1, game_state="FUT", game_number=4 ) assert series_summary(game) == "Game 4 of 7" def test_fut_game_without_game_number_uses_fallback(self): game = make_playoff_game(top_wins=1, bottom_wins=1, game_state="FUT") assert series_summary(game) == "Game 3 of 7" class TestIsPinned: def test_game7_live_is_pinned(self): game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="LIVE") assert is_pinned(game) is True def test_game7_pre_is_pinned(self): game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="FUT") assert is_pinned(game) is True def test_game7_final_not_pinned(self): game = make_playoff_game(top_wins=3, bottom_wins=3, game_state="OFF") assert is_pinned(game) is False def test_non_game7_not_pinned(self): game = make_playoff_game(top_wins=2, bottom_wins=1) assert is_pinned(game) is False def test_regular_season_not_pinned(self): game = make_game() # game_type=2, no series assert is_pinned(game) is False class TestIsPlayoffOt: def test_playoff_period_4_live(self): game = make_playoff_game(period=4, game_state="LIVE") assert is_playoff_ot(game) is True def test_playoff_period_5_live(self): game = make_playoff_game(period=5, game_state="LIVE") assert is_playoff_ot(game) is True def test_playoff_period_3_not_ot(self): game = make_playoff_game(period=3, game_state="LIVE") assert is_playoff_ot(game) is False def test_regular_season_ot_not_playoff_ot(self): game = make_game(period=4, game_state="LIVE", game_type=2) assert is_playoff_ot(game) is False def test_crit_state_counts_as_live(self): game = make_playoff_game(period=4, game_state="CRIT") assert is_playoff_ot(game) is True def test_final_state_not_playoff_ot(self): game = make_playoff_game(period=4, game_state="OFF") assert is_playoff_ot(game) is False class TestOtLabel: def test_period_4_is_ot(self): assert ot_label(4) == "OT" def test_period_5_is_2ot(self): assert ot_label(5) == "2OT" def test_period_6_is_3ot(self): assert ot_label(6) == "3OT" def test_pre_ot_returns_empty(self): assert ot_label(3) == "" assert ot_label(0) == "" class TestIsPlayoffGame: def test_playoff_raw_shape(self): assert is_playoff_game(make_playoff_game()) is True def test_regular_raw_shape(self): assert is_playoff_game(make_game(game_type=2)) is False def test_parsed_shape(self): # parse_games produces {"Game Type": 3} — is_playoff_game should handle both assert is_playoff_game({"Game Type": 3}) is True assert is_playoff_game({"Game Type": 2}) is False class TestTodayMeta: def test_no_playoff_games_off_mode(self): meta = today_meta([make_game(game_type=2)]) assert meta["playoff_mode"] is False assert meta["round_label"] is None def test_playoff_games_on_mode(self): games = [ make_playoff_game(series_letter="A"), make_playoff_game(series_letter="B"), ] meta = today_meta(games) assert meta["playoff_mode"] is True assert meta["series_active"] == 2 assert meta["round_label"] == "First Round" def test_counts_game7(self): games = [ make_playoff_game(top_wins=3, bottom_wins=3, series_letter="A"), make_playoff_game(top_wins=1, bottom_wins=0, series_letter="B"), ] meta = today_meta(games) assert meta["game7_count"] == 1 assert meta["elimination_count"] == 0 def test_counts_elimination_games(self): games = [ make_playoff_game(top_wins=3, bottom_wins=1, series_letter="A"), make_playoff_game(top_wins=3, bottom_wins=2, series_letter="B"), ] meta = today_meta(games) assert meta["elimination_count"] == 2 assert meta["game7_count"] == 0 def test_round_label_reflects_highest_active_round(self): games = [ make_playoff_game(round_num=1, series_letter="A"), make_playoff_game(round_num=2, series_letter="I"), ] meta = today_meta(games) assert meta["round_label"] == "Second Round" def test_cup_final_label(self): games = [make_playoff_game(round_num=4, series_letter="P")] meta = today_meta(games) assert meta["round_label"] == "Stanley Cup Final" def test_does_not_count_final_games_as_elimination(self): games = [ make_playoff_game( top_wins=3, bottom_wins=0, series_letter="A", game_state="OFF" ), make_playoff_game( top_wins=3, bottom_wins=1, series_letter="B", game_state="LIVE" ), ] meta = today_meta(games) # Only the LIVE card counts; the FINAL card describes a completed game. assert meta["elimination_count"] == 1 def test_does_not_count_final_game7(self): games = [ make_playoff_game( top_wins=3, bottom_wins=3, series_letter="A", game_state="OFF" ) ] meta = today_meta(games) assert meta["game7_count"] == 0