import math import pytest from helpers.refractor_test_data import ( build_batter_plays, build_decision_data, build_game_data, build_pitcher_plays, calculate_plays_needed, ) class TestCalculatePlaysNeeded: """Test the pure function that computes how many synthetic plays are needed to push a card's refractor value over the next tier threshold. The formulas are: - batter: each HR play = 9 value (1 PA + 4 TB * 2) - sp/rp: each K play = 4/3 value (1/3 IP + 1 K) """ def test_batter_exact_threshold(self): """When the gap is exactly divisible by 9, no extra plays needed.""" result = calculate_plays_needed(gap=27, card_type="batter") assert result["num_plays"] == 3 assert result["total_value"] == 27 assert result["value_per_play"] == 9 def test_batter_rounds_up(self): """When gap isn't divisible by 9, round up to overshoot.""" result = calculate_plays_needed(gap=10, card_type="batter") assert result["num_plays"] == 2 # ceil(10/9) = 2 assert result["total_value"] == 18 def test_batter_gap_of_one(self): """Even a gap of 1 requires one play.""" result = calculate_plays_needed(gap=1, card_type="batter") assert result["num_plays"] == 1 assert result["total_value"] == 9 def test_sp_exact_threshold(self): """SP: each K play = 4/3 value.""" result = calculate_plays_needed(gap=4, card_type="sp") assert result["num_plays"] == 3 # ceil(4 / (4/3)) = 3 assert result["value_per_play"] == pytest.approx(4 / 3) def test_rp_same_as_sp(self): """RP uses the same formula as SP.""" result = calculate_plays_needed(gap=4, card_type="rp") assert result["num_plays"] == 3 def test_zero_gap_returns_one_play(self): """If already at threshold, still need 1 play to push over.""" result = calculate_plays_needed(gap=0, card_type="batter") assert result["num_plays"] == 1 class TestBuildGameData: """Test synthetic game record construction for refractor testing. build_game_data creates a self-play game (team vs itself) with game_type='test' and all score/ranking fields zeroed out. This gives the minimum valid game payload to POST to the API. """ def test_basic_structure(self): """Core IDs, type flags, and boolean fields are correct.""" result = build_game_data(team_id=31, season=11) assert result["away_team_id"] == 31 assert result["home_team_id"] == 31 assert result["season"] == 11 assert result["game_type"] == "test" assert result["short_game"] is True assert result["ranked"] is False assert result["forfeit"] is False def test_score_reflects_zero(self): """Scores start at zero — no actual game was simulated.""" result = build_game_data(team_id=31, season=11) assert result["away_score"] == 0 assert result["home_score"] == 0 class TestBuildBatterPlays: """Test synthetic HR play construction for batter refractor testing. Each play is a solo HR: PA=1, AB=1, H=1, HR=1, R=1, RBI=1. Structural fields are filled with safe defaults (inning 1, bot half, no runners on base, zero scores). play_num is sequential from 1. """ def test_correct_count(self): """num_plays controls how many play dicts are returned.""" plays = build_batter_plays( game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=3 ) assert len(plays) == 3 def test_play_fields(self): """Each play has correct IDs and HR stat values.""" plays = build_batter_plays( game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=1 ) play = plays[0] assert play["game_id"] == 1 assert play["batter_id"] == 100 assert play["batter_team_id"] == 31 assert play["pitcher_id"] == 200 assert play["pitcher_team_id"] == 31 assert play["pa"] == 1 assert play["ab"] == 1 assert play["hit"] == 1 assert play["homerun"] == 1 assert play["run"] == 1 assert play["rbi"] == 1 def test_play_nums_sequential(self): """play_num increments from 1 for each play in the batch.""" plays = build_batter_plays( game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=4 ) assert [p["play_num"] for p in plays] == [1, 2, 3, 4] def test_required_structural_fields(self): """Structural fields are filled with safe defaults for API acceptance.""" plays = build_batter_plays( game_id=1, batter_id=100, team_id=31, pitcher_id=200, num_plays=1 ) play = plays[0] assert play["on_base_code"] == "000" assert play["inning_half"] == "bot" assert play["inning_num"] == 1 assert play["batting_order"] == 1 assert play["starting_outs"] == 0 assert play["away_score"] == 0 assert play["home_score"] == 0 class TestBuildPitcherPlays: """Test synthetic strikeout play construction for pitcher refractor testing. Each play is a K: PA=1, AB=1, SO=1, outs=1, hit=0, homerun=0. Structural fields mirror the batter play defaults. """ def test_correct_count(self): """num_plays controls how many play dicts are returned.""" plays = build_pitcher_plays( game_id=1, pitcher_id=200, team_id=31, batter_id=100, num_plays=5 ) assert len(plays) == 5 def test_play_fields(self): """Each play has correct IDs and K stat values.""" plays = build_pitcher_plays( game_id=1, pitcher_id=200, team_id=31, batter_id=100, num_plays=1 ) play = plays[0] assert play["game_id"] == 1 assert play["pitcher_id"] == 200 assert play["pitcher_team_id"] == 31 assert play["batter_id"] == 100 assert play["batter_team_id"] == 31 assert play["pa"] == 1 assert play["ab"] == 1 assert play["so"] == 1 assert play["outs"] == 1 assert play["hit"] == 0 assert play["homerun"] == 0 class TestBuildDecisionData: """Test synthetic pitcher decision construction for refractor testing. Returns a decisions payload with a single no-decision start entry. All win/loss/hold/save flags default to 0; is_start is True. """ def test_basic_structure(self): """Decisions payload has correct IDs, season, and default flags.""" result = build_decision_data(game_id=1, pitcher_id=200, team_id=31, season=11) assert result["decisions"][0]["game_id"] == 1 assert result["decisions"][0]["pitcher_id"] == 200 assert result["decisions"][0]["pitcher_team_id"] == 31 assert result["decisions"][0]["season"] == 11 assert result["decisions"][0]["is_start"] is True assert result["decisions"][0]["win"] == 0 assert result["decisions"][0]["loss"] == 0