"""Tests for the CardService.

These tests verify that the CardService correctly loads card definitions
from JSON files and provides efficient lookup and search operations.
"""

import json
import tempfile
from pathlib import Path

import pytest

from app.core.enums import CardType, EnergyType, PokemonStage, PokemonVariant
from app.services.card_service import CardService, SetInfo


@pytest.fixture
def sample_pokemon_card() -> dict:
    """Sample Pokemon card definition for testing."""
    return {
        "id": "test-001-pikachu",
        "name": "Pikachu",
        "card_type": "pokemon",
        "hp": 60,
        "pokemon_type": "lightning",
        "stage": "basic",
        "variant": "normal",
        "set_id": "test",
        "rarity": "common",
        "retreat_cost": 1,
        "attacks": [
            {
                "name": "Thunder Shock",
                "cost": ["lightning"],
                "damage": 20,
                "damage_display": "20",
            }
        ],
        "weakness": {"energy_type": "fighting", "value": 20},
    }


@pytest.fixture
def sample_ex_pokemon_card() -> dict:
    """Sample EX Pokemon card definition for testing."""
    return {
        "id": "test-002-pikachu-ex",
        "name": "Pikachu ex",
        "card_type": "pokemon",
        "hp": 120,
        "pokemon_type": "lightning",
        "stage": "basic",
        "variant": "ex",
        "set_id": "test",
        "rarity": "double rare",
        "retreat_cost": 1,
    }


@pytest.fixture
def sample_trainer_card() -> dict:
    """Sample Trainer card definition for testing."""
    return {
        "id": "test-101-potion",
        "name": "Potion",
        "card_type": "trainer",
        "trainer_type": "item",
        "set_id": "test",
        "rarity": "common",
        "effect_description": "Heal 30 damage from 1 of your Pokemon.",
    }


@pytest.fixture
def sample_energy_card() -> dict:
    """Sample Energy card definition for testing."""
    return {
        "id": "energy-basic-lightning",
        "name": "Lightning Energy",
        "card_type": "energy",
        "energy_type": "lightning",
        "energy_provides": ["lightning"],
        "set_id": "basic",
        "rarity": "common",
    }


@pytest.fixture
def temp_definitions_dir(
    sample_pokemon_card,
    sample_ex_pokemon_card,
    sample_trainer_card,
    sample_energy_card,
) -> Path:
    """Create a temporary definitions directory with sample cards.

    This fixture creates a complete definitions directory structure
    with sample cards for testing CardService loading functionality.
    """
    with tempfile.TemporaryDirectory() as tmpdir:
        root = Path(tmpdir)

        # Create directory structure
        (root / "pokemon" / "test").mkdir(parents=True)
        (root / "trainer" / "test").mkdir(parents=True)
        (root / "energy" / "basic").mkdir(parents=True)

        # Write Pokemon cards
        with open(root / "pokemon" / "test" / "001-pikachu.json", "w") as f:
            json.dump(sample_pokemon_card, f)
        with open(root / "pokemon" / "test" / "002-pikachu-ex.json", "w") as f:
            json.dump(sample_ex_pokemon_card, f)

        # Write Trainer card
        with open(root / "trainer" / "test" / "101-potion.json", "w") as f:
            json.dump(sample_trainer_card, f)

        # Write Energy card
        with open(root / "energy" / "basic" / "lightning.json", "w") as f:
            json.dump(sample_energy_card, f)

        # Write index file
        index = {
            "generated_at": "2026-01-27T00:00:00Z",
            "schema_version": "1.0",
            "total_cards": 4,
            "sets": {
                "test": {"name": "Test Set", "card_count": 3},
                "basic": {"name": "Basic Energy", "card_count": 1},
            },
            "cards": [],
        }
        with open(root / "_index.json", "w") as f:
            json.dump(index, f)

        yield root


