"""Tests for the visibility filter module.

This module contains SECURITY-CRITICAL tests that verify hidden information
is never leaked to unauthorized viewers. These tests are essential for
preventing cheating in multiplayer games.

Test categories:
    - Own information visibility (player can see their own hand, etc.)
    - Opponent information hiding (opponent's hand, deck hidden)
    - Public information visibility (battlefield, discard, scores)
    - Spectator mode (no hands visible)
    - Edge cases and error handling
"""

import pytest

from app.core.config import RulesConfig
from app.core.enums import (
    CardType,
    EnergyType,
    GameEndReason,
    PokemonStage,
    PokemonVariant,
    TrainerType,
    TurnPhase,
)
from app.core.models.card import CardDefinition, CardInstance
from app.core.models.game_state import ForcedAction, GameState, PlayerState
from app.core.visibility import (
    VisibleGameState,
    get_spectator_state,
    get_visible_state,
)

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


@pytest.fixture
def pokemon_def() -> CardDefinition:
    """Create a basic Pokemon card definition."""
    return CardDefinition(
        id="pikachu-001",
        name="Pikachu",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        variant=PokemonVariant.NORMAL,
        hp=60,
        pokemon_type=EnergyType.LIGHTNING,
    )


@pytest.fixture
def energy_def() -> CardDefinition:
    """Create a basic energy card definition."""
    return CardDefinition(
        id="lightning-energy-001",
        name="Lightning Energy",
        card_type=CardType.ENERGY,
        energy_type=EnergyType.LIGHTNING,
    )


@pytest.fixture
def trainer_def() -> CardDefinition:
    """Create a trainer card definition."""
    return CardDefinition(
        id="potion-001",
        name="Potion",
        card_type=CardType.TRAINER,
        trainer_type=TrainerType.ITEM,
    )


@pytest.fixture
def full_game(
    pokemon_def: CardDefinition,
    energy_def: CardDefinition,
    trainer_def: CardDefinition,
) -> GameState:
    """Create a full game state with cards in various zones.

    Player 1 has:
        - 3 cards in hand (including a secret trainer)
        - 1 active Pokemon
        - 2 benched Pokemon
        - 5 cards in deck
        - 3 prize cards
        - 2 cards in discard
        - 3 cards in energy deck
        - 1 card in energy zone

    Player 2 has:
        - 4 cards in hand
        - 1 active Pokemon
        - 1 benched Pokemon
        - 4 cards in deck
        - 3 prize cards
        - 1 card in discard
        - 2 cards in energy deck
        - 2 cards in energy zone
    """
    # Create card instances for player 1
    p1_hand = [
        CardInstance(instance_id="p1-hand-0", definition_id=pokemon_def.id),
        CardInstance(instance_id="p1-hand-1", definition_id=energy_def.id),
        CardInstance(instance_id="p1-hand-2", definition_id=trainer_def.id),  # Secret!
    ]
    p1_active = CardInstance(instance_id="p1-active", definition_id=pokemon_def.id)
    p1_bench = [
        CardInstance(instance_id="p1-bench-0", definition_id=pokemon_def.id),
        CardInstance(instance_id="p1-bench-1", definition_id=pokemon_def.id),
    ]
    p1_deck = [
        CardInstance(instance_id=f"p1-deck-{i}", definition_id=pokemon_def.id) for i in range(5)
    ]
    p1_prizes = [
        CardInstance(instance_id=f"p1-prize-{i}", definition_id=pokemon_def.id) for i in range(3)
    ]
    p1_discard = [
        CardInstance(instance_id="p1-discard-0", definition_id=energy_def.id),
        CardInstance(instance_id="p1-discard-1", definition_id=trainer_def.id),
    ]
    p1_energy_deck = [
        CardInstance(instance_id=f"p1-energy-deck-{i}", definition_id=energy_def.id)
        for i in range(3)
    ]
    p1_energy_zone = [
        CardInstance(instance_id="p1-energy-zone-0", definition_id=energy_def.id),
    ]

    # Create card instances for player 2
    p2_hand = [
        CardInstance(instance_id=f"p2-hand-{i}", definition_id=pokemon_def.id) for i in range(4)
    ]
    p2_active = CardInstance(instance_id="p2-active", definition_id=pokemon_def.id)
    p2_bench = [
        CardInstance(instance_id="p2-bench-0", definition_id=pokemon_def.id),
    ]
    p2_deck = [
        CardInstance(instance_id=f"p2-deck-{i}", definition_id=pokemon_def.id) for i in range(4)
    ]
    p2_prizes = [
        CardInstance(instance_id=f"p2-prize-{i}", definition_id=pokemon_def.id) for i in range(3)
    ]
    p2_discard = [
        CardInstance(instance_id="p2-discard-0", definition_id=energy_def.id),
    ]
    p2_energy_deck = [
        CardInstance(instance_id=f"p2-energy-deck-{i}", definition_id=energy_def.id)
        for i in range(2)
    ]
    p2_energy_zone = [
        CardInstance(instance_id=f"p2-energy-zone-{i}", definition_id=energy_def.id)
        for i in range(2)
    ]

    # Build player states
    p1 = PlayerState(player_id="player1", score=2)
    for card in p1_hand:
        p1.hand.add(card)
    p1.active.add(p1_active)
    for card in p1_bench:
        p1.bench.add(card)
    for card in p1_deck:
        p1.deck.add(card)
    for card in p1_prizes:
        p1.prizes.add(card)
    for card in p1_discard:
        p1.discard.add(card)
    for card in p1_energy_deck:
        p1.energy_deck.add(card)
    for card in p1_energy_zone:
        p1.energy_zone.add(card)

    p2 = PlayerState(player_id="player2", score=1)
    for card in p2_hand:
        p2.hand.add(card)
    p2.active.add(p2_active)
    for card in p2_bench:
        p2.bench.add(card)
    for card in p2_deck:
        p2.deck.add(card)
    for card in p2_prizes:
        p2.prizes.add(card)
    for card in p2_discard:
        p2.discard.add(card)
    for card in p2_energy_deck:
        p2.energy_deck.add(card)
    for card in p2_energy_zone:
        p2.energy_zone.add(card)

    # Build game state
    game = GameState(
        game_id="test-game",
        rules=RulesConfig(),
        card_registry={
            pokemon_def.id: pokemon_def,
            energy_def.id: energy_def,
            trainer_def.id: trainer_def,
        },
        players={"player1": p1, "player2": p2},
        current_player_id="player1",
        turn_number=3,
        phase=TurnPhase.MAIN,
        turn_order=["player1", "player2"],
    )

    return game


