"""Tests for SQLAlchemy database models.

This module contains comprehensive tests for all database models:
- User: OAuth accounts, premium subscriptions
- Collection: Card ownership tracking
- Deck: Deck configurations with JSONB
- CampaignProgress: Single-player campaign state
- ActiveGame: In-progress games
- GameHistory: Completed game records

Each test is isolated via transaction rollback - no cleanup needed.
"""

from datetime import UTC, datetime, timedelta

import pytest
from sqlalchemy import select
from sqlalchemy.exc import IntegrityError
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.models import (
    CardSource,
    EndReason,
    GameType,
    User,
)
from tests.factories import (
    ActiveGameFactory,
    CampaignProgressFactory,
    CollectionFactory,
    DeckFactory,
    GameHistoryFactory,
    UserFactory,
)

# =============================================================================
# User Model Tests
# =============================================================================


class TestUserModel:
    """Tests for the User model."""

    @pytest.mark.asyncio
    async def test_create_user(self, db_session: AsyncSession):
        """Test creating a basic user with required fields.

        Verifies that a user can be created with OAuth credentials
        and that the ID is generated automatically.
        """
        user = await UserFactory.create(db_session)

        assert user.id is not None
        assert user.email is not None
        assert user.display_name is not None
        assert user.oauth_provider == "google"
        assert user.oauth_id is not None
        assert user.is_premium is False

    @pytest.mark.asyncio
    async def test_user_has_timestamps(self, db_session: AsyncSession):
        """Test that users get created_at and updated_at timestamps.

        These are inherited from Base and should be auto-populated.
        """
        user = await UserFactory.create(db_session)

        assert user.created_at is not None
        assert user.updated_at is not None
        assert isinstance(user.created_at, datetime)

    @pytest.mark.asyncio
    async def test_user_email_unique(self, db_session: AsyncSession):
        """Test that duplicate emails are rejected.

        Email is a unique constraint on the User model.
        """
        email = "duplicate@example.com"
        await UserFactory.create(db_session, email=email)

        # Attempt to create another user with same email
        with pytest.raises(IntegrityError):
            await UserFactory.create(db_session, email=email)

    @pytest.mark.asyncio
    async def test_user_oauth_unique(self, db_session: AsyncSession):
        """Test that duplicate OAuth provider+id combinations are rejected.

        The (oauth_provider, oauth_id) pair must be unique.
        """
        provider = "discord"
        oauth_id = "discord_123456"

        await UserFactory.create(db_session, oauth_provider=provider, oauth_id=oauth_id)

        # Attempt to create another user with same OAuth credentials
        with pytest.raises(IntegrityError):
            await UserFactory.create(db_session, oauth_provider=provider, oauth_id=oauth_id)

    @pytest.mark.asyncio
    async def test_premium_user(self, db_session: AsyncSession):
        """Test creating a premium user with subscription expiration.

        Verifies that premium fields are stored correctly.
        """
        premium_until = datetime.now(UTC) + timedelta(days=30)
        user = await UserFactory.create(
            db_session,
            is_premium=True,
            premium_until=premium_until,
        )

        assert user.is_premium is True
        assert user.premium_until is not None
        assert user.premium_until > datetime.now(UTC)

    @pytest.mark.asyncio
    async def test_has_active_premium_property(self, db_session: AsyncSession):
        """Test the has_active_premium property logic.

        Premium is active only if is_premium=True AND premium_until is in future.
        """
        # Active premium
        active = await UserFactory.create_premium(db_session, days_remaining=30)
        assert active.has_active_premium is True

        # Expired premium
        expired = await UserFactory.create(
            db_session,
            is_premium=True,
            premium_until=datetime.now(UTC) - timedelta(days=1),
        )
        assert expired.has_active_premium is False

        # Non-premium
        free = await UserFactory.create(db_session, is_premium=False)
        assert free.has_active_premium is False

    @pytest.mark.asyncio
    async def test_max_decks_property(self, db_session: AsyncSession):
        """Test that max_decks returns correct limits based on premium status.

        Free users: 5 decks
        Premium users: 999 decks (effectively unlimited)
        """
        free_user = await UserFactory.create(db_session)
        assert free_user.max_decks == 5

        premium_user = await UserFactory.create_premium(db_session)
        assert premium_user.max_decks == 999

    @pytest.mark.asyncio
    async def test_update_user(self, db_session: AsyncSession):
        """Test updating user fields.

        Verifies that changes persist after flush.
        """
        user = await UserFactory.create(db_session, display_name="Original")

        user.display_name = "Updated Name"
        user.last_login = datetime.now(UTC)
        await db_session.flush()
        await db_session.refresh(user)

        assert user.display_name == "Updated Name"
        assert user.last_login is not None

    @pytest.mark.asyncio
    async def test_read_user_by_id(self, db_session: AsyncSession):
        """Test reading a user by their ID.

        Verifies that we can query users back from the database.
        """
        created = await UserFactory.create(db_session)

        # Read back
        result = await db_session.execute(select(User).where(User.id == created.id))
        fetched = result.scalar_one()

        assert fetched.id == created.id
        assert fetched.email == created.email

    @pytest.mark.asyncio
    async def test_read_user_by_oauth(self, db_session: AsyncSession):
        """Test finding a user by OAuth credentials.

        This is the primary lookup method during authentication.
        """
        user = await UserFactory.create(
            db_session, oauth_provider="discord", oauth_id="discord_unique_123"
        )

        result = await db_session.execute(
            select(User).where(
                User.oauth_provider == "discord",
                User.oauth_id == "discord_unique_123",
            )
        )
        fetched = result.scalar_one()

        assert fetched.id == user.id


