from __future__ import annotations

import asyncio
import sys
import threading
import types
from unittest.mock import AsyncMock, MagicMock

import pytest

from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
from app.startup.initializers import agent as agent_initializer


@pytest.mark.anyio
async def test_disabled_initializer_does_not_materialize_manager(monkeypatch) -> None:
    """功能关闭时启动阶段不得解析完整 Agent 模块。"""
    activate = AsyncMock(return_value=None)
    monkeypatch.setattr(
        agent_initializer,
        "activate_agent_service",
        activate,
    )
    initializer = agent_initializer.AgentInitializer()

    assert await initializer.initialize() is True
    assert initializer._initialized is False
    activate.assert_awaited_once_with()


@pytest.mark.anyio
async def test_cleanup_without_initialized_manager_does_not_query(monkeypatch) -> None:
    """清理空状态只能关闭已持有资源，不能为清理而触发首次导入。"""
    activate = AsyncMock(side_effect=AssertionError("service activated"))
    monkeypatch.setattr(agent_initializer, "activate_agent_service", activate)

    await agent_initializer.AgentInitializer().cleanup()

    activate.assert_not_awaited()


@pytest.mark.anyio
async def test_failed_initialize_keeps_manager_for_shutdown_cleanup(
    monkeypatch,
) -> None:
    """初始化中途失败时仍须保留实际 manager，供应用关闭释放部分资源。"""
    manager = AsyncMock()
    manager.initialize.side_effect = RuntimeError("partial initialization")
    monkeypatch.setattr(agent_initializer.settings, "AI_AGENT_ENABLE", True)
    monkeypatch.setattr(agent_initializer, "agent_manager", manager)
    initializer = agent_initializer.AgentInitializer()

    assert await initializer.initialize() is False
    await initializer.cleanup()

    manager.close.assert_awaited_once_with()
    assert initializer._manager is None


@pytest.mark.anyio
async def test_compat_stop_closes_injected_manager_without_building_runtime(
    monkeypatch,
) -> None:
    """显式注入对象由兼容路径关闭，不为其构建空 Capability Runtime。"""
    events = []
    manager = AsyncMock()
    manager.initialize.side_effect = lambda: events.append("initialize")
    manager.close.side_effect = lambda: events.append("close")
    monkeypatch.setattr(agent_initializer.settings, "AI_AGENT_ENABLE", True)
    monkeypatch.setattr(agent_initializer, "agent_manager", manager)
    shutdown = AsyncMock(side_effect=lambda: events.append("shutdown_gate"))
    monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", shutdown)
    monkeypatch.setattr(
        agent_initializer,
        "agent_initializer",
        agent_initializer.AgentInitializer(),
    )

    assert await agent_initializer.init_agent() is True
    await agent_initializer.stop_agent()

    assert events == ["initialize", "close"]
    manager.initialize.assert_awaited_once_with()
    manager.close.assert_awaited_once_with()
    shutdown.assert_not_awaited()


@pytest.mark.anyio
async def test_production_initializer_delegates_lifecycle_to_runtime(
    monkeypatch,
) -> None:
    """生产路径只协调 service，不得再次手工 initialize 或 close canonical manager。"""
    manager = AsyncMock()
    activate = AsyncMock(return_value=manager)
    monkeypatch.setattr(agent_initializer, "agent_manager", None)
    monkeypatch.setattr(agent_initializer, "activate_agent_service", activate)
    initializer = agent_initializer.AgentInitializer()

    assert await initializer.initialize() is True
    await initializer.cleanup()

    activate.assert_awaited_once_with()
    manager.initialize.assert_not_awaited()
    manager.close.assert_not_awaited()
    assert initializer._manager is None


@pytest.mark.anyio
async def test_production_stop_seals_runtime_without_manually_closing_manager(
    monkeypatch,
) -> None:
    """生产关闭由 Runtime 关闸并释放 service，initializer 只清理自身引用。"""
    manager = AsyncMock()
    initializer = agent_initializer.AgentInitializer()
    initializer._manager = manager
    initializer._initialized = True
    initializer._compat_injected = False
    shutdown = AsyncMock(return_value=True)
    monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", shutdown)
    monkeypatch.setattr(agent_initializer, "agent_initializer", initializer)
    monkeypatch.setattr(
        agent_initializer,
        "is_tool_factory_materialized",
        lambda: False,
    )

    assert await agent_initializer.stop_agent() is True

    shutdown.assert_awaited_once_with()
    manager.close.assert_not_awaited()
    assert initializer._manager is None


