"""Tests for built-in effect handlers.

These tests verify each effect handler:
1. Applies the correct game state changes
2. Returns appropriate success/failure results
3. Handles edge cases gracefully
4. Includes correct details in results

Note: Handlers are imported to trigger registration, then tested via resolve_effect.
"""

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, EffectType
from app.core.effects.registry import resolve_effect
from app.core.enums import CardType, EnergyType, PokemonStage, StatusCondition
from app.core.models.card import CardDefinition, CardInstance, WeaknessResistance
from app.core.models.game_state import GameState, PlayerState
from app.core.rng import SeededRandom

# ============================================================================
# Test Fixtures
# ============================================================================


@pytest.fixture
def game_state() -> GameState:
    """Create a game state with two players and active Pokemon."""
    pikachu_def = CardDefinition(
        id="pikachu-001",
        name="Pikachu",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        hp=60,
        pokemon_type=EnergyType.LIGHTNING,
    )
    charmander_def = CardDefinition(
        id="charmander-001",
        name="Charmander",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        hp=70,
        pokemon_type=EnergyType.FIRE,
    )
    bulbasaur_def = CardDefinition(
        id="bulbasaur-001",
        name="Bulbasaur",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        hp=60,
        pokemon_type=EnergyType.GRASS,
    )
    energy_def = CardDefinition(
        id="lightning-energy",
        name="Lightning Energy",
        card_type=CardType.ENERGY,
        energy_type=EnergyType.LIGHTNING,
        energy_provides=[EnergyType.LIGHTNING],
    )

    game = GameState(
        game_id="test-game",
        rules=RulesConfig(),
        card_registry={
            "pikachu-001": pikachu_def,
            "charmander-001": charmander_def,
            "bulbasaur-001": bulbasaur_def,
            "lightning-energy": energy_def,
        },
        players={
            "player1": PlayerState(player_id="player1"),
            "player2": PlayerState(player_id="player2"),
        },
        turn_order=["player1", "player2"],
        current_player_id="player1",
        turn_number=1,
    )

    # Add active Pokemon
    pikachu = CardInstance(instance_id="pikachu-inst", definition_id="pikachu-001")
    charmander = CardInstance(instance_id="charmander-inst", definition_id="charmander-001")

    game.players["player1"].active.add(pikachu)
    game.players["player2"].active.add(charmander)

    # Add some cards to player1's deck
    for i in range(5):
        card = CardInstance(instance_id=f"deck-card-{i}", definition_id="bulbasaur-001")
        game.players["player1"].deck.add(card)

    return game


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


def make_context(
    game: GameState,
    rng: SeededRandom,
    source_player_id: str = "player1",
    **kwargs,
) -> EffectContext:
    """Create an EffectContext with defaults."""
    return EffectContext(
        game=game,
        source_player_id=source_player_id,
        rng=rng,
        **kwargs,
    )


# ============================================================================
# deal_damage Tests (Raw Damage Primitive)
# ============================================================================


