""" Tests for refractor-related models and BattingSeasonStats. Covers WP-01 acceptance criteria: - RefractorTrack: CRUD and unique-name constraint - RefractorCardState: CRUD, defaults, unique-(player,team) constraint, and FK resolution back to RefractorTrack - BattingSeasonStats: CRUD with defaults, unique-(player, team, season), and in-place stat accumulation Each test class is self-contained: fixtures from conftest.py supply the minimal parent rows needed to satisfy FK constraints, and every assertion targets a single, clearly-named behaviour so failures are easy to trace. """ import pytest from peewee import IntegrityError from playhouse.shortcuts import model_to_dict from app.db_engine import ( BattingSeasonStats, RefractorCardState, RefractorTrack, ) # --------------------------------------------------------------------------- # RefractorTrack # --------------------------------------------------------------------------- class TestRefractorTrack: """Tests for the RefractorTrack model. RefractorTrack defines a named progression path (formula + tier thresholds) for a card type. The name column carries a UNIQUE constraint so that accidental duplicates are caught at the database level. """ def test_create_track(self, track): """Creating a track persists all fields and they round-trip correctly. Reads back via model_to_dict (recurse=False) to verify the raw column values, not Python-object representations, match what was inserted. """ data = model_to_dict(track, recurse=False) assert data["name"] == "Batter Track" assert data["card_type"] == "batter" assert data["formula"] == "pa + tb * 2" assert data["t1_threshold"] == 37 assert data["t2_threshold"] == 149 assert data["t3_threshold"] == 448 assert data["t4_threshold"] == 896 def test_track_unique_name(self, track): """Inserting a second track with the same name raises IntegrityError. The UNIQUE constraint on RefractorTrack.name must prevent two tracks from sharing the same identifier, as the name is used as a human-readable key throughout the evolution system. """ with pytest.raises(IntegrityError): RefractorTrack.create( name="Batter Track", # duplicate card_type="sp", formula="outs * 3", t1_threshold=10, t2_threshold=40, t3_threshold=120, t4_threshold=240, ) # --------------------------------------------------------------------------- # RefractorCardState # --------------------------------------------------------------------------- class TestRefractorCardState: """Tests for RefractorCardState, which tracks per-player refractor progress. Each row represents one card (player) owned by one team, linked to a specific RefractorTrack. The model records the current tier (0-4), accumulated progress value, and whether the card is fully evolved. """ def test_create_card_state(self, player, team, track): """Creating a card state stores all fields and defaults are correct. Defaults under test: current_tier → 0 (fresh card, no tier unlocked yet) current_value → 0.0 (no formula progress accumulated) fully_evolved → False (evolution is not complete at creation) last_evaluated_at → None (never evaluated yet) """ state = RefractorCardState.create(player=player, team=team, track=track) fetched = RefractorCardState.get_by_id(state.id) assert fetched.player_id == player.player_id assert fetched.team_id == team.id assert fetched.track_id == track.id assert fetched.current_tier == 0 assert fetched.current_value == 0.0 assert fetched.fully_evolved is False assert fetched.last_evaluated_at is None def test_card_state_unique_player_team(self, player, team, track): """A second card state for the same (player, team) pair raises IntegrityError. The unique index on (player, team) enforces that each player card has at most one refractor state per team roster slot, preventing duplicate refractor progress rows for the same physical card. """ RefractorCardState.create(player=player, team=team, track=track) with pytest.raises(IntegrityError): RefractorCardState.create(player=player, team=team, track=track) def test_card_state_fk_track(self, player, team, track): """Accessing card_state.track returns the original RefractorTrack instance. This confirms the FK is correctly wired and that Peewee resolves the relationship, returning an object with the same primary key and name as the track used during creation. """ state = RefractorCardState.create(player=player, team=team, track=track) fetched = RefractorCardState.get_by_id(state.id) resolved_track = fetched.track assert resolved_track.id == track.id assert resolved_track.name == "Batter Track" # --------------------------------------------------------------------------- # BattingSeasonStats # --------------------------------------------------------------------------- class TestBattingSeasonStats: """Tests for BattingSeasonStats, the per-season batting accumulation table. Each row aggregates game-by-game batting stats for one player on one team in one season. The three-column unique constraint prevents double-counting and ensures a single authoritative row for each (player, team, season) combination. """ def test_create_season_stats(self, player, team): """Creating a stats row with explicit values stores everything correctly. Also verifies the integer stat defaults (all 0) for columns that are not provided, which is the initial state before any games are processed. """ stats = BattingSeasonStats.create( player=player, team=team, season=11, games=5, pa=20, ab=18, hits=6, doubles=1, triples=0, hr=2, bb=2, hbp=0, strikeouts=4, rbi=5, runs=3, sb=1, cs=0, ) fetched = BattingSeasonStats.get_by_id(stats.id) assert fetched.player_id == player.player_id assert fetched.team_id == team.id assert fetched.season == 11 assert fetched.games == 5 assert fetched.pa == 20 assert fetched.hits == 6 assert fetched.hr == 2 assert fetched.strikeouts == 4 # Nullable meta fields assert fetched.last_game is None assert fetched.last_updated_at is None def test_season_stats_unique_constraint(self, player, team): """A second row for the same (player, team, season) raises IntegrityError. The unique index on these three columns guarantees that each player-team-season combination has exactly one accumulation row, preventing duplicate stat aggregation that would inflate totals. """ BattingSeasonStats.create(player=player, team=team, season=11) with pytest.raises(IntegrityError): BattingSeasonStats.create(player=player, team=team, season=11) def test_season_stats_increment(self, player, team): """Manually incrementing hits on an existing row persists the change. Simulates the common pattern used by the stats accumulator: fetch the row, add the game delta, save. Verifies that save() writes back to the database and that subsequent reads reflect the updated value. """ stats = BattingSeasonStats.create( player=player, team=team, season=11, hits=10, ) stats.hits += 3 stats.save() refreshed = BattingSeasonStats.get_by_id(stats.id) assert refreshed.hits == 13