""" 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 - RefractorTierBoost: CRUD and unique-(track, tier, boost_type, boost_target) - RefractorCosmetic: CRUD and unique-name constraint - 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, RefractorCosmetic, RefractorTierBoost, 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" # --------------------------------------------------------------------------- # RefractorTierBoost # --------------------------------------------------------------------------- class TestRefractorTierBoost: """Tests for RefractorTierBoost, the per-tier stat/rating bonus table. Each row maps a (track, tier) combination to a single boost — the specific stat or rating column to buff and by how much. The four- column unique constraint prevents double-booking the same boost slot. """ def test_create_tier_boost(self, track): """Creating a boost row persists all fields accurately. Verifies boost_type, boost_target, and boost_value are stored and retrieved without modification. """ boost = RefractorTierBoost.create( track=track, tier=1, boost_type="rating", boost_target="contact_vl", boost_value=1.5, ) fetched = RefractorTierBoost.get_by_id(boost.id) assert fetched.track_id == track.id assert fetched.tier == 1 assert fetched.boost_type == "rating" assert fetched.boost_target == "contact_vl" assert fetched.boost_value == 1.5 def test_tier_boost_unique_constraint(self, track): """Duplicate (track, tier, boost_type, boost_target) raises IntegrityError. The four-column unique index ensures that a single boost slot (e.g. Tier-1 contact_vl rating) cannot be defined twice for the same track, which would create ambiguity during evolution evaluation. """ RefractorTierBoost.create( track=track, tier=2, boost_type="rating", boost_target="power_vr", boost_value=2.0, ) with pytest.raises(IntegrityError): RefractorTierBoost.create( track=track, tier=2, boost_type="rating", boost_target="power_vr", boost_value=3.0, # different value, same identity columns ) # --------------------------------------------------------------------------- # RefractorCosmetic # --------------------------------------------------------------------------- class TestRefractorCosmetic: """Tests for RefractorCosmetic, decorative unlocks tied to evolution tiers. Cosmetics are purely visual rewards (frames, badges, themes) that a card unlocks when it reaches a required tier. The name column is the stable identifier and carries a UNIQUE constraint. """ def test_create_cosmetic(self): """Creating a cosmetic persists all fields correctly. Verifies all columns including optional ones (css_class, asset_url) are stored and retrieved. """ cosmetic = RefractorCosmetic.create( name="Gold Frame", tier_required=2, cosmetic_type="frame", css_class="evo-frame-gold", asset_url="https://cdn.example.com/frames/gold.png", ) fetched = RefractorCosmetic.get_by_id(cosmetic.id) assert fetched.name == "Gold Frame" assert fetched.tier_required == 2 assert fetched.cosmetic_type == "frame" assert fetched.css_class == "evo-frame-gold" assert fetched.asset_url == "https://cdn.example.com/frames/gold.png" def test_cosmetic_unique_name(self): """Inserting a second cosmetic with the same name raises IntegrityError. The UNIQUE constraint on RefractorCosmetic.name prevents duplicate cosmetic definitions that could cause ambiguous tier unlock lookups. """ RefractorCosmetic.create( name="Silver Badge", tier_required=1, cosmetic_type="badge", ) with pytest.raises(IntegrityError): RefractorCosmetic.create( name="Silver Badge", # duplicate tier_required=3, cosmetic_type="badge", ) # --------------------------------------------------------------------------- # 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