import asyncio
import json
import os
import shlex
import subprocess
import sys
from datetime import datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from langchain_core.messages import ToolMessage

from app.agent.middleware.policy import AgentPolicyMiddleware
from app.agent.middleware.subagents import SubAgentTaskControlMiddleware
from app.agent.orchestrator import MoviePilotAgent
from app.agent.policy.contracts import (
    AuthSource,
    PrincipalType,
    ToolOrigin,
    ToolPolicyContext,
)
from app.agent.policy.orchestrator import DEFAULT_TOOL_POLICY_ORCHESTRATOR
from app.agent.tools.base import MoviePilotTool
from app.agent.tools.catalog import ToolCatalogSnapshot
from app.agent.tools.impl._terminal_session import (
    _TerminalSession,
    _TerminalSessionManager,
)


class _SlowWriteTool(MoviePilotTool):
    """模拟超时后外部写操作仍可能继续的工具。"""

    name: str = "plugin_write"
    description: str = "Test a slow write tool."

    async def run(self, **kwargs) -> str:
        """等待足够久以触发测试超时。"""
        await asyncio.sleep(1)
        return "finished"


def _policy_context() -> ToolPolicyContext:
    """构造策略观测所需的最小宿主上下文。"""
    return ToolPolicyContext(
        session_id="session-1",
        user_id="user-1",
        origin=ToolOrigin.OPERATOR_DIRECT,
        principal_type=PrincipalType.HUMAN,
        auth_source=AuthSource.INTERNAL,
        agent_context={"is_admin": True},
    )


def _shell_command(code: str) -> str:
    """构造跨平台的短生命周期 Python 子进程命令。"""
    args = [sys.executable, "-c", code]
    if os.name == "nt":
        return subprocess.list2cmdline(args)
    return " ".join(shlex.quote(arg) for arg in args)


def test_timeout_marks_unknown_external_state_for_write_tools() -> None:
    """写类工具超时后必须明确提示外部状态可能仍在继续。"""
    tool = type("DynamicWriteTool", (), {"name": "plugin_write", "args_schema": None})()
    observation = DEFAULT_TOOL_POLICY_ORCHESTRATOR.start(
        context=_policy_context(),
        tool=tool,
        arguments={},
    )

    receipt = DEFAULT_TOOL_POLICY_ORCHESTRATOR.fail(
        observation,
        TimeoutError("tool timeout"),
    )

    assert receipt.external_may_continue is True
    assert receipt.needs_reconcile is True


def test_timeout_does_not_mark_safe_reads_for_reconciliation() -> None:
    """只读工具超时不应伪造外部副作用终态。"""
    tool = type("SafeReadTool", (), {"name": "query_personas", "args_schema": None})()
    observation = DEFAULT_TOOL_POLICY_ORCHESTRATOR.start(
        context=_policy_context(),
        tool=tool,
        arguments={},
    )

    receipt = DEFAULT_TOOL_POLICY_ORCHESTRATOR.fail(
        observation,
        TimeoutError("tool timeout"),
    )

    assert receipt.external_may_continue is False
    assert receipt.needs_reconcile is False


@pytest.mark.anyio
async def test_terminal_manager_close_terminates_running_pipe_session() -> None:
    """应用关闭时终端管理器必须终止仍在运行的管道进程。"""
    manager = _TerminalSessionManager()
    payload = await manager.start(
        command=_shell_command("import time; time.sleep(30)"),
        use_pty=False,
    )
    session = manager.get_session(payload["session_id"])

    await manager.close()

    assert session.process is not None
    assert session.process.returncode is not None
    assert session.status == "killed"
    assert manager._sessions == {}


