"""插件配置运行时测试。"""

from __future__ import annotations

from pathlib import Path
from types import SimpleNamespace
from typing import Any, Dict, Mapping, Optional, Tuple, cast

import tomllib

import pytest

from src.plugin_runtime.component_query import component_query_service
from src.plugin_runtime.protocol.envelope import (
    Envelope,
    InspectPluginConfigPayload,
    MessageType,
    RegisterPluginPayload,
    ValidatePluginConfigPayload,
)
from src.plugin_runtime.runner.runner_main import PluginRunner
from src.webui.routers.plugin.config_routes import get_plugin_config, get_plugin_config_schema, update_plugin_config
from src.webui.routers.plugin.schemas import UpdatePluginConfigRequest


class _DemoConfigPlugin:
    """用于测试 Runner 配置归一化流程的伪插件。"""

    _config_version: str = "2.0.0"

    def __init__(self) -> None:
        """初始化测试插件状态。"""

        self.received_config: Dict[str, Any] = {}

    def normalize_plugin_config(self, config_data: Optional[Mapping[str, Any]]) -> Tuple[Dict[str, Any], bool]:
        """补齐测试插件的默认配置。

        Args:
            config_data: 原始配置数据。

        Returns:
            Tuple[Dict[str, Any], bool]: 补齐后的配置，以及是否发生变更。
        """

        current_config = dict(config_data or {})
        plugin_section = dict(current_config.get("plugin", {}))
        changed = "retry_count" not in plugin_section or "config_version" not in plugin_section
        plugin_section.setdefault("config_version", self._config_version)
        plugin_section.setdefault("enabled", True)
        plugin_section.setdefault("retry_count", 3)
        return {"plugin": plugin_section}, changed

    def set_plugin_config(self, config: Dict[str, Any]) -> None:
        """记录 Runner 注入的配置内容。

        Args:
            config: 当前最新配置。
        """

        self.received_config = config

    def get_default_config(self) -> Dict[str, Any]:
        """返回测试插件的默认配置。

        Returns:
            Dict[str, Any]: 默认配置字典。
        """

        return {"plugin": {"config_version": self._config_version, "enabled": True, "retry_count": 3}}

    def get_webui_config_schema(
        self,
        *,
        plugin_id: str = "",
        plugin_name: str = "",
        plugin_version: str = "",
        plugin_description: str = "",
        plugin_author: str = "",
    ) -> Dict[str, Any]:
        """返回测试插件的 WebUI 配置 Schema。

        Args:
            plugin_id: 插件 ID。
            plugin_name: 插件名称。
            plugin_version: 插件版本。
            plugin_description: 插件描述。
            plugin_author: 插件作者。

        Returns:
            Dict[str, Any]: 测试配置 Schema。
        """

        del plugin_name, plugin_description, plugin_author
        return {
            "plugin_id": plugin_id,
            "plugin_info": {
                "name": "Demo",
                "version": plugin_version,
                "description": "",
                "author": "",
            },
            "sections": {
                "plugin": {
                    "fields": {
                        "enabled": {
                            "type": "boolean",
                            "label": "启用",
                            "default": True,
                            "ui_type": "switch",
                        }
                    }
                }
            },
            "layout": {"type": "auto", "tabs": []},
        }


class _StrictConfigPlugin:
    """用于测试配置校验错误的伪插件。"""

    def normalize_plugin_config(self, config_data: Optional[Mapping[str, Any]]) -> Tuple[Dict[str, Any], bool]:
        """校验重试次数不能为负数。

        Args:
            config_data: 原始配置数据。

        Returns:
            Tuple[Dict[str, Any], bool]: 规范化配置结果。

        Raises:
            ValueError: 当重试次数为负数时抛出。
        """

        current_config = dict(config_data or {})
        plugin_section = dict(current_config.get("plugin", {}))
        plugin_section.setdefault("config_version", "2.0.0")
        retry_count = int(plugin_section.get("retry_count", 0))
        if retry_count < 0:
            raise ValueError("重试次数不能小于 0")
        plugin_section.setdefault("enabled", True)
        return {"plugin": plugin_section}, False

    def set_plugin_config(self, config: Dict[str, Any]) -> None:
        """兼容 Runner 配置注入接口。

        Args:
            config: 当前配置字典。
        """

        del config

    def get_default_config(self) -> Dict[str, Any]:
        """返回测试插件的默认配置。

        Returns:
            Dict[str, Any]: 默认配置字典。
        """

        return {"plugin": {"config_version": "2.0.0", "enabled": True, "retry_count": 0}}


