"""Tests for the evolution stack and attached cards mechanics.

This module tests the new CardInstance fields and their integration:
- cards_underneath: Evolution stack tracking (Basic -> Stage 1 -> Stage 2)
- attached_energy: Energy cards as CardInstance objects stored on Pokemon
- attached_tools: Tool cards as CardInstance objects stored on Pokemon

Also tests the devolve effect handler and knockout processing with attachments.

Test categories:
    - Evolution stack building (evolve mechanics)
    - Energy/tool attachment and transfer during evolution
    - Damage carryover on evolution
    - Devolve effect handler
    - Knockout processing with attached cards
    - find_card_instance for cards in attachments/stack
"""

import pytest

# Import handlers to register them
import app.core.effects.handlers  # noqa: F401
from app.core.config import RulesConfig
from app.core.effects.base import EffectContext
from app.core.effects.registry import resolve_effect
from app.core.engine import GameEngine
from app.core.enums import (
    CardType,
    EnergyType,
    PokemonStage,
    PokemonVariant,
    StatusCondition,
    TrainerType,
    TurnPhase,
)
from app.core.models.actions import EvolvePokemonAction
from app.core.models.card import Attack, CardDefinition, CardInstance
from app.core.models.game_state import GameState, PlayerState
from app.core.rng import SeededRandom
from app.core.turn_manager import TurnManager

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


@pytest.fixture
def basic_pokemon_def() -> CardDefinition:
    """Basic Pokemon (Charmander) - can evolve to Stage 1."""
    return CardDefinition(
        id="charmander-001",
        name="Charmander",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        variant=PokemonVariant.NORMAL,
        hp=50,
        pokemon_type=EnergyType.FIRE,
        retreat_cost=1,
    )


@pytest.fixture
def stage1_pokemon_def() -> CardDefinition:
    """Stage 1 Pokemon (Charmeleon) - evolves from Basic, can evolve to Stage 2."""
    return CardDefinition(
        id="charmeleon-001",
        name="Charmeleon",
        card_type=CardType.POKEMON,
        stage=PokemonStage.STAGE_1,
        variant=PokemonVariant.NORMAL,
        evolves_from="Charmander",  # Uses name, not ID
        hp=80,
        pokemon_type=EnergyType.FIRE,
        retreat_cost=2,
        attacks=[
            Attack(
                name="Slash",
                damage=30,
                cost=[EnergyType.FIRE],
            ),
        ],
    )


@pytest.fixture
def stage2_pokemon_def() -> CardDefinition:
    """Stage 2 Pokemon (Charizard) - evolves from Stage 1."""
    return CardDefinition(
        id="charizard-001",
        name="Charizard",
        card_type=CardType.POKEMON,
        stage=PokemonStage.STAGE_2,
        variant=PokemonVariant.NORMAL,
        evolves_from="Charmeleon",  # Uses name, not ID
        hp=150,
        pokemon_type=EnergyType.FIRE,
        retreat_cost=3,
        attacks=[
            Attack(
                name="Fire Spin",
                damage=120,
                cost=[EnergyType.FIRE, EnergyType.FIRE, EnergyType.FIRE],
            ),
        ],
    )


@pytest.fixture
def fire_energy_def() -> CardDefinition:
    """Fire energy card definition."""
    return CardDefinition(
        id="fire-energy-001",
        name="Fire Energy",
        card_type=CardType.ENERGY,
        energy_type=EnergyType.FIRE,
        energy_provides=[EnergyType.FIRE],
    )


@pytest.fixture
def tool_def() -> CardDefinition:
    """Pokemon Tool card definition."""
    return CardDefinition(
        id="choice-band-001",
        name="Choice Band",
        card_type=CardType.TRAINER,
        trainer_type=TrainerType.TOOL,
        effect_id="modify_damage",
        effect_params={"amount": 30, "condition": "vs_gx_ex"},
        effect_description="+30 damage against GX/EX",
    )