@pytest.mark.anyio
async def test_terminal_manager_close_waits_for_starting_session() -> None:
    """关闭必须接管已经获准但尚未登记的终端启动。"""
    manager = _TerminalSessionManager()
    start_entered = asyncio.Event()
    allow_start = asyncio.Event()
    session = _TerminalSession(
        session_id="term-starting",
        command="sleep",
        cwd=".",
        pid=12345,
        use_pty=False,
    )

    async def _start_session(*_args) -> _TerminalSession:
        start_entered.set()
        await allow_start.wait()
        return session

    manager._start_pipe_session = _start_session
    manager._terminate_session = AsyncMock()

    start_task = asyncio.create_task(
        manager.start(command="sleep", use_pty=False)
    )
    await start_entered.wait()
    close_task = asyncio.create_task(manager.close())
    await asyncio.sleep(0)

    assert close_task.done() is False

    allow_start.set()
    with pytest.raises(RuntimeError, match="已关闭"):
        await start_task
    await close_task

    manager._terminate_session.assert_awaited_once_with(session)
    assert manager._sessions == {}


@pytest.mark.anyio
async def test_terminal_manager_rejects_start_after_close() -> None:
    """应用关闭后的终端管理器不得重新创建外部进程。"""
    manager = _TerminalSessionManager()

    await manager.close()

    with pytest.raises(RuntimeError, match="已关闭"):
        await manager.start(command="echo closed", use_pty=False)


@pytest.mark.anyio
async def test_terminal_manager_cancellation_terminates_unregistered_session() -> None:
    """调用方取消时必须回收已经创建但尚未登记的终端进程。"""
    manager = _TerminalSessionManager()
    session_created = asyncio.Event()
    registration_locked = asyncio.Event()
    release_registration = asyncio.Event()
    termination_started = asyncio.Event()
    session = _TerminalSession(
        session_id="term-cancelled",
        command="sleep",
        cwd=".",
        pid=12345,
        use_pty=False,
    )

    async def _hold_registration_lock() -> None:
        await session_created.wait()
        async with manager._lock:
            registration_locked.set()
            await release_registration.wait()

    async def _start_session(*_args) -> _TerminalSession:
        session_created.set()
        await registration_locked.wait()
        return session

    async def _terminate_session(_session: _TerminalSession) -> None:
        termination_started.set()

    lock_holder = asyncio.create_task(_hold_registration_lock())
    manager._start_pipe_session = _start_session
    manager._terminate_session = AsyncMock(side_effect=_terminate_session)
    start_task = asyncio.create_task(
        manager.start(command="sleep", use_pty=False)
    )
    await registration_locked.wait()
    await asyncio.sleep(0)

    start_task.cancel()
    await termination_started.wait()
    release_registration.set()
    with pytest.raises(asyncio.CancelledError):
        await start_task
    await lock_holder

    manager._terminate_session.assert_awaited_once_with(session)
    assert manager._sessions == {}
    assert manager._starting == 0


@pytest.mark.anyio
async def test_langchain_timeout_records_policy_failure() -> None:
    """LangChain 工具超时必须形成不泄露异常凭据的失败消息。"""
    tool = _SlowWriteTool(session_id="session-1", user_id="user-1")
    orchestrator = MagicMock()
    orchestrator.start.side_effect = DEFAULT_TOOL_POLICY_ORCHESTRATOR.start
    orchestrator.fail.side_effect = DEFAULT_TOOL_POLICY_ORCHESTRATOR.fail
    orchestrator.finish.side_effect = DEFAULT_TOOL_POLICY_ORCHESTRATOR.finish
    middleware = AgentPolicyMiddleware(
        context=_policy_context(),
        orchestrator=orchestrator,
        tools=[tool],
    )
    request = SimpleNamespace(
        tool=tool,
        tool_call={"id": "call-timeout", "name": tool.name, "args": {}},
    )

    async def _handler(_request):
        result = await tool._arun()
        return ToolMessage(content=result, tool_call_id="call-timeout")

    with patch("app.agent.tools.base.settings.LLM_TOOL_TIMEOUT", 0.01):
        result = await middleware.awrap_tool_call(request, _handler)

    assert "工具执行超时" in result.content
    assert "若工具包含外部写操作" in result.content
    assert "请先确认实际状态再重试" in result.content
    assert result.status == "error"
    orchestrator.fail.assert_called_once()
    orchestrator.finish.assert_not_called()


