# -*- coding: utf-8 -*-
"""
MCPClientManager 单元测试。
重点验证：服务器启动失败时 AsyncExitStack 必须被正确关闭，不泄漏 async generator。
"""
import asyncio
import gc
import json
import os
import tempfile
from pathlib import Path

import pytest

from abyss.mcp.manager import MCPClientManager


def _write_temp_config(config: dict) -> Path:
    """将配置字典写入临时 JSON 文件，返回路径。"""
    fd, path = tempfile.mkstemp(suffix=".json")
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        json.dump(config, f)
    return Path(path)


def test_start_all_cleans_up_stack_on_failure():
    """服务器启动失败时，AsyncExitStack 必须被关闭，不泄漏 async generator。

    复现 bug：_start_all_async 的 except 块只 continue，不 close stack。
    泄漏的 stdio_client async generator 在 GC 时被另一个 Task 关闭，
    触发 'Attempted to exit cancel scope in a different task' RuntimeError。

    关键：必须用 create_task 包裹 _start_all_async，模拟 run_coroutine_threadsafe
    在后台事件循环中创建 Task 的行为。泄漏的 generator 在 Task-A 中进入，
    GC 在 Task-B 中关闭 → cancel scope 跨 Task → RuntimeError。
    """

    async def _run():
        config = {
            "mcp_servers": [
                {
                    "name": "bad_server",
                    "transport": "stdio",
                    "command": "python",
                    "args": ["nonexistent_file_xyz.py"],
                }
            ]
        }
        config_path = _write_temp_config(config)
        try:
            manager = MCPClientManager(config_path=config_path)

            errors = []
            loop = asyncio.get_event_loop()
            loop.set_exception_handler(
                lambda l, ctx: errors.append(ctx.get("message", str(ctx)))
            )

            # 用 create_task 包裹，模拟 start_all() 中 run_coroutine_threadsafe 的行为。
            # generator 在此 Task 中进入，稍后 GC 在不同 Task 中关闭 → 跨 Task 错误。
            task = asyncio.create_task(manager._start_all_async())
            await task

            # 触发泄漏 async generator 的 GC → __del__ 调度 aclose() 为新 Task
            gc.collect()
            # 让事件循环处理 aclose() Task
            await asyncio.sleep(0.3)
            await loop.shutdown_asyncgens()

            assert not errors, f"泄漏的 async generator 导致异常: {errors}"
            assert len(manager._cm_stacks) == 0, "失败的服务器不应残留 stack"
            assert len(manager._sessions) == 0, "失败的服务器不应残留 session"
        finally:
            config_path.unlink(missing_ok=True)

    asyncio.new_event_loop().run_until_complete(_run())