class TestCardServiceLoading:
    """Tests for CardService loading functionality."""

    @pytest.mark.asyncio
    async def test_load_all_loads_cards(self, temp_definitions_dir):
        """Test that load_all loads all cards from the definitions directory.

        Verifies that Pokemon, Trainer, and Energy cards are all loaded
        from their respective subdirectories.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        assert service.card_count == 4
        assert service.is_loaded is True

    @pytest.mark.asyncio
    async def test_load_all_creates_indexes(self, temp_definitions_dir):
        """Test that load_all creates all required indexes.

        The service should maintain indexes by type, set, and Pokemon type
        for efficient querying.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        # Check type index
        pokemon = service.search(card_type=CardType.POKEMON)
        assert len(pokemon) == 2

        trainers = service.search(card_type=CardType.TRAINER)
        assert len(trainers) == 1

        energy = service.search(card_type=CardType.ENERGY)
        assert len(energy) == 1

    @pytest.mark.asyncio
    async def test_load_all_idempotent(self, temp_definitions_dir):
        """Test that calling load_all multiple times is safe.

        The service should only load cards once and warn on subsequent calls.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()
        initial_count = service.card_count

        await service.load_all()  # Should be no-op

        assert service.card_count == initial_count

    @pytest.mark.asyncio
    async def test_load_all_missing_directory_raises(self):
        """Test that load_all raises FileNotFoundError for missing directory.

        If the definitions directory doesn't exist, the service should
        fail with a clear error message.
        """
        service = CardService(definitions_dir=Path("/nonexistent/path"))

        with pytest.raises(FileNotFoundError):
            await service.load_all()

    @pytest.mark.asyncio
    async def test_load_all_loads_set_metadata(self, temp_definitions_dir):
        """Test that set metadata is loaded from the index file.

        The service should parse _index.json to get set names and counts.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        sets = service.get_sets()
        assert len(sets) == 2

        test_set = next((s for s in sets if s.code == "test"), None)
        assert test_set is not None
        assert test_set.name == "Test Set"


class TestCardServiceGetCard:
    """Tests for CardService.get_card()."""

    @pytest.mark.asyncio
    async def test_get_card_found(self, temp_definitions_dir):
        """Test getting a card that exists.

        Should return the CardDefinition for the given ID.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        card = service.get_card("test-001-pikachu")

        assert card is not None
        assert card.name == "Pikachu"
        assert card.hp == 60

    @pytest.mark.asyncio
    async def test_get_card_not_found(self, temp_definitions_dir):
        """Test getting a card that doesn't exist.

        Should return None rather than raising an exception.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        card = service.get_card("nonexistent-card")

        assert card is None


class TestCardServiceGetAllCards:
    """Tests for CardService.get_all_cards()."""

    @pytest.mark.asyncio
    async def test_get_all_cards_returns_registry(self, temp_definitions_dir):
        """Test that get_all_cards returns a complete card registry.

        The returned dict should be suitable for passing to GameEngine.create_game().
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        registry = service.get_all_cards()

        assert len(registry) == 4
        assert "test-001-pikachu" in registry
        assert "energy-basic-lightning" in registry

    @pytest.mark.asyncio
    async def test_get_all_cards_returns_copy(self, temp_definitions_dir):
        """Test that get_all_cards returns a copy, not the internal dict.

        Modifying the returned dict should not affect the service's state.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        registry = service.get_all_cards()
        registry["new-card"] = None  # Try to modify

        # Service should be unaffected
        assert service.get_card("new-card") is None
        assert service.card_count == 4


class TestCardServiceGetCardsByIds:
    """Tests for CardService.get_cards_by_ids()."""

    @pytest.mark.asyncio
    async def test_get_cards_by_ids_all_found(self, temp_definitions_dir):
        """Test getting multiple cards by ID when all exist.

        Should return cards in the same order as the input IDs.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        ids = ["test-001-pikachu", "energy-basic-lightning"]
        cards = service.get_cards_by_ids(ids)

        assert len(cards) == 2
        assert cards[0].id == "test-001-pikachu"
        assert cards[1].id == "energy-basic-lightning"

    @pytest.mark.asyncio
    async def test_get_cards_by_ids_missing_raises(self, temp_definitions_dir):
        """Test that missing card IDs raise KeyError.

        If any requested ID doesn't exist, the method should fail
        rather than return partial results.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        with pytest.raises(KeyError, match="nonexistent"):
            service.get_cards_by_ids(["test-001-pikachu", "nonexistent"])


