"""Tests for the effect system base types (EffectContext, EffectResult).

These tests verify:
1. EffectResult creation and factory methods
2. EffectContext initialization and helper methods
3. Player and card access through context
4. Parameter access helpers
5. Random operations through context
"""

from app.core.config import RulesConfig
from app.core.effects.base import EffectContext, EffectResult, EffectType
from app.core.enums import CardType, EnergyType, PokemonStage
from app.core.models.card import CardDefinition, CardInstance
from app.core.models.game_state import GameState, PlayerState
from app.core.rng import SeededRandom

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


def make_test_game() -> GameState:
    """Create a simple game state for testing."""
    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,
    )

    game = GameState(
        game_id="test-game",
        rules=RulesConfig(),
        card_registry={
            "pikachu-001": pikachu_def,
            "charmander-001": charmander_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 for each player
    pikachu = CardInstance(instance_id="pikachu-inst-1", definition_id="pikachu-001")
    charmander = CardInstance(instance_id="charmander-inst-1", definition_id="charmander-001")

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

    return game


# ============================================================================
# EffectResult Tests
# ============================================================================


class TestEffectResult:
    """Tests for EffectResult creation and methods."""

    def test_success_result_basic(self) -> None:
        """
        Verify success_result creates a successful result with message.
        """
        result = EffectResult.success_result("Dealt 30 damage")

        assert result.success is True
        assert result.message == "Dealt 30 damage"
        assert result.effect_type is None
        assert result.details == {}

    def test_success_result_with_type(self) -> None:
        """
        Verify success_result can include an effect type.
        """
        result = EffectResult.success_result(
            "Dealt 30 damage",
            effect_type=EffectType.DAMAGE,
        )

        assert result.success is True
        assert result.effect_type == EffectType.DAMAGE

    def test_success_result_with_details(self) -> None:
        """
        Verify success_result can include additional details.
        """
        result = EffectResult.success_result(
            "Dealt 30 damage",
            effect_type=EffectType.DAMAGE,
            details={"amount": 30, "target": "charmander-inst-1"},
        )

        assert result.details["amount"] == 30
        assert result.details["target"] == "charmander-inst-1"

    def test_failure_result(self) -> None:
        """
        Verify failure creates a failed result.
        """
        result = EffectResult.failure("No valid target")

        assert result.success is False
        assert result.message == "No valid target"
        assert result.effect_type is None

    def test_result_json_round_trip(self) -> None:
        """
        Verify EffectResult survives JSON serialization.
        """
        original = EffectResult.success_result(
            "Drew 2 cards",
            effect_type=EffectType.DRAW,
            details={"count": 2},
        )

        json_str = original.model_dump_json()
        restored = EffectResult.model_validate_json(json_str)

        assert restored.success is True
        assert restored.message == "Drew 2 cards"
        assert restored.effect_type == EffectType.DRAW
        assert restored.details["count"] == 2


class TestEffectType:
    """Tests for EffectType enum."""

    def test_all_effect_types_exist(self) -> None:
        """
        Verify all expected effect types are defined.
        """
        expected = [
            "damage",
            "heal",
            "draw",
            "discard",
            "search",
            "shuffle",
            "status",
            "energy",
            "bench",
            "modifier",
            "coin_flip",
            "special",
        ]

        for effect_type in expected:
            assert effect_type in [e.value for e in EffectType]

    def test_effect_type_is_string(self) -> None:
        """
        Verify EffectType values are strings for JSON compatibility.
        """
        assert EffectType.DAMAGE == "damage"
        assert EffectType.HEAL == "heal"


# ============================================================================
# EffectContext Tests
# ============================================================================


class TestEffectContextCreation:
    """Tests for EffectContext initialization."""

    def test_basic_context_creation(self) -> None:
        """
        Verify EffectContext can be created with minimal required fields.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)

        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            rng=rng,
        )

        assert ctx.game is game
        assert ctx.source_player_id == "player1"
        assert ctx.rng is rng
        assert ctx.source_card_id is None
        assert ctx.target_player_id is None
        assert ctx.target_card_id is None
        assert ctx.params == {}

    def test_full_context_creation(self) -> None:
        """
        Verify EffectContext can be created with all optional fields.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)

        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            rng=rng,
            source_card_id="pikachu-inst-1",
            target_player_id="player2",
            target_card_id="charmander-inst-1",
            params={"amount": 30, "effect": "paralysis"},
        )

        assert ctx.source_card_id == "pikachu-inst-1"
        assert ctx.target_player_id == "player2"
        assert ctx.target_card_id == "charmander-inst-1"
        assert ctx.params["amount"] == 30