@pytest.mark.anyio
async def test_langchain_timeout_message_sanitizes_dynamic_error() -> None:
    """非宿主工具抛出的超时异常不得把凭据带入模型上下文。"""
    tool = SimpleNamespace(name="dynamic_tool")
    middleware = AgentPolicyMiddleware(
        context=_policy_context(),
        tools=[tool],
    )
    request = SimpleNamespace(
        tool=tool,
        tool_call={"id": "call-timeout", "name": tool.name, "args": {}},
    )

    async def _handler(_request):
        raise TimeoutError("Authorization: Bearer secret-value")

    result = await middleware.awrap_tool_call(request, _handler)

    assert result.status == "error"
    assert "secret-value" not in result.content


@pytest.mark.anyio
async def test_agent_cleanup_closes_subagent_middlewares() -> None:
    """会话资源清理必须覆盖脱离当前回合的 subagent 控制器。"""
    agent = MoviePilotAgent(session_id="session-1", user_id="user-1")
    closed = []

    class _Middleware:
        async def close(self) -> None:
            closed.append(True)

    agent._subagent_middlewares = (_Middleware(),)

    assert await agent.cleanup() is True

    assert closed == [True]
    assert agent._subagent_middlewares == ()


@pytest.mark.anyio
async def test_agent_cache_replacement_closes_previous_subagent_middleware() -> None:
    """Agent 图被替换时必须释放旧图持有的子代理控制器。"""
    agent = MoviePilotAgent(session_id="session-1", user_id="user-1")
    old_middleware = SimpleNamespace(close=AsyncMock())
    new_middleware = SimpleNamespace(close=AsyncMock())
    catalog = ToolCatalogSnapshot.from_tools(
        [], plugin_revision=0, factory_revision="factory-v1"
    )
    agent._subagent_middlewares = (old_middleware,)

    await agent._cache_agent(
        signature=("new",),
        agent=object(),
        streaming=False,
        tool_catalog=catalog,
        subagent_catalog=catalog,
        mcp_config_signature="mcp-config",
        subagent_middlewares=(new_middleware,),
    )

    old_middleware.close.assert_awaited_once()
    new_middleware.close.assert_not_awaited()
    assert agent._subagent_middlewares == (new_middleware,)


@pytest.mark.anyio
async def test_agent_execution_failure_closes_cached_subagent_middleware() -> None:
    """图执行失败失效缓存时必须同步释放该图持有的子代理控制器。"""
    agent = MoviePilotAgent(session_id="session-1", user_id="user-1")
    middleware = SimpleNamespace(close=AsyncMock())
    graph = SimpleNamespace(ainvoke=AsyncMock(side_effect=RuntimeError("failed")))
    agent._compiled_agent_bundle = SimpleNamespace(agent=graph)
    agent._subagent_middlewares = (middleware,)
    agent._should_stream = lambda: False
    agent._create_agent = AsyncMock(return_value=graph)
    agent._dispatch_execution_notice = AsyncMock()
    agent.stream_handler = SimpleNamespace(
        stop_streaming=AsyncMock(return_value=(False, ""))
    )

    result, _ = await agent._execute_agent([])

    assert "failed" in result
    middleware.close.assert_awaited_once()
    assert agent._compiled_agent_bundle is None
    assert agent._subagent_middlewares == ()


def test_subagent_control_middleware_close_is_idempotent() -> None:
    """子代理控制器的全局关闭路径应可重复调用。"""
    middleware = object.__new__(SubAgentTaskControlMiddleware)
    middleware._tasks = {}

    async def _close() -> None:
        assert await middleware.close() is True
        assert await middleware.close() is True

    asyncio.run(_close())
    assert middleware._tasks == {}