@pytest.mark.anyio
async def test_production_stop_retains_manager_when_runtime_does_not_converge(
    monkeypatch,
) -> None:
    """Agent service 未收敛时不得释放 initializer 引用或下游工具资源。"""
    manager = AsyncMock()
    initializer = agent_initializer.AgentInitializer()
    initializer._manager = manager
    initializer._initialized = True
    initializer._compat_injected = False
    shutdown = AsyncMock(return_value=False)
    executor_seal = MagicMock()
    executor_close = AsyncMock(return_value=True)
    monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", shutdown)
    monkeypatch.setattr(agent_initializer, "agent_initializer", initializer)
    monkeypatch.setattr(
        agent_initializer,
        "is_tool_factory_materialized",
        lambda: True,
    )
    fake_base = types.ModuleType("app.agent.tools.base")
    fake_base.begin_blocking_executor_shutdown = executor_seal
    fake_base.close_blocking_executors = executor_close
    monkeypatch.setitem(sys.modules, "app.agent.tools.base", fake_base)

    assert await agent_initializer.stop_agent() is False

    shutdown.assert_awaited_once_with()
    assert initializer._manager is manager
    assert initializer._shutdown_started is True
    assert initializer._shutdown_complete is False
    executor_seal.assert_called_once_with(cancel_futures=True)
    executor_close.assert_awaited_once_with(
        timeout_seconds=(
            agent_initializer.AGENT_BLOCKING_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS
        ),
        cancel_futures=True,
    )

    event = agent_initializer.Event(
        agent_initializer.EventType.ConfigChanged,
        {"key": "AI_AGENT_ENABLE"},
    )
    reconcile = AsyncMock()
    monkeypatch.setattr(agent_initializer, "reconcile_agent_service", reconcile)
    await initializer.handle_config_changed(event)
    reconcile.assert_not_awaited()
    assert initializer._manager is manager


@pytest.mark.anyio
async def test_config_listener_delegates_watch_filter_to_runtime(monkeypatch) -> None:
    """配置监听器只转交 changed keys，不维护第二份启用开关。"""
    manager = AsyncMock()
    reconcile = AsyncMock(return_value=manager)
    monkeypatch.setattr(agent_initializer, "reconcile_agent_service", reconcile)
    initializer = agent_initializer.AgentInitializer()
    event = agent_initializer.Event(
        agent_initializer.EventType.ConfigChanged,
        {"key": {"AI_AGENT_ENABLE"}},
    )

    await initializer.handle_config_changed(event)

    reconcile.assert_awaited_once_with(
        reason="agent_service_config_changed",
        changed_keys={"AI_AGENT_ENABLE"},
        retry=True,
    )
    assert initializer._manager is manager
    assert initializer._initialized is True


def test_config_listener_registration_is_idempotent_and_instance_free() -> None:
    """重复构造 initializer 不得累积监听器或持有过期实例。"""
    subscribers = getattr(
        agent_initializer.eventmanager,
        "_EventManager__broadcast_subscribers",
    )
    AgentInitializer = agent_initializer.AgentInitializer
    AgentInitializer()
    AgentInitializer()

    listeners = tuple(
        subscribers.get(agent_initializer.EventType.ConfigChanged, {}).values()
    )
    matching = [
        listener
        for listener in listeners
        if listener is agent_initializer._handle_agent_config_changed
    ]
    assert len(matching) == 1
    assert agent_initializer._handle_agent_config_changed.__closure__ is None


@pytest.mark.anyio
async def test_config_event_after_shutdown_is_fail_closed(monkeypatch) -> None:
    """关闭后的配置事件不得把 service 重新标为初始化成功。"""
    reconcile = AsyncMock(side_effect=CapabilityRuntimeClosedError("closed"))
    monkeypatch.setattr(agent_initializer, "reconcile_agent_service", reconcile)
    initializer = agent_initializer.AgentInitializer()
    initializer._shutdown_complete = True
    event = agent_initializer.Event(
        agent_initializer.EventType.ConfigChanged,
        {"key": "AI_AGENT_ENABLE"},
    )

    await initializer.handle_config_changed(event)

    reconcile.assert_not_awaited()
    assert initializer._manager is None
    assert initializer._initialized is False