# =============================================================================
# Own Information Visibility Tests
# =============================================================================


class TestOwnInformationVisibility:
    """Tests verifying players can see their own information."""

    def test_player_sees_own_hand_contents(self, full_game: GameState):
        """
        Test that a player can see the full contents of their own hand.

        This is essential for gameplay - players must know what cards
        they can play.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.hand.count == 3
        assert len(own_state.hand.cards) == 3
        # Verify specific cards are visible
        hand_ids = [c.instance_id for c in own_state.hand.cards]
        assert "p1-hand-0" in hand_ids
        assert "p1-hand-1" in hand_ids
        assert "p1-hand-2" in hand_ids

    def test_player_sees_own_active_pokemon(self, full_game: GameState):
        """
        Test that a player can see their own active Pokemon.

        Active Pokemon details are always public.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.active.count == 1
        assert len(own_state.active.cards) == 1
        assert own_state.active.cards[0].instance_id == "p1-active"

    def test_player_sees_own_bench(self, full_game: GameState):
        """
        Test that a player can see their own benched Pokemon.

        Bench contents are always public.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.bench.count == 2
        assert len(own_state.bench.cards) == 2

    def test_player_sees_own_discard(self, full_game: GameState):
        """
        Test that a player can see their own discard pile.

        Discard piles are always public.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.discard.count == 2
        assert len(own_state.discard.cards) == 2

    def test_player_sees_own_deck_count_only(self, full_game: GameState):
        """
        Test that a player sees their deck count but not contents.

        Even your own deck order is hidden to maintain game integrity.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.deck_count == 5

    def test_player_sees_own_prize_count_only(self, full_game: GameState):
        """
        Test that a player sees their prize count but not contents.

        Prize cards are revealed only when taken.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.prizes_count == 3

    def test_player_sees_own_energy_zone(self, full_game: GameState):
        """
        Test that a player can see their energy zone (available energy).

        Energy zone is public - it shows what energy can be attached.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.energy_zone.count == 1
        assert len(own_state.energy_zone.cards) == 1

    def test_player_sees_own_score(self, full_game: GameState):
        """
        Test that a player can see their own score.

        Scores are always public.
        """
        visible = get_visible_state(full_game, "player1")
        own_state = visible.players["player1"]

        assert own_state.score == 2

    def test_is_current_player_flag(self, full_game: GameState):
        """
        Test that is_current_player is set correctly for self.

        This flag helps the UI know which player state is "mine".
        """
        visible = get_visible_state(full_game, "player1")

        assert visible.players["player1"].is_current_player is True
        assert visible.players["player2"].is_current_player is False


# =============================================================================
# Opponent Information Hiding Tests (SECURITY CRITICAL)
# =============================================================================


class TestOpponentInformationHiding:
    """SECURITY CRITICAL: Tests verifying opponent information is hidden."""

    def test_opponent_hand_contents_hidden(self, full_game: GameState):
        """
        SECURITY: Opponent's hand contents must NEVER be visible.

        This is the most critical security test. Leaking hand contents
        would allow cheating.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        # Count should be visible
        assert opponent_state.hand.count == 4

        # Contents must be empty
        assert len(opponent_state.hand.cards) == 0

    def test_opponent_hand_card_ids_not_leaked(self, full_game: GameState):
        """
        SECURITY: Opponent's hand card IDs must not be exposed anywhere.

        Verify no hand card IDs appear in the visible state.
        """
        visible = get_visible_state(full_game, "player1")

        # Serialize to check for any leakage
        json_str = visible.model_dump_json()

        # These are player 2's hand cards - they should not appear
        assert "p2-hand-0" not in json_str
        assert "p2-hand-1" not in json_str
        assert "p2-hand-2" not in json_str
        assert "p2-hand-3" not in json_str

    def test_opponent_deck_order_hidden(self, full_game: GameState):
        """
        SECURITY: Opponent's deck order must not be visible.

        Only the count is allowed.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        # Only count, no card data
        assert opponent_state.deck_count == 4

    def test_opponent_deck_cards_not_leaked(self, full_game: GameState):
        """
        SECURITY: Opponent's deck card IDs must not appear anywhere.
        """
        visible = get_visible_state(full_game, "player1")
        json_str = visible.model_dump_json()

        # Player 2's deck cards should not appear
        for i in range(4):
            assert f"p2-deck-{i}" not in json_str

    def test_opponent_prizes_hidden(self, full_game: GameState):
        """
        SECURITY: Opponent's prize card contents must be hidden.

        Only count is visible.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.prizes_count == 3

    def test_opponent_prize_cards_not_leaked(self, full_game: GameState):
        """
        SECURITY: Opponent's prize card IDs must not appear anywhere.
        """
        visible = get_visible_state(full_game, "player1")
        json_str = visible.model_dump_json()

        for i in range(3):
            assert f"p2-prize-{i}" not in json_str

    def test_opponent_energy_deck_hidden(self, full_game: GameState):
        """
        SECURITY: Opponent's energy deck order must be hidden.

        Only count visible.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.energy_deck_count == 2

    def test_own_deck_contents_also_hidden(self, full_game: GameState):
        """
        SECURITY: Even own deck contents are hidden (integrity).

        This prevents any deck manipulation or tracking beyond what
        cards have been seen.
        """
        visible = get_visible_state(full_game, "player1")
        json_str = visible.model_dump_json()

        # Own deck cards should also not appear
        for i in range(5):
            assert f"p1-deck-{i}" not in json_str

    def test_own_prize_contents_hidden(self, full_game: GameState):
        """
        SECURITY: Own prize card contents are hidden until taken.
        """
        visible = get_visible_state(full_game, "player1")
        json_str = visible.model_dump_json()

        for i in range(3):
            assert f"p1-prize-{i}" not in json_str


# =============================================================================
# Public Information Tests
# =============================================================================


class TestPublicInformation:
    """Tests verifying public information is correctly visible."""

    def test_opponent_active_visible(self, full_game: GameState):
        """
        Test that opponent's active Pokemon is fully visible.

        Active Pokemon are on the battlefield - always public.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.active.count == 1
        assert len(opponent_state.active.cards) == 1
        assert opponent_state.active.cards[0].instance_id == "p2-active"

    def test_opponent_bench_visible(self, full_game: GameState):
        """
        Test that opponent's benched Pokemon are fully visible.

        Bench is part of the battlefield - always public.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.bench.count == 1
        assert len(opponent_state.bench.cards) == 1
        assert opponent_state.bench.cards[0].instance_id == "p2-bench-0"

    def test_opponent_discard_visible(self, full_game: GameState):
        """
        Test that opponent's discard pile is fully visible.

        Discard piles are always public knowledge.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.discard.count == 1
        assert len(opponent_state.discard.cards) == 1
        assert opponent_state.discard.cards[0].instance_id == "p2-discard-0"

    def test_opponent_energy_zone_visible(self, full_game: GameState):
        """
        Test that opponent's energy zone is visible.

        Energy zone shows available energy - public information.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.energy_zone.count == 2
        assert len(opponent_state.energy_zone.cards) == 2

    def test_opponent_score_visible(self, full_game: GameState):
        """
        Test that opponent's score is visible.

        Scores are always public.
        """
        visible = get_visible_state(full_game, "player1")
        opponent_state = visible.players["player2"]

        assert opponent_state.score == 1

    def test_stadium_visible(self, full_game: GameState, pokemon_def: CardDefinition):
        """
        Test that stadium in play is visible to all.
        """
        stadium = CardInstance(instance_id="stadium-1", definition_id=pokemon_def.id)
        full_game.stadium_in_play = stadium

        visible = get_visible_state(full_game, "player1")

        assert visible.stadium_in_play is not None
        assert visible.stadium_in_play.instance_id == "stadium-1"

    def test_card_registry_included(self, full_game: GameState):
        """
        Test that card registry is included for display purposes.

        Clients need card definitions to render cards properly.
        """
        visible = get_visible_state(full_game, "player1")

        assert len(visible.card_registry) == 3
        assert "pikachu-001" in visible.card_registry
        assert "lightning-energy-001" in visible.card_registry
        assert "potion-001" in visible.card_registry


# =============================================================================
# Game State Information Tests
# =============================================================================


class TestGameStateInformation:
    """Tests for game-level visible information."""

    def test_game_id_visible(self, full_game: GameState):
        """
        Test that game ID is included.
        """
        visible = get_visible_state(full_game, "player1")
        assert visible.game_id == "test-game"

    def test_viewer_id_set(self, full_game: GameState):
        """
        Test that viewer_id identifies who the view is for.
        """
        visible = get_visible_state(full_game, "player1")
        assert visible.viewer_id == "player1"

    def test_current_player_visible(self, full_game: GameState):
        """
        Test that current player is visible.
        """
        visible = get_visible_state(full_game, "player1")
        assert visible.current_player_id == "player1"

    def test_turn_number_visible(self, full_game: GameState):
        """
        Test that turn number is visible.
        """
        visible = get_visible_state(full_game, "player1")
        assert visible.turn_number == 3

    def test_phase_visible(self, full_game: GameState):
        """
        Test that current phase is visible.
        """
        visible = get_visible_state(full_game, "player1")
        assert visible.phase == TurnPhase.MAIN

    def test_is_my_turn_flag(self, full_game: GameState):
        """
        Test that is_my_turn is correctly set.
        """
        visible_p1 = get_visible_state(full_game, "player1")
        visible_p2 = get_visible_state(full_game, "player2")

        assert visible_p1.is_my_turn is True
        assert visible_p2.is_my_turn is False

    def test_winner_visible_when_game_over(self, full_game: GameState):
        """
        Test that winner is visible when game ends.
        """
        full_game.winner_id = "player1"
        full_game.end_reason = GameEndReason.PRIZES_TAKEN

        visible = get_visible_state(full_game, "player2")

        assert visible.winner_id == "player1"
        assert visible.end_reason == GameEndReason.PRIZES_TAKEN

    def test_forced_action_visible(self, full_game: GameState):
        """
        Test that forced action information is visible.

        Both players need to know when a forced action is pending.
        """
        full_game.add_forced_action(
            ForcedAction(
                player_id="player1",
                action_type="select_active",
                reason="Your active Pokemon was knocked out.",
            )
        )

        visible = get_visible_state(full_game, "player2")

        assert visible.forced_action_player == "player1"
        assert visible.forced_action_type == "select_active"
        assert visible.forced_action_reason == "Your active Pokemon was knocked out."


# =============================================================================
# Spectator Mode Tests
# =============================================================================


class TestSpectatorMode:
    """Tests for spectator view (no hands visible)."""

    def test_spectator_sees_no_hands(self, full_game: GameState):
        """
        Test that spectators cannot see any player's hand.
        """
        visible = get_spectator_state(full_game)

        # Both hands should show count only
        assert visible.players["player1"].hand.count == 3
        assert len(visible.players["player1"].hand.cards) == 0

        assert visible.players["player2"].hand.count == 4
        assert len(visible.players["player2"].hand.cards) == 0

    def test_spectator_sees_battlefield(self, full_game: GameState):
        """
        Test that spectators can see all battlefield information.
        """
        visible = get_spectator_state(full_game)

        # Active Pokemon visible
        assert len(visible.players["player1"].active.cards) == 1
        assert len(visible.players["player2"].active.cards) == 1

        # Bench visible
        assert len(visible.players["player1"].bench.cards) == 2
        assert len(visible.players["player2"].bench.cards) == 1

        # Discard visible
        assert len(visible.players["player1"].discard.cards) == 2
        assert len(visible.players["player2"].discard.cards) == 1

    def test_spectator_is_not_current_player(self, full_game: GameState):
        """
        Test that spectator is never marked as current player.
        """
        visible = get_spectator_state(full_game)

        assert visible.is_my_turn is False
        assert visible.players["player1"].is_current_player is False
        assert visible.players["player2"].is_current_player is False

    def test_spectator_viewer_id(self, full_game: GameState):
        """
        Test that spectator has special viewer ID.
        """
        visible = get_spectator_state(full_game)
        assert visible.viewer_id == "__spectator__"


# =============================================================================
# Edge Cases and Error Handling
# =============================================================================


class TestEdgeCases:
    """Tests for edge cases and error handling."""

    def test_invalid_viewer_raises_error(self, full_game: GameState):
        """
        Test that requesting view for non-existent player raises error.
        """
        with pytest.raises(ValueError) as exc_info:
            get_visible_state(full_game, "nonexistent")

        assert "nonexistent" in str(exc_info.value)
        assert "not a player" in str(exc_info.value)

    def test_empty_zones_handled(self):
        """
        Test that empty zones are handled correctly.
        """
        game = GameState(
            game_id="empty-game",
            players={
                "player1": PlayerState(player_id="player1"),
                "player2": PlayerState(player_id="player2"),
            },
            current_player_id="player1",
            turn_order=["player1", "player2"],
        )

        visible = get_visible_state(game, "player1")

        assert visible.players["player1"].hand.count == 0
        assert visible.players["player1"].deck_count == 0
        assert visible.players["player1"].active.count == 0

    def test_no_stadium_in_play(self, full_game: GameState):
        """
        Test that missing stadium is handled correctly.
        """
        full_game.stadium_in_play = None

        visible = get_visible_state(full_game, "player1")

        assert visible.stadium_in_play is None

    def test_no_forced_action(self, full_game: GameState):
        """
        Test that missing forced action is handled correctly.
        """
        full_game.clear_forced_actions()

        visible = get_visible_state(full_game, "player1")

        assert visible.forced_action_player is None
        assert visible.forced_action_type is None
        assert visible.forced_action_reason is None

    def test_gx_vstar_flags_visible(self, full_game: GameState):
        """
        Test that GX/VSTAR usage flags are visible.

        These are public - opponents need to know if you've used
        your once-per-game abilities.
        """
        full_game.players["player1"].gx_attack_used = True
        full_game.players["player2"].vstar_power_used = True

        visible = get_visible_state(full_game, "player1")

        assert visible.players["player1"].gx_attack_used is True
        assert visible.players["player2"].vstar_power_used is True


# =============================================================================
# Serialization Tests
# =============================================================================


class TestSerialization:
    """Tests verifying visible state serializes correctly."""

    def test_visible_state_serializes_to_json(self, full_game: GameState):
        """
        Test that VisibleGameState can be serialized to JSON.

        This is essential for sending to clients over WebSocket.
        """
        visible = get_visible_state(full_game, "player1")

        # Should not raise
        json_str = visible.model_dump_json()

        assert isinstance(json_str, str)
        assert len(json_str) > 0

    def test_visible_state_roundtrips(self, full_game: GameState):
        """
        Test that VisibleGameState can be deserialized from JSON.
        """
        visible = get_visible_state(full_game, "player1")
        json_str = visible.model_dump_json()

        # Should not raise
        restored = VisibleGameState.model_validate_json(json_str)

        assert restored.game_id == visible.game_id
        assert restored.viewer_id == visible.viewer_id
        assert restored.turn_number == visible.turn_number