@pytest.fixture
def game_state(
    basic_pokemon_def: CardDefinition,
    stage1_pokemon_def: CardDefinition,
    stage2_pokemon_def: CardDefinition,
    fire_energy_def: CardDefinition,
    tool_def: CardDefinition,
) -> GameState:
    """Create a game state set up for evolution testing.

    Player 1 has:
    - Active: Charmander (Basic, played last turn)
    - Hand: Charmeleon, Charizard, 2x Fire Energy
    - Deck: 5 cards

    Player 2 has:
    - Active: Charmander
    """
    game = GameState(
        game_id="test-evolution",
        rules=RulesConfig(),
        card_registry={
            basic_pokemon_def.id: basic_pokemon_def,
            stage1_pokemon_def.id: stage1_pokemon_def,
            stage2_pokemon_def.id: stage2_pokemon_def,
            fire_energy_def.id: fire_energy_def,
            tool_def.id: tool_def,
        },
        players={
            "player1": PlayerState(player_id="player1"),
            "player2": PlayerState(player_id="player2"),
        },
        turn_order=["player1", "player2"],
        current_player_id="player1",
        turn_number=2,  # Turn 2 so evolution is allowed
        phase=TurnPhase.MAIN,
        first_turn_completed=True,
    )

    # Player 1 setup
    p1 = game.players["player1"]

    # Active Charmander (played on turn 1)
    charmander = CardInstance(
        instance_id="p1-charmander",
        definition_id=basic_pokemon_def.id,
        turn_played=1,
    )
    p1.active.add(charmander)

    # Evolution cards in hand
    charmeleon = CardInstance(instance_id="p1-charmeleon", definition_id=stage1_pokemon_def.id)
    charizard = CardInstance(instance_id="p1-charizard", definition_id=stage2_pokemon_def.id)
    p1.hand.add(charmeleon)
    p1.hand.add(charizard)

    # Energy cards in hand
    for i in range(2):
        energy = CardInstance(instance_id=f"p1-energy-{i}", definition_id=fire_energy_def.id)
        p1.hand.add(energy)

    # Tool in hand
    tool = CardInstance(instance_id="p1-tool", definition_id=tool_def.id)
    p1.hand.add(tool)

    # Deck
    for i in range(5):
        card = CardInstance(instance_id=f"p1-deck-{i}", definition_id=basic_pokemon_def.id)
        p1.deck.add(card)

    # Player 2 setup - simple active
    p2 = game.players["player2"]
    p2_charmander = CardInstance(
        instance_id="p2-charmander",
        definition_id=basic_pokemon_def.id,
        turn_played=1,
    )
    p2.active.add(p2_charmander)

    for i in range(5):
        card = CardInstance(instance_id=f"p2-deck-{i}", definition_id=basic_pokemon_def.id)
        p2.deck.add(card)

    return game


@pytest.fixture
def seeded_rng() -> SeededRandom:
    """Create a seeded RNG for deterministic tests."""
    return SeededRandom(seed=42)


@pytest.fixture
def engine(seeded_rng: SeededRandom) -> GameEngine:
    """Create a game engine with seeded RNG."""
    return GameEngine(rng=seeded_rng)


# =============================================================================
# Evolution Stack Tests
# =============================================================================


class TestEvolutionStack:
    """Tests for the evolution stack (cards_underneath) mechanics."""

    @pytest.mark.asyncio
    async def test_basic_to_stage1_creates_stack(
        self, engine: GameEngine, game_state: GameState
    ) -> None:
        """
        Test that evolving Basic to Stage 1 creates correct evolution stack.

        When Charmander evolves to Charmeleon, the Charmander CardInstance
        should be stored in Charmeleon's cards_underneath list.
        """
        action = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )

        result = await engine.execute_action(game_state, "player1", action)

        assert result.success, f"Evolution failed: {result.message}"

        # Get the evolved Pokemon (now Charmeleon)
        active = game_state.players["player1"].get_active_pokemon()
        assert active is not None
        assert active.definition_id == "charmeleon-001"
        assert active.instance_id == "p1-charmeleon"

        # Check evolution stack contains the Basic
        assert len(active.cards_underneath) == 1
        basic_in_stack = active.cards_underneath[0]
        assert basic_in_stack.instance_id == "p1-charmander"
        assert basic_in_stack.definition_id == "charmander-001"

    @pytest.mark.asyncio
    async def test_stage1_to_stage2_preserves_full_stack(
        self, engine: GameEngine, game_state: GameState
    ) -> None:
        """
        Test that evolving Stage 1 to Stage 2 preserves full evolution stack.

        After Charmander -> Charmeleon -> Charizard, the Charizard should have
        both Charmander and Charmeleon in its cards_underneath list in order.
        """
        # First evolution: Basic -> Stage 1
        action1 = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )
        result1 = await engine.execute_action(game_state, "player1", action1)
        assert result1.success

        # Advance turn to allow second evolution (can't evolve same turn)
        game_state.turn_number = 3
        charmeleon = game_state.players["player1"].get_active_pokemon()
        charmeleon.turn_played = 2  # Played last turn

        # Second evolution: Stage 1 -> Stage 2
        action2 = EvolvePokemonAction(
            evolution_card_id="p1-charizard",
            target_pokemon_id="p1-charmeleon",
        )
        result2 = await engine.execute_action(game_state, "player1", action2)
        assert result2.success, f"Second evolution failed: {result2.message}"

        # Get the final evolved Pokemon (Charizard)
        active = game_state.players["player1"].get_active_pokemon()
        assert active is not None
        assert active.definition_id == "charizard-001"

        # Check evolution stack has both previous stages
        assert len(active.cards_underneath) == 2

        # Index 0 should be the oldest (Basic)
        assert active.cards_underneath[0].instance_id == "p1-charmander"
        assert active.cards_underneath[0].definition_id == "charmander-001"

        # Index 1 should be the more recent (Stage 1)
        assert active.cards_underneath[1].instance_id == "p1-charmeleon"
        assert active.cards_underneath[1].definition_id == "charmeleon-001"

    @pytest.mark.asyncio
    async def test_energy_transfers_on_evolution(
        self, engine: GameEngine, game_state: GameState, fire_energy_def: CardDefinition
    ) -> None:
        """
        Test that attached energy transfers to the evolved Pokemon.

        Energy attached to Charmander should transfer to Charmeleon when
        Charmander evolves. The energy CardInstance objects should be preserved.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Attach energy to Charmander before evolution
        energy1 = CardInstance(instance_id="attached-energy-1", definition_id=fire_energy_def.id)
        energy2 = CardInstance(instance_id="attached-energy-2", definition_id=fire_energy_def.id)
        charmander.attach_energy(energy1)
        charmander.attach_energy(energy2)

        assert len(charmander.attached_energy) == 2

        # Evolve
        action = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )
        result = await engine.execute_action(game_state, "player1", action)
        assert result.success

        # Get evolved Pokemon
        charmeleon = p1.get_active_pokemon()

        # Energy should have transferred
        assert len(charmeleon.attached_energy) == 2
        energy_ids = [e.instance_id for e in charmeleon.attached_energy]
        assert "attached-energy-1" in energy_ids
        assert "attached-energy-2" in energy_ids

    @pytest.mark.asyncio
    async def test_tools_transfer_on_evolution(
        self, engine: GameEngine, game_state: GameState, tool_def: CardDefinition
    ) -> None:
        """
        Test that attached tools transfer to the evolved Pokemon.

        Tools attached to the Basic should transfer to the evolved Pokemon.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Attach tool to Charmander
        tool = CardInstance(instance_id="attached-tool", definition_id=tool_def.id)
        charmander.attach_tool(tool)

        assert len(charmander.attached_tools) == 1

        # Evolve
        action = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )
        result = await engine.execute_action(game_state, "player1", action)
        assert result.success

        # Get evolved Pokemon
        charmeleon = p1.get_active_pokemon()

        # Tool should have transferred
        assert len(charmeleon.attached_tools) == 1
        assert charmeleon.attached_tools[0].instance_id == "attached-tool"

    @pytest.mark.asyncio
    async def test_damage_carries_over_on_evolution(
        self, engine: GameEngine, game_state: GameState
    ) -> None:
        """
        Test that damage carries over to the evolved Pokemon.

        If Charmander has 30 damage and evolves to Charmeleon (80 HP),
        Charmeleon should still have 30 damage.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Deal damage to Charmander
        charmander.damage = 30

        # Evolve
        action = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )
        result = await engine.execute_action(game_state, "player1", action)
        assert result.success

        # Get evolved Pokemon
        charmeleon = p1.get_active_pokemon()

        # Damage should have carried over
        assert charmeleon.damage == 30

    @pytest.mark.asyncio
    async def test_status_conditions_clear_on_evolution(
        self, engine: GameEngine, game_state: GameState
    ) -> None:
        """
        Test that status conditions are cleared when a Pokemon evolves.

        This is standard Pokemon TCG behavior - evolution removes status.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Apply status conditions
        charmander.add_status(StatusCondition.POISONED)
        charmander.add_status(StatusCondition.CONFUSED)

        assert len(charmander.status_conditions) == 2

        # Evolve
        action = EvolvePokemonAction(
            evolution_card_id="p1-charmeleon",
            target_pokemon_id="p1-charmander",
        )
        result = await engine.execute_action(game_state, "player1", action)
        assert result.success

        # Get evolved Pokemon
        charmeleon = p1.get_active_pokemon()

        # Status should be cleared
        assert len(charmeleon.status_conditions) == 0