@pytest.mark.anyio
async def test_stop_skips_tool_executor_cleanup_when_factory_is_unresolved(
    monkeypatch,
) -> None:
    """工具能力从未解析时，关闭路径不得为线程池清理导入工具基础模块。"""
    fake_base = types.ModuleType("app.agent.tools.base")
    cleanup = MagicMock()
    fake_base.shutdown_blocking_executors = cleanup
    monkeypatch.setitem(sys.modules, "app.agent.tools.base", fake_base)
    monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", AsyncMock())
    monkeypatch.setattr(
        agent_initializer,
        "is_tool_factory_materialized",
        lambda: False,
    )
    monkeypatch.setattr(
        agent_initializer,
        "agent_initializer",
        agent_initializer.AgentInitializer(),
    )

    await agent_initializer.stop_agent()

    cleanup.assert_not_called()


@pytest.mark.anyio
async def test_stop_closes_tool_executor_after_factory_materialization(
    monkeypatch,
) -> None:
    """工具能力已解析时，应取消仍排队的阻塞工具任务。"""
    fake_base = types.ModuleType("app.agent.tools.base")
    seal = MagicMock()
    cleanup = AsyncMock(return_value=True)
    fake_base.begin_blocking_executor_shutdown = seal
    fake_base.close_blocking_executors = cleanup
    monkeypatch.setitem(sys.modules, "app.agent.tools.base", fake_base)
    monkeypatch.setattr(agent_initializer, "begin_agent_shutdown", AsyncMock())
    monkeypatch.setattr(
        agent_initializer,
        "is_tool_factory_materialized",
        lambda: True,
    )
    monkeypatch.setattr(
        agent_initializer,
        "agent_initializer",
        agent_initializer.AgentInitializer(),
    )

    assert await agent_initializer.stop_agent() is True

    seal.assert_called_once_with(cancel_futures=True)
    cleanup.assert_awaited_once_with(
        timeout_seconds=(
            agent_initializer.AGENT_BLOCKING_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS
        ),
        cancel_futures=True,
    )


@pytest.mark.anyio
async def test_stop_retains_running_blocking_tool_until_retry(monkeypatch) -> None:
    """阻塞工具超过预算时 stop 返回 False，真实结束后重试才释放 owner。"""
    from app.agent.tools.base import (
        MoviePilotTool,
        _blocking_futures,
        _blocking_retiring_executors,
        close_blocking_executors,
        reopen_blocking_executors,
    )

    started = threading.Event()
    release = threading.Event()

    assert reopen_blocking_executors() is True

    def _blocking_call() -> str:
        started.set()
        release.wait()
        return "done"

    async def _wait_until_started() -> None:
        """等待阻塞工具线程启动，避免把永久等待任务留在默认线程池。"""
        while not started.is_set():
            await asyncio.sleep(0)

    worker = asyncio.create_task(
        MoviePilotTool.run_blocking("web", _blocking_call)
    )
    await asyncio.wait_for(_wait_until_started(), timeout=1)
    monkeypatch.setattr(
        agent_initializer,
        "begin_agent_shutdown",
        AsyncMock(return_value=True),
    )
    monkeypatch.setattr(
        agent_initializer,
        "close_materialized_terminal_sessions",
        AsyncMock(),
    )
    monkeypatch.setattr(
        agent_initializer,
        "AGENT_BLOCKING_EXECUTOR_SHUTDOWN_TIMEOUT_SECONDS",
        0.01,
    )
    monkeypatch.setattr(
        agent_initializer,
        "is_tool_factory_materialized",
        lambda: True,
    )
    monkeypatch.setattr(
        agent_initializer,
        "agent_initializer",
        agent_initializer.AgentInitializer(),
    )

    try:
        assert await asyncio.wait_for(agent_initializer.stop_agent(), timeout=0.2) is False
        assert worker.done() is False
        assert _blocking_futures
        assert _blocking_retiring_executors

        with pytest.raises(RuntimeError, match="正在关闭"):
            await MoviePilotTool.run_blocking("web", lambda: "late")

        release.set()
        assert await asyncio.wait_for(worker, timeout=1) == "done"
        assert await asyncio.wait_for(agent_initializer.stop_agent(), timeout=0.2) is True
        assert not _blocking_futures
        assert not _blocking_retiring_executors
    finally:
        release.set()
        if not worker.done():
            await asyncio.wait_for(worker, timeout=1)
        await close_blocking_executors(timeout_seconds=1, cancel_futures=True)
        assert reopen_blocking_executors() is True
