"""Tests for the game state models (Zone, PlayerState, GameState).

These tests verify:
1. Zone operations (add, remove, draw, shuffle, etc.)
2. PlayerState zone management and turn flags
3. GameState turn tracking and player management
4. JSON serialization round-trips
"""

from app.core.config import RulesConfig
from app.core.enums import CardType, EnergyType, GameEndReason, PokemonStage, TurnPhase
from app.core.models.card import CardDefinition, CardInstance
from app.core.models.game_state import GameState, PlayerState, Zone
from app.core.rng import SeededRandom

# ============================================================================
# Test Fixtures (local to this module, will move to conftest.py later)
# ============================================================================


def make_card_instance(instance_id: str, definition_id: str = "test_card") -> CardInstance:
    """Create a CardInstance for testing."""
    return CardInstance(instance_id=instance_id, definition_id=definition_id)


def make_pokemon_definition(card_id: str, name: str = "Test Pokemon") -> CardDefinition:
    """Create a basic Pokemon CardDefinition for testing."""
    return CardDefinition(
        id=card_id,
        name=name,
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        hp=60,
        pokemon_type=EnergyType.COLORLESS,
    )


# ============================================================================
# Zone Tests
# ============================================================================


class TestZoneBasicOperations:
    """Tests for basic Zone operations."""

    def test_zone_starts_empty(self) -> None:
        """
        Verify a new Zone has no cards.

        Zones should initialize as empty lists.
        """
        zone = Zone()
        assert len(zone) == 0
        assert zone.is_empty()
        assert not zone

    def test_zone_add_card(self) -> None:
        """
        Verify cards can be added to a Zone.

        add() should append to the end of the zone.
        """
        zone = Zone()
        card = make_card_instance("card-1")

        zone.add(card)

        assert len(zone) == 1
        assert not zone.is_empty()
        assert zone

    def test_zone_add_to_top(self) -> None:
        """
        Verify add_to_top places cards at index 0.

        This is useful for effects that put cards on top of deck.
        """
        zone = Zone()
        card1 = make_card_instance("card-1")
        card2 = make_card_instance("card-2")

        zone.add(card1)
        zone.add_to_top(card2)

        assert zone.cards[0].instance_id == "card-2"
        assert zone.cards[1].instance_id == "card-1"

    def test_zone_add_to_bottom(self) -> None:
        """
        Verify add_to_bottom places cards at the end.

        Alias for add(), but explicit about intent.
        """
        zone = Zone()
        card1 = make_card_instance("card-1")
        card2 = make_card_instance("card-2")

        zone.add(card1)
        zone.add_to_bottom(card2)

        assert zone.cards[0].instance_id == "card-1"
        assert zone.cards[1].instance_id == "card-2"

    def test_zone_contains(self) -> None:
        """
        Verify __contains__ checks for instance_id.

        Allows using 'in' operator with instance IDs.
        """
        zone = Zone()
        card = make_card_instance("card-1")
        zone.add(card)

        assert "card-1" in zone
        assert "card-2" not in zone

    def test_zone_get_card(self) -> None:
        """
        Verify get() returns card without removing it.
        """
        zone = Zone()
        card = make_card_instance("card-1")
        zone.add(card)

        result = zone.get("card-1")

        assert result is card
        assert len(zone) == 1  # Card still in zone

    def test_zone_get_nonexistent(self) -> None:
        """
        Verify get() returns None for missing cards.
        """
        zone = Zone()
        result = zone.get("nonexistent")
        assert result is None

    def test_zone_remove_card(self) -> None:
        """
        Verify remove() extracts and returns a card.
        """
        zone = Zone()
        card = make_card_instance("card-1")
        zone.add(card)

        result = zone.remove("card-1")

        assert result is card
        assert len(zone) == 0

    def test_zone_remove_nonexistent(self) -> None:
        """
        Verify remove() returns None for missing cards.
        """
        zone = Zone()
        result = zone.remove("nonexistent")
        assert result is None


