import asyncio
import json
from types import SimpleNamespace
from typing import Iterator
from unittest.mock import patch

import pytest

from app.agent.tools.base import MoviePilotTool
from app.agent.tools.factory import MoviePilotToolFactory
from app.agent.tools.manager import MoviePilotToolsManager
from app.agent.tools.catalog import ToolCatalogSnapshot
from app.api.endpoints import mcp
from app.runtime.extensions.plugin_manager import PluginManager
from app.foundation.singleton import Singleton


class DemoPluginTool(MoviePilotTool):
    """测试用插件 MCP 工具。"""

    name: str = "demo_plugin_tool"
    description: str = "测试插件动态注册的 MCP 工具"

    async def run(self, **kwargs) -> str:
        """返回固定测试结果。"""
        return "plugin-ok"


@pytest.fixture
def plugin_manager() -> Iterator[PluginManager]:
    """构造隔离的插件管理器并在测试后恢复原单例。"""
    singleton_key = (PluginManager, (), frozenset())
    previous_instance = Singleton._instances.pop(singleton_key, None)
    manager = PluginManager()
    yield manager
    Singleton._instances.pop(singleton_key, None)
    if previous_instance is not None:
        Singleton._instances[singleton_key] = previous_instance


def _build_plugin() -> SimpleNamespace:
    """构造声明一个 Agent 工具的已启用插件。"""
    return SimpleNamespace(
        plugin_name="Demo Plugin",
        get_state=lambda: True,
        get_agent_tools=lambda: [DemoPluginTool],
    )


def test_mcp_refreshes_tools_after_plugin_lifecycle_change(
    plugin_manager: PluginManager,
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """MCP 管理器应发现初始化后新增的插件工具，并在插件移除后停止暴露。"""
    with patch.object(
        MoviePilotToolFactory,
        "_get_builtin_tool_classes",
        return_value=[],
    ):
        tool_manager = MoviePilotToolsManager(
            session_id="mcp-plugin-test",
            user_id="api_user",
        )
        monkeypatch.setattr(mcp, "moviepilot_tool_manager", tool_manager)

        assert asyncio.run(mcp.handle_tools_list()) == {"tools": []}

        plugin_manager.running_plugins["DemoPlugin"] = _build_plugin()
        plugin_manager.clear_plugin_agent_tools_cache()

        listed_tools = asyncio.run(mcp.handle_tools_list())["tools"]
        assert [tool["name"] for tool in listed_tools] == ["demo_plugin_tool"]

        call_result = asyncio.run(
            mcp.handle_tools_call(
                {
                    "name": "demo_plugin_tool",
                    "arguments": {},
                }
            )
        )
        assert call_result == {
            "content": [{"type": "text", "text": "plugin-ok"}]
        }

        plugin_manager.running_plugins.pop("DemoPlugin")
        plugin_manager.clear_plugin_agent_tools_cache()

        assert asyncio.run(mcp.handle_tools_list()) == {"tools": []}
        missing_result = asyncio.run(
            mcp.handle_tools_call(
                {
                    "name": "demo_plugin_tool",
                    "arguments": {},
                }
            )
        )
        missing_payload = json.loads(missing_result["content"][0]["text"])
        assert "未找到" in missing_payload["error"]


def test_direct_manager_preserves_legacy_lookup_and_exposes_strict_resolution() -> None:
    """普通 direct 调用保留 first-wins，严格调用可拒绝同名身份。"""
    first = DemoPluginTool(session_id="session", user_id="user")
    second = DemoPluginTool(session_id="session", user_id="user")
    manager = MoviePilotToolsManager(session_id="session", user_id="user")
    manager.tools = [first, second]
    manager.catalog = ToolCatalogSnapshot.from_tools(
        manager.tools,
        plugin_revision=manager._plugin_agent_tools_revision,
        factory_revision=MoviePilotToolFactory.catalog_factory_revision(),
    )

    assert manager.get_tool("demo_plugin_tool") is first
    with pytest.raises(RuntimeError, match="TOOL_IDENTITY_AMBIGUOUS"):
        manager.get_strict_tool("demo_plugin_tool")

    result = asyncio.run(manager.call_tool("demo_plugin_tool", {}))
    assert result == "plugin-ok"