class TestCardServiceSearch:
    """Tests for CardService.search()."""

    @pytest.mark.asyncio
    async def test_search_by_name(self, temp_definitions_dir):
        """Test searching cards by name substring.

        Should match cards whose name contains the search string (case-insensitive).
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        results = service.search(name="pika")

        assert len(results) == 2  # Pikachu and Pikachu ex

    @pytest.mark.asyncio
    async def test_search_by_card_type(self, temp_definitions_dir):
        """Test searching cards by type (Pokemon, Trainer, Energy).

        Uses the type index for efficient lookup.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        pokemon = service.search(card_type=CardType.POKEMON)
        trainers = service.search(card_type=CardType.TRAINER)
        energy = service.search(card_type=CardType.ENERGY)

        assert len(pokemon) == 2
        assert len(trainers) == 1
        assert len(energy) == 1

    @pytest.mark.asyncio
    async def test_search_by_pokemon_type(self, temp_definitions_dir):
        """Test searching Pokemon by their energy type.

        Uses the pokemon_type index for efficient lookup.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        lightning_pokemon = service.search(pokemon_type=EnergyType.LIGHTNING)
        fire_pokemon = service.search(pokemon_type=EnergyType.FIRE)

        assert len(lightning_pokemon) == 2
        assert len(fire_pokemon) == 0

    @pytest.mark.asyncio
    async def test_search_by_set(self, temp_definitions_dir):
        """Test searching cards by set ID.

        Uses the set index for efficient lookup.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        test_cards = service.search(set_id="test")
        basic_cards = service.search(set_id="basic")

        assert len(test_cards) == 3  # 2 Pokemon + 1 Trainer
        assert len(basic_cards) == 1  # 1 Energy

    @pytest.mark.asyncio
    async def test_search_by_stage(self, temp_definitions_dir):
        """Test searching Pokemon by evolution stage."""
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        basics = service.search(stage=PokemonStage.BASIC)

        assert len(basics) == 2  # Both Pikachus are basic

    @pytest.mark.asyncio
    async def test_search_by_variant(self, temp_definitions_dir):
        """Test searching Pokemon by variant (normal, ex, etc.).

        Note: variant defaults to 'normal' for non-Pokemon cards too,
        so we combine with card_type filter for accurate Pokemon counts.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        normal_pokemon = service.search(
            card_type=CardType.POKEMON,
            variant=PokemonVariant.NORMAL,
        )
        ex_pokemon = service.search(
            card_type=CardType.POKEMON,
            variant=PokemonVariant.EX,
        )

        assert len(normal_pokemon) == 1  # Regular Pikachu
        assert len(ex_pokemon) == 1  # Pikachu ex

    @pytest.mark.asyncio
    async def test_search_multiple_criteria(self, temp_definitions_dir):
        """Test searching with multiple criteria (AND logic).

        All criteria must match for a card to be included in results.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        results = service.search(
            card_type=CardType.POKEMON,
            pokemon_type=EnergyType.LIGHTNING,
            variant=PokemonVariant.EX,
        )

        assert len(results) == 1
        assert results[0].name == "Pikachu ex"

    @pytest.mark.asyncio
    async def test_search_no_results(self, temp_definitions_dir):
        """Test that search returns empty list when no cards match."""
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        results = service.search(pokemon_type=EnergyType.FIRE)

        assert results == []


class TestCardServiceGetSetCards:
    """Tests for CardService.get_set_cards()."""

    @pytest.mark.asyncio
    async def test_get_set_cards(self, temp_definitions_dir):
        """Test getting all cards from a specific set.

        Returns all cards where set_id matches, regardless of card type.
        """
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        cards = service.get_set_cards("test")

        assert len(cards) == 3
        card_types = {c.card_type for c in cards}
        assert CardType.POKEMON in card_types
        assert CardType.TRAINER in card_types

    @pytest.mark.asyncio
    async def test_get_set_cards_nonexistent_set(self, temp_definitions_dir):
        """Test that nonexistent set returns empty list."""
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        cards = service.get_set_cards("nonexistent")

        assert cards == []


class TestCardServiceGetSets:
    """Tests for CardService.get_sets()."""

    @pytest.mark.asyncio
    async def test_get_sets_returns_set_info(self, temp_definitions_dir):
        """Test that get_sets returns SetInfo objects with correct data."""
        service = CardService(definitions_dir=temp_definitions_dir)
        await service.load_all()

        sets = service.get_sets()

        assert len(sets) == 2
        assert all(isinstance(s, SetInfo) for s in sets)

        test_set = next((s for s in sets if s.code == "test"), None)
        assert test_set.name == "Test Set"
        assert test_set.card_count == 3


class TestCardServiceWithRealData:
    """Tests that run against the actual definitions directory.

    These tests verify that the CardService works with the real
    card definitions generated by the converter script.
    """

    @pytest.mark.asyncio
    async def test_load_real_definitions(self):
        """Test loading the actual card definitions.

        This test uses the real data/definitions/ directory to verify
        that all cards load correctly in a realistic scenario.
        """
        service = CardService()  # Uses default path

        # Only run if definitions exist
        if not service._definitions_dir.exists():
            pytest.skip("Definitions directory not found")

        await service.load_all()

        # Should have loaded cards
        assert service.card_count > 0

        # Should have all card types
        pokemon = service.search(card_type=CardType.POKEMON)
        energy = service.search(card_type=CardType.ENERGY)

        assert len(pokemon) > 0
        assert len(energy) == 10  # Basic energy

    @pytest.mark.asyncio
    async def test_real_card_lookup(self):
        """Test looking up a known card from real data."""
        service = CardService()

        if not service._definitions_dir.exists():
            pytest.skip("Definitions directory not found")

        await service.load_all()

        # Try to find Bulbasaur (should exist in a1)
        bulbasaur = service.get_card("a1-001-bulbasaur")

        if bulbasaur:
            assert bulbasaur.name == "Bulbasaur"
            assert bulbasaur.card_type == CardType.POKEMON
            assert bulbasaur.pokemon_type == EnergyType.GRASS