class TestZoneDeckOperations:
    """Tests for deck-like Zone operations (draw, peek, shuffle)."""

    def test_zone_draw_from_top(self) -> None:
        """
        Verify draw() removes and returns the top card (index 0).

        Standard deck draw operation.
        """
        zone = Zone()
        card1 = make_card_instance("card-1")
        card2 = make_card_instance("card-2")
        zone.add(card1)
        zone.add(card2)

        drawn = zone.draw()

        assert drawn is card1
        assert len(zone) == 1
        assert zone.cards[0] is card2

    def test_zone_draw_from_empty(self) -> None:
        """
        Verify draw() returns None when zone is empty.

        This is used to detect deck-out condition.
        """
        zone = Zone()
        result = zone.draw()
        assert result is None

    def test_zone_draw_bottom(self) -> None:
        """
        Verify draw_bottom() removes from the end.

        Useful for some card effects.
        """
        zone = Zone()
        card1 = make_card_instance("card-1")
        card2 = make_card_instance("card-2")
        zone.add(card1)
        zone.add(card2)

        drawn = zone.draw_bottom()

        assert drawn is card2
        assert len(zone) == 1

    def test_zone_peek(self) -> None:
        """
        Verify peek() returns top cards without removing them.
        """
        zone = Zone()
        for i in range(5):
            zone.add(make_card_instance(f"card-{i}"))

        peeked = zone.peek(3)

        assert len(peeked) == 3
        assert peeked[0].instance_id == "card-0"
        assert len(zone) == 5  # Cards still in zone

    def test_zone_peek_more_than_available(self) -> None:
        """
        Verify peek() returns all cards if count exceeds zone size.
        """
        zone = Zone()
        zone.add(make_card_instance("card-1"))

        peeked = zone.peek(10)

        assert len(peeked) == 1

    def test_zone_peek_bottom(self) -> None:
        """
        Verify peek_bottom() returns cards from the end.
        """
        zone = Zone()
        for i in range(5):
            zone.add(make_card_instance(f"card-{i}"))

        peeked = zone.peek_bottom(2)

        assert len(peeked) == 2
        assert peeked[0].instance_id == "card-3"
        assert peeked[1].instance_id == "card-4"

    def test_zone_peek_bottom_more_than_available(self) -> None:
        """
        Verify peek_bottom() returns all cards if count exceeds zone size.

        This edge case ensures we don't crash when asking for more cards
        than are available.
        """
        zone = Zone()
        zone.add(make_card_instance("card-1"))
        zone.add(make_card_instance("card-2"))

        peeked = zone.peek_bottom(10)

        assert len(peeked) == 2

    def test_zone_draw_bottom_from_empty(self) -> None:
        """
        Verify draw_bottom() returns None when zone is empty.

        Mirrors the behavior of draw() for consistency.
        """
        zone = Zone()

        result = zone.draw_bottom()

        assert result is None

    def test_zone_shuffle_deterministic(self) -> None:
        """
        Verify shuffle() produces deterministic results with SeededRandom.

        Same seed should produce same shuffle order.
        """
        zone1 = Zone()
        zone2 = Zone()
        for i in range(10):
            zone1.add(make_card_instance(f"card-{i}"))
            zone2.add(make_card_instance(f"card-{i}"))

        rng1 = SeededRandom(seed=42)
        rng2 = SeededRandom(seed=42)

        zone1.shuffle(rng1)
        zone2.shuffle(rng2)

        order1 = [c.instance_id for c in zone1.cards]
        order2 = [c.instance_id for c in zone2.cards]
        assert order1 == order2

    def test_zone_shuffle_changes_order(self) -> None:
        """
        Verify shuffle() changes the order of cards.

        With 10 cards, probability of unchanged order is negligible.
        """
        zone = Zone()
        for i in range(10):
            zone.add(make_card_instance(f"card-{i}"))

        original_order = [c.instance_id for c in zone.cards]
        rng = SeededRandom(seed=12345)
        zone.shuffle(rng)
        new_order = [c.instance_id for c in zone.cards]

        assert original_order != new_order

    def test_zone_clear(self) -> None:
        """
        Verify clear() removes and returns all cards.
        """
        zone = Zone()
        for i in range(3):
            zone.add(make_card_instance(f"card-{i}"))

        cleared = zone.clear()

        assert len(cleared) == 3
        assert zone.is_empty()

    def test_zone_get_all(self) -> None:
        """
        Verify get_all() returns a copy of all cards.
        """
        zone = Zone()
        for i in range(3):
            zone.add(make_card_instance(f"card-{i}"))

        all_cards = zone.get_all()

        assert len(all_cards) == 3
        # Modifying the returned list shouldn't affect zone
        all_cards.pop()
        assert len(zone) == 3


