"""Tests for the effect handler registry.

These tests verify:
1. Effect handler registration via decorator
2. Effect handler registration via function
3. Effect resolution and execution
4. Error handling for unknown effects
5. Registry management functions
"""

import pytest

from app.core.config import RulesConfig
from app.core.effects.base import EffectContext, EffectResult, EffectType
from app.core.effects.registry import (
    clear_registry,
    effect_handler,
    get_handler,
    is_registered,
    list_effects,
    register_handler,
    resolve_effect,
    unregister_handler,
)
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
# ============================================================================


@pytest.fixture(autouse=True)
def clean_registry():
    """Clean registry before and after each test.

    This ensures tests don't interfere with each other.
    """
    clear_registry()
    yield
    clear_registry()


def make_test_context() -> EffectContext:
    """Create a minimal EffectContext for testing."""
    pikachu_def = CardDefinition(
        id="pikachu-001",
        name="Pikachu",
        card_type=CardType.POKEMON,
        stage=PokemonStage.BASIC,
        hp=60,
        pokemon_type=EnergyType.LIGHTNING,
    )

    game = GameState(
        game_id="test-game",
        rules=RulesConfig(),
        card_registry={"pikachu-001": pikachu_def},
        players={
            "player1": PlayerState(player_id="player1"),
            "player2": PlayerState(player_id="player2"),
        },
        turn_order=["player1", "player2"],
        current_player_id="player1",
    )

    pikachu = CardInstance(instance_id="pikachu-inst-1", definition_id="pikachu-001")
    game.players["player1"].active.add(pikachu)

    rng = SeededRandom(seed=42)

    return EffectContext(
        game=game,
        source_player_id="player1",
        rng=rng,
        params={"amount": 30},
    )


# ============================================================================
# Registration Tests
# ============================================================================


