"""Tests for evolution Peewee models (WP-01). Unit tests verify model structure and defaults on unsaved instances without touching a database. Integration tests use an in-memory SQLite database to verify table creation, FK relationships, and unique constraints. """ import pytest from peewee import SqliteDatabase, IntegrityError from playhouse.shortcuts import model_to_dict from app.models.evolution import ( EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic, ) from app.db_engine import Rarity, Event, Cardset, MlbPlayer, Player, Team # All models that must exist in the test database (dependency order). _TEST_MODELS = [ Rarity, Event, Cardset, MlbPlayer, Player, Team, EvolutionTrack, EvolutionCardState, EvolutionTierBoost, EvolutionCosmetic, ] _test_db = SqliteDatabase(":memory:", pragmas={"foreign_keys": 1}) @pytest.fixture(autouse=True) def setup_test_db(): """Bind all models to an in-memory SQLite database, create tables, and tear them down after each test so each test starts from a clean state.""" _test_db.bind(_TEST_MODELS) _test_db.create_tables(_TEST_MODELS) yield _test_db _test_db.drop_tables(list(reversed(_TEST_MODELS)), safe=True) # ── Fixture helpers ──────────────────────────────────────────────────────── def make_rarity(): return Rarity.create(value=1, name="Common", color="#ffffff") def make_cardset(): return Cardset.create(name="2025", description="2025 Season", total_cards=100) def make_player(cardset, rarity): return Player.create( player_id=1, p_name="Test Player", cost=100, image="test.png", mlbclub="BOS", franchise="Boston", cardset=cardset, set_num=1, rarity=rarity, pos_1="OF", description="Test", ) def make_team(): return Team.create( abbrev="TEST", sname="Test", lname="Test Team", gmid=123456789, gmname="testuser", gsheet="https://example.com", wallet=1000, team_value=1000, collection_value=1000, season=1, ) def make_track(card_type="batter"): return EvolutionTrack.create( name="Batter", card_type=card_type, formula="pa+tb*2", t1_threshold=37, t2_threshold=149, t3_threshold=448, t4_threshold=896, ) # ── Unit: model field validation ─────────────────────────────────────────── class TestEvolutionTrackFields: """model_to_dict works on unsaved EvolutionTrack instances and all fields are accessible with the correct values.""" def test_model_to_dict_unsaved(self): """All EvolutionTrack fields appear in model_to_dict on an unsaved instance.""" track = EvolutionTrack( name="Batter", card_type="batter", formula="pa+tb*2", t1_threshold=37, t2_threshold=149, t3_threshold=448, t4_threshold=896, ) data = model_to_dict(track, recurse=False) assert data["name"] == "Batter" 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_all_threshold_fields_present(self): """EvolutionTrack exposes all four tier threshold columns.""" fields = EvolutionTrack._meta.fields for col in ("t1_threshold", "t2_threshold", "t3_threshold", "t4_threshold"): assert col in fields, f"Missing column: {col}" class TestEvolutionCardStateFields: """model_to_dict works on unsaved EvolutionCardState instances and default values match the spec.""" def test_model_to_dict_defaults(self): """Defaults: current_tier=0, current_value=0.0, fully_evolved=False, last_evaluated_at=None.""" state = EvolutionCardState() data = model_to_dict(state, recurse=False) assert data["current_tier"] == 0 assert data["current_value"] == 0.0 assert data["fully_evolved"] is False assert data["last_evaluated_at"] is None def test_no_progress_since_field(self): """EvolutionCardState must not have a progress_since field (removed from spec).""" assert "progress_since" not in EvolutionCardState._meta.fields class TestEvolutionStubFields: """Phase 2 stub models are importable and respond to model_to_dict.""" def test_tier_boost_importable(self): assert EvolutionTierBoost is not None def test_cosmetic_importable(self): assert EvolutionCosmetic is not None def test_tier_boost_model_to_dict_unsaved(self): """model_to_dict on an unsaved EvolutionTierBoost returns a dict.""" data = model_to_dict(EvolutionTierBoost(), recurse=False) assert isinstance(data, dict) def test_cosmetic_model_to_dict_unsaved(self): """model_to_dict on an unsaved EvolutionCosmetic returns a dict.""" data = model_to_dict(EvolutionCosmetic(), recurse=False) assert isinstance(data, dict) # ── Unit: constraint definitions ────────────────────────────────────────── class TestTierConstraints: """current_tier defaults to 0 and valid tier values (0-4) can be saved.""" def test_tier_zero_is_default(self): """EvolutionCardState.current_tier defaults to 0 on create.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create(player=player, team=team, track=track) assert state.current_tier == 0 def test_tier_four_is_valid(self): """Tier 4 (fully evolved cap) can be persisted without error.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create( player=player, team=team, track=track, current_tier=4 ) assert state.current_tier == 4 class TestUniqueConstraint: """Unique index on (player_id, team_id) is enforced at the DB level.""" def test_duplicate_player_team_raises(self): """A second EvolutionCardState for the same (player, team) raises IntegrityError, even when a different track is used.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track1 = make_track("batter") track2 = EvolutionTrack.create( name="SP", card_type="sp", formula="ip+k", t1_threshold=10, t2_threshold=40, t3_threshold=120, t4_threshold=240, ) EvolutionCardState.create(player=player, team=team, track=track1) with pytest.raises(IntegrityError): EvolutionCardState.create(player=player, team=team, track=track2) def test_same_player_different_teams_allowed(self): """One EvolutionCardState per team is allowed for the same player.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team1 = make_team() team2 = Team.create( abbrev="TM2", sname="T2", lname="Team Two", gmid=987654321, gmname="user2", gsheet="https://example.com", wallet=1000, team_value=1000, collection_value=1000, season=1, ) track = make_track() EvolutionCardState.create(player=player, team=team1, track=track) state2 = EvolutionCardState.create(player=player, team=team2, track=track) assert state2.id is not None # ── Integration: table creation ──────────────────────────────────────────── class TestTableCreation: """All four evolution tables are created in the test DB and are queryable.""" def test_evolution_track_table_exists(self): assert EvolutionTrack.select().count() == 0 def test_evolution_card_state_table_exists(self): assert EvolutionCardState.select().count() == 0 def test_evolution_tier_boost_table_exists(self): assert EvolutionTierBoost.select().count() == 0 def test_evolution_cosmetic_table_exists(self): assert EvolutionCosmetic.select().count() == 0 # ── Integration: FK enforcement ──────────────────────────────────────────── class TestFKEnforcement: """FK columns resolve to the correct related instances.""" def test_card_state_player_fk_resolves(self): """EvolutionCardState.player_id matches the Player we inserted.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create(player=player, team=team, track=track) fetched = EvolutionCardState.get_by_id(state.id) assert fetched.player_id == player.player_id def test_card_state_team_fk_resolves(self): """EvolutionCardState.team_id matches the Team we inserted.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create(player=player, team=team, track=track) fetched = EvolutionCardState.get_by_id(state.id) assert fetched.team_id == team.id def test_card_state_track_fk_resolves(self): """EvolutionCardState.track_id matches the EvolutionTrack we inserted.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create(player=player, team=team, track=track) fetched = EvolutionCardState.get_by_id(state.id) assert fetched.track_id == track.id # ── Integration: model_to_dict on saved instances ────────────────────────── class TestModelToDictOnSaved: """model_to_dict() works correctly on saved instances of all four models.""" def test_evolution_track_saved(self): """Saved EvolutionTrack round-trips through model_to_dict correctly.""" track = make_track() data = model_to_dict(track, recurse=False) assert data["name"] == "Batter" assert data["card_type"] == "batter" assert data["formula"] == "pa+tb*2" assert data["t1_threshold"] == 37 def test_evolution_card_state_saved(self): """Saved EvolutionCardState round-trips through model_to_dict correctly.""" rarity = make_rarity() cardset = make_cardset() player = make_player(cardset, rarity) team = make_team() track = make_track() state = EvolutionCardState.create( player=player, team=team, track=track, current_value=42.5, current_tier=2 ) data = model_to_dict(state, recurse=False) assert data["current_value"] == 42.5 assert data["current_tier"] == 2 assert data["fully_evolved"] is False