def test_runner_apply_plugin_config_generates_config_file(tmp_path: Path) -> None:
    """Runner 注入配置时应自动补齐并落盘 config.toml。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir=str(tmp_path), instance=plugin)

    runner._apply_plugin_config(
        cast(Any, meta),
        config_data={"plugin": {"config_version": "2.0.0", "enabled": False}},
    )

    config_path = tmp_path / "config.toml"
    assert config_path.exists()
    assert plugin.received_config == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}

    with config_path.open("rb") as handle:
        saved_config = tomllib.load(handle)
    assert saved_config == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}


def test_runner_apply_plugin_config_preserves_existing_comments(tmp_path: Path) -> None:
    """Runner 在版本升级时应尽量保留现有 config.toml 注释。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir=str(tmp_path), instance=plugin)
    config_path = tmp_path / "config.toml"
    config_path.write_text(
        '# 插件配置头注释\n[plugin]\nconfig_version = "1.0.0"\nenabled = false # 启用开关注释\n',
        encoding="utf-8",
    )

    runner._apply_plugin_config(cast(Any, meta))

    config_text = config_path.read_text(encoding="utf-8")
    assert "# 插件配置头注释" in config_text
    assert "# 启用开关注释" in config_text

    with config_path.open("rb") as handle:
        saved_config = tomllib.load(handle)
    assert saved_config == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}


def test_runner_apply_plugin_config_same_version_does_not_rewrite_file(tmp_path: Path) -> None:
    """Runner 在配置版本未变化时不应仅因补齐默认值而重写文件。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir=str(tmp_path), instance=plugin)
    config_path = tmp_path / "config.toml"
    original_config_text = '# 原始注释\n[plugin]\nconfig_version = "2.0.0"\nenabled = false\n'
    config_path.write_text(original_config_text, encoding="utf-8")

    runner._apply_plugin_config(cast(Any, meta))

    assert plugin.received_config == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}
    assert config_path.read_text(encoding="utf-8") == original_config_text


def test_runner_apply_plugin_config_requires_config_version(tmp_path: Path) -> None:
    """Runner 应拒绝缺少配置版本号的插件配置文件。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir=str(tmp_path), instance=plugin)
    config_path = tmp_path / "config.toml"
    config_path.write_text("[plugin]\nenabled = true\n", encoding="utf-8")

    with pytest.raises(ValueError, match="config_version"):
        runner._apply_plugin_config(cast(Any, meta))


def test_component_query_service_returns_plugin_config_schema(monkeypatch: Any) -> None:
    """组件查询服务应支持按插件 ID 返回配置 Schema。"""

    payload = RegisterPluginPayload(
        plugin_id="demo.plugin",
        plugin_version="1.0.0",
        default_config={"plugin": {"enabled": True}},
        config_schema={
            "plugin_id": "demo.plugin",
            "plugin_info": {
                "name": "Demo",
                "version": "1.0.0",
                "description": "",
                "author": "",
            },
            "sections": {"plugin": {"fields": {}}},
            "layout": {"type": "auto", "tabs": []},
        },
    )
    fake_supervisor = SimpleNamespace(_registered_plugins={"demo.plugin": payload})
    fake_manager = SimpleNamespace(_get_supervisor_for_plugin=lambda plugin_id: fake_supervisor)

    monkeypatch.setattr(
        type(component_query_service),
        "_get_runtime_manager",
        staticmethod(lambda: fake_manager),
    )

    assert component_query_service.get_plugin_config_schema("demo.plugin") == payload.config_schema
    assert component_query_service.get_plugin_default_config("demo.plugin") == payload.default_config


