"""Integration tests for CollectionService.

Tests collection management operations with real PostgreSQL database.
Uses dev containers (docker compose up -d) for database access.

These tests verify:
- Getting user collections
- Adding cards with upsert behavior
- Removing cards with quantity management
- Ownership checking for deck validation
- Starter deck card granting
"""

import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession

from app.db.models.collection import CardSource
from app.repositories.postgres.collection import PostgresCollectionRepository
from app.services.card_service import CardService
from app.services.collection_service import CollectionService
from tests.factories import UserFactory

# =============================================================================
# Test card IDs - real cards from the loaded data
# =============================================================================

# These are real card IDs from the a1 set that exist in the data
TEST_CARD_1 = "a1-001-bulbasaur"
TEST_CARD_2 = "a1-002-ivysaur"
TEST_CARD_3 = "a1-033-charmander"


# =============================================================================
# Fixtures
# =============================================================================


@pytest_asyncio.fixture
async def card_service() -> CardService:
    """Create a CardService with real card data loaded.

    Returns a CardService with actual card definitions from data/definitions/.
    This is necessary because CollectionService validates card IDs.
    """
    service = CardService()
    await service.load_all()
    return service


@pytest.fixture
def collection_service(db_session: AsyncSession, card_service: CardService) -> CollectionService:
    """Create a CollectionService with PostgreSQL repository."""
    repo = PostgresCollectionRepository(db_session)
    return CollectionService(repo, card_service)


# =============================================================================
# Get Collection Tests
# =============================================================================


class TestGetCollection:
    """Tests for retrieving user collections."""

    @pytest.mark.asyncio
    async def test_get_collection_empty_for_new_user(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that a new user has an empty collection.

        New users should start with no cards until they select a starter deck
        or receive cards through other means.
        """
        user = await UserFactory.create(db_session)

        collection = await collection_service.get_collection(user.id)

        assert collection == []

    @pytest.mark.asyncio
    async def test_get_collection_returns_all_cards(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that get_collection returns all cards owned by user.

        Each unique card should appear once with its quantity.
        """
        user = await UserFactory.create(db_session)

        # Add multiple different cards
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 3, CardSource.BOOSTER)
        await collection_service.add_cards(user.id, "a1-002-ivysaur", 1, CardSource.REWARD)
        await collection_service.add_cards(user.id, "a1-033-charmander", 4, CardSource.STARTER)

        collection = await collection_service.get_collection(user.id)

        assert len(collection) == 3
        card_ids = {entry.card_definition_id for entry in collection}
        assert card_ids == {"a1-001-bulbasaur", "a1-002-ivysaur", "a1-033-charmander"}


# =============================================================================
# Add Cards Tests
# =============================================================================


