"""host_dispatch.main_inbox_continue：与 RuntimeContext 解耦的轻量夹具 + mock chat。"""

from __future__ import annotations

import asyncio
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

from aicd.agent.idle_pulse import MAIN_LIFECYCLE_WAKEUP_PREFIX
from aicd.db.store import TaskStore
from aicd.host_dispatch import dispatch_host_request
from aicd.host_tokens import issue_host_nonce
from aicd.tasks.main_inbox import main_inbox_id


def _make_ctx(
    *,
    sess: str,
    store: TaskStore,
    conn: object,
) -> SimpleNamespace:
    # dispatch_host_request 开头读取 invocation / plugin_registry（capabilities 等）；
    # main_inbox_continue 分支仅用 agent、store、conn。
    return SimpleNamespace(
        agent=SimpleNamespace(chat_session_id=sess),
        store=store,
        conn=conn,
        invocation=None,
        plugin_registry=None,
    )


def test_main_inbox_continue_skips_when_no_unread(tmp_path: Path) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-none"
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            resp = await dispatch_host_request(
                ctx,
                {"id": "r0", "method": "main_inbox_continue", "params": {}},
                state_dir=tmp_path,
                require_nonce=False,
            )
            assert resp == {
                "ok": True,
                "id": "r0",
                "result": {
                    "skipped": True,
                    "reason": "no_unread_matching_messages",
                    "lifecycle_only": True,
                },
            }
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_skips_status_only_when_lifecycle_only(
    tmp_path: Path,
) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-status-only"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="completed",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="status",
                body={"n": 1},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            resp = await dispatch_host_request(
                ctx,
                {"id": "r1", "method": "main_inbox_continue", "params": {}},
                state_dir=tmp_path,
                require_nonce=False,
            )
            assert resp["ok"] is True
            assert resp["result"]["skipped"] is True
            assert resp["result"]["reason"] == "no_unread_matching_messages"
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_calls_one_chat_turn_with_default_wakeup(
    tmp_path: Path,
) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-lc"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="completed",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="lifecycle",
                body={"phase": "completed"},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            with patch(
                "aicd.host_dispatch.one_chat_turn", new_callable=AsyncMock
            ) as m_turn:
                m_turn.return_value = (0, "ok-reply")
                resp = await dispatch_host_request(
                    ctx,
                    {"id": "r2", "method": "main_inbox_continue", "params": {}},
                    state_dir=tmp_path,
                    require_nonce=False,
                )
            assert resp == {
                "ok": True,
                "id": "r2",
                "result": {
                    "reply": "ok-reply",
                    "tool_lines": [],
                    "drained_unread_n": 1,
                    "lifecycle_only": True,
                },
            }
            m_turn.assert_awaited_once()
            cargs, ckwargs = m_turn.call_args
            assert cargs[0] is ctx
            assert cargs[1].startswith(MAIN_LIFECYCLE_WAKEUP_PREFIX)
            assert ckwargs.get("emit") is not None
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_custom_message_and_lifecycle_only_off(
    tmp_path: Path,
) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-custom"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="running",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="status",
                body={"x": 1},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            with patch(
                "aicd.host_dispatch.one_chat_turn", new_callable=AsyncMock
            ) as m_turn:
                m_turn.return_value = (0, "done")
                resp = await dispatch_host_request(
                    ctx,
                    {
                        "id": "r3",
                        "method": "main_inbox_continue",
                        "params": {
                            "lifecycle_only": False,
                            "message": "  ping-from-host ",
                        },
                    },
                    state_dir=tmp_path,
                    require_nonce=False,
                )
            assert resp["ok"] is True
            assert resp["result"]["reply"] == "done"
            assert resp["result"]["drained_unread_n"] == 1
            assert resp["result"]["lifecycle_only"] is False
            m_turn.assert_awaited_once()
            assert m_turn.call_args[0][1] == "ping-from-host"
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_one_chat_turn_nonzero(tmp_path: Path) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-fail"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="completed",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="lifecycle",
                body={"phase": "completed"},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            with patch(
                "aicd.host_dispatch.one_chat_turn", new_callable=AsyncMock
            ) as m_turn:
                m_turn.return_value = (1, None)
                resp = await dispatch_host_request(
                    ctx,
                    {"id": "r-fail", "method": "main_inbox_continue", "params": {}},
                    state_dir=tmp_path,
                    require_nonce=False,
                )
            assert resp == {
                "ok": False,
                "id": "r-fail",
                "code": "chat",
                "error": "main_inbox_continue failed",
                "tool_lines": [],
            }
            m_turn.assert_awaited_once()
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_nonce_required_missing(tmp_path: Path) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            ctx = _make_ctx(sess="sess-nonce", store=store, conn=conn)
            resp = await dispatch_host_request(
                ctx,
                {"id": "r-nx", "method": "main_inbox_continue", "params": {}},
                state_dir=tmp_path,
                require_nonce=True,
            )
            assert resp == {
                "ok": False,
                "id": "r-nx",
                "error": "auth.nonce required",
                "code": "auth",
            }
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_nonce_invalid_when_required(tmp_path: Path) -> None:
    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            ctx = _make_ctx(sess="sess-bad-nonce", store=store, conn=conn)
            resp = await dispatch_host_request(
                ctx,
                {
                    "id": "r-bad",
                    "method": "main_inbox_continue",
                    "params": {},
                    "auth": {"nonce": "deadbeef:not_a_real_secret"},
                },
                state_dir=tmp_path,
                require_nonce=True,
            )
            assert resp["ok"] is False
            assert resp["id"] == "r-bad"
            assert resp.get("code") == "auth"
            assert "nonce" in str(resp.get("error", "")).lower()
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_with_valid_nonce(tmp_path: Path) -> None:
    async def _go() -> None:
        tok = issue_host_nonce(tmp_path, ttl_sec=600)
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-ok-nonce"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="completed",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="lifecycle",
                body={"phase": "completed"},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            with patch(
                "aicd.host_dispatch.one_chat_turn", new_callable=AsyncMock
            ) as m_turn:
                m_turn.return_value = (0, "via-nonce")
                resp = await dispatch_host_request(
                    ctx,
                    {
                        "id": "r-ok",
                        "method": "main_inbox_continue",
                        "params": {},
                        "auth": {"nonce": tok},
                    },
                    state_dir=tmp_path,
                    require_nonce=True,
                )
            assert resp["ok"] is True
            assert resp["result"]["reply"] == "via-nonce"
            m_turn.assert_awaited_once()
        finally:
            await conn.close()

    asyncio.run(_go())