# =============================================================================
# Collection Model Tests
# =============================================================================


class TestCollectionModel:
    """Tests for the Collection model."""

    @pytest.mark.asyncio
    async def test_create_collection_entry(self, db_session: AsyncSession):
        """Test creating a collection entry for a user.

        Verifies basic card ownership tracking.
        """
        user = await UserFactory.create(db_session)
        entry = await CollectionFactory.create(
            db_session,
            user_id=user.id,
            card_definition_id="pikachu_base_001",
            quantity=3,
        )

        assert entry.id is not None
        assert entry.user_id == user.id
        assert entry.card_definition_id == "pikachu_base_001"
        assert entry.quantity == 3

    @pytest.mark.asyncio
    async def test_collection_card_source_enum(self, db_session: AsyncSession):
        """Test that all CardSource enum values work correctly.

        Each source type should be valid and stored properly.
        """
        user = await UserFactory.create(db_session)

        for source in CardSource:
            entry = await CollectionFactory.create(
                db_session,
                user_id=user.id,
                source=source,
            )
            assert entry.source == source

    @pytest.mark.asyncio
    async def test_collection_unique_constraint(self, db_session: AsyncSession):
        """Test that (user_id, card_definition_id) is unique.

        Users can't have duplicate entries for the same card.
        """
        user = await UserFactory.create(db_session)
        card_id = "duplicate_card"

        await CollectionFactory.create(db_session, user_id=user.id, card_definition_id=card_id)

        with pytest.raises(IntegrityError):
            await CollectionFactory.create(db_session, user_id=user.id, card_definition_id=card_id)

    @pytest.mark.asyncio
    async def test_collection_same_card_different_users(self, db_session: AsyncSession):
        """Test that different users can own the same card.

        The unique constraint is per-user, not global.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)
        card_id = "shared_card"

        entry1 = await CollectionFactory.create(
            db_session, user_id=user1.id, card_definition_id=card_id
        )
        entry2 = await CollectionFactory.create(
            db_session, user_id=user2.id, card_definition_id=card_id
        )

        assert entry1.id != entry2.id
        assert entry1.user_id != entry2.user_id

    @pytest.mark.asyncio
    async def test_collection_timestamps(self, db_session: AsyncSession):
        """Test that obtained_at tracks when card was added.

        obtained_at is separate from created_at for historical tracking.
        """
        user = await UserFactory.create(db_session)
        before = datetime.now(UTC)

        entry = await CollectionFactory.create(db_session, user_id=user.id)

        assert entry.obtained_at is not None
        assert entry.obtained_at >= before

    @pytest.mark.asyncio
    async def test_update_collection_quantity(self, db_session: AsyncSession):
        """Test updating card quantity in collection.

        Quantity should be updateable without constraint issues.
        """
        user = await UserFactory.create(db_session)
        entry = await CollectionFactory.create(db_session, user_id=user.id, quantity=1)

        entry.quantity = 4
        await db_session.flush()
        await db_session.refresh(entry)

        assert entry.quantity == 4


# =============================================================================
# Deck Model Tests
# =============================================================================


class TestDeckModel:
    """Tests for the Deck model."""

    @pytest.mark.asyncio
    async def test_create_empty_deck(self, db_session: AsyncSession):
        """Test creating an empty deck.

        Decks start empty and are populated later.
        """
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create(db_session, user_id=user.id)

        assert deck.id is not None
        assert deck.user_id == user.id
        assert deck.cards == {}
        assert deck.energy_cards == {}
        assert deck.is_valid is False

    @pytest.mark.asyncio
    async def test_deck_jsonb_cards(self, db_session: AsyncSession):
        """Test storing card data in JSONB format.

        Cards are stored as {card_id: quantity} mapping.
        """
        user = await UserFactory.create(db_session)
        cards = {
            "pikachu_base_001": 4,
            "raichu_base_001": 2,
            "lightning_energy_001": 10,
        }

        deck = await DeckFactory.create(db_session, user_id=user.id, cards=cards)

        assert deck.cards == cards
        assert deck.cards["pikachu_base_001"] == 4

    @pytest.mark.asyncio
    async def test_deck_jsonb_energy(self, db_session: AsyncSession):
        """Test storing energy card data separately.

        Energy is tracked by type for easier UI display.
        """
        user = await UserFactory.create(db_session)
        energy = {"lightning": 8, "fire": 4, "colorless": 4}

        deck = await DeckFactory.create(db_session, user_id=user.id, energy_cards=energy)

        assert deck.energy_cards == energy
        assert deck.energy_cards["lightning"] == 8

    @pytest.mark.asyncio
    async def test_deck_total_cards_property(self, db_session: AsyncSession):
        """Test the total_cards computed property.

        Should sum all card quantities (excluding energy).
        """
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create(
            db_session,
            user_id=user.id,
            cards={"card_a": 4, "card_b": 3, "card_c": 2},
            energy_cards={"fire": 10},
        )

        assert deck.total_cards == 9  # 4 + 3 + 2
        assert deck.total_energy == 10
        assert deck.deck_size == 19  # 9 + 10

    @pytest.mark.asyncio
    async def test_deck_validation_errors_jsonb(self, db_session: AsyncSession):
        """Test storing validation errors in JSONB.

        validation_errors can be null or a list of error messages.
        """
        user = await UserFactory.create(db_session)
        errors = ["Deck too small (30/40)", "Missing basic Pokemon"]

        deck = await DeckFactory.create(
            db_session,
            user_id=user.id,
            is_valid=False,
            validation_errors=errors,
        )

        assert deck.validation_errors == errors
        assert len(deck.validation_errors) == 2

    @pytest.mark.asyncio
    async def test_starter_deck(self, db_session: AsyncSession):
        """Test creating a starter deck.

        Starter decks have special flags for type identification.
        """
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create_starter_deck(db_session, user, starter_type="water")

        assert deck.is_starter is True
        assert deck.starter_type == "water"
        assert "Water" in deck.name

    @pytest.mark.asyncio
    async def test_user_multiple_decks(self, db_session: AsyncSession):
        """Test that users can have multiple decks.

        No unique constraint on (user_id, name) - names can be duplicated.
        """
        user = await UserFactory.create(db_session)

        deck1 = await DeckFactory.create(db_session, user_id=user.id, name="Deck A")
        deck2 = await DeckFactory.create(db_session, user_id=user.id, name="Deck B")
        deck3 = await DeckFactory.create(db_session, user_id=user.id, name="Deck A")

        assert deck1.id != deck2.id != deck3.id
        # Same name is allowed
        assert deck1.name == deck3.name

    @pytest.mark.asyncio
    async def test_update_deck_cards(self, db_session: AsyncSession):
        """Test updating deck card composition.

        JSONB fields should be fully replaceable.
        """
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create(db_session, user_id=user.id, cards={"old_card": 4})

        new_cards = {"new_card_a": 4, "new_card_b": 4}
        deck.cards = new_cards
        deck.is_valid = True
        await db_session.flush()
        await db_session.refresh(deck)

        assert deck.cards == new_cards
        assert "old_card" not in deck.cards


# =============================================================================
# CampaignProgress Model Tests
# =============================================================================


class TestCampaignProgressModel:
    """Tests for the CampaignProgress model."""

    @pytest.mark.asyncio
    async def test_create_campaign_progress(self, db_session: AsyncSession):
        """Test creating initial campaign progress.

        New campaigns start at grass_club with zero progress.
        """
        user = await UserFactory.create(db_session)
        progress = await CampaignProgressFactory.create(db_session, user_id=user.id)

        assert progress.id is not None
        assert progress.user_id == user.id
        assert progress.current_club == "grass_club"
        assert progress.medals == []
        assert progress.mantibucks == 0

    @pytest.mark.asyncio
    async def test_campaign_one_per_user(self, db_session: AsyncSession):
        """Test that each user can only have one campaign progress.

        user_id has a unique constraint (one-to-one relationship).
        """
        user = await UserFactory.create(db_session)
        await CampaignProgressFactory.create(db_session, user_id=user.id)

        with pytest.raises(IntegrityError):
            await CampaignProgressFactory.create(db_session, user_id=user.id)

    @pytest.mark.asyncio
    async def test_campaign_medals_jsonb(self, db_session: AsyncSession):
        """Test storing medals as JSONB array.

        Medals are a list of medal IDs earned.
        """
        user = await UserFactory.create(db_session)
        medals = ["grass_medal", "fire_medal", "water_medal"]

        progress = await CampaignProgressFactory.create(db_session, user_id=user.id, medals=medals)

        assert progress.medals == medals
        assert progress.medal_count == 3

    @pytest.mark.asyncio
    async def test_campaign_defeated_npcs_jsonb(self, db_session: AsyncSession):
        """Test storing defeated NPCs as JSONB array.

        Tracks which NPCs have been beaten (for reward eligibility).
        """
        user = await UserFactory.create(db_session)
        npcs = ["trainer_1", "trainer_2", "leader_grass"]

        progress = await CampaignProgressFactory.create(
            db_session, user_id=user.id, defeated_npcs=npcs
        )

        assert progress.defeated_npcs == npcs
        assert progress.has_defeated("trainer_1") is True
        assert progress.has_defeated("trainer_99") is False

    @pytest.mark.asyncio
    async def test_campaign_has_medal_helper(self, db_session: AsyncSession):
        """Test the has_medal helper method.

        Convenient way to check medal ownership.
        """
        user = await UserFactory.create(db_session)
        progress = await CampaignProgressFactory.create(
            db_session, user_id=user.id, medals=["fire_medal"]
        )

        assert progress.has_medal("fire_medal") is True
        assert progress.has_medal("water_medal") is False

    @pytest.mark.asyncio
    async def test_campaign_win_rate_property(self, db_session: AsyncSession):
        """Test the win_rate computed property.

        Should calculate wins / total games.
        """
        user = await UserFactory.create(db_session)

        # 75% win rate
        progress = await CampaignProgressFactory.create(
            db_session, user_id=user.id, total_wins=15, total_losses=5
        )
        assert progress.win_rate == 0.75

        # No games played
        new_progress = await CampaignProgressFactory.create(
            db_session,
            user_id=(await UserFactory.create(db_session)).id,
            total_wins=0,
            total_losses=0,
        )
        assert new_progress.win_rate == 0.0

    @pytest.mark.asyncio
    async def test_campaign_can_face_grand_masters(self, db_session: AsyncSession):
        """Test the can_face_grand_masters property.

        Requires 8 medals to unlock Grand Masters.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)

        # Not enough medals
        progress1 = await CampaignProgressFactory.create(
            db_session, user_id=user1.id, medals=["m1", "m2", "m3"]
        )
        assert progress1.can_face_grand_masters is False

        # Enough medals
        all_medals = [f"medal_{i}" for i in range(8)]
        progress2 = await CampaignProgressFactory.create(
            db_session, user_id=user2.id, medals=all_medals
        )
        assert progress2.can_face_grand_masters is True

    @pytest.mark.asyncio
    async def test_update_campaign_progress(self, db_session: AsyncSession):
        """Test updating campaign progress after a match.

        Simulates winning a match and earning rewards.
        """
        user = await UserFactory.create(db_session)
        progress = await CampaignProgressFactory.create(db_session, user_id=user.id)

        # Win a match
        progress.total_wins += 1
        progress.booster_packs += 2
        progress.mantibucks += 100
        progress.defeated_npcs = progress.defeated_npcs + ["new_npc"]
        await db_session.flush()
        await db_session.refresh(progress)

        assert progress.total_wins == 1
        assert progress.booster_packs == 2
        assert progress.mantibucks == 100
        assert "new_npc" in progress.defeated_npcs