class TestAddCards:
    """Tests for adding cards to collections."""

    @pytest.mark.asyncio
    async def test_add_cards_creates_new_entry(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that adding a card creates a new collection entry.

        The entry should have the correct card ID, quantity, and source.
        """
        user = await UserFactory.create(db_session)

        entry = await collection_service.add_cards(
            user.id, "a1-001-bulbasaur", 2, CardSource.BOOSTER
        )

        assert entry.card_definition_id == "a1-001-bulbasaur"
        assert entry.quantity == 2
        assert entry.source == CardSource.BOOSTER
        assert entry.user_id == user.id

    @pytest.mark.asyncio
    async def test_add_cards_increments_existing_quantity(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that adding more of an existing card increases quantity.

        The upsert pattern should increment quantity rather than creating
        duplicate entries.
        """
        user = await UserFactory.create(db_session)

        # Add initial cards
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 2, CardSource.BOOSTER)

        # Add more of the same card
        entry = await collection_service.add_cards(
            user.id, "a1-001-bulbasaur", 3, CardSource.REWARD
        )

        assert entry.quantity == 5
        # Source should remain the original source
        assert entry.source == CardSource.BOOSTER

    @pytest.mark.asyncio
    async def test_add_cards_single_card(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test adding a single card (quantity=1).

        This is the common case for single card rewards.
        """
        user = await UserFactory.create(db_session)

        entry = await collection_service.add_cards(user.id, "a1-001-bulbasaur", 1, CardSource.GIFT)

        assert entry.quantity == 1


# =============================================================================
# Remove Cards Tests
# =============================================================================


class TestRemoveCards:
    """Tests for removing cards from collections."""

    @pytest.mark.asyncio
    async def test_remove_cards_decrements_quantity(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that removing cards decreases quantity.

        Should retain the entry with reduced quantity.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 5, CardSource.BOOSTER)

        entry = await collection_service.remove_cards(user.id, "a1-001-bulbasaur", 2)

        assert entry is not None
        assert entry.quantity == 3

    @pytest.mark.asyncio
    async def test_remove_cards_deletes_entry_when_quantity_zero(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that removing all copies deletes the collection entry.

        The card should no longer appear in the user's collection.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 3, CardSource.BOOSTER)

        result = await collection_service.remove_cards(user.id, "a1-001-bulbasaur", 3)

        assert result is None  # Entry was deleted

        # Verify it's gone from collection
        quantity = await collection_service.get_card_quantity(user.id, "a1-001-bulbasaur")
        assert quantity == 0

    @pytest.mark.asyncio
    async def test_remove_cards_returns_none_if_not_owned(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that removing unowned cards returns None.

        Should not raise an error, just return None.
        """
        user = await UserFactory.create(db_session)

        result = await collection_service.remove_cards(user.id, "nonexistent-card", 1)

        assert result is None

    @pytest.mark.asyncio
    async def test_remove_cards_clamps_to_zero(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that removing more cards than owned removes all and deletes entry.

        Should not go negative - just remove all and delete the entry.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 2, CardSource.BOOSTER)

        result = await collection_service.remove_cards(user.id, "a1-001-bulbasaur", 10)

        assert result is None  # Entry deleted

        quantity = await collection_service.get_card_quantity(user.id, "a1-001-bulbasaur")
        assert quantity == 0


# =============================================================================
# Card Quantity Tests
# =============================================================================


class TestGetCardQuantity:
    """Tests for checking individual card quantities."""

    @pytest.mark.asyncio
    async def test_get_card_quantity_returns_owned_amount(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that get_card_quantity returns correct quantity.

        Should return the exact number of copies owned.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 4, CardSource.BOOSTER)

        quantity = await collection_service.get_card_quantity(user.id, "a1-001-bulbasaur")

        assert quantity == 4

    @pytest.mark.asyncio
    async def test_get_card_quantity_returns_zero_if_not_owned(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that get_card_quantity returns 0 for unowned cards.

        Should not raise an error, just return 0.
        """
        user = await UserFactory.create(db_session)

        quantity = await collection_service.get_card_quantity(user.id, "nonexistent-card")

        assert quantity == 0


# =============================================================================
# Ownership Check Tests
# =============================================================================


class TestHasCards:
    """Tests for ownership validation (deck building support)."""

    @pytest.mark.asyncio
    async def test_has_cards_returns_true_when_owned(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that has_cards returns True when user owns enough cards.

        Should verify all required cards are owned in sufficient quantity.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 4, CardSource.BOOSTER)
        await collection_service.add_cards(user.id, "a1-002-ivysaur", 2, CardSource.BOOSTER)

        has_all = await collection_service.has_cards(
            user.id, {"a1-001-bulbasaur": 3, "a1-002-ivysaur": 2}
        )

        assert has_all is True

    @pytest.mark.asyncio
    async def test_has_cards_returns_false_when_insufficient(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that has_cards returns False when user doesn't own enough.

        Should fail if any single card is insufficient.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 2, CardSource.BOOSTER)

        has_all = await collection_service.has_cards(user.id, {"a1-001-bulbasaur": 4})

        assert has_all is False

    @pytest.mark.asyncio
    async def test_has_cards_returns_false_for_unowned_card(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that has_cards returns False for cards not in collection.

        Should fail if any required card is completely missing.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 4, CardSource.BOOSTER)

        has_all = await collection_service.has_cards(
            user.id, {"a1-001-bulbasaur": 2, "a1-053-squirtle": 1}  # squirtle not owned
        )

        assert has_all is False

    @pytest.mark.asyncio
    async def test_has_cards_empty_requirements_returns_true(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that has_cards returns True for empty requirements.

        Edge case: no cards required means user "has" all of them.
        """
        user = await UserFactory.create(db_session)

        has_all = await collection_service.has_cards(user.id, {})

        assert has_all is True


# =============================================================================
# Owned Cards Dict Tests
# =============================================================================


class TestGetOwnedCardsDict:
    """Tests for getting collection as dictionary (for deck validation)."""

    @pytest.mark.asyncio
    async def test_get_owned_cards_dict_returns_mapping(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that get_owned_cards_dict returns correct card->quantity mapping.

        This is used by deck validation to check ownership efficiently.
        """
        user = await UserFactory.create(db_session)
        await collection_service.add_cards(user.id, "a1-001-bulbasaur", 4, CardSource.BOOSTER)
        await collection_service.add_cards(user.id, "a1-002-ivysaur", 2, CardSource.REWARD)

        owned = await collection_service.get_owned_cards_dict(user.id)

        assert owned == {"a1-001-bulbasaur": 4, "a1-002-ivysaur": 2}

    @pytest.mark.asyncio
    async def test_get_owned_cards_dict_empty_for_new_user(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that get_owned_cards_dict returns empty dict for new user.

        New users have no cards.
        """
        user = await UserFactory.create(db_session)

        owned = await collection_service.get_owned_cards_dict(user.id)

        assert owned == {}


# =============================================================================
# Starter Deck Tests
# =============================================================================


class TestGrantStarterDeck:
    """Tests for starter deck card granting."""

    @pytest.mark.asyncio
    async def test_grant_starter_deck_adds_cards(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that grant_starter_deck adds all starter deck cards.

        Should add all cards from the specified starter deck to collection
        with STARTER source.
        """
        user = await UserFactory.create(db_session)

        entries = await collection_service.grant_starter_deck(user.id, "grass")

        assert len(entries) > 0
        # All entries should have STARTER source
        assert all(entry.source == CardSource.STARTER for entry in entries)

    @pytest.mark.asyncio
    async def test_grant_starter_deck_invalid_type_raises(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that grant_starter_deck raises ValueError for invalid type.

        Only valid starter types should be accepted.
        """
        user = await UserFactory.create(db_session)

        with pytest.raises(ValueError, match="Invalid starter type"):
            await collection_service.grant_starter_deck(user.id, "invalid_type")

    @pytest.mark.asyncio
    async def test_grant_starter_deck_all_types(
        self, db_session: AsyncSession, collection_service: CollectionService
    ):
        """
        Test that all starter deck types can be granted.

        Verifies all 5 starter types: grass, fire, water, psychic, lightning.
        """
        starter_types = ["grass", "fire", "water", "psychic", "lightning"]

        for starter_type in starter_types:
            user = await UserFactory.create(db_session)
            entries = await collection_service.grant_starter_deck(user.id, starter_type)
            assert len(entries) > 0, f"Starter type {starter_type} should add cards"