class TestZoneSearch:
    """Tests for Zone search operations."""

    def test_zone_find_by_definition(self) -> None:
        """
        Verify find_by_definition() returns cards with matching definition_id.
        """
        zone = Zone()
        zone.add(make_card_instance("inst-1", definition_id="pikachu"))
        zone.add(make_card_instance("inst-2", definition_id="charmander"))
        zone.add(make_card_instance("inst-3", definition_id="pikachu"))

        found = zone.find_by_definition("pikachu")

        assert len(found) == 2
        assert all(c.definition_id == "pikachu" for c in found)

    def test_zone_find_by_definition_none_found(self) -> None:
        """
        Verify find_by_definition() returns empty list when no matches.
        """
        zone = Zone()
        zone.add(make_card_instance("inst-1", definition_id="pikachu"))

        found = zone.find_by_definition("mewtwo")

        assert found == []

    def test_zone_count(self) -> None:
        """
        Verify count() returns the number of cards.

        Alias for __len__().
        """
        zone = Zone()
        assert zone.count() == 0

        zone.add(make_card_instance("card-1"))
        zone.add(make_card_instance("card-2"))

        assert zone.count() == 2


# ============================================================================
# PlayerState Tests
# ============================================================================


class TestPlayerStateCreation:
    """Tests for PlayerState creation and initialization."""

    def test_player_state_defaults(self) -> None:
        """
        Verify PlayerState initializes with empty zones and default counters.

        All per-turn counters should start at 0, and per-game flags should
        start as False.
        """
        player = PlayerState(player_id="player1")

        assert player.player_id == "player1"
        assert player.deck.is_empty()
        assert player.hand.is_empty()
        assert player.active.is_empty()
        assert player.bench.is_empty()
        assert player.discard.is_empty()
        assert player.prizes.is_empty()
        assert player.score == 0
        # Per-turn counters start at 0
        assert player.energy_attachments_this_turn == 0
        assert player.supporters_played_this_turn == 0
        assert player.stadiums_played_this_turn == 0
        assert player.items_played_this_turn == 0
        assert player.retreats_this_turn == 0
        # Per-game flags start as False
        assert not player.gx_attack_used
        assert not player.vstar_power_used

    def test_player_state_zone_types(self) -> None:
        """
        Verify zones are labeled with their zone_type.
        """
        player = PlayerState(player_id="player1")

        assert player.deck.zone_type == "deck"
        assert player.hand.zone_type == "hand"
        assert player.active.zone_type == "active"
        assert player.bench.zone_type == "bench"


class TestPlayerStatePokemonTracking:
    """Tests for PlayerState Pokemon-related methods."""

    def test_get_active_pokemon(self) -> None:
        """
        Verify get_active_pokemon() returns the active Pokemon.
        """
        player = PlayerState(player_id="player1")
        card = make_card_instance("pokemon-1")
        player.active.add(card)

        active = player.get_active_pokemon()

        assert active is card

    def test_get_active_pokemon_none(self) -> None:
        """
        Verify get_active_pokemon() returns None when no active.
        """
        player = PlayerState(player_id="player1")
        assert player.get_active_pokemon() is None

    def test_has_active_pokemon(self) -> None:
        """
        Verify has_active_pokemon() checks active zone.
        """
        player = PlayerState(player_id="player1")
        assert not player.has_active_pokemon()

        player.active.add(make_card_instance("pokemon-1"))
        assert player.has_active_pokemon()

    def test_has_benched_pokemon(self) -> None:
        """
        Verify has_benched_pokemon() checks bench zone.
        """
        player = PlayerState(player_id="player1")
        assert not player.has_benched_pokemon()

        player.bench.add(make_card_instance("pokemon-1"))
        assert player.has_benched_pokemon()

    def test_has_pokemon_in_play(self) -> None:
        """
        Verify has_pokemon_in_play() checks both active and bench.
        """
        player = PlayerState(player_id="player1")
        assert not player.has_pokemon_in_play()

        # Only bench
        player.bench.add(make_card_instance("pokemon-1"))
        assert player.has_pokemon_in_play()

        # Clear and add only active
        player.bench.clear()
        player.active.add(make_card_instance("pokemon-2"))
        assert player.has_pokemon_in_play()

    def test_get_all_pokemon_in_play(self) -> None:
        """
        Verify get_all_pokemon_in_play() returns active + bench.
        """
        player = PlayerState(player_id="player1")
        active = make_card_instance("active-1")
        bench1 = make_card_instance("bench-1")
        bench2 = make_card_instance("bench-2")

        player.active.add(active)
        player.bench.add(bench1)
        player.bench.add(bench2)

        all_pokemon = player.get_all_pokemon_in_play()

        assert len(all_pokemon) == 3
        assert active in all_pokemon
        assert bench1 in all_pokemon
        assert bench2 in all_pokemon


class TestPlayerStateTurnActions:
    """Tests for PlayerState turn action tracking."""

    def test_can_attach_energy_default_rules(self) -> None:
        """
        Verify can_attach_energy() respects turn counter with default rules.

        Default rules allow 1 energy attachment per turn. Counter starts at 0,
        so first attachment is allowed, second is not.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default: attachments_per_turn = 1

        assert player.can_attach_energy(rules)

        player.energy_attachments_this_turn = 1
        assert not player.can_attach_energy(rules)

    def test_can_attach_energy_multiple_per_turn(self) -> None:
        """
        Verify can_attach_energy() respects custom attachments_per_turn rule.

        Some game modes allow multiple energy attachments per turn. This tests
        that the counter-based approach works with configurable limits.
        """
        player = PlayerState(player_id="player1")
        from app.core.config import EnergyConfig

        rules = RulesConfig(energy=EnergyConfig(attachments_per_turn=3))

        assert player.can_attach_energy(rules)

        player.energy_attachments_this_turn = 1
        assert player.can_attach_energy(rules)

        player.energy_attachments_this_turn = 2
        assert player.can_attach_energy(rules)

        player.energy_attachments_this_turn = 3
        assert not player.can_attach_energy(rules)

    def test_can_play_supporter_default_rules(self) -> None:
        """
        Verify can_play_supporter() respects turn counter with default rules.

        Default rules allow 1 Supporter per turn.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default: supporters_per_turn = 1

        assert player.can_play_supporter(rules)

        player.supporters_played_this_turn = 1
        assert not player.can_play_supporter(rules)

    def test_can_play_supporter_multiple_per_turn(self) -> None:
        """
        Verify can_play_supporter() respects custom supporters_per_turn rule.

        Tests configurable Supporter limits for custom game modes.
        """
        player = PlayerState(player_id="player1")
        from app.core.config import TrainerConfig

        rules = RulesConfig(trainer=TrainerConfig(supporters_per_turn=2))

        assert player.can_play_supporter(rules)

        player.supporters_played_this_turn = 1
        assert player.can_play_supporter(rules)

        player.supporters_played_this_turn = 2
        assert not player.can_play_supporter(rules)

    def test_can_play_stadium(self) -> None:
        """
        Verify can_play_stadium() respects turn counter and rules.

        Default rules allow 1 Stadium per turn.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default: stadiums_per_turn = 1

        assert player.can_play_stadium(rules)

        player.stadiums_played_this_turn = 1
        assert not player.can_play_stadium(rules)

    def test_can_play_item_unlimited(self) -> None:
        """
        Verify can_play_item() allows unlimited Items when items_per_turn is None.

        In standard Pokemon TCG rules, Items have no per-turn limit.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default: items_per_turn = None (unlimited)

        assert player.can_play_item(rules)

        player.items_played_this_turn = 10
        assert player.can_play_item(rules)

        player.items_played_this_turn = 100
        assert player.can_play_item(rules)

    def test_can_play_item_limited(self) -> None:
        """
        Verify can_play_item() respects limit when items_per_turn is set.

        Some custom game modes may limit Items per turn for balance.
        """
        player = PlayerState(player_id="player1")
        from app.core.config import TrainerConfig

        rules = RulesConfig(trainer=TrainerConfig(items_per_turn=2))

        assert player.can_play_item(rules)

        player.items_played_this_turn = 1
        assert player.can_play_item(rules)

        player.items_played_this_turn = 2
        assert not player.can_play_item(rules)

    def test_can_retreat_default_rules(self) -> None:
        """
        Verify can_retreat() respects turn counter with default rules.

        Default rules allow 1 retreat per turn.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default: retreats_per_turn = 1

        assert player.can_retreat(rules)

        player.retreats_this_turn = 1
        assert not player.can_retreat(rules)

    def test_can_retreat_disabled(self) -> None:
        """
        Verify can_retreat() returns False when retreats_per_turn is 0.

        Some game modes may disable retreating entirely.
        """
        player = PlayerState(player_id="player1")
        from app.core.config import RetreatConfig

        rules = RulesConfig(retreat=RetreatConfig(retreats_per_turn=0))

        assert not player.can_retreat(rules)

    def test_can_retreat_multiple_per_turn(self) -> None:
        """
        Verify can_retreat() respects custom retreats_per_turn rule.

        Some game modes may allow multiple retreats per turn.
        """
        player = PlayerState(player_id="player1")
        from app.core.config import RetreatConfig

        rules = RulesConfig(retreat=RetreatConfig(retreats_per_turn=2))

        assert player.can_retreat(rules)

        player.retreats_this_turn = 1
        assert player.can_retreat(rules)

        player.retreats_this_turn = 2
        assert not player.can_retreat(rules)

    def test_bench_space_available(self) -> None:
        """
        Verify bench_space_available() calculates remaining bench slots.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()  # Default max_bench = 5

        assert player.bench_space_available(rules) == 5

        for i in range(3):
            player.bench.add(make_card_instance(f"pokemon-{i}"))

        assert player.bench_space_available(rules) == 2

    def test_can_bench_pokemon(self) -> None:
        """
        Verify can_bench_pokemon() checks bench space.
        """
        player = PlayerState(player_id="player1")
        rules = RulesConfig()

        assert player.can_bench_pokemon(rules)

        # Fill bench
        for i in range(5):
            player.bench.add(make_card_instance(f"pokemon-{i}"))

        assert not player.can_bench_pokemon(rules)

    def test_reset_turn_state(self) -> None:
        """
        Verify reset_turn_state() resets all per-turn counters to 0.

        This is called at the start of each turn to allow the player to
        take all their per-turn actions again.
        """
        player = PlayerState(player_id="player1")
        player.energy_attachments_this_turn = 3
        player.supporters_played_this_turn = 2
        player.stadiums_played_this_turn = 1
        player.items_played_this_turn = 5
        player.retreats_this_turn = 2

        # Add a pokemon with ability uses this turn
        pokemon = make_card_instance("pokemon-1")
        pokemon.ability_uses_this_turn = {0: 3, 1: 1}
        player.active.add(pokemon)

        player.reset_turn_state()

        assert player.energy_attachments_this_turn == 0
        assert player.supporters_played_this_turn == 0
        assert player.stadiums_played_this_turn == 0
        assert player.items_played_this_turn == 0
        assert player.retreats_this_turn == 0
        assert pokemon.ability_uses_this_turn == {}


# ============================================================================
# GameState Tests
# ============================================================================


