"""Tests for SQLAlchemy model relationships and cascades.

This module tests:
- Foreign key relationships between models
- Cascade delete behavior (deleting parent deletes children)
- Relationship loading (lazy vs eager)
- Orphan record prevention

These tests ensure referential integrity is maintained and that
deleting a user properly cleans up all associated data.
"""

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

from app.db.models import (
    ActiveGame,
    CampaignProgress,
    Collection,
    Deck,
    GameHistory,
    User,
)
from tests.factories import (
    ActiveGameFactory,
    CampaignProgressFactory,
    CollectionFactory,
    DeckFactory,
    GameHistoryFactory,
    UserFactory,
)

# =============================================================================
# User Cascade Delete Tests
# =============================================================================


class TestUserCascadeDelete:
    """Tests for cascade delete when removing a User.

    When a user is deleted, all their associated records should
    be deleted automatically via CASCADE.
    """

    @pytest.mark.asyncio
    async def test_delete_user_cascades_to_collection(self, db_session: AsyncSession):
        """Test that deleting a user deletes their card collection.

        Collection entries have ON DELETE CASCADE to users.
        """
        user = await UserFactory.create(db_session)
        entries = await CollectionFactory.create_for_user(db_session, user, card_count=5)
        entry_ids = [e.id for e in entries]

        # Delete the user
        await db_session.delete(user)
        await db_session.flush()

        # Verify collection entries are gone
        result = await db_session.execute(select(Collection).where(Collection.id.in_(entry_ids)))
        remaining = result.scalars().all()
        assert len(remaining) == 0

    @pytest.mark.asyncio
    async def test_delete_user_cascades_to_decks(self, db_session: AsyncSession):
        """Test that deleting a user deletes their decks.

        Decks have ON DELETE CASCADE to users.
        """
        user = await UserFactory.create(db_session)
        deck1 = await DeckFactory.create_for_user(db_session, user)
        deck2 = await DeckFactory.create_for_user(db_session, user)
        deck_ids = [deck1.id, deck2.id]

        # Delete the user
        await db_session.delete(user)
        await db_session.flush()

        # Verify decks are gone
        result = await db_session.execute(select(Deck).where(Deck.id.in_(deck_ids)))
        remaining = result.scalars().all()
        assert len(remaining) == 0

    @pytest.mark.asyncio
    async def test_delete_user_cascades_to_campaign(self, db_session: AsyncSession):
        """Test that deleting a user deletes their campaign progress.

        CampaignProgress has ON DELETE CASCADE to users.
        """
        user = await UserFactory.create(db_session)
        progress = await CampaignProgressFactory.create_for_user(db_session, user)
        progress_id = progress.id

        # Delete the user
        await db_session.delete(user)
        await db_session.flush()

        # Verify campaign progress is gone
        result = await db_session.execute(
            select(CampaignProgress).where(CampaignProgress.id == progress_id)
        )
        remaining = result.scalar_one_or_none()
        assert remaining is None

    @pytest.mark.asyncio
    async def test_delete_user_cascades_to_active_games(self, db_session: AsyncSession):
        """Test that deleting player1 cascades to active games.

        ActiveGame.player1_id has ON DELETE CASCADE.
        """
        user = await UserFactory.create(db_session)
        game = await ActiveGameFactory.create_campaign_game(db_session, user)
        game_id = game.id

        # Delete the user
        await db_session.delete(user)
        await db_session.flush()

        # Verify active game is gone
        result = await db_session.execute(select(ActiveGame).where(ActiveGame.id == game_id))
        remaining = result.scalar_one_or_none()
        assert remaining is None

    @pytest.mark.asyncio
    async def test_delete_user_sets_null_on_player2(self, db_session: AsyncSession):
        """Test that deleting player2 sets player2_id to NULL.

        ActiveGame.player2_id has ON DELETE SET NULL (game continues).
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)
        game = await ActiveGameFactory.create_pvp_game(db_session, user1, user2)
        game_id = game.id

        # Delete player2
        await db_session.delete(user2)
        await db_session.flush()

        # Refresh game from DB
        await db_session.refresh(game)

        # Game still exists, but player2 is NULL
        assert game.id == game_id
        assert game.player1_id == user1.id
        assert game.player2_id is None

    @pytest.mark.asyncio
    async def test_delete_user_preserves_game_history(self, db_session: AsyncSession):
        """Test that game history is preserved when user is deleted.

        GameHistory uses ON DELETE SET NULL for player IDs to preserve records.
        Note: Must expire the history object before re-querying so SQLAlchemy
        fetches fresh data from DB instead of returning the cached instance.
        """
        user1 = await UserFactory.create(db_session)
        user2 = await UserFactory.create(db_session)
        history = await GameHistoryFactory.create_pvp_game(db_session, user1, user2, winner=user1)
        history_id = history.id
        user2_id = user2.id

        # Expire history so re-query fetches fresh from DB (after FK cascade)
        db_session.expire(history)

        # Delete the winner (DB will SET NULL on game_history FK columns)
        await db_session.delete(user1)
        await db_session.flush()

        # Game history still exists with NULL player1_id and winner_id
        result = await db_session.execute(select(GameHistory).where(GameHistory.id == history_id))
        preserved = result.scalar_one()

        assert preserved is not None
        assert preserved.player1_id is None  # SET NULL by FK cascade
        assert preserved.player2_id == user2_id
        assert preserved.winner_id is None  # SET NULL by FK cascade


# =============================================================================
# Foreign Key Integrity Tests
# =============================================================================


class TestForeignKeyIntegrity:
    """Tests for foreign key constraints.

    Ensures orphan records cannot be created.
    """

    @pytest.mark.asyncio
    async def test_collection_requires_valid_user(self, db_session: AsyncSession):
        """Test that collection entries require an existing user.

        Cannot create collection entry with non-existent user_id.
        """
        from uuid import uuid4

        fake_user_id = uuid4()

        with pytest.raises(IntegrityError):
            await CollectionFactory.create(db_session, user_id=fake_user_id)

    @pytest.mark.asyncio
    async def test_deck_requires_valid_user(self, db_session: AsyncSession):
        """Test that decks require an existing user.

        Cannot create deck with non-existent user_id.
        """
        from uuid import uuid4

        fake_user_id = uuid4()

        with pytest.raises(IntegrityError):
            await DeckFactory.create(db_session, user_id=fake_user_id)

    @pytest.mark.asyncio
    async def test_campaign_requires_valid_user(self, db_session: AsyncSession):
        """Test that campaign progress requires an existing user.

        Cannot create campaign progress with non-existent user_id.
        """
        from uuid import uuid4

        fake_user_id = uuid4()

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

    @pytest.mark.asyncio
    async def test_active_game_requires_valid_player1(self, db_session: AsyncSession):
        """Test that active games require a valid player1.

        Cannot create game with non-existent player1_id.
        """
        from uuid import uuid4

        fake_user_id = uuid4()

        with pytest.raises(IntegrityError):
            await ActiveGameFactory.create(db_session, player1_id=fake_user_id)

    @pytest.mark.asyncio
    async def test_game_history_requires_valid_player1(self, db_session: AsyncSession):
        """Test that game history requires a valid player1.

        Cannot create history with non-existent player1_id.
        """
        from uuid import uuid4

        fake_user_id = uuid4()

        with pytest.raises(IntegrityError):
            await GameHistoryFactory.create(db_session, player1_id=fake_user_id)


# =============================================================================
# Relationship Loading Tests
# =============================================================================


class TestRelationshipLoading:
    """Tests for loading related objects.

    Ensures relationships can be loaded both lazily and eagerly
    in async context without issues.
    """

    @pytest.mark.asyncio
    async def test_user_load_decks_eager(self, db_session: AsyncSession):
        """Test eagerly loading user's decks with selectinload.

        selectinload is the recommended strategy for async.
        Note: Must expire the user first since the decks were added after
        the user was loaded, and expire_on_commit=False keeps cached state.
        """
        user = await UserFactory.create(db_session)
        user_id = user.id  # Save ID before expire
        await DeckFactory.create_for_user(db_session, user, name="Deck 1")
        await DeckFactory.create_for_user(db_session, user, name="Deck 2")

        # Expire the user so selectinload fetches fresh data
        db_session.expire(user)

        # Query with eager loading
        result = await db_session.execute(
            select(User).where(User.id == user_id).options(selectinload(User.decks))
        )
        loaded_user = result.scalar_one()

        # Decks should already be loaded (no additional query)
        assert len(loaded_user.decks) == 2
        assert {d.name for d in loaded_user.decks} == {"Deck 1", "Deck 2"}

    @pytest.mark.asyncio
    async def test_user_load_collection_eager(self, db_session: AsyncSession):
        """Test eagerly loading user's card collection.

        Collection can have many entries; eager load is efficient.
        """
        user = await UserFactory.create(db_session)
        user_id = user.id
        await CollectionFactory.create_for_user(db_session, user, card_count=10)
        db_session.expire(user)

        result = await db_session.execute(
            select(User).where(User.id == user_id).options(selectinload(User.collection))
        )
        loaded_user = result.scalar_one()

        assert len(loaded_user.collection) == 10

    @pytest.mark.asyncio
    async def test_user_load_campaign_progress(self, db_session: AsyncSession):
        """Test loading user's campaign progress (one-to-one).

        One-to-one relationships use uselist=False.
        """
        user = await UserFactory.create(db_session)
        user_id = user.id
        await CampaignProgressFactory.create_for_user(db_session, user, mantibucks=500)
        db_session.expire(user)

        result = await db_session.execute(
            select(User).where(User.id == user_id).options(selectinload(User.campaign_progress))
        )
        loaded_user = result.scalar_one()

        assert loaded_user.campaign_progress is not None
        assert loaded_user.campaign_progress.mantibucks == 500

    @pytest.mark.asyncio
    async def test_user_no_campaign_progress(self, db_session: AsyncSession):
        """Test that campaign_progress is None for new users.

        Users start without campaign progress until they begin campaign.
        """
        user = await UserFactory.create(db_session)

        result = await db_session.execute(
            select(User).where(User.id == user.id).options(selectinload(User.campaign_progress))
        )
        loaded_user = result.scalar_one()

        assert loaded_user.campaign_progress is None

    @pytest.mark.asyncio
    async def test_load_multiple_relationships(self, db_session: AsyncSession):
        """Test loading multiple relationships in one query.

        Efficient loading of all user data at once.
        """
        user = await UserFactory.create(db_session)
        user_id = user.id
        await DeckFactory.create_for_user(db_session, user)
        await CollectionFactory.create_for_user(db_session, user, card_count=3)
        await CampaignProgressFactory.create_for_user(db_session, user)
        db_session.expire(user)

        result = await db_session.execute(
            select(User)
            .where(User.id == user_id)
            .options(
                selectinload(User.decks),
                selectinload(User.collection),
                selectinload(User.campaign_progress),
            )
        )
        loaded_user = result.scalar_one()

        assert len(loaded_user.decks) == 1
        assert len(loaded_user.collection) == 3
        assert loaded_user.campaign_progress is not None

    @pytest.mark.asyncio
    async def test_collection_back_populates_user(self, db_session: AsyncSession):
        """Test that Collection.user back-populates correctly.

        The relationship is bidirectional.
        """
        user = await UserFactory.create(db_session)
        entry = await CollectionFactory.create(db_session, user_id=user.id)

        # Refresh to load relationship
        await db_session.refresh(entry, ["user"])

        assert entry.user is not None
        assert entry.user.id == user.id

    @pytest.mark.asyncio
    async def test_deck_back_populates_user(self, db_session: AsyncSession):
        """Test that Deck.user back-populates correctly."""
        user = await UserFactory.create(db_session)
        deck = await DeckFactory.create_for_user(db_session, user)

        await db_session.refresh(deck, ["user"])

        assert deck.user is not None
        assert deck.user.id == user.id


# =============================================================================
# Isolation Verification Tests
# =============================================================================


class TestTestIsolation:
    """Tests to verify that test isolation works correctly.

    These tests create data and verify it doesn't persist to other tests.
    If isolation fails, these tests will fail intermittently.
    """

    @pytest.mark.asyncio
    async def test_isolation_creates_user(self, db_session: AsyncSession):
        """Create a user with a specific email to test isolation.

        This test creates a user that would conflict with the next test
        if isolation wasn't working.
        """
        user = await UserFactory.create(db_session, email="isolation_test@example.com")
        assert user.id is not None

        # Verify we can query it within this test
        result = await db_session.execute(
            select(User).where(User.email == "isolation_test@example.com")
        )
        found = result.scalar_one()
        assert found.id == user.id

    @pytest.mark.asyncio
    async def test_isolation_same_email_works(self, db_session: AsyncSession):
        """Create another user with the same email.

        This would fail with IntegrityError if the previous test's
        data wasn't rolled back. Success proves isolation works.
        """
        # This should work because previous test was rolled back
        user = await UserFactory.create(db_session, email="isolation_test@example.com")
        assert user.id is not None

    @pytest.mark.asyncio
    async def test_isolation_no_leftover_data(self, db_session: AsyncSession):
        """Verify no data from previous tests exists.

        Query for users created by other tests - should find none.
        """
        result = await db_session.execute(
            select(User).where(User.email == "isolation_test@example.com")
        )
        users = result.scalars().all()

        # If isolation works, this is empty or just from this test
        # (which hasn't created one yet)
        assert len(users) == 0