class TestEffectContextPlayerAccess:
    """Tests for player access methods."""

    def test_get_source_player(self) -> None:
        """
        Verify get_source_player returns the correct player.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        player = ctx.get_source_player()

        assert player.player_id == "player1"

    def test_get_target_player_when_specified(self) -> None:
        """
        Verify get_target_player returns the target when specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            target_player_id="player2",
            rng=rng,
        )

        player = ctx.get_target_player()

        assert player is not None
        assert player.player_id == "player2"

    def test_get_target_player_when_not_specified(self) -> None:
        """
        Verify get_target_player returns None when not specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        player = ctx.get_target_player()

        assert player is None

    def test_get_opponent(self) -> None:
        """
        Verify get_opponent returns the other player.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        opponent = ctx.get_opponent()

        assert opponent.player_id == "player2"

    def test_get_opponent_id(self) -> None:
        """
        Verify get_opponent_id returns the opponent's ID.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        opponent_id = ctx.get_opponent_id()

        assert opponent_id == "player2"


class TestEffectContextCardAccess:
    """Tests for card access methods."""

    def test_get_source_card(self) -> None:
        """
        Verify get_source_card returns the source card when specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            source_card_id="pikachu-inst-1",
            rng=rng,
        )

        card = ctx.get_source_card()

        assert card is not None
        assert card.instance_id == "pikachu-inst-1"

    def test_get_source_card_when_not_specified(self) -> None:
        """
        Verify get_source_card returns None when not specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        card = ctx.get_source_card()

        assert card is None

    def test_get_target_card(self) -> None:
        """
        Verify get_target_card returns the target card when specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            target_card_id="charmander-inst-1",
            rng=rng,
        )

        card = ctx.get_target_card()

        assert card is not None
        assert card.instance_id == "charmander-inst-1"

    def test_get_source_pokemon(self) -> None:
        """
        Verify get_source_pokemon returns source player's active Pokemon.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        pokemon = ctx.get_source_pokemon()

        assert pokemon is not None
        assert pokemon.definition_id == "pikachu-001"

    def test_get_target_pokemon_with_explicit_target(self) -> None:
        """
        Verify get_target_pokemon returns explicit target when specified.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            target_card_id="charmander-inst-1",
            rng=rng,
        )

        pokemon = ctx.get_target_pokemon()

        assert pokemon is not None
        assert pokemon.instance_id == "charmander-inst-1"

    def test_get_target_pokemon_defaults_to_opponent_active(self) -> None:
        """
        Verify get_target_pokemon defaults to opponent's active Pokemon.

        This is the common case for attack effects.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        pokemon = ctx.get_target_pokemon()

        assert pokemon is not None
        assert pokemon.definition_id == "charmander-001"

    def test_get_card_definition(self) -> None:
        """
        Verify get_card_definition looks up the definition for a card.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        card = ctx.get_source_pokemon()
        assert card is not None

        definition = ctx.get_card_definition(card)

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

    def test_find_card(self) -> None:
        """
        Verify find_card locates a card anywhere in the game.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        card, zone = ctx.find_card("pikachu-inst-1")

        assert card is not None
        assert zone == "active"


class TestEffectContextParameters:
    """Tests for parameter access methods."""

    def test_get_param(self) -> None:
        """
        Verify get_param retrieves parameters.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            rng=rng,
            params={"amount": 30, "status": "paralysis"},
        )

        assert ctx.get_param("amount") == 30
        assert ctx.get_param("status") == "paralysis"
        assert ctx.get_param("missing") is None
        assert ctx.get_param("missing", "default") == "default"

    def test_get_int_param(self) -> None:
        """
        Verify get_int_param retrieves and converts to int.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            rng=rng,
            params={"amount": 30, "string_num": "50", "invalid": "not_a_number"},
        )

        assert ctx.get_int_param("amount") == 30
        assert ctx.get_int_param("string_num") == 50
        assert ctx.get_int_param("missing") == 0
        assert ctx.get_int_param("missing", 10) == 10
        assert ctx.get_int_param("invalid", 99) == 99

    def test_get_str_param(self) -> None:
        """
        Verify get_str_param retrieves and converts to string.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(
            game=game,
            source_player_id="player1",
            rng=rng,
            params={"status": "paralysis", "count": 5},
        )

        assert ctx.get_str_param("status") == "paralysis"
        assert ctx.get_str_param("count") == "5"
        assert ctx.get_str_param("missing") == ""
        assert ctx.get_str_param("missing", "default") == "default"


class TestEffectContextRandomOperations:
    """Tests for random operations through context."""

    def test_flip_coin_deterministic(self) -> None:
        """
        Verify flip_coin is deterministic with seeded RNG.
        """
        game = make_test_game()
        rng1 = SeededRandom(seed=42)
        rng2 = SeededRandom(seed=42)

        ctx1 = EffectContext(game=game, source_player_id="player1", rng=rng1)
        ctx2 = EffectContext(game=game, source_player_id="player1", rng=rng2)

        results1 = [ctx1.flip_coin() for _ in range(10)]
        results2 = [ctx2.flip_coin() for _ in range(10)]

        assert results1 == results2

    def test_flip_coins(self) -> None:
        """
        Verify flip_coins returns correct number of results.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        results = ctx.flip_coins(5)

        assert len(results) == 5
        assert all(isinstance(r, bool) for r in results)

    def test_count_heads(self) -> None:
        """
        Verify count_heads returns count of True values.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        # With seed 42, we should get deterministic results
        heads = ctx.count_heads(10)

        assert isinstance(heads, int)
        assert 0 <= heads <= 10


class TestEffectContextRulesAccess:
    """Tests for rules access through context."""

    def test_rules_property(self) -> None:
        """
        Verify rules property returns the game's RulesConfig.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        rules = ctx.rules

        assert rules is game.rules
        assert rules.energy.attachments_per_turn == 1

    def test_is_first_turn(self) -> None:
        """
        Verify is_first_turn reflects game state.
        """
        game = make_test_game()
        rng = SeededRandom(seed=42)
        ctx = EffectContext(game=game, source_player_id="player1", rng=rng)

        assert ctx.is_first_turn is True

        game.first_turn_completed = True
        assert ctx.is_first_turn is False
