#!/usr/bin/env python3
"""utils/event_hooks.py 테스트 스위트 (TDD — RED → GREEN)"""

from __future__ import annotations

import asyncio
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.event_hooks import Event, HookRegistry, emit, get_registry

# ---------------------------------------------------------------------------
# 헬퍼
# ---------------------------------------------------------------------------


def _fresh_registry() -> HookRegistry:
    """싱글턴 우회용 독립 레지스트리."""
    return HookRegistry()


# ---------------------------------------------------------------------------
# 1. 핸들러 등록
# ---------------------------------------------------------------------------


class TestHandlerRegistration:
    """핸들러 등록 기본 동작"""

    def test_register_sync_handler(self):
        """동기 핸들러를 이벤트 타입에 등록할 수 있다."""
        reg = _fresh_registry()
        called = []

        def handler(ctx):
            called.append(ctx)

        reg.register(Event.DISPATCH_START, handler)
        asyncio.run(reg.emit(Event.DISPATCH_START, {"x": 1}))
        assert called == [{"x": 1}]

    def test_register_async_handler(self):
        """비동기 핸들러를 이벤트 타입에 등록할 수 있다."""
        reg = _fresh_registry()
        called = []

        async def handler(ctx):
            called.append(ctx)

        reg.register(Event.SESSION_START, handler)
        asyncio.run(reg.emit(Event.SESSION_START, "hello"))
        assert called == ["hello"]

    def test_register_named_handler(self):
        """name 파라미터로 핸들러에 이름 부여가 가능하다."""
        reg = _fresh_registry()

        def handler(ctx):
            pass

        reg.register(Event.AGENT_STEP, handler, name="my_handler")
        names = [info["name"] for info in reg._handlers.get(Event.AGENT_STEP, [])]
        assert "my_handler" in names

    def test_multiple_handlers_for_same_event(self):
        """같은 이벤트에 여러 핸들러를 등록할 수 있다."""
        reg = _fresh_registry()
        results = []

        reg.register(Event.DISPATCH_END, lambda ctx: results.append("A"))
        reg.register(Event.DISPATCH_END, lambda ctx: results.append("B"))
        asyncio.run(reg.emit(Event.DISPATCH_END, None))
        assert "A" in results and "B" in results

    def test_no_handlers_emit_is_noop(self):
        """핸들러가 없는 이벤트에 emit해도 예외가 발생하지 않는다."""
        reg = _fresh_registry()
        asyncio.run(reg.emit("nonexistent:event", {}))  # no exception


# ---------------------------------------------------------------------------
# 2. 와일드카드 매칭
# ---------------------------------------------------------------------------


class TestWildcardMatching:
    """dispatch:* 스타일 와일드카드 매칭"""

    def test_wildcard_matches_prefixed_events(self):
        """dispatch:* 핸들러가 dispatch:start 에 호출된다."""
        reg = _fresh_registry()
        calls = []

        reg.register("dispatch:*", lambda ctx: calls.append(ctx))
        asyncio.run(reg.emit(Event.DISPATCH_START, "s"))
        assert "s" in calls

    def test_wildcard_matches_multiple_events(self):
        """dispatch:* 핸들러가 dispatch:end 에도 호출된다."""
        reg = _fresh_registry()
        calls = []

        reg.register("dispatch:*", lambda ctx: calls.append(ctx))
        asyncio.run(reg.emit(Event.DISPATCH_START, 1))
        asyncio.run(reg.emit(Event.DISPATCH_END, 2))
        assert calls == [1, 2]

    def test_wildcard_does_not_match_different_prefix(self):
        """session:* 핸들러가 dispatch:start 에 호출되지 않는다."""
        reg = _fresh_registry()
        calls = []

        reg.register("session:*", lambda ctx: calls.append(ctx))
        asyncio.run(reg.emit(Event.DISPATCH_START, "x"))
        assert calls == []

    def test_exact_and_wildcard_both_fire(self):
        """정확한 이벤트 핸들러와 와일드카드 핸들러가 함께 호출된다."""
        reg = _fresh_registry()
        calls = []

        reg.register(Event.DISPATCH_START, lambda ctx: calls.append("exact"))
        reg.register("dispatch:*", lambda ctx: calls.append("wild"))
        asyncio.run(reg.emit(Event.DISPATCH_START, None))
        assert "exact" in calls and "wild" in calls


# ---------------------------------------------------------------------------
# 3. 오류 격리
# ---------------------------------------------------------------------------


class TestErrorIsolation:
    """핸들러 예외가 메인 파이프라인을 블록하지 않음"""

    def test_sync_handler_exception_is_caught(self):
        """동기 핸들러 예외가 catch 되어 다음 핸들러가 실행된다."""
        reg = _fresh_registry()
        second_called = []

        def bad_handler(ctx):
            raise RuntimeError("boom")

        def good_handler(ctx):
            second_called.append(True)

        reg.register(Event.DISPATCH_ERROR, bad_handler)
        reg.register(Event.DISPATCH_ERROR, good_handler)
        asyncio.run(reg.emit(Event.DISPATCH_ERROR, {}))
        assert second_called == [True]

    def test_async_handler_exception_is_caught(self):
        """비동기 핸들러 예외도 catch 되어 다음 핸들러가 실행된다."""
        reg = _fresh_registry()
        second_called = []

        async def bad_handler(ctx):
            raise ValueError("async boom")

        def good_handler(ctx):
            second_called.append(True)

        reg.register(Event.SESSION_END, bad_handler)
        reg.register(Event.SESSION_END, good_handler)
        asyncio.run(reg.emit(Event.SESSION_END, {}))
        assert second_called == [True]

    def test_exception_does_not_propagate_to_caller(self):
        """emit()을 호출한 쪽에 예외가 전파되지 않는다."""
        reg = _fresh_registry()

        def exploding(ctx):
            raise Exception("should not propagate")

        reg.register(Event.SECURITY_BLOCK, exploding)
        # 예외 없이 완료되어야 한다
        asyncio.run(reg.emit(Event.SECURITY_BLOCK, {}))


# ---------------------------------------------------------------------------
# 4. emit_sync
# ---------------------------------------------------------------------------


class TestEmitSync:
    """동기 컨텍스트용 emit_sync"""

    def test_emit_sync_calls_handler(self):
        """emit_sync()가 핸들러를 호출한다."""
        reg = _fresh_registry()
        results = []

        reg.register(Event.SKILL_INSTALL, lambda ctx: results.append(ctx))
        reg.emit_sync(Event.SKILL_INSTALL, "pkg")
        assert results == ["pkg"]

    def test_emit_sync_runs_async_handler(self):
        """emit_sync()가 비동기 핸들러도 실행한다."""
        reg = _fresh_registry()
        results = []

        async def ahandler(ctx):
            results.append(ctx)

        reg.register(Event.SKILL_INSTALL, ahandler)
        reg.emit_sync(Event.SKILL_INSTALL, "async_pkg")
        assert results == ["async_pkg"]


# ---------------------------------------------------------------------------
# 5. 싱글턴 패턴
# ---------------------------------------------------------------------------


class TestSingleton:
    """get_registry()가 싱글턴을 반환"""

    def test_get_registry_returns_same_instance(self):
        """get_registry()를 두 번 호출하면 같은 객체를 반환한다."""
        r1 = get_registry()
        r2 = get_registry()
        assert r1 is r2

    def test_global_emit_uses_singleton(self):
        """전역 emit()이 싱글턴 레지스트리에 등록된 핸들러를 호출한다."""
        reg = get_registry()
        results = []
        # 고유한 이벤트 타입으로 충돌 방지
        unique_event = "test:singleton_emit_check"
        reg.register(unique_event, lambda ctx: results.append(ctx))
        asyncio.run(emit(unique_event, "ping"))
        assert "ping" in results


# ---------------------------------------------------------------------------
# 6. discover_and_load
# ---------------------------------------------------------------------------


class TestDiscoverAndLoad:
    """HOOK.yaml + handler.py 디렉토리 기반 동적 로드"""

    def test_discover_registers_handler_from_directory(self, tmp_path):
        """HOOK.yaml과 handler.py가 있는 훅이 자동으로 등록된다."""
        hook_dir = tmp_path / "myhook"
        hook_dir.mkdir()

        (hook_dir / "HOOK.yaml").write_text("event: dispatch:start\nname: test_hook\n", encoding="utf-8")
        (hook_dir / "handler.py").write_text(
            "def handle(ctx):\n    pass\n",
            encoding="utf-8",
        )

        reg = _fresh_registry()
        reg.discover_and_load(str(tmp_path))
        # dispatch:start 에 핸들러가 등록되어 있어야 한다
        handlers = reg._handlers.get(Event.DISPATCH_START, []) + reg._handlers.get("dispatch:start", [])
        assert len(handlers) >= 1

    def test_discover_skips_missing_yaml(self, tmp_path):
        """HOOK.yaml이 없는 디렉토리는 무시된다."""
        hook_dir = tmp_path / "badhook"
        hook_dir.mkdir()
        (hook_dir / "handler.py").write_text("def handle(ctx): pass\n", encoding="utf-8")

        reg = _fresh_registry()
        reg.discover_and_load(str(tmp_path))
        # 예외 없이 완료되어야 한다
        assert True

    def test_discover_skips_missing_handler(self, tmp_path):
        """handler.py가 없는 디렉토리는 무시된다."""
        hook_dir = tmp_path / "nohook"
        hook_dir.mkdir()
        (hook_dir / "HOOK.yaml").write_text("event: dispatch:start\nname: test\n", encoding="utf-8")

        reg = _fresh_registry()
        reg.discover_and_load(str(tmp_path))
        assert True

    def test_discover_nonexistent_dir_is_noop(self):
        """존재하지 않는 디렉토리에 discover_and_load해도 예외가 없다."""
        reg = _fresh_registry()
        reg.discover_and_load("/tmp/nonexistent_hooks_dir_xyz123")
        assert True


# ---------------------------------------------------------------------------
# 7. Event 상수
# ---------------------------------------------------------------------------


class TestEventConstants:
    """Event 클래스 상수 존재 확인"""

    def test_event_constants_defined(self):
        """모든 Event 상수가 정의되어 있다."""
        assert Event.DISPATCH_START
        assert Event.DISPATCH_END
        assert Event.DISPATCH_ERROR
        assert Event.SESSION_START
        assert Event.SESSION_END
        assert Event.AGENT_STEP
        assert Event.SECURITY_BLOCK
        assert Event.SECURITY_WARN
        assert Event.SKILL_INSTALL