# =============================================================================
# Devolve Effect Tests
# =============================================================================


class TestDevolveEffect:
    """Tests for the devolve effect handler."""

    @pytest.fixture(autouse=True)
    def ensure_handlers_registered(self) -> None:
        """Ensure effect handlers are registered before each test.

        This is needed because test_registry.py clears the registry for its tests,
        and pytest may run our tests after that cleanup without re-importing.
        """
        from app.core.effects.registry import list_effects

        # Only re-import if registry was cleared (devolve not present)
        if "devolve" not in list_effects():
            import importlib

            import app.core.effects.handlers

            importlib.reload(app.core.effects.handlers)

    @pytest.fixture
    def evolved_game_state(
        self,
        basic_pokemon_def: CardDefinition,
        stage1_pokemon_def: CardDefinition,
        stage2_pokemon_def: CardDefinition,
        fire_energy_def: CardDefinition,
    ) -> GameState:
        """Create a game state with an already-evolved Pokemon for devolve testing.

        Player 1 has Charizard active with full evolution stack and attachments.
        """
        game = GameState(
            game_id="test-devolve",
            rules=RulesConfig(),
            card_registry={
                basic_pokemon_def.id: basic_pokemon_def,
                stage1_pokemon_def.id: stage1_pokemon_def,
                stage2_pokemon_def.id: stage2_pokemon_def,
                fire_energy_def.id: fire_energy_def,
            },
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player2",  # Opponent's turn (for devolve effect)
            turn_number=5,
            phase=TurnPhase.MAIN,
        )

        p1 = game.players["player1"]

        # Build the evolution stack manually for testing
        charmander = CardInstance(
            instance_id="p1-charmander",
            definition_id=basic_pokemon_def.id,
            turn_played=1,
        )
        charmeleon = CardInstance(
            instance_id="p1-charmeleon",
            definition_id=stage1_pokemon_def.id,
            turn_played=2,
        )
        charizard = CardInstance(
            instance_id="p1-charizard",
            definition_id=stage2_pokemon_def.id,
            turn_played=3,
            # Evolution stack: [Charmander, Charmeleon]
            cards_underneath=[charmander, charmeleon],
        )

        # Attach energy to Charizard
        for i in range(3):
            energy = CardInstance(
                instance_id=f"charizard-energy-{i}", definition_id=fire_energy_def.id
            )
            charizard.attach_energy(energy)

        # Add some damage
        charizard.damage = 40

        p1.active.add(charizard)

        # Add cards to deck
        for i in range(5):
            card = CardInstance(instance_id=f"p1-deck-{i}", definition_id=basic_pokemon_def.id)
            p1.deck.add(card)

        # Player 2 setup
        p2 = game.players["player2"]
        p2_pokemon = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
        p2.active.add(p2_pokemon)

        for i in range(5):
            card = CardInstance(instance_id=f"p2-deck-{i}", definition_id=basic_pokemon_def.id)
            p2.deck.add(card)

        return game

    def test_devolve_stage2_to_stage1(
        self, evolved_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test devolving a Stage 2 back to Stage 1 (single stage).

        Charizard should become Charmeleon, and Charizard goes to hand.
        Energy, damage, and remaining stack should transfer.
        """
        ctx = EffectContext(
            game=evolved_game_state,
            source_player_id="player2",
            source_card_id="p2-active",
            params={"stages": 1, "destination": "hand"},
            target_card_id="p1-charizard",
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)

        assert result.success
        assert "Devolved 1 stage" in result.message

        # Check the active is now Charmeleon
        active = evolved_game_state.players["player1"].get_active_pokemon()
        assert active is not None
        assert active.definition_id == "charmeleon-001"
        assert active.instance_id == "p1-charmeleon"

        # Check Charizard went to hand
        p1_hand = evolved_game_state.players["player1"].hand
        assert "p1-charizard" in p1_hand

        # Check energy transferred to Charmeleon
        assert len(active.attached_energy) == 3

        # Check damage transferred
        assert active.damage == 40

        # Check remaining evolution stack (just Charmander now)
        assert len(active.cards_underneath) == 1
        assert active.cards_underneath[0].instance_id == "p1-charmander"

    def test_devolve_stage2_to_basic(
        self, evolved_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test devolving a Stage 2 back to Basic (two stages).

        Charizard should become Charmander, and both Charizard and
        Charmeleon go to hand.
        """
        ctx = EffectContext(
            game=evolved_game_state,
            source_player_id="player2",
            source_card_id="p2-active",
            params={"stages": 2, "destination": "hand"},
            target_card_id="p1-charizard",
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)

        assert result.success
        assert "Devolved 2 stage" in result.message

        # Check the active is now Charmander (Basic)
        active = evolved_game_state.players["player1"].get_active_pokemon()
        assert active is not None
        assert active.definition_id == "charmander-001"
        assert active.instance_id == "p1-charmander"

        # Check both Charizard and Charmeleon went to hand
        p1_hand = evolved_game_state.players["player1"].hand
        assert "p1-charizard" in p1_hand
        assert "p1-charmeleon" in p1_hand

        # Check energy still attached
        assert len(active.attached_energy) == 3

        # Check evolution stack is now empty
        assert len(active.cards_underneath) == 0

    def test_devolve_to_discard(
        self, evolved_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test devolving with destination set to discard pile.

        The removed evolution cards should go to the discard pile.
        """
        ctx = EffectContext(
            game=evolved_game_state,
            source_player_id="player2",
            source_card_id="p2-active",
            params={"stages": 1, "destination": "discard"},
            target_card_id="p1-charizard",
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)

        assert result.success

        # Check Charizard went to discard (not hand)
        p1 = evolved_game_state.players["player1"]
        assert "p1-charizard" not in p1.hand
        assert "p1-charizard" in p1.discard

    def test_devolve_triggers_knockout_if_damage_exceeds_hp(
        self, evolved_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that devolving can cause a knockout if damage exceeds new HP.

        If Charizard (150 HP) has 60 damage and devolves to Charmander (50 HP),
        the damage exceeds HP and should flag as knockout.
        """
        # Set high damage that exceeds Charmander's HP
        p1 = evolved_game_state.players["player1"]
        charizard = p1.get_active_pokemon()
        charizard.damage = 60  # Exceeds Charmander's 50 HP

        ctx = EffectContext(
            game=evolved_game_state,
            source_player_id="player2",
            source_card_id="p2-active",
            params={"stages": 2, "destination": "hand"},  # Devolve to Basic
            target_card_id="p1-charizard",
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)

        assert result.success
        assert "knocked out" in result.message.lower()
        assert result.details.get("knockout") is True

    def test_cannot_devolve_basic_pokemon(
        self, game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that attempting to devolve a Basic Pokemon fails.

        A Basic Pokemon has no cards_underneath and cannot be devolved.
        """
        ctx = EffectContext(
            game=game_state,
            source_player_id="player2",
            source_card_id="p2-charmander",
            params={"stages": 1},
            target_card_id="p1-charmander",  # This is a Basic
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)

        assert not result.success
        assert "not an evolved Pokemon" in result.message

    def test_devolve_energy_remains_attached(
        self, evolved_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that energy remains attached after devolve.

        This is the designed behavior - energy stays with the Pokemon
        through devolution.
        """
        # Verify initial energy count
        p1 = evolved_game_state.players["player1"]
        charizard = p1.get_active_pokemon()
        initial_energy_ids = [e.instance_id for e in charizard.attached_energy]
        assert len(initial_energy_ids) == 3

        ctx = EffectContext(
            game=evolved_game_state,
            source_player_id="player2",
            source_card_id="p2-active",
            params={"stages": 2},  # Devolve all the way to Basic
            target_card_id="p1-charizard",
            rng=seeded_rng,
        )

        result = resolve_effect("devolve", ctx)
        assert result.success

        # Get the devolved Pokemon (now Charmander)
        charmander = p1.get_active_pokemon()

        # All energy should still be attached
        final_energy_ids = [e.instance_id for e in charmander.attached_energy]
        assert len(final_energy_ids) == 3
        assert set(final_energy_ids) == set(initial_energy_ids)


# =============================================================================
# Knockout with Attachments Tests
# =============================================================================


class TestKnockoutWithAttachments:
    """Tests for knockout processing with attached cards."""

    @pytest.fixture
    def knockout_game_state(
        self,
        basic_pokemon_def: CardDefinition,
        stage1_pokemon_def: CardDefinition,
        fire_energy_def: CardDefinition,
        tool_def: CardDefinition,
    ) -> GameState:
        """Create a game state for knockout testing with attached cards."""
        game = GameState(
            game_id="test-knockout",
            rules=RulesConfig(),
            card_registry={
                basic_pokemon_def.id: basic_pokemon_def,
                stage1_pokemon_def.id: stage1_pokemon_def,
                fire_energy_def.id: fire_energy_def,
                tool_def.id: tool_def,
            },
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player2",
            turn_number=5,
            phase=TurnPhase.END,
        )

        p1 = game.players["player1"]

        # Build Charmeleon with Charmander underneath
        charmander = CardInstance(
            instance_id="ko-charmander",
            definition_id=basic_pokemon_def.id,
        )
        charmeleon = CardInstance(
            instance_id="ko-charmeleon",
            definition_id=stage1_pokemon_def.id,
            cards_underneath=[charmander],
        )

        # Attach energy and tool
        energy1 = CardInstance(instance_id="ko-energy-1", definition_id=fire_energy_def.id)
        energy2 = CardInstance(instance_id="ko-energy-2", definition_id=fire_energy_def.id)
        tool = CardInstance(instance_id="ko-tool", definition_id=tool_def.id)

        charmeleon.attach_energy(energy1)
        charmeleon.attach_energy(energy2)
        charmeleon.attach_tool(tool)

        # Set damage to cause knockout (HP is 80)
        charmeleon.damage = 80

        p1.active.add(charmeleon)

        # Add bench Pokemon for after KO
        bench_pokemon = CardInstance(
            instance_id="p1-bench",
            definition_id=basic_pokemon_def.id,
        )
        p1.bench.add(bench_pokemon)

        # Deck
        for i in range(5):
            card = CardInstance(instance_id=f"p1-deck-{i}", definition_id=basic_pokemon_def.id)
            p1.deck.add(card)

        # Player 2
        p2 = game.players["player2"]
        p2_active = CardInstance(instance_id="p2-active", definition_id=basic_pokemon_def.id)
        p2.active.add(p2_active)

        for i in range(5):
            card = CardInstance(instance_id=f"p2-deck-{i}", definition_id=basic_pokemon_def.id)
            p2.deck.add(card)

        return game

    def test_attached_energy_goes_to_discard_on_knockout(
        self, knockout_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that attached energy cards go to the owner's discard pile on knockout.

        When a Pokemon is knocked out, all attached energy CardInstance objects
        should be moved to the player's discard pile.
        """
        turn_manager = TurnManager()
        p1 = knockout_game_state.players["player1"]

        # Verify energy is attached
        charmeleon = p1.get_active_pokemon()
        assert len(charmeleon.attached_energy) == 2

        # Initial discard should be empty
        assert len(p1.discard.cards) == 0

        # Process knockout (knocked_out_id, opponent_id who scores)
        turn_manager.process_knockout(knockout_game_state, "ko-charmeleon", "player2")

        # Energy should now be in discard
        assert "ko-energy-1" in p1.discard
        assert "ko-energy-2" in p1.discard

    def test_attached_tools_go_to_discard_on_knockout(
        self, knockout_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that attached tool cards go to the owner's discard pile on knockout.
        """
        turn_manager = TurnManager()
        p1 = knockout_game_state.players["player1"]

        charmeleon = p1.get_active_pokemon()
        assert len(charmeleon.attached_tools) == 1

        turn_manager.process_knockout(knockout_game_state, "ko-charmeleon", "player2")

        assert "ko-tool" in p1.discard

    def test_evolution_stack_goes_to_discard_on_knockout(
        self, knockout_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that the entire evolution stack goes to discard on knockout.

        When Charmeleon is knocked out, both Charmeleon and the Charmander
        underneath should go to the discard pile.
        """
        turn_manager = TurnManager()
        p1 = knockout_game_state.players["player1"]

        charmeleon = p1.get_active_pokemon()
        assert len(charmeleon.cards_underneath) == 1

        turn_manager.process_knockout(knockout_game_state, "ko-charmeleon", "player2")

        # Both the evolved Pokemon and the Basic underneath should be in discard
        assert "ko-charmeleon" in p1.discard
        assert "ko-charmander" in p1.discard

    def test_all_attachments_discard_together_on_knockout(
        self, knockout_game_state: GameState, seeded_rng: SeededRandom
    ) -> None:
        """
        Test that all attachments (energy, tools, stack) go to discard on knockout.

        Comprehensive test that energy, tools, and evolution stack all end up
        in the discard pile when a Pokemon is knocked out.
        """
        turn_manager = TurnManager()
        p1 = knockout_game_state.players["player1"]

        initial_discard_count = len(p1.discard.cards)

        turn_manager.process_knockout(knockout_game_state, "ko-charmeleon", "player2")

        # Should have added: 2 energy + 1 tool + 1 Pokemon underneath + 1 KO'd Pokemon = 5 cards
        expected_cards_in_discard = initial_discard_count + 5
        assert len(p1.discard.cards) == expected_cards_in_discard

        # Verify all specific cards
        assert "ko-energy-1" in p1.discard
        assert "ko-energy-2" in p1.discard
        assert "ko-tool" in p1.discard
        assert "ko-charmander" in p1.discard
        assert "ko-charmeleon" in p1.discard


# =============================================================================
# find_card_instance Tests for Attached Cards
# =============================================================================


class TestFindCardInstanceWithAttachments:
    """Tests for finding cards in attachments via find_card_instance."""

    def test_find_attached_energy_by_id(
        self, game_state: GameState, fire_energy_def: CardDefinition
    ) -> None:
        """
        Test that find_card_instance can locate energy attached to a Pokemon.

        Energy cards stored in attached_energy should be findable by their
        instance_id.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Attach energy
        energy = CardInstance(instance_id="findable-energy", definition_id=fire_energy_def.id)
        charmander.attach_energy(energy)

        # Find the energy
        card, zone_name = game_state.find_card_instance("findable-energy")

        assert card is not None
        assert card.instance_id == "findable-energy"
        assert zone_name == "attached_energy"

    def test_find_attached_tool_by_id(
        self, game_state: GameState, tool_def: CardDefinition
    ) -> None:
        """
        Test that find_card_instance can locate tools attached to a Pokemon.
        """
        p1 = game_state.players["player1"]
        charmander = p1.get_active_pokemon()

        # Attach tool
        tool = CardInstance(instance_id="findable-tool", definition_id=tool_def.id)
        charmander.attach_tool(tool)

        # Find the tool
        card, zone_name = game_state.find_card_instance("findable-tool")

        assert card is not None
        assert card.instance_id == "findable-tool"
        assert zone_name == "attached_tools"

    def test_find_card_in_evolution_stack(
        self,
        game_state: GameState,
        basic_pokemon_def: CardDefinition,
        stage1_pokemon_def: CardDefinition,
    ) -> None:
        """
        Test that find_card_instance can locate cards in the evolution stack.

        When searching for the Basic that's underneath a Stage 1, it should
        be found in cards_underneath.
        """
        p1 = game_state.players["player1"]

        # Build evolved Pokemon manually
        charmander = CardInstance(
            instance_id="underneath-charmander",
            definition_id=basic_pokemon_def.id,
        )
        charmeleon = CardInstance(
            instance_id="evolved-charmeleon",
            definition_id=stage1_pokemon_def.id,
            cards_underneath=[charmander],
        )

        # Replace active with the evolved Pokemon
        p1.active.clear()
        p1.active.add(charmeleon)

        # Find the Pokemon underneath
        card, zone_name = game_state.find_card_instance("underneath-charmander")

        assert card is not None
        assert card.instance_id == "underneath-charmander"
        assert zone_name == "cards_underneath"

    def test_find_attached_card_on_bench_pokemon(
        self,
        game_state: GameState,
        fire_energy_def: CardDefinition,
        basic_pokemon_def: CardDefinition,
    ) -> None:
        """
        Test finding attached cards on benched Pokemon.

        Energy attached to bench Pokemon should also be findable.
        """
        p1 = game_state.players["player1"]

        # Add a bench Pokemon with energy
        bench_pokemon = CardInstance(
            instance_id="bench-pokemon",
            definition_id=basic_pokemon_def.id,
        )
        energy = CardInstance(instance_id="bench-energy", definition_id=fire_energy_def.id)
        bench_pokemon.attach_energy(energy)
        p1.bench.add(bench_pokemon)

        # Find the bench Pokemon's energy
        card, zone_name = game_state.find_card_instance("bench-energy")

        assert card is not None
        assert card.instance_id == "bench-energy"
        assert zone_name == "attached_energy"

    def test_find_returns_none_for_nonexistent_card(self, game_state: GameState) -> None:
        """
        Test that find_card_instance returns None for cards that don't exist.
        """
        card, zone_name = game_state.find_card_instance("nonexistent-card-id")
        assert card is None
        assert zone_name is None


# =============================================================================
# CardInstance Method Tests
# =============================================================================


class TestCardInstanceAttachmentMethods:
    """Tests for CardInstance attachment methods."""

    def test_attach_energy_adds_card_instance(self, fire_energy_def: CardDefinition) -> None:
        """
        Test that attach_energy properly adds a CardInstance to attached_energy.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")
        energy = CardInstance(instance_id="energy-1", definition_id=fire_energy_def.id)

        pokemon.attach_energy(energy)

        assert len(pokemon.attached_energy) == 1
        assert pokemon.attached_energy[0] is energy
        assert pokemon.attached_energy[0].instance_id == "energy-1"

    def test_detach_energy_removes_and_returns_card_instance(
        self, fire_energy_def: CardDefinition
    ) -> None:
        """
        Test that detach_energy removes and returns the CardInstance.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")
        energy1 = CardInstance(instance_id="energy-1", definition_id=fire_energy_def.id)
        energy2 = CardInstance(instance_id="energy-2", definition_id=fire_energy_def.id)

        pokemon.attach_energy(energy1)
        pokemon.attach_energy(energy2)

        assert len(pokemon.attached_energy) == 2

        # Detach energy-1
        detached = pokemon.detach_energy("energy-1")

        assert detached is not None
        assert detached.instance_id == "energy-1"
        assert len(pokemon.attached_energy) == 1
        assert pokemon.attached_energy[0].instance_id == "energy-2"

    def test_detach_energy_returns_none_for_not_attached(self) -> None:
        """
        Test that detach_energy returns None when the energy isn't attached.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")

        result = pokemon.detach_energy("nonexistent-energy")

        assert result is None

    def test_attach_tool_adds_card_instance(self, tool_def: CardDefinition) -> None:
        """
        Test that attach_tool properly adds a CardInstance to attached_tools.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")
        tool = CardInstance(instance_id="tool-1", definition_id=tool_def.id)

        pokemon.attach_tool(tool)

        assert len(pokemon.attached_tools) == 1
        assert pokemon.attached_tools[0] is tool

    def test_detach_tool_removes_and_returns_card_instance(self, tool_def: CardDefinition) -> None:
        """
        Test that detach_tool removes and returns the CardInstance.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")
        tool = CardInstance(instance_id="tool-1", definition_id=tool_def.id)

        pokemon.attach_tool(tool)

        detached = pokemon.detach_tool("tool-1")

        assert detached is not None
        assert detached.instance_id == "tool-1"
        assert len(pokemon.attached_tools) == 0

    def test_get_all_attached_cards_returns_all(
        self, fire_energy_def: CardDefinition, tool_def: CardDefinition
    ) -> None:
        """
        Test that get_all_attached_cards returns energy and tools.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")
        energy = CardInstance(instance_id="energy", definition_id=fire_energy_def.id)
        tool = CardInstance(instance_id="tool", definition_id=tool_def.id)

        pokemon.attach_energy(energy)
        pokemon.attach_tool(tool)

        all_attached = pokemon.get_all_attached_cards()

        assert len(all_attached) == 2
        ids = [c.instance_id for c in all_attached]
        assert "energy" in ids
        assert "tool" in ids

    def test_multiple_energy_attachment(self, fire_energy_def: CardDefinition) -> None:
        """
        Test that multiple energy cards can be attached.
        """
        pokemon = CardInstance(instance_id="pokemon", definition_id="charmander-001")

        for i in range(5):
            energy = CardInstance(instance_id=f"energy-{i}", definition_id=fire_energy_def.id)
            pokemon.attach_energy(energy)

        assert len(pokemon.attached_energy) == 5

        # Verify order is preserved
        for i in range(5):
            assert pokemon.attached_energy[i].instance_id == f"energy-{i}"