@pytest.mark.asyncio
async def test_runner_validate_plugin_config_handler_returns_normalized_config(monkeypatch: pytest.MonkeyPatch) -> None:
    """Runner 应返回插件模型归一化后的配置。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir="", instance=plugin)
    monkeypatch.setattr(runner._loader, "get_plugin", lambda plugin_id: meta if plugin_id == "demo.plugin" else None)

    envelope = Envelope(
        request_id=1,
        message_type=MessageType.REQUEST,
        method="plugin.validate_config",
        plugin_id="demo.plugin",
        payload=ValidatePluginConfigPayload(
            config_data={"plugin": {"config_version": "2.0.0", "enabled": False}}
        ).model_dump(),
    )

    response = await runner._handle_validate_plugin_config(envelope)

    assert response.error is None
    assert response.payload["success"] is True
    assert response.payload["normalized_config"] == {
        "plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}
    }


@pytest.mark.asyncio
async def test_runner_inspect_plugin_config_handler_supports_unloaded_plugin(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Runner 应支持对未加载插件执行冷检查。"""

    plugin = _DemoConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(
        plugin_id="demo.plugin",
        plugin_dir="/tmp/demo-plugin",
        instance=plugin,
        manifest=SimpleNamespace(
            name="Demo",
            description="",
            author=SimpleNamespace(name="tester"),
        ),
        version="1.0.0",
    )
    purged_plugins: list[tuple[str, str]] = []

    monkeypatch.setattr(
        runner,
        "_resolve_plugin_meta_for_config_request",
        lambda plugin_id: (meta, True, None) if plugin_id == "demo.plugin" else (None, False, "not-found"),
    )
    monkeypatch.setattr(
        runner._loader,
        "purge_plugin_modules",
        lambda plugin_id, plugin_dir: purged_plugins.append((plugin_id, plugin_dir)),
    )

    envelope = Envelope(
        request_id=1,
        message_type=MessageType.REQUEST,
        method="plugin.inspect_config",
        plugin_id="demo.plugin",
        payload=InspectPluginConfigPayload(
            config_data={"plugin": {"enabled": False}},
            use_provided_config=True,
        ).model_dump(),
    )

    response = await runner._handle_inspect_plugin_config(envelope)

    assert response.error is None
    assert response.payload["success"] is True
    assert response.payload["enabled"] is False
    assert response.payload["normalized_config"] == {
        "plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}
    }
    assert response.payload["default_config"] == {
        "plugin": {"config_version": "2.0.0", "enabled": True, "retry_count": 3}
    }
    assert purged_plugins == [("demo.plugin", "/tmp/demo-plugin")]