@pytest.mark.anyio
async def test_subagent_close_retains_stubborn_owner_until_retry() -> None:
    """子代理忽略取消时 close 返回 False，任务结束后重试才清理记录。"""
    middleware = object.__new__(SubAgentTaskControlMiddleware)
    release = asyncio.Event()
    cancelled = asyncio.Event()

    async def _ignore_first_cancel() -> None:
        try:
            await asyncio.Future()
        except asyncio.CancelledError:
            cancelled.set()
            await release.wait()

    task = asyncio.create_task(_ignore_first_cancel())
    record = SimpleNamespace(
        task_id="subagent-stubborn",
        description="stubborn",
        subagent_type="general-purpose",
        task=task,
        created_at=datetime.now(),
        started_at=datetime.now(),
        finished_at=None,
    )
    middleware._tasks = {record.task_id: record}
    await asyncio.sleep(0)

    with patch(
        "app.agent.middleware.subagents.SUBAGENT_CANCEL_GRACE_SECONDS",
        0.01,
    ):
        assert await asyncio.wait_for(middleware.close(), timeout=0.2) is False

    assert cancelled.is_set()
    assert task.done() is False
    assert middleware._tasks == {record.task_id: record}
    assert task in middleware._close_cancel_requested

    release.set()
    await asyncio.wait_for(task, timeout=0.2)
    assert await middleware.close() is True
    assert middleware._tasks == {}
    assert middleware._close_cancel_requested == set()


@pytest.mark.anyio
async def test_subagent_seal_rejects_new_detached_task() -> None:
    """控制器封口后必须同步拒绝 start，且不能创建新的 asyncio Task。"""
    middleware = object.__new__(SubAgentTaskControlMiddleware)
    middleware._tasks = {}
    middleware._accepting_tasks = True
    middleware._close_cancel_requested = set()

    middleware.seal()
    payload = await middleware._control_task(
        action="start",
        description="late task",
    )

    result = json.loads(payload)
    assert result["success"] is False
    assert "正在关闭" in result["error"]
    assert middleware._tasks == {}


@pytest.mark.anyio
async def test_agent_cleanup_retains_nonconverged_subagent_middleware() -> None:
    """Agent cleanup 必须保留返回 False 的 middleware，供重复调用继续收口。"""
    agent = MoviePilotAgent(session_id="session-owner", user_id="user-owner")
    middleware = SimpleNamespace(
        seal=MagicMock(),
        close=AsyncMock(side_effect=[False, True]),
    )
    agent._subagent_middlewares = (middleware,)

    assert await agent.cleanup() is False
    assert agent._subagent_middlewares == (middleware,)

    assert await agent.cleanup() is True
    assert agent._subagent_middlewares == ()
    assert middleware.seal.call_count == 2
    assert middleware.close.await_count == 2


@pytest.mark.anyio
async def test_subagent_cancel_reports_tasks_still_stopping() -> None:
    """取消上限到达后不得把仍运行的子代理报告为取消成功。"""
    middleware = object.__new__(SubAgentTaskControlMiddleware)
    release = asyncio.Event()

    async def _ignore_first_cancel() -> None:
        try:
            await asyncio.Future()
        except asyncio.CancelledError:
            await release.wait()

    task = asyncio.create_task(_ignore_first_cancel())
    record = SimpleNamespace(
        task_id="subagent-stopping",
        description="stopping",
        subagent_type="general-purpose",
        task=task,
        created_at=datetime.now(),
        started_at=datetime.now(),
        finished_at=None,
    )
    middleware._tasks = {record.task_id: record}
    await asyncio.sleep(0)

    with patch(
        "app.agent.middleware.subagents.SUBAGENT_CANCEL_GRACE_SECONDS",
        0.01,
    ):
        payload = await middleware._control_task(
            action="cancel",
            task_id=record.task_id,
        )

    result = json.loads(payload)
    assert result["success"] is False
    assert result["cancel_pending_task_ids"] == [record.task_id]
    assert result["tasks"][0]["status"] == "running"

    release.set()
    await asyncio.wait_for(task, timeout=0.2)