# =============================================================================
# ActiveGame Model Tests
# =============================================================================


class TestActiveGameModel:
    """Tests for the ActiveGame model."""

    @pytest.mark.asyncio
    async def test_create_campaign_game(self, db_session: AsyncSession):
        """Test creating an active campaign game.

        Campaign games have one player vs an NPC.
        """
        user = await UserFactory.create(db_session)
        game = await ActiveGameFactory.create_campaign_game(db_session, user)

        assert game.id is not None
        assert game.game_type == GameType.CAMPAIGN
        assert game.player1_id == user.id
        assert game.player2_id is None
        assert game.npc_id is not None

    @pytest.mark.asyncio
    async def test_create_pvp_game(self, db_session: AsyncSession):
        """Test creating an active PvP game.

        PvP games have two players and no NPC.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)

        game = await ActiveGameFactory.create_pvp_game(db_session, user1, user2)

        assert game.player1_id == user1.id
        assert game.player2_id == user2.id
        assert game.npc_id is None

    @pytest.mark.asyncio
    async def test_game_type_enum(self, db_session: AsyncSession):
        """Test that all GameType enum values work.

        CAMPAIGN, FREEPLAY, RANKED, PRACTICE should all be valid.
        """
        user = await UserFactory.create(db_session)

        for game_type in GameType:
            game = await ActiveGameFactory.create(
                db_session, player1_id=user.id, game_type=game_type
            )
            assert game.game_type == game_type

    @pytest.mark.asyncio
    async def test_game_state_jsonb(self, db_session: AsyncSession):
        """Test storing game state in JSONB.

        The full GameState serializes to JSONB for persistence.
        """
        user = await UserFactory.create(db_session)
        state = {
            "turn": 5,
            "phase": "attack",
            "players": {
                "player1": {"active": "pikachu_inst_1", "bench": []},
                "player2": {"active": "charizard_inst_1", "bench": []},
            },
        }

        game = await ActiveGameFactory.create(db_session, player1_id=user.id, game_state=state)

        assert game.game_state == state
        assert game.game_state["turn"] == 5

    @pytest.mark.asyncio
    async def test_game_rules_config_jsonb(self, db_session: AsyncSession):
        """Test storing rules configuration in JSONB.

        Different game modes may have different rules.
        """
        user = await UserFactory.create(db_session)
        rules = {
            "prize_count": 6,
            "deck_size": 60,
            "max_hand_size": 7,
            "first_turn_attack": False,
        }

        game = await ActiveGameFactory.create(db_session, player1_id=user.id, rules_config=rules)

        assert game.rules_config == rules
        assert game.rules_config["prize_count"] == 6

    @pytest.mark.asyncio
    async def test_game_timestamps(self, db_session: AsyncSession):
        """Test that game timing fields work correctly.

        started_at, last_action_at track game timing.
        """
        user = await UserFactory.create(db_session)
        game = await ActiveGameFactory.create(db_session, player1_id=user.id)

        assert game.started_at is not None
        assert game.last_action_at is not None

        # Update last action
        new_time = datetime.now(UTC)
        game.last_action_at = new_time
        game.turn_number += 1
        await db_session.flush()

        assert game.last_action_at == new_time
        assert game.turn_number == 2

    @pytest.mark.asyncio
    async def test_game_turn_deadline(self, db_session: AsyncSession):
        """Test optional turn deadline for timed games.

        turn_deadline is set for games with time controls.
        """
        user = await UserFactory.create(db_session)
        deadline = datetime.now(UTC) + timedelta(minutes=2)

        game = await ActiveGameFactory.create(
            db_session, player1_id=user.id, turn_deadline=deadline
        )

        assert game.turn_deadline is not None
        assert game.turn_deadline == deadline


# =============================================================================
# GameHistory Model Tests
# =============================================================================


class TestGameHistoryModel:
    """Tests for the GameHistory model."""

    @pytest.mark.asyncio
    async def test_create_player_win(self, db_session: AsyncSession):
        """Test recording a player victory.

        Player wins are tracked with winner_id pointing to the player.
        """
        user = await UserFactory.create(db_session)
        history = await GameHistoryFactory.create_player_win(db_session, user)

        assert history.id is not None
        assert history.winner_id == user.id
        assert history.winner_is_npc is False
        assert history.end_reason == EndReason.PRIZES_TAKEN

    @pytest.mark.asyncio
    async def test_create_player_loss(self, db_session: AsyncSession):
        """Test recording a player loss to NPC.

        NPC wins have winner_is_npc=True and winner_id=None.
        """
        user = await UserFactory.create(db_session)
        history = await GameHistoryFactory.create_player_loss(db_session, user)

        assert history.winner_id is None
        assert history.winner_is_npc is True

    @pytest.mark.asyncio
    async def test_end_reason_enum(self, db_session: AsyncSession):
        """Test that all EndReason enum values work.

        All ways a game can end should be valid.
        """
        user = await UserFactory.create(db_session)

        for reason in EndReason:
            history = await GameHistoryFactory.create(
                db_session, player1_id=user.id, end_reason=reason
            )
            assert history.end_reason == reason

    @pytest.mark.asyncio
    async def test_is_draw_property(self, db_session: AsyncSession):
        """Test the is_draw computed property.

        Draws have end_reason=DRAW and no winner.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)

        draw_game = await GameHistoryFactory.create_pvp_game(db_session, user1, user2, winner=None)
        assert draw_game.is_draw is True
        assert draw_game.winner_id is None

        won_game = await GameHistoryFactory.create_pvp_game(db_session, user1, user2, winner=user1)
        assert won_game.is_draw is False

    @pytest.mark.asyncio
    async def test_game_statistics(self, db_session: AsyncSession):
        """Test that game statistics are stored correctly.

        turn_count and duration_seconds track game length.
        """
        user = await UserFactory.create(db_session)
        history = await GameHistoryFactory.create(
            db_session,
            player1_id=user.id,
            turn_count=25,
            duration_seconds=600,
        )

        assert history.turn_count == 25
        assert history.duration_seconds == 600

    @pytest.mark.asyncio
    async def test_replay_data_jsonb(self, db_session: AsyncSession):
        """Test storing replay data in JSONB.

        Replay data captures all actions for playback.
        """
        user = await UserFactory.create(db_session)
        replay = {
            "version": "1.0",
            "actions": [
                {"type": "play_card", "card": "pikachu", "turn": 1},
                {"type": "attack", "attack": "thunder_shock", "turn": 1},
            ],
            "seed": 12345,
        }

        history = await GameHistoryFactory.create(
            db_session, player1_id=user.id, replay_data=replay
        )

        assert history.replay_data == replay
        assert len(history.replay_data["actions"]) == 2

    @pytest.mark.asyncio
    async def test_pvp_game_history(self, db_session: AsyncSession):
        """Test recording a PvP match.

        PvP games have both player1_id and player2_id set.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)

        history = await GameHistoryFactory.create_pvp_game(
            db_session,
            user1,
            user2,
            winner=user2,
            game_type=GameType.RANKED,
        )

        assert history.player1_id == user1.id
        assert history.player2_id == user2.id
        assert history.winner_id == user2.id
        assert history.game_type == GameType.RANKED
        assert history.npc_id is None

    @pytest.mark.asyncio
    async def test_played_at_timestamp(self, db_session: AsyncSession):
        """Test that played_at records completion time.

        played_at should be set when the game ends.
        """
        user = await UserFactory.create(db_session)
        before = datetime.now(UTC)

        history = await GameHistoryFactory.create(db_session, player1_id=user.id)

        assert history.played_at is not None
        assert history.played_at >= before


# =============================================================================
# Model __repr__ Tests
# =============================================================================


class TestModelRepr:
    """Tests for model __repr__ methods.

    These ensure debugging output is useful and doesn't break.
    """

    @pytest.mark.asyncio
    async def test_user_repr(self, db_session: AsyncSession):
        """Test User __repr__ includes id and email."""
        user = await UserFactory.create(db_session, email="repr_test@example.com")
        repr_str = repr(user)

        assert "User" in repr_str
        assert str(user.id) in repr_str
        assert "repr_test@example.com" in repr_str

    @pytest.mark.asyncio
    async def test_collection_repr(self, db_session: AsyncSession):
        """Test Collection __repr__ includes relevant info."""
        user = await UserFactory.create(db_session)
        entry = await CollectionFactory.create(
            db_session, user_id=user.id, card_definition_id="test_card_repr"
        )
        repr_str = repr(entry)

        assert "Collection" in repr_str
        # Collection repr uses user_id and card, not the collection id
        assert str(user.id) in repr_str
        assert "test_card_repr" in repr_str

    @pytest.mark.asyncio
    async def test_deck_repr(self, db_session: AsyncSession):
        """Test Deck __repr__ includes id and name."""
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create_for_user(db_session, user, name="Repr Test Deck")
        repr_str = repr(deck)

        assert "Deck" in repr_str
        assert str(deck.id) in repr_str
        assert "Repr Test Deck" in repr_str

    @pytest.mark.asyncio
    async def test_campaign_progress_repr(self, db_session: AsyncSession):
        """Test CampaignProgress __repr__ includes user_id and club."""
        user = await UserFactory.create(db_session)
        progress = await CampaignProgressFactory.create_for_user(
            db_session, user, current_club="fire_club"
        )
        repr_str = repr(progress)

        assert "CampaignProgress" in repr_str
        # CampaignProgress repr uses user_id, not the progress id
        assert str(user.id) in repr_str
        assert "fire_club" in repr_str

    @pytest.mark.asyncio
    async def test_active_game_repr(self, db_session: AsyncSession):
        """Test ActiveGame __repr__ includes id, type, and turn."""
        user = await UserFactory.create(db_session)
        game = await ActiveGameFactory.create_campaign_game(db_session, user)
        repr_str = repr(game)

        assert "ActiveGame" in repr_str
        assert str(game.id) in repr_str
        assert "campaign" in repr_str.lower() or "CAMPAIGN" in repr_str

    @pytest.mark.asyncio
    async def test_game_history_repr(self, db_session: AsyncSession):
        """Test GameHistory __repr__ includes id, type, turns, and end reason."""
        user = await UserFactory.create(db_session)
        history = await GameHistoryFactory.create_player_win(db_session, user)
        repr_str = repr(history)

        assert "GameHistory" in repr_str
        assert str(history.id) in repr_str