@pytest.mark.asyncio
async def test_runner_validate_plugin_config_handler_returns_error_on_invalid_config(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """Runner 应在插件拒绝配置时返回错误响应。"""

    plugin = _StrictConfigPlugin()
    runner = PluginRunner(
        host_address="ipc://unused",
        session_token="session-token",
        plugin_dirs=[],
    )
    meta = SimpleNamespace(plugin_id="demo.plugin", plugin_dir="", instance=plugin)
    monkeypatch.setattr(runner._loader, "get_plugin", lambda plugin_id: meta if plugin_id == "demo.plugin" else None)

    envelope = Envelope(
        request_id=1,
        message_type=MessageType.REQUEST,
        method="plugin.validate_config",
        plugin_id="demo.plugin",
        payload=ValidatePluginConfigPayload(
            config_data={"plugin": {"config_version": "2.0.0", "retry_count": -1}}
        ).model_dump(),
    )

    response = await runner._handle_validate_plugin_config(envelope)

    assert response.error is not None
    assert response.error["message"] == "重试次数不能小于 0"


@pytest.mark.asyncio
async def test_update_plugin_config_prefers_runtime_validation(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """WebUI 保存插件配置时应优先使用运行时校验结果。"""

    config_path = tmp_path / "config.toml"

    async def _mock_validate_plugin_config(plugin_id: str, config_data: Dict[str, Any]) -> Dict[str, Any] | None:
        """返回运行时归一化后的配置。

        Args:
            plugin_id: 插件 ID。
            config_data: 原始配置。

        Returns:
            Dict[str, Any] | None: 归一化后的配置。
        """

        assert plugin_id == "demo.plugin"
        assert config_data == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}
        return {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}

    async def _mock_inspect_plugin_config(
        plugin_id: str,
        config_data: Optional[Dict[str, Any]] = None,
        *,
        use_provided_config: bool = False,
    ) -> SimpleNamespace | None:
        """返回运行时配置快照。

        Args:
            plugin_id: 插件 ID。
            config_data: 可选配置。
            use_provided_config: 是否使用传入配置。

        Returns:
            SimpleNamespace | None: 运行时配置快照。
        """

        del config_data, use_provided_config
        if plugin_id != "demo.plugin":
            return None
        return SimpleNamespace(
            normalized_config={"plugin": {"config_version": "2.0.0", "enabled": True, "retry_count": 3}}
        )

    fake_runtime_manager = SimpleNamespace(
        inspect_plugin_config=_mock_inspect_plugin_config,
        validate_plugin_config=_mock_validate_plugin_config,
    )

    monkeypatch.setattr(
        "src.webui.routers.plugin.config_routes.require_plugin_token",
        lambda session: session or "session-token",
    )
    monkeypatch.setattr(
        "src.webui.routers.plugin.config_routes.find_plugin_path_by_id",
        lambda plugin_id: tmp_path if plugin_id == "demo.plugin" else None,
    )
    monkeypatch.setattr(
        "src.plugin_runtime.integration.get_plugin_runtime_manager",
        lambda: fake_runtime_manager,
    )

    response = await update_plugin_config(
        "demo.plugin",
        UpdatePluginConfigRequest(config={"plugin.enabled": False}),
        maibot_session="session-token",
    )

    assert response["success"] is True
    with config_path.open("rb") as handle:
        saved_config = tomllib.load(handle)
    assert saved_config == {"plugin": {"config_version": "2.0.0", "enabled": False, "retry_count": 3}}


@pytest.mark.asyncio
async def test_webui_config_endpoints_use_runtime_inspection_for_unloaded_plugin(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """WebUI 在插件未加载时也应从代码定义返回配置与 Schema。"""

    async def _mock_inspect_plugin_config(
        plugin_id: str,
        config_data: Optional[Dict[str, Any]] = None,
        *,
        use_provided_config: bool = False,
    ) -> SimpleNamespace | None:
        """返回运行时冷检查结果。

        Args:
            plugin_id: 插件 ID。
            config_data: 可选配置。
            use_provided_config: 是否使用传入配置。

        Returns:
            SimpleNamespace | None: 冷检查结果。
        """

        del config_data, use_provided_config
        if plugin_id != "demo.plugin":
            return None
        return SimpleNamespace(
            config_schema={
                "plugin_id": "demo.plugin",
                "plugin_info": {
                    "name": "Demo",
                    "version": "1.0.0",
                    "description": "",
                    "author": "",
                },
                "sections": {"plugin": {"fields": {}}},
                "layout": {"type": "auto", "tabs": []},
            },
            normalized_config={"plugin": {"config_version": "2.0.0", "enabled": True, "retry_count": 3}},
            enabled=True,
        )

    fake_runtime_manager = SimpleNamespace(inspect_plugin_config=_mock_inspect_plugin_config)

    monkeypatch.setattr(
        "src.webui.routers.plugin.config_routes.require_plugin_token",
        lambda session: session or "session-token",
    )
    monkeypatch.setattr(
        "src.webui.routers.plugin.config_routes.find_plugin_path_by_id",
        lambda plugin_id: tmp_path if plugin_id == "demo.plugin" else None,
    )
    monkeypatch.setattr(
        "src.plugin_runtime.integration.get_plugin_runtime_manager",
        lambda: fake_runtime_manager,
    )

    schema_response = await get_plugin_config_schema("demo.plugin", maibot_session="session-token")
    config_response = await get_plugin_config("demo.plugin", maibot_session="session-token")

    assert schema_response["success"] is True
    assert schema_response["schema"]["plugin_id"] == "demo.plugin"
    assert config_response == {
        "success": True,
        "config": {"plugin": {"config_version": "2.0.0", "enabled": True, "retry_count": 3}},
        "message": "配置文件不存在，已返回默认配置",
    }