class TestDealDamage:
    """Tests for the deal_damage effect (raw damage primitive).

    deal_damage applies damage directly without any combat calculations.
    Use for: poison, burn, recoil, ability damage, bench spread.
    """

    def test_deals_raw_damage_to_opponent_active(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify deal_damage deals raw damage to opponent's active Pokemon by default.
        No modifiers, weakness, or resistance are applied.
        """
        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert result.effect_type == EffectType.DAMAGE
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30

    def test_deals_damage_to_specific_target(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify deal_damage can target a specific card by instance ID.
        """
        ctx = make_context(
            game_state,
            rng,
            target_card_id="charmander-inst",
            params={"amount": 20},
        )

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 20

    def test_ignores_damage_modifier(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify deal_damage does NOT apply source's damage_modifier.
        Raw damage is raw - use attack_damage for combat calculations.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.damage_modifier = 10

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20},
        )

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 20  # Raw 20, modifier ignored

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify deal_damage fails when no valid target exists.
        """
        game_state.players["player2"].active.clear()

        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("deal_damage", ctx)

        assert not result.success
        assert "No valid target" in result.message

    def test_fails_with_zero_damage(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify deal_damage fails with zero or negative amount.
        """
        ctx = make_context(game_state, rng, params={"amount": 0})

        result = resolve_effect("deal_damage", ctx)

        assert not result.success
        assert "must be positive" in result.message

    def test_detects_knockout(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify deal_damage sets knockout flag when damage kills target.

        This test ensures that damage handlers report knockouts in the result
        details, allowing the engine to handle knockout processing.
        Charmander has 70 HP, so 70+ damage should knock it out.
        """
        ctx = make_context(game_state, rng, params={"amount": 70})

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert result.details.get("knockout") is True
        assert result.details.get("knockout_pokemon_id") == "charmander-inst"
        assert "knocked out" in result.message

    def test_no_knockout_flag_when_alive(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify deal_damage does NOT set knockout flag when target survives.

        Charmander has 70 HP, so 30 damage should leave it alive.
        """
        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert "knockout" not in result.details
        assert "knocked out" not in result.message

    def test_detects_knockout_with_existing_damage(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify deal_damage detects knockout correctly when target has prior damage.

        Charmander has 70 HP. Give it 40 damage first, then 30 more should KO.
        """
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        target.damage = 40  # Pre-existing damage

        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert target.damage == 70  # 40 + 30
        assert result.details.get("knockout") is True

    def test_respects_hp_modifier_for_knockout(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify deal_damage respects HP modifiers when detecting knockout.

        Charmander has 70 base HP. With +30 hp_modifier, effective HP is 100.
        70 damage should NOT knock it out; 100 damage should.
        """
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        target.hp_modifier = 30  # Effective HP = 100

        # 70 damage should NOT KO
        ctx = make_context(game_state, rng, params={"amount": 70})
        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert target.damage == 70
        assert "knockout" not in result.details  # Still alive with 30 effective HP remaining

    def test_negative_hp_modifier_makes_knockout_easier(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify deal_damage respects negative HP modifiers for knockout.

        Charmander has 70 base HP. With -20 hp_modifier, effective HP is 50.
        50 damage should knock it out even though base HP is 70.
        """
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        target.hp_modifier = -20  # Effective HP = 50

        ctx = make_context(game_state, rng, params={"amount": 50})
        result = resolve_effect("deal_damage", ctx)

        assert result.success
        assert target.damage == 50
        assert result.details.get("knockout") is True


# ============================================================================
# attack_damage Tests (Combat Damage with Modifiers)
# ============================================================================


class TestAttackDamage:
    """Tests for the attack_damage effect (full combat damage).

    attack_damage applies damage with all combat calculations:
    - Source Pokemon's damage_modifier
    - Target's weakness (x2 typically)
    - Target's resistance (-30 typically)
    """

    def test_deals_base_damage(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage deals base damage when no modifiers apply.
        """
        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        assert result.effect_type == EffectType.DAMAGE
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30
        assert result.details["base_amount"] == 30
        assert result.details["final_damage"] == 30

    def test_includes_damage_modifier(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage includes source Pokemon's damage_modifier.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.damage_modifier = 10

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30  # 20 + 10 modifier
        assert result.details["damage_modifier"] == 10

    def test_can_exclude_modifiers(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage can exclude damage modifiers via param.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.damage_modifier = 10

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20, "include_modifiers": False},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 20  # No modifier applied

    def test_applies_weakness(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage applies weakness using game's CombatConfig defaults.

        Setup: Pikachu (Lightning) attacks Charmander who is weak to Lightning.
        Default CombatConfig uses multiplicative x2 weakness.
        """
        # Update Charmander to be weak to Lightning (uses game defaults)
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 60  # 30 * 2 weakness (default multiplicative x2)
        assert result.details["weakness"]["mode"] == "multiplicative"
        assert result.details["weakness"]["value"] == 2

    def test_applies_resistance(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage applies resistance using game's CombatConfig defaults.

        Setup: Pikachu (Lightning) attacks Charmander who resists Lightning.
        Default CombatConfig uses additive -30 resistance.
        """
        # Update Charmander to resist Lightning (uses game defaults)
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            resistance=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 50},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 20  # 50 - 30 resistance (default additive -30)
        assert result.details["resistance"]["mode"] == "additive"
        assert result.details["resistance"]["value"] == -30

    def test_resistance_cannot_go_negative(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage floors at 0 when resistance exceeds damage.
        """
        # Charmander resists Lightning (uses game default -30)
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            resistance=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20},  # 20 - 30 = -10, should floor to 0
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 0  # Floored at 0
        assert result.details["final_damage"] == 0

    def test_weakness_then_resistance(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify weakness is applied before resistance (per Pokemon TCG rules).
        This is an edge case - typically a Pokemon doesn't have both to same type.
        """
        # Charmander has both weakness and resistance to Lightning (uses game defaults)
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),  # default x2
            resistance=WeaknessResistance(energy_type=EnergyType.LIGHTNING),  # default -30
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        # 30 * 2 (weakness) = 60, then 60 - 30 (resistance) = 30
        assert target.damage == 30
        assert result.details["after_weakness"] == 60
        assert result.details["after_resistance"] == 30

    def test_can_disable_weakness_resistance(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify attack_damage can skip weakness/resistance calculation.
        Useful for effects that say "this attack's damage isn't affected by..."
        """
        # Charmander weak to Lightning
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30, "apply_weakness_resistance": False},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30  # No weakness applied
        assert "weakness" not in result.details

    def test_additive_weakness_from_rules(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage uses additive weakness when configured in RulesConfig.
        """
        from app.core.config import CombatConfig
        from app.core.enums import ModifierMode

        # Configure game to use additive weakness (+20)
        game_state.rules.combat = CombatConfig(
            weakness_mode=ModifierMode.ADDITIVE,
            weakness_value=20,
        )

        # Charmander weak to Lightning (will use game's additive +20)
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 50  # 30 + 20 (additive weakness)
        assert result.details["weakness"]["mode"] == "additive"
        assert result.details["weakness"]["value"] == 20

    def test_multiplicative_resistance_from_rules(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify attack_damage uses multiplicative resistance when configured.
        Note: For true half damage (x0.5), you'd need float support.
        This test verifies the mode switching works with integer values.
        """
        from app.core.config import CombatConfig
        from app.core.enums import ModifierMode

        # Configure game to use multiplicative resistance (x1 = no change)
        game_state.rules.combat = CombatConfig(
            resistance_mode=ModifierMode.MULTIPLICATIVE,
            resistance_value=1,
        )

        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            resistance=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30  # 30 * 1 = 30
        assert result.details["resistance"]["mode"] == "multiplicative"

    def test_card_overrides_weakness_mode(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify a card can override the game's default weakness mode.
        Even if game uses multiplicative x2, card can specify additive +40.
        """
        from app.core.enums import ModifierMode

        # Game uses default multiplicative x2, but card overrides to additive +40
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(
                energy_type=EnergyType.LIGHTNING,
                mode=ModifierMode.ADDITIVE,
                value=40,
            ),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 70  # 30 + 40 (card's additive override)
        assert result.details["weakness"]["mode"] == "additive"
        assert result.details["weakness"]["value"] == 40

    def test_card_overrides_only_value(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify a card can override just the value while keeping default mode.
        Card specifies x3 weakness instead of default x2.
        """
        # Card only overrides value, keeps default multiplicative mode
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(
                energy_type=EnergyType.LIGHTNING,
                value=3,  # x3 instead of default x2
            ),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 60  # 20 * 3 (card's x3 with default multiplicative mode)
        assert result.details["weakness"]["mode"] == "multiplicative"
        assert result.details["weakness"]["value"] == 3

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage fails when no valid target exists.
        """
        game_state.players["player2"].active.clear()

        ctx = make_context(game_state, rng, params={"amount": 30})

        result = resolve_effect("attack_damage", ctx)

        assert not result.success
        assert "No valid target" in result.message

    def test_fails_with_zero_damage(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage fails with zero or negative base amount.
        """
        ctx = make_context(game_state, rng, params={"amount": 0})

        result = resolve_effect("attack_damage", ctx)

        assert not result.success
        assert "must be positive" in result.message

    def test_validation_prevents_pokemon_without_type(self) -> None:
        """
        Verify CardDefinition validation prevents creating Pokemon without pokemon_type.

        This ensures all Pokemon in the game have a type, which is required for
        proper weakness/resistance calculations. The attack_damage handler still
        has defensive code for edge cases (e.g., missing registry entries), but
        CardDefinition validation now prevents typeless Pokemon at creation time.
        """
        with pytest.raises(ValueError, match="pokemon_type"):
            CardDefinition(
                id="typeless-pokemon",
                name="Typeless",
                card_type=CardType.POKEMON,
                stage=PokemonStage.BASIC,
                hp=60,
                pokemon_type=None,  # Should fail validation
            )

    def test_skips_weakness_when_source_definition_missing(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify attack_damage handles missing source definition gracefully.

        If the source card's definition isn't in the registry for some reason,
        the attack should still deal base damage without crashing.
        """
        # Remove source definition from registry
        del game_state.card_registry["pikachu-001"]

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30  # Base damage only
        assert "weakness" not in result.details

    def test_skips_weakness_when_target_definition_missing(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify attack_damage handles missing target definition gracefully.

        If the target card's definition isn't in the registry, weakness/resistance
        can't be checked, but damage should still be dealt.
        """
        # Remove target definition from registry
        del game_state.card_registry["charmander-001"]

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.damage == 30
        assert "weakness" not in result.details

    def test_detects_knockout(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage sets knockout flag when damage kills target.

        Charmander has 70 HP, so 70+ damage should knock it out.
        The knockout flag allows the engine to handle knockout processing.
        """
        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 70},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        assert result.details.get("knockout") is True
        assert result.details.get("knockout_pokemon_id") == "charmander-inst"
        assert "knocked out" in result.message

    def test_no_knockout_flag_when_alive(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage does NOT set knockout flag when target survives.

        Charmander has 70 HP, so 30 damage leaves it alive.
        """
        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 30},
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        assert "knockout" not in result.details
        assert "knocked out" not in result.message

    def test_detects_knockout_with_weakness(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify attack_damage detects knockout after weakness multiplier applied.

        Charmander has 70 HP and is weak to Lightning. Pikachu deals 40 damage,
        which becomes 80 after x2 weakness, causing a knockout.
        """
        # Make Charmander weak to Lightning
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            weakness=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 40},  # 40 * 2 = 80 > 70 HP
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        assert result.details["final_damage"] == 80
        assert result.details.get("knockout") is True
        assert "knocked out" in result.message

    def test_no_knockout_after_resistance_saves(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify attack_damage correctly reports no knockout when resistance prevents it.

        Charmander has 70 HP with 60 prior damage. Pikachu deals 20 damage,
        but Charmander resists Lightning for -30, so final damage is 0.
        """
        # Make Charmander resist Lightning
        game_state.card_registry["charmander-001"] = CardDefinition(
            id="charmander-001",
            name="Charmander",
            card_type=CardType.POKEMON,
            stage=PokemonStage.BASIC,
            hp=70,
            pokemon_type=EnergyType.FIRE,
            resistance=WeaknessResistance(energy_type=EnergyType.LIGHTNING),
        )

        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        target.damage = 60  # 10 HP remaining

        ctx = make_context(
            game_state,
            rng,
            source_card_id="pikachu-inst",
            params={"amount": 20},  # 20 - 30 = 0 (floored)
        )

        result = resolve_effect("attack_damage", ctx)

        assert result.success
        assert result.details["final_damage"] == 0
        assert target.damage == 60  # Unchanged
        assert "knockout" not in result.details


# ============================================================================
# heal Tests
# ============================================================================


class TestHeal:
    """Tests for the heal effect."""

    def test_heals_source_pokemon_by_default(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify heal heals source player's active Pokemon by default.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.damage = 40

        ctx = make_context(game_state, rng, params={"amount": 20})

        result = resolve_effect("heal", ctx)

        assert result.success
        assert result.effect_type == EffectType.HEAL
        assert source.damage == 20

    def test_heals_specific_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify heal can target a specific card.
        """
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        target.damage = 50

        ctx = make_context(
            game_state,
            rng,
            target_card_id="charmander-inst",
            params={"amount": 30},
        )

        result = resolve_effect("heal", ctx)

        assert result.success
        assert target.damage == 20

    def test_does_not_overheal(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify heal doesn't heal more than current damage.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.damage = 20

        ctx = make_context(game_state, rng, params={"amount": 50})

        result = resolve_effect("heal", ctx)

        assert result.success
        assert source.damage == 0
        assert result.details["amount"] == 20  # Only healed 20

    def test_fails_with_zero_amount(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify heal fails with zero or negative amount.
        """
        ctx = make_context(game_state, rng, params={"amount": 0})

        result = resolve_effect("heal", ctx)

        assert not result.success

    def test_fails_with_no_active_pokemon(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify heal fails when source player has no active Pokemon.

        This tests the branch where target resolution fails because there's
        no active Pokemon to heal by default.
        """
        game_state.players["player1"].active.clear()

        ctx = make_context(game_state, rng, params={"amount": 20})

        result = resolve_effect("heal", ctx)

        assert not result.success
        assert "No valid target" in result.message


# ============================================================================
# draw_cards Tests
# ============================================================================


class TestDrawCards:
    """Tests for the draw_cards effect."""

    def test_draws_one_card_by_default(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify draw_cards draws one card by default.
        """
        initial_deck_size = len(game_state.players["player1"].deck)
        initial_hand_size = len(game_state.players["player1"].hand)

        ctx = make_context(game_state, rng)

        result = resolve_effect("draw_cards", ctx)

        assert result.success
        assert result.effect_type == EffectType.DRAW
        assert len(game_state.players["player1"].deck) == initial_deck_size - 1
        assert len(game_state.players["player1"].hand) == initial_hand_size + 1

    def test_draws_multiple_cards(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify draw_cards can draw multiple cards.
        """
        initial_deck_size = len(game_state.players["player1"].deck)

        ctx = make_context(game_state, rng, params={"count": 3})

        result = resolve_effect("draw_cards", ctx)

        assert result.success
        assert result.details["count"] == 3
        assert len(game_state.players["player1"].deck) == initial_deck_size - 3
        assert len(game_state.players["player1"].hand) == 3

    def test_draws_partial_if_deck_runs_out(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify draw_cards draws what it can if deck doesn't have enough.
        """
        # Deck has 5 cards
        ctx = make_context(game_state, rng, params={"count": 10})

        result = resolve_effect("draw_cards", ctx)

        assert result.success
        assert result.details["count"] == 5
        assert len(game_state.players["player1"].deck) == 0
        assert len(game_state.players["player1"].hand) == 5

    def test_fails_with_empty_deck(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify draw_cards fails when deck is empty.
        """
        game_state.players["player1"].deck.clear()

        ctx = make_context(game_state, rng, params={"count": 1})

        result = resolve_effect("draw_cards", ctx)

        assert not result.success
        assert "empty" in result.message.lower()

    def test_fails_with_zero_count(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify draw_cards fails when count is zero or negative.

        This tests the validation branch that rejects invalid count values.
        """
        ctx = make_context(game_state, rng, params={"count": 0})

        result = resolve_effect("draw_cards", ctx)

        assert not result.success
        assert "positive" in result.message.lower()


# ============================================================================
# discard_from_hand Tests
# ============================================================================


class TestDiscardFromHand:
    """Tests for the discard_from_hand effect.

    This effect moves specified cards from a player's hand to their discard pile.
    """

    def test_discards_single_card(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_from_hand moves a single card from hand to discard.
        """
        # Add a card to player's hand
        hand_card = CardInstance(instance_id="hand-card-1", definition_id="bulbasaur-001")
        game_state.players["player1"].hand.add(hand_card)

        ctx = make_context(game_state, rng, params={"card_ids": ["hand-card-1"]})

        result = resolve_effect("discard_from_hand", ctx)

        assert result.success
        assert result.effect_type == EffectType.DISCARD
        assert result.details["count"] == 1
        assert "hand-card-1" in result.details["card_ids"]

        # Verify card moved from hand to discard
        assert len(game_state.players["player1"].hand) == 0
        assert len(game_state.players["player1"].discard) == 1
        assert game_state.players["player1"].discard.cards[0].instance_id == "hand-card-1"

    def test_discards_multiple_cards(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_from_hand can discard multiple cards at once.
        """
        # Add multiple cards to player's hand
        for i in range(3):
            card = CardInstance(instance_id=f"hand-card-{i}", definition_id="bulbasaur-001")
            game_state.players["player1"].hand.add(card)

        ctx = make_context(
            game_state,
            rng,
            params={"card_ids": ["hand-card-0", "hand-card-2"]},  # Discard 2 of 3
        )

        result = resolve_effect("discard_from_hand", ctx)

        assert result.success
        assert result.details["count"] == 2
        assert len(game_state.players["player1"].hand) == 1  # One card remains
        assert len(game_state.players["player1"].discard) == 2

    def test_fails_with_empty_card_ids(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_from_hand fails when card_ids list is empty.
        """
        ctx = make_context(game_state, rng, params={"card_ids": []})

        result = resolve_effect("discard_from_hand", ctx)

        assert not result.success
        assert "No cards specified" in result.message

    def test_fails_with_no_card_ids_param(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_from_hand fails when card_ids param is missing.
        """
        ctx = make_context(game_state, rng, params={})

        result = resolve_effect("discard_from_hand", ctx)

        assert not result.success
        assert "No cards specified" in result.message

    def test_fails_when_cards_not_in_hand(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_from_hand fails when none of the specified cards are in hand.

        This can happen if the card IDs are invalid or the cards are elsewhere.
        """
        ctx = make_context(
            game_state,
            rng,
            params={"card_ids": ["nonexistent-card-1", "nonexistent-card-2"]},
        )

        result = resolve_effect("discard_from_hand", ctx)

        assert not result.success
        assert "were in hand" in result.message.lower()

    def test_partial_discard_when_some_cards_missing(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify discard_from_hand discards what it can when some cards are missing.

        If some card_ids are valid and some are not, the valid ones should still
        be discarded successfully.
        """
        # Add one card to hand
        hand_card = CardInstance(instance_id="valid-card", definition_id="bulbasaur-001")
        game_state.players["player1"].hand.add(hand_card)

        ctx = make_context(
            game_state,
            rng,
            params={"card_ids": ["valid-card", "invalid-card"]},
        )

        result = resolve_effect("discard_from_hand", ctx)

        # Should succeed because at least one card was discarded
        assert result.success
        assert result.details["count"] == 1
        assert "valid-card" in result.details["card_ids"]
        assert len(game_state.players["player1"].hand) == 0
        assert len(game_state.players["player1"].discard) == 1


# ============================================================================
# apply_status / remove_status Tests
# ============================================================================


class TestApplyStatus:
    """Tests for the apply_status effect."""

    def test_applies_status_to_opponent(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify apply_status applies status to opponent's active by default.
        """
        ctx = make_context(game_state, rng, params={"status": "poisoned"})

        result = resolve_effect("apply_status", ctx)

        assert result.success
        assert result.effect_type == EffectType.STATUS
        target = game_state.players["player2"].get_active_pokemon()
        assert target is not None
        assert target.has_status(StatusCondition.POISONED)

    def test_applies_all_status_types(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify apply_status works with all status conditions.
        """
        statuses = ["poisoned", "burned", "asleep", "paralyzed", "confused"]

        for status in statuses:
            # Reset target
            target = game_state.players["player2"].get_active_pokemon()
            assert target is not None
            target.clear_all_status()

            ctx = make_context(game_state, rng, params={"status": status})
            result = resolve_effect("apply_status", ctx)

            assert result.success, f"Failed to apply {status}"
            assert target.has_status(StatusCondition(status))

    def test_fails_with_invalid_status(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify apply_status fails with invalid status name.
        """
        ctx = make_context(game_state, rng, params={"status": "invalid_status"})

        result = resolve_effect("apply_status", ctx)

        assert not result.success
        assert "Invalid" in result.message


class TestRemoveStatus:
    """Tests for the remove_status effect."""

    def test_removes_specific_status(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify remove_status removes a specific status condition.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.add_status(StatusCondition.POISONED)

        ctx = make_context(game_state, rng, params={"status": "poisoned"})

        result = resolve_effect("remove_status", ctx)

        assert result.success
        assert not source.has_status(StatusCondition.POISONED)

    def test_removes_all_status(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify remove_status can remove all status conditions.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.add_status(StatusCondition.POISONED)
        source.add_status(StatusCondition.BURNED)

        ctx = make_context(game_state, rng, params={"all": True})

        result = resolve_effect("remove_status", ctx)

        assert result.success
        assert len(source.status_conditions) == 0

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify remove_status fails when source player has no active Pokemon.
        """
        game_state.players["player1"].active.clear()

        ctx = make_context(game_state, rng, params={"status": "poisoned"})

        result = resolve_effect("remove_status", ctx)

        assert not result.success
        assert "No valid target" in result.message

    def test_fails_without_status_param_and_not_all(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify remove_status fails when neither 'status' nor 'all=True' is specified.

        The handler needs to know what to remove - either a specific status or all.
        """
        ctx = make_context(game_state, rng, params={})  # No status, all defaults to False

        result = resolve_effect("remove_status", ctx)

        assert not result.success
        assert "No status specified" in result.message

    def test_fails_with_invalid_status(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify remove_status fails when given an invalid status condition name.
        """
        ctx = make_context(game_state, rng, params={"status": "invalid_status_name"})

        result = resolve_effect("remove_status", ctx)

        assert not result.success
        assert "Invalid" in result.message

    def test_succeeds_when_target_didnt_have_status(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify remove_status succeeds but indicates target didn't have the status.

        This is not an error - it's valid to try to remove a status that isn't there.
        The result should indicate this case for logging purposes.
        """
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        # Don't add poisoned - source is clean

        ctx = make_context(game_state, rng, params={"status": "poisoned"})

        result = resolve_effect("remove_status", ctx)

        assert result.success
        assert result.details.get("had_status") is False


# ============================================================================
# apply_status edge cases
# ============================================================================


class TestApplyStatusEdgeCases:
    """Additional edge case tests for apply_status."""

    def test_fails_with_no_status_param(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify apply_status fails when status param is missing.
        """
        ctx = make_context(game_state, rng, params={})

        result = resolve_effect("apply_status", ctx)

        assert not result.success
        assert "No status specified" in result.message

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify apply_status fails when opponent has no active Pokemon.
        """
        game_state.players["player2"].active.clear()

        ctx = make_context(game_state, rng, params={"status": "poisoned"})

        result = resolve_effect("apply_status", ctx)

        assert not result.success
        assert "No valid target" in result.message


# ============================================================================
# coin_flip_damage Tests
# ============================================================================


class TestCoinFlipDamage:
    """Tests for the coin_flip_damage effect."""

    def test_deals_damage_per_heads(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify coin_flip_damage deals damage based on heads count.
        """
        ctx = make_context(
            game_state,
            rng,
            params={"damage_per_heads": 20, "flip_count": 3},
        )

        result = resolve_effect("coin_flip_damage", ctx)

        assert result.success
        assert result.effect_type == EffectType.COIN_FLIP
        assert len(result.details["flips"]) == 3
        # Damage should be heads_count * 20
        expected_damage = result.details["heads_count"] * 20
        assert result.details["damage"] == expected_damage

    def test_flip_until_tails(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify flip_until_tails mode keeps flipping until tails.
        """
        ctx = make_context(
            game_state,
            rng,
            params={"damage_per_heads": 10, "flip_until_tails": True},
        )

        result = resolve_effect("coin_flip_damage", ctx)

        assert result.success
        # Last flip should be tails
        assert result.details["flips"][-1] == "tails"

    def test_deterministic_with_seed(self, game_state: GameState) -> None:
        """
        Verify coin flips are deterministic with same seed.
        """
        rng1 = SeededRandom(seed=12345)
        rng2 = SeededRandom(seed=12345)

        ctx1 = make_context(game_state, rng1, params={"damage_per_heads": 10, "flip_count": 5})
        ctx2 = make_context(game_state, rng2, params={"damage_per_heads": 10, "flip_count": 5})

        result1 = resolve_effect("coin_flip_damage", ctx1)
        result2 = resolve_effect("coin_flip_damage", ctx2)

        assert result1.details["flips"] == result2.details["flips"]

    def test_fails_with_zero_damage_per_heads(
        self, game_state: GameState, rng: SeededRandom
    ) -> None:
        """
        Verify coin_flip_damage fails when damage_per_heads is zero or negative.
        """
        ctx = make_context(
            game_state,
            rng,
            params={"damage_per_heads": 0, "flip_count": 3},
        )

        result = resolve_effect("coin_flip_damage", ctx)

        assert not result.success
        assert "positive" in result.message.lower()

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify coin_flip_damage fails when opponent has no active Pokemon.
        """
        game_state.players["player2"].active.clear()

        ctx = make_context(
            game_state,
            rng,
            params={"damage_per_heads": 20, "flip_count": 3},
        )

        result = resolve_effect("coin_flip_damage", ctx)

        assert not result.success
        assert "No valid target" in result.message


# ============================================================================
# discard_energy Tests
# ============================================================================


class TestDiscardEnergy:
    """Tests for the discard_energy effect."""

    def test_discards_energy_from_source(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_energy removes energy from source Pokemon by default.

        Energy is now stored as CardInstance objects directly on the Pokemon.
        When discarded, energy moves to the owner's discard pile.
        """
        from app.core.models.card import CardInstance

        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        # Attach energy as CardInstance objects
        source.attached_energy = [
            CardInstance(instance_id="energy-1", definition_id="fire_energy"),
            CardInstance(instance_id="energy-2", definition_id="fire_energy"),
            CardInstance(instance_id="energy-3", definition_id="fire_energy"),
        ]

        ctx = make_context(game_state, rng, params={"count": 2})

        result = resolve_effect("discard_energy", ctx)

        assert result.success
        assert result.effect_type == EffectType.ENERGY
        assert len(source.attached_energy) == 1
        assert result.details["count"] == 2
        # Check that discarded energy went to discard pile
        assert len(game_state.players["player1"].discard) >= 2

    def test_discards_specific_energy(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_energy can discard specific energy cards.

        Energy is now stored as CardInstance objects.
        """
        from app.core.models.card import CardInstance

        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        source.attached_energy = [
            CardInstance(instance_id="energy-1", definition_id="fire_energy"),
            CardInstance(instance_id="energy-2", definition_id="fire_energy"),
            CardInstance(instance_id="energy-3", definition_id="fire_energy"),
        ]

        ctx = make_context(
            game_state,
            rng,
            params={"energy_ids": ["energy-1", "energy-3"]},
        )

        result = resolve_effect("discard_energy", ctx)

        assert result.success
        # Only energy-2 should remain
        assert len(source.attached_energy) == 1
        assert source.attached_energy[0].instance_id == "energy-2"

    def test_fails_with_no_energy(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_energy fails when no energy attached.
        """
        ctx = make_context(game_state, rng, params={"count": 1})

        result = resolve_effect("discard_energy", ctx)

        assert not result.success
        assert "No energy" in result.message

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify discard_energy fails when source player has no active Pokemon.
        """
        game_state.players["player1"].active.clear()

        ctx = make_context(game_state, rng, params={"count": 1})

        result = resolve_effect("discard_energy", ctx)

        assert not result.success
        assert "No valid target" in result.message


# ============================================================================
# Modifier Tests
# ============================================================================


class TestModifyHP:
    """Tests for the modify_hp effect."""

    def test_increases_hp_modifier(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify modify_hp can increase HP modifier.
        """
        ctx = make_context(game_state, rng, params={"amount": 20})

        result = resolve_effect("modify_hp", ctx)

        assert result.success
        assert result.effect_type == EffectType.MODIFIER
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        assert source.hp_modifier == 20

    def test_decreases_hp_modifier(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify modify_hp can decrease HP modifier.
        """
        ctx = make_context(game_state, rng, params={"amount": -10})

        result = resolve_effect("modify_hp", ctx)

        assert result.success
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        assert source.hp_modifier == -10

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify modify_hp fails when source player has no active Pokemon.
        """
        game_state.players["player1"].active.clear()

        ctx = make_context(game_state, rng, params={"amount": 20})

        result = resolve_effect("modify_hp", ctx)

        assert not result.success
        assert "No valid target" in result.message


class TestModifyRetreatCost:
    """Tests for the modify_retreat_cost effect."""

    def test_decreases_retreat_cost(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify modify_retreat_cost can reduce retreat cost (e.g., Float Stone).
        """
        ctx = make_context(game_state, rng, params={"amount": -2})

        result = resolve_effect("modify_retreat_cost", ctx)

        assert result.success
        source = game_state.players["player1"].get_active_pokemon()
        assert source is not None
        assert source.retreat_cost_modifier == -2

    def test_fails_with_no_target(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify modify_retreat_cost fails when source player has no active Pokemon.
        """
        game_state.players["player1"].active.clear()

        ctx = make_context(game_state, rng, params={"amount": -2})

        result = resolve_effect("modify_retreat_cost", ctx)

        assert not result.success
        assert "No valid target" in result.message


# ============================================================================
# bench_damage Tests
# ============================================================================


class TestBenchDamage:
    """Tests for the bench_damage effect."""

    def test_damages_all_opponent_bench(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify bench_damage hits all opponent's benched Pokemon by default.
        """
        # Add benched Pokemon
        for i in range(3):
            pokemon = CardInstance(instance_id=f"bench-{i}", definition_id="bulbasaur-001")
            game_state.players["player2"].bench.add(pokemon)

        ctx = make_context(game_state, rng, params={"amount": 10})

        result = resolve_effect("bench_damage", ctx)

        assert result.success
        assert result.effect_type == EffectType.BENCH
        assert result.details["total_damage"] == 30  # 10 * 3

        for pokemon in game_state.players["player2"].bench.cards:
            assert pokemon.damage == 10

    def test_damages_limited_targets(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify bench_damage can target limited number of Pokemon.
        """
        for i in range(3):
            pokemon = CardInstance(instance_id=f"bench-{i}", definition_id="bulbasaur-001")
            game_state.players["player2"].bench.add(pokemon)

        ctx = make_context(
            game_state,
            rng,
            params={"amount": 20, "target_all": False, "target_count": 2},
        )

        result = resolve_effect("bench_damage", ctx)

        assert result.success
        assert len(result.details["target_ids"]) == 2

    def test_handles_empty_bench(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify bench_damage succeeds gracefully with empty bench.
        """
        ctx = make_context(game_state, rng, params={"amount": 10})

        result = resolve_effect("bench_damage", ctx)

        assert result.success
        assert result.details["targets_hit"] == 0

    def test_fails_with_zero_damage(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify bench_damage fails when amount is zero or negative.
        """
        ctx = make_context(game_state, rng, params={"amount": 0})

        result = resolve_effect("bench_damage", ctx)

        assert not result.success
        assert "positive" in result.message.lower()


# ============================================================================
# shuffle_deck Tests
# ============================================================================


class TestShuffleDeck:
    """Tests for the shuffle_deck effect."""

    def test_shuffles_source_deck(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify shuffle_deck shuffles source player's deck by default.

        Note: With 5 cards, there's a 1/120 chance the shuffle produces the same
        order. We accept this small risk in tests rather than over-complicating.
        """
        ctx = make_context(game_state, rng)

        result = resolve_effect("shuffle_deck", ctx)

        assert result.success
        assert result.effect_type == EffectType.SHUFFLE
        assert result.details["player_id"] == "player1"

    def test_shuffles_opponent_deck(self, game_state: GameState, rng: SeededRandom) -> None:
        """
        Verify shuffle_deck can shuffle opponent's deck.
        """
        # Add cards to opponent's deck
        for i in range(5):
            card = CardInstance(instance_id=f"opp-deck-{i}", definition_id="bulbasaur-001")
            game_state.players["player2"].deck.add(card)

        ctx = make_context(game_state, rng, params={"target_opponent": True})

        result = resolve_effect("shuffle_deck", ctx)

        assert result.success
        assert result.details["player_id"] == "player2"
