"""Tests for TriggerManager — 主动触发器（定时/Webhook/文件监听）。"""

from __future__ import annotations

import asyncio
import json
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock

from sage.vida.vida_trigger_manager import TriggerManager, VidaEvent


def _make_agent() -> MagicMock:
    agent = MagicMock()
    result = MagicMock()
    result.ok = True
    agent.handle_event = AsyncMock(return_value=result)
    return agent


class TestTriggerManagerLifecycle:
    """生命周期测试。"""

    def test_start_stop(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            assert manager.is_running
            await manager.stop()
            assert not manager.is_running

        asyncio.run(_run())


class TestDispatchAndEvent:
    """统一事件分发测试。"""

    def test_emit_event_calls_agent_handle_event(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            event = VidaEvent(event_type="manual", source="test", payload={"x": 1})
            await manager.emit_event(event)
            agent.handle_event.assert_awaited_once()
            dispatched = agent.handle_event.await_args.args[0]
            assert dispatched.event_type == "manual"
            assert dispatched.source == "test"
            await manager.stop()

        asyncio.run(_run())


class TestIntervalTrigger:
    """定时触发器测试。"""

    def test_interval_trigger_dispatches_events(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            await manager.register_interval_trigger(
                name="heartbeat",
                interval_seconds=0.05,
                payload_factory=lambda: {"content": "ping", "user_id": "system"},
            )
            await asyncio.sleep(0.16)
            await manager.stop()
            assert agent.handle_event.await_count >= 2

        asyncio.run(_run())


class TestCronTrigger:
    """Cron 触发器测试。"""

    def test_cron_trigger_dispatches_events(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            await manager.register_cron_trigger(
                name="cron1",
                cron_expr="*/1 * * * * *",
                payload_factory=lambda: {"content": "cron_ping"},
            )
            await asyncio.sleep(1.2)
            await manager.stop()

            assert agent.handle_event.await_count >= 1
            events = [call.args[0] for call in agent.handle_event.await_args_list]
            assert any(ev.event_type == "cron" for ev in events)

        asyncio.run(_run())


class TestCustomHookTrigger:
    """自定义钩子触发器测试。"""

    def test_custom_hook_trigger(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _hook() -> dict[str, str]:
            return {"content": "hook_event", "user_id": "hook_user"}

        async def _run() -> None:
            await manager.start()
            await manager.register_custom_hook_trigger(
                name="hook1",
                interval_seconds=0.05,
                hook=_hook,
            )
            await asyncio.sleep(0.15)
            await manager.stop()

            assert agent.handle_event.await_count >= 2
            first_event = agent.handle_event.await_args_list[0].args[0]
            assert first_event.event_type == "custom_hook"
            assert first_event.source == "hook1"

        asyncio.run(_run())


class TestFileWatchTrigger:
    """文件监听触发器测试。"""

    def test_file_watch_trigger_detects_modification(self, tmp_path: Path):
        watch_file = tmp_path / "watched.txt"
        watch_file.write_text("v1", encoding="utf-8")

        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            await manager.register_file_watch_trigger(
                name="watch1",
                file_path=str(watch_file),
                poll_interval_seconds=0.05,
            )

            await asyncio.sleep(0.08)
            watch_file.write_text("v2", encoding="utf-8")
            await asyncio.sleep(0.12)
            await manager.stop()

            assert agent.handle_event.await_count >= 1
            events = [call.args[0] for call in agent.handle_event.await_args_list]
            assert any(ev.event_type == "file_watch" for ev in events)
            assert any(ev.payload.get("event") in {"modified", "created", "moved"} for ev in events)

        asyncio.run(_run())


class TestWebhookTrigger:
    """Webhook 触发器测试。"""

    def test_webhook_post_dispatches_event(self):
        agent = _make_agent()
        manager = TriggerManager(agent)

        async def _run() -> None:
            await manager.start()
            await manager.register_webhook_trigger(
                name="webhook1",
                host="127.0.0.1",
                port=0,
                path="/vida/webhook",
                secret="token-123",
            )

            assert manager._server is not None
            sockets = manager._server.sockets
            assert sockets is not None
            port = sockets[0].getsockname()[1]

            reader, writer = await asyncio.open_connection("127.0.0.1", port)
            body = json.dumps({"content": "from webhook", "user_id": "wh_user"})
            req = (
                "POST /vida/webhook HTTP/1.1\r\n"
                "Host: 127.0.0.1\r\n"
                "Content-Type: application/json\r\n"
                "X-Vida-Secret: token-123\r\n"
                f"Content-Length: {len(body.encode('utf-8'))}\r\n"
                "\r\n"
                f"{body}"
            )
            writer.write(req.encode("utf-8"))
            await writer.drain()
            response = await reader.read(4096)
            writer.close()
            await writer.wait_closed()

            assert b"200 OK" in response
            await asyncio.sleep(0.05)
            await manager.stop()

            assert agent.handle_event.await_count >= 1
            event = agent.handle_event.await_args_list[-1].args[0]
            assert event.event_type == "webhook"
            assert event.source == "webhook1"

        asyncio.run(_run())