def test_main_inbox_continue_lifecycle_only_string_false_drains_status(
    tmp_path: Path,
) -> None:
    """字符串 lifecycle_only（如部分 HTTP 客户端）须按 false 解析，否则会误判为仅 lifecycle。"""

    async def _go() -> None:
        db = tmp_path / "t.db"
        store = TaskStore(db)
        conn = await store.connect()
        try:
            sess = "sess-str-false"
            tid = await store.insert_task(
                conn,
                task_type="ai_agent",
                status="running",
                parent_id=None,
                payload={"thread_root_id": sess},
            )
            inbox = main_inbox_id(sess)
            await store.insert_task_message(
                conn,
                to_task_id=inbox,
                from_task_id=tid,
                kind="status",
                body={"n": 1},
                thread_root_id=sess,
            )
            ctx = _make_ctx(sess=sess, store=store, conn=conn)
            with patch(
                "aicd.host_dispatch.one_chat_turn", new_callable=AsyncMock
            ) as m_turn:
                m_turn.return_value = (0, "ok")
                resp = await dispatch_host_request(
                    ctx,
                    {
                        "id": "r-str",
                        "method": "main_inbox_continue",
                        "params": {"lifecycle_only": "false"},
                    },
                    state_dir=tmp_path,
                    require_nonce=False,
                )
            assert resp["ok"] is True
            assert resp["result"].get("skipped") is not True
            assert resp["result"]["lifecycle_only"] is False
            assert resp["result"]["drained_unread_n"] == 1
            m_turn.assert_awaited_once()
        finally:
            await conn.close()

    asyncio.run(_go())