class TestGameStateCreation:
    """Tests for GameState creation and initialization."""

    def test_game_state_minimal(self) -> None:
        """
        Verify GameState can be created with minimal fields.
        """
        game = GameState(game_id="game-1")

        assert game.game_id == "game-1"
        assert game.turn_number == 0
        assert game.phase == TurnPhase.SETUP
        assert game.winner_id is None
        assert not game.is_game_over()

    def test_game_state_with_players(self) -> None:
        """
        Verify GameState can be created with players.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player1",
        )

        assert game.get_player_count() == 2
        assert "player1" in game.players
        assert "player2" in game.players

    def test_game_state_with_card_registry(self) -> None:
        """
        Verify card_registry stores CardDefinitions.
        """
        pikachu = make_pokemon_definition("pikachu-001", "Pikachu")
        charmander = make_pokemon_definition("charmander-001", "Charmander")

        game = GameState(
            game_id="game-1",
            card_registry={
                "pikachu-001": pikachu,
                "charmander-001": charmander,
            },
        )

        assert game.get_card_definition("pikachu-001") is pikachu
        assert game.get_card_definition("charmander-001") is charmander
        assert game.get_card_definition("nonexistent") is None


class TestGameStatePlayerAccess:
    """Tests for GameState player access methods."""

    def test_get_current_player(self) -> None:
        """
        Verify get_current_player() returns the active player.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            current_player_id="player1",
        )

        current = game.get_current_player()

        assert current.player_id == "player1"

    def test_get_opponent_id(self) -> None:
        """
        Verify get_opponent_id() returns the other player's ID.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
        )

        assert game.get_opponent_id("player1") == "player2"
        assert game.get_opponent_id("player2") == "player1"

    def test_get_opponent(self) -> None:
        """
        Verify get_opponent() returns the opponent's PlayerState.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
        )

        opponent = game.get_opponent("player1")

        assert opponent.player_id == "player2"

    def test_is_player_turn(self) -> None:
        """
        Verify is_player_turn() checks current player.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            current_player_id="player1",
        )

        assert game.is_player_turn("player1")
        assert not game.is_player_turn("player2")


class TestGameStateTurnManagement:
    """Tests for GameState turn management."""

    def test_is_first_turn(self) -> None:
        """
        Verify is_first_turn() returns True only on turn 1.
        """
        game = GameState(game_id="game-1", turn_number=1)

        assert game.is_first_turn()

        game.first_turn_completed = True
        assert not game.is_first_turn()

        game.turn_number = 2
        assert not game.is_first_turn()

    def test_advance_turn(self) -> None:
        """
        Verify advance_turn() switches players and updates state.

        Tests that advancing the turn:
        1. Switches to the next player in turn order
        2. Marks first turn as completed after turn 1
        3. Sets phase to DRAW
        4. Resets the new player's turn counters
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player1",
            turn_number=1,
            phase=TurnPhase.END,
        )

        # Set some turn counters on player 2 to verify reset
        game.players["player2"].energy_attachments_this_turn = 1
        game.players["player2"].supporters_played_this_turn = 1

        game.advance_turn()

        assert game.current_player_id == "player2"
        assert game.first_turn_completed is True
        assert game.phase == TurnPhase.DRAW
        assert game.players["player2"].energy_attachments_this_turn == 0
        assert game.players["player2"].supporters_played_this_turn == 0

    def test_advance_turn_wraps_around(self) -> None:
        """
        Verify advance_turn() wraps back to first player and increments turn.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player2",
            turn_number=1,
            first_turn_completed=True,
        )

        game.advance_turn()

        assert game.current_player_id == "player1"
        assert game.turn_number == 2


class TestGameStateWinConditions:
    """Tests for GameState win condition tracking."""

    def test_set_winner(self) -> None:
        """
        Verify set_winner() ends the game.
        """
        game = GameState(game_id="game-1")

        game.set_winner("player1", GameEndReason.PRIZES_TAKEN)

        assert game.is_game_over()
        assert game.winner_id == "player1"
        assert game.end_reason == GameEndReason.PRIZES_TAKEN

    def test_is_game_over(self) -> None:
        """
        Verify is_game_over() checks winner_id.
        """
        game = GameState(game_id="game-1")

        assert not game.is_game_over()

        game.winner_id = "player1"
        assert game.is_game_over()


class TestForcedActionQueue:
    """Tests for the forced action queue system.

    The forced action queue allows multiple forced actions to be queued
    and processed in FIFO order. This is essential for scenarios like
    double knockouts where both players need to select new active Pokemon.
    """

    def test_has_forced_action_empty(self) -> None:
        """
        Verify has_forced_action returns False when queue is empty.
        """
        game = GameState(game_id="test")
        assert not game.has_forced_action()

    def test_has_forced_action_with_items(self) -> None:
        """
        Verify has_forced_action returns True when queue has items.
        """
        from app.core.models.game_state import ForcedAction

        game = GameState(game_id="test")
        game.add_forced_action(
            ForcedAction(
                player_id="player1",
                action_type="select_active",
                reason="Test",
            )
        )
        assert game.has_forced_action()

    def test_add_forced_action_fifo_order(self) -> None:
        """
        Verify forced actions are processed in FIFO order.

        First action added should be first action returned.
        """
        from app.core.models.game_state import ForcedAction

        game = GameState(game_id="test")

        # Add actions in order
        game.add_forced_action(
            ForcedAction(player_id="player1", action_type="select_active", reason="First")
        )
        game.add_forced_action(
            ForcedAction(player_id="player2", action_type="select_active", reason="Second")
        )
        game.add_forced_action(
            ForcedAction(player_id="player1", action_type="select_prize", reason="Third")
        )

        # First should be player1's first action
        first = game.get_current_forced_action()
        assert first.player_id == "player1"
        assert first.reason == "First"

    def test_pop_forced_action_removes_first(self) -> None:
        """
        Verify pop_forced_action removes and returns the first item.
        """
        from app.core.models.game_state import ForcedAction

        game = GameState(game_id="test")

        game.add_forced_action(
            ForcedAction(player_id="player1", action_type="select_active", reason="First")
        )
        game.add_forced_action(
            ForcedAction(player_id="player2", action_type="select_active", reason="Second")
        )

        # Pop first
        popped = game.pop_forced_action()
        assert popped.reason == "First"

        # Second should now be current
        current = game.get_current_forced_action()
        assert current.reason == "Second"

        # Pop second
        popped = game.pop_forced_action()
        assert popped.reason == "Second"

        # Queue should now be empty
        assert not game.has_forced_action()
        assert game.get_current_forced_action() is None
        assert game.pop_forced_action() is None

    def test_clear_forced_actions(self) -> None:
        """
        Verify clear_forced_actions removes all items.
        """
        from app.core.models.game_state import ForcedAction

        game = GameState(game_id="test")

        game.add_forced_action(
            ForcedAction(player_id="player1", action_type="select_active", reason="First")
        )
        game.add_forced_action(
            ForcedAction(player_id="player2", action_type="select_active", reason="Second")
        )

        assert game.has_forced_action()

        game.clear_forced_actions()

        assert not game.has_forced_action()
        assert len(game.forced_actions) == 0

    def test_double_knockout_scenario(self) -> None:
        """
        Test the double knockout scenario where both players need actions.

        When both players' active Pokemon are knocked out simultaneously
        (e.g., from mutual damage or poison), both need to select new actives.
        The queue ensures both actions are tracked and processed in order.
        """
        from app.core.models.game_state import ForcedAction

        game = GameState(game_id="test")

        # Simulate double KO - both players need to select new active
        # Player 1's active was KO'd first (or turn order determines)
        game.add_forced_action(
            ForcedAction(
                player_id="player1",
                action_type="select_active",
                reason="Player1's active was knocked out",
            )
        )
        # Player 2's active was also KO'd
        game.add_forced_action(
            ForcedAction(
                player_id="player2",
                action_type="select_active",
                reason="Player2's active was knocked out",
            )
        )

        # Two actions should be queued
        assert len(game.forced_actions) == 2

        # Player 1 acts first
        current = game.get_current_forced_action()
        assert current.player_id == "player1"

        # Player 1 completes their action
        game.pop_forced_action()

        # Now player 2 should act
        current = game.get_current_forced_action()
        assert current.player_id == "player2"

        # Player 2 completes their action
        game.pop_forced_action()

        # Queue should be empty
        assert not game.has_forced_action()


class TestGameStateCardSearch:
    """Tests for GameState card search operations."""

    def test_find_card_instance(self) -> None:
        """
        Verify find_card_instance() searches all zones.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
            },
        )

        card = make_card_instance("test-card")
        game.players["player1"].hand.add(card)

        found, zone = game.find_card_instance("test-card")

        assert found is card
        assert zone == "hand"

    def test_find_card_instance_not_found(self) -> None:
        """
        Verify find_card_instance() returns None for missing cards.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
            },
        )

        found, zone = game.find_card_instance("nonexistent")

        assert found is None
        assert zone is None

    def test_get_all_cards_in_play(self) -> None:
        """
        Verify get_all_cards_in_play() returns all active/bench Pokemon.
        """
        game = GameState(
            game_id="game-1",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
        )

        p1_active = make_card_instance("p1-active")
        p1_bench = make_card_instance("p1-bench")
        p2_active = make_card_instance("p2-active")

        game.players["player1"].active.add(p1_active)
        game.players["player1"].bench.add(p1_bench)
        game.players["player2"].active.add(p2_active)

        all_in_play = game.get_all_cards_in_play()

        assert len(all_in_play) == 3
        assert p1_active in all_in_play
        assert p1_bench in all_in_play
        assert p2_active in all_in_play


class TestGameStateActionLog:
    """Tests for GameState action logging."""

    def test_log_action(self) -> None:
        """
        Verify log_action() appends to action_log.
        """
        game = GameState(game_id="game-1")

        game.log_action({"type": "play_pokemon", "card_id": "pikachu-1"})
        game.log_action({"type": "attack", "attack_index": 0})

        assert len(game.action_log) == 2
        assert game.action_log[0]["type"] == "play_pokemon"
        assert game.action_log[1]["type"] == "attack"


class TestGameStateJsonRoundTrip:
    """Tests for JSON serialization of GameState."""

    def test_zone_round_trip(self) -> None:
        """
        Verify Zone serializes and deserializes correctly.
        """
        zone = Zone(zone_type="hand")
        zone.add(make_card_instance("card-1", "pikachu"))
        zone.add(make_card_instance("card-2", "charmander"))

        json_str = zone.model_dump_json()
        restored = Zone.model_validate_json(json_str)

        assert restored.zone_type == "hand"
        assert len(restored) == 2
        assert restored.cards[0].instance_id == "card-1"

    def test_player_state_round_trip(self) -> None:
        """
        Verify PlayerState serializes and deserializes correctly.

        Tests that all fields including turn counters survive JSON round-trip.
        """
        player = PlayerState(player_id="player1")
        player.hand.add(make_card_instance("hand-1"))
        player.score = 3
        player.energy_attachments_this_turn = 2
        player.supporters_played_this_turn = 1

        json_str = player.model_dump_json()
        restored = PlayerState.model_validate_json(json_str)

        assert restored.player_id == "player1"
        assert len(restored.hand) == 1
        assert restored.score == 3
        assert restored.energy_attachments_this_turn == 2
        assert restored.supporters_played_this_turn == 1

    def test_game_state_round_trip(self) -> None:
        """
        Verify complete GameState serializes and deserializes correctly.

        This is critical for saving/loading games and sending state to clients.
        """
        pikachu = make_pokemon_definition("pikachu-001", "Pikachu")

        game = GameState(
            game_id="game-123",
            rules=RulesConfig(),
            card_registry={"pikachu-001": pikachu},
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            turn_order=["player1", "player2"],
            current_player_id="player1",
            turn_number=3,
            phase=TurnPhase.MAIN,
        )

        # Add some cards
        card = make_card_instance("inst-1", "pikachu-001")
        game.players["player1"].active.add(card)

        json_str = game.model_dump_json()
        restored = GameState.model_validate_json(json_str)

        assert restored.game_id == "game-123"
        assert restored.turn_number == 3
        assert restored.phase == TurnPhase.MAIN
        assert "pikachu-001" in restored.card_registry
        assert len(restored.players["player1"].active) == 1