class TestEffectHandlerDecorator:
    """Tests for the @effect_handler decorator."""

    def test_decorator_registers_handler(self) -> None:
        """
        Verify decorator adds handler to registry.
        """

        @effect_handler("test_effect")
        def my_handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Test passed")

        assert is_registered("test_effect")
        assert get_handler("test_effect") is my_handler

    def test_decorator_preserves_function(self) -> None:
        """
        Verify decorator returns the original function.
        """

        @effect_handler("test_effect")
        def my_handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Test passed")

        # The decorated function should still be callable
        ctx = make_test_context()
        result = my_handler(ctx)
        assert result.success

    def test_duplicate_registration_raises(self) -> None:
        """
        Verify registering the same effect_id twice raises ValueError.
        """

        @effect_handler("duplicate_effect")
        def handler1(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Handler 1")

        with pytest.raises(ValueError, match="already registered"):

            @effect_handler("duplicate_effect")
            def handler2(ctx: EffectContext) -> EffectResult:
                return EffectResult.success_result("Handler 2")


class TestRegisterHandler:
    """Tests for programmatic registration."""

    def test_register_handler_adds_to_registry(self) -> None:
        """
        Verify register_handler adds handler to registry.
        """

        def my_handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Registered")

        register_handler("programmatic_effect", my_handler)

        assert is_registered("programmatic_effect")
        assert get_handler("programmatic_effect") is my_handler

    def test_register_handler_duplicate_raises(self) -> None:
        """
        Verify registering duplicate effect_id raises ValueError.
        """

        def handler1(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Handler 1")

        def handler2(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Handler 2")

        register_handler("dup_effect", handler1)

        with pytest.raises(ValueError, match="already registered"):
            register_handler("dup_effect", handler2)


# ============================================================================
# Resolution Tests
# ============================================================================


class TestResolveEffect:
    """Tests for effect resolution and execution."""

    def test_resolve_effect_calls_handler(self) -> None:
        """
        Verify resolve_effect calls the registered handler.
        """
        called = {"count": 0}

        @effect_handler("counted_effect")
        def counting_handler(ctx: EffectContext) -> EffectResult:
            called["count"] += 1
            return EffectResult.success_result("Called")

        ctx = make_test_context()
        result = resolve_effect("counted_effect", ctx)

        assert result.success
        assert called["count"] == 1

    def test_resolve_effect_passes_context(self) -> None:
        """
        Verify resolve_effect passes the context to handler.
        """
        received_amount = {"value": None}

        @effect_handler("param_effect")
        def param_handler(ctx: EffectContext) -> EffectResult:
            received_amount["value"] = ctx.get_int_param("amount")
            return EffectResult.success_result("Got params")

        ctx = make_test_context()
        ctx.params["amount"] = 42
        resolve_effect("param_effect", ctx)

        assert received_amount["value"] == 42

    def test_resolve_unknown_effect_returns_failure(self) -> None:
        """
        Verify resolve_effect returns failure for unknown effect.
        """
        ctx = make_test_context()
        result = resolve_effect("nonexistent_effect", ctx)

        assert result.success is False
        assert "Unknown effect" in result.message
        assert "nonexistent_effect" in result.message

    def test_resolve_effect_catches_exceptions(self) -> None:
        """
        Verify resolve_effect catches handler exceptions.

        This prevents a buggy handler from crashing the game.
        """

        @effect_handler("buggy_effect")
        def buggy_handler(ctx: EffectContext) -> EffectResult:
            raise RuntimeError("Intentional error")

        ctx = make_test_context()
        result = resolve_effect("buggy_effect", ctx)

        assert result.success is False
        assert "failed" in result.message
        assert "Intentional error" in result.message

    def test_resolve_effect_logs_exceptions(self) -> None:
        """
        Verify resolve_effect logs full exception details when handler fails.

        This ensures debugging information is preserved even though the game
        continues gracefully. The log should include the effect_id, context
        details, and the full traceback.

        Note: Uses a custom log handler instead of caplog due to pytest-asyncio
        compatibility issues with log capture.
        """
        import logging

        # Create a custom handler to capture log records
        captured_records: list[logging.LogRecord] = []

        class CaptureHandler(logging.Handler):
            def emit(self, record: logging.LogRecord) -> None:
                captured_records.append(record)

        # Set up the handler on the registry logger
        registry_logger = logging.getLogger("app.core.effects.registry")
        original_level = registry_logger.level
        original_disabled = registry_logger.disabled
        registry_logger.setLevel(logging.ERROR)
        registry_logger.disabled = False  # Pytest may disable loggers
        handler = CaptureHandler()
        handler.setLevel(logging.ERROR)
        registry_logger.addHandler(handler)

        try:

            @effect_handler("logging_buggy_effect")
            def buggy_handler(ctx: EffectContext) -> EffectResult:
                raise ValueError("Detailed error message for logging test")

            ctx = make_test_context()
            ctx.source_card_id = "test-source-card"
            ctx.target_card_id = "test-target-card"
            ctx.params = {"test_param": 123}

            result = resolve_effect("logging_buggy_effect", ctx)

            # Verify the result is still a failure
            assert result.success is False

            # Verify the exception was logged with context
            assert len(captured_records) == 1, f"Expected 1 record, got {len(captured_records)}"
            log_record = captured_records[0]
            assert log_record.levelname == "ERROR"
            message = log_record.getMessage()
            assert "logging_buggy_effect" in message
            assert "player1" in message  # source_player_id
            assert "test-source-card" in message
            assert "test-target-card" in message

            # Verify traceback is included (logger.exception includes exc_info)
            assert log_record.exc_info is not None
            assert "Detailed error message for logging test" in str(log_record.exc_info[1])
        finally:
            # Clean up handler and restore level/disabled state
            registry_logger.removeHandler(handler)
            registry_logger.setLevel(original_level)
            registry_logger.disabled = original_disabled

    def test_resolve_effect_returns_handler_result(self) -> None:
        """
        Verify resolve_effect returns the handler's result.
        """

        @effect_handler("detailed_effect")
        def detailed_handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result(
                "Detailed result",
                effect_type=EffectType.DAMAGE,
                details={"amount": 50},
            )

        ctx = make_test_context()
        result = resolve_effect("detailed_effect", ctx)

        assert result.success
        assert result.message == "Detailed result"
        assert result.effect_type == EffectType.DAMAGE
        assert result.details["amount"] == 50


# ============================================================================
# Registry Management Tests
# ============================================================================


class TestRegistryManagement:
    """Tests for registry query and management functions."""

    def test_is_registered_true(self) -> None:
        """
        Verify is_registered returns True for registered effects.
        """

        @effect_handler("registered_effect")
        def handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Registered")

        assert is_registered("registered_effect") is True

    def test_is_registered_false(self) -> None:
        """
        Verify is_registered returns False for unregistered effects.
        """
        assert is_registered("unregistered_effect") is False

    def test_get_handler_returns_function(self) -> None:
        """
        Verify get_handler returns the handler function.
        """

        @effect_handler("get_handler_test")
        def handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Got it")

        retrieved = get_handler("get_handler_test")
        assert retrieved is handler

    def test_get_handler_returns_none_for_unknown(self) -> None:
        """
        Verify get_handler returns None for unknown effects.
        """
        assert get_handler("unknown_effect") is None

    def test_list_effects_returns_sorted(self) -> None:
        """
        Verify list_effects returns all registered effects sorted.
        """

        @effect_handler("effect_c")
        def handler_c(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("C")

        @effect_handler("effect_a")
        def handler_a(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("A")

        @effect_handler("effect_b")
        def handler_b(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("B")

        effects = list_effects()

        assert effects == ["effect_a", "effect_b", "effect_c"]

    def test_list_effects_empty_when_cleared(self) -> None:
        """
        Verify list_effects returns empty list after clear.
        """
        # Registry is cleared by fixture, so should be empty
        assert list_effects() == []

    def test_clear_registry_removes_all(self) -> None:
        """
        Verify clear_registry removes all handlers.
        """

        @effect_handler("effect_1")
        def handler_1(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("1")

        @effect_handler("effect_2")
        def handler_2(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("2")

        assert len(list_effects()) == 2

        clear_registry()

        assert len(list_effects()) == 0
        assert not is_registered("effect_1")
        assert not is_registered("effect_2")

    def test_unregister_handler_removes_effect(self) -> None:
        """
        Verify unregister_handler removes a specific handler.
        """

        @effect_handler("to_remove")
        def handler(ctx: EffectContext) -> EffectResult:
            return EffectResult.success_result("Remove me")

        assert is_registered("to_remove")

        result = unregister_handler("to_remove")

        assert result is True
        assert not is_registered("to_remove")

    def test_unregister_handler_returns_false_for_unknown(self) -> None:
        """
        Verify unregister_handler returns False for unknown effects.
        """
        result = unregister_handler("never_registered")
        assert result is False


# ============================================================================
# Integration Tests
# ============================================================================


class TestEffectHandlerIntegration:
    """Integration tests for realistic effect scenarios."""

    def test_deal_damage_effect(self) -> None:
        """
        Verify a realistic damage-dealing effect works.
        """

        @effect_handler("integration_damage")
        def deal_damage(ctx: EffectContext) -> EffectResult:
            target = ctx.get_target_pokemon()
            if target is None:
                return EffectResult.failure("No target")

            amount = ctx.get_int_param("amount")
            target.damage += amount

            return EffectResult.success_result(
                f"Dealt {amount} damage",
                effect_type=EffectType.DAMAGE,
                details={"amount": amount, "target": target.instance_id},
            )

        # Set up game with target
        ctx = make_test_context()
        target = CardInstance(instance_id="target-inst", definition_id="pikachu-001")
        ctx.game.players["player2"].active.add(target)
        ctx.params["amount"] = 30

        result = resolve_effect("integration_damage", ctx)

        assert result.success
        assert target.damage == 30
        assert result.effect_type == EffectType.DAMAGE

    def test_coin_flip_effect(self) -> None:
        """
        Verify a coin flip effect is deterministic with seeded RNG.
        """

        @effect_handler("flip_for_damage")
        def flip_damage(ctx: EffectContext) -> EffectResult:
            if ctx.flip_coin():
                return EffectResult.success_result(
                    "Heads! Effect succeeds",
                    effect_type=EffectType.COIN_FLIP,
                    details={"flip_result": "heads"},
                )
            else:
                return EffectResult.success_result(
                    "Tails! Effect fails",
                    effect_type=EffectType.COIN_FLIP,
                    details={"flip_result": "tails"},
                )

        # Run twice with same seed - should get same result
        ctx1 = make_test_context()
        ctx1.rng = SeededRandom(seed=12345)
        result1 = resolve_effect("flip_for_damage", ctx1)

        ctx2 = make_test_context()
        ctx2.rng = SeededRandom(seed=12345)
        result2 = resolve_effect("flip_for_damage", ctx2)

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