"""插件可变事务停机准入的确定性测试。"""

import asyncio
import threading
from collections.abc import Iterator
from concurrent.futures import ThreadPoolExecutor
from contextvars import copy_context
from types import SimpleNamespace
from unittest.mock import MagicMock

import pytest

from app.foundation.singleton import Singleton
from app.runtime.extensions.plugin.admission import (
    PluginMutationAdmission,
    PluginMutationRejectedError,
)
from app.runtime.extensions.plugin.system import reset_plugin_system
from app.runtime.extensions.plugin_manager import PluginManager
from app.schemas.plugin import PluginRuntimeStatus
from app.schemas.types import EventType


class _GatedCondition:
    """让指定测试线程在取得真实 Condition 前停住。"""

    def __init__(self, blocked_thread_prefix: str) -> None:
        """初始化内部 Condition 和可控的竞态闸门。"""
        self._condition = threading.Condition()
        self._blocked_thread_prefix = blocked_thread_prefix
        self._blocked = False
        self.attempted = threading.Event()
        self.release = threading.Event()

    def __enter__(self):
        """在目标线程首次进入时等待测试放行。"""
        if (
            threading.current_thread().name.startswith(self._blocked_thread_prefix)
            and not self._blocked
        ):
            self._blocked = True
            self.attempted.set()
            if not self.release.wait(timeout=1):
                raise TimeoutError("测试未及时放行 Condition")
        return self._condition.__enter__()

    def __exit__(self, exc_type, exc_value, traceback):
        """把上下文退出委托给内部 Condition。"""
        return self._condition.__exit__(exc_type, exc_value, traceback)

    def wait(self) -> bool:
        """等待 admission 活动计数变化。"""
        return self._condition.wait()

    def notify_all(self) -> None:
        """唤醒全部等待 admission 空闲的线程。"""
        self._condition.notify_all()


@pytest.fixture
def plugin_manager() -> Iterator[PluginManager]:
    """构造隔离的插件管理器，并在用例结束后清除单例状态。"""
    key = (PluginManager, (), frozenset())
    Singleton._instances.pop(key, None)
    reset_plugin_system()
    manager = PluginManager()
    try:
        yield manager
    finally:
        Singleton._instances.pop(key, None)


def test_seal_rejects_new_root_but_allows_propagated_nested_lease() -> None:
    """封口拒绝无关调用，但已获准事务跨线程后的嵌套仍能完成。"""
    admission = PluginMutationAdmission()
    calls: list[str] = []

    def mutate(label: str) -> None:
        """在测试线程中尝试取得 lease 并记录副作用。"""
        with admission.hold(label):
            calls.append(label)

    with admission.hold("外层事务"):
        propagated = copy_context()
        assert admission.active_count == 1
        assert admission.seal() == 1
        with ThreadPoolExecutor(max_workers=2) as executor:
            executor.submit(propagated.run, mutate, "嵌套事务").result(timeout=1)
            rejected = executor.submit(mutate, "新事务")
            with pytest.raises(PluginMutationRejectedError):
                rejected.result(timeout=1)

        assert calls == ["嵌套事务"]
        assert admission.active_count == 1

    assert admission.active_count == 0
    assert admission.reopen() is True


def test_stale_propagated_context_is_rechecked_atomically_after_seal() -> None:
    """外层退出后才取得 Condition 的复制上下文不得冒充嵌套事务。"""
    admission = PluginMutationAdmission()
    calls: list[str] = []
    outer = admission.hold("外层事务")
    outer.__enter__()
    propagated = copy_context()
    condition = _GatedCondition("stale-admission")
    admission._condition = condition
    outer_closed = False

    def mutate() -> None:
        """使用复制上下文尝试执行延迟到封口后的写入。"""
        with admission.hold("延迟嵌套事务"):
            calls.append("mutated")

    try:
        with ThreadPoolExecutor(
            max_workers=1,
            thread_name_prefix="stale-admission",
        ) as executor:
            future = executor.submit(propagated.run, mutate)
            assert condition.attempted.wait(timeout=1)
            outer.__exit__(None, None, None)
            outer_closed = True
            assert admission.seal() == 0
            admission.wait_until_idle()

            condition.release.set()
            with pytest.raises(PluginMutationRejectedError):
                future.result(timeout=1)
    finally:
        condition.release.set()
        if not outer_closed:
            outer.__exit__(None, None, None)

    assert calls == []
    assert admission.active_count == 0


@pytest.mark.asyncio
async def test_quiesce_timeout_retains_admitted_owner_and_nested_reload(
    plugin_manager: PluginManager,
    monkeypatch,
) -> None:
    """停机超时保留 active owner，且封口前获准的事务可完成内部重载。"""
    manager = plugin_manager
    entered = asyncio.Event()
    release = asyncio.Event()
    manager._plugin_lifecycle.reload = MagicMock(
        return_value=PluginRuntimeStatus.ACTIVE
    )
    manager._plugin_lifecycle.quiesce_handlers = MagicMock(return_value=True)
    manager._plugin_lifecycle.finalize = MagicMock(return_value=True)

    async def mutate() -> PluginRuntimeStatus:
        """持有外层 lease，等待封口后再调用嵌套 Manager 写入口。"""
        with manager.mutation("安装插件 DemoPlugin"):
            entered.set()
            await release.wait()
            return manager.reload_plugin("DemoPlugin")

    with ThreadPoolExecutor(max_workers=1) as executor:
        monkeypatch.setattr(
            "app.runtime.extensions.plugin_manager.ThreadHelper",
            lambda: SimpleNamespace(submit=executor.submit),
        )
        mutation_task = asyncio.create_task(mutate())
        await entered.wait()

        assert await manager.quiesce_plugins(timeout=0.01) is False
        owner = manager._plugin_quiesce_future
        assert owner is not None
        assert owner.done() is False
        assert manager._plugin_mutation_admission.active_count == 1
        assert manager.finalize_plugins() is False
        with pytest.raises(PluginMutationRejectedError):
            with manager.mutation("新的配置写入"):
                pass

        release.set()
        assert await mutation_task is PluginRuntimeStatus.ACTIVE
        assert await asyncio.wrap_future(owner) is True

    manager._plugin_lifecycle.reload.assert_called_once_with(
        "DemoPlugin",
        EventType.PluginReload,
    )
    assert manager._plugin_mutation_admission.active_count == 0
    assert manager.finalize_plugins() is True


@pytest.mark.asyncio
async def test_quiesce_inside_mutation_fails_without_sealing(
    plugin_manager: PluginManager,
) -> None:
    """事务不能等待自身退出，快速失败时也不得误封口运行时。"""
    with plugin_manager.mutation("配置插件"):
        assert await plugin_manager.quiesce_plugins(timeout=0) is False
        assert plugin_manager._plugin_mutation_admission.accepting is True
        assert plugin_manager._plugin_runtime_closed is False


@pytest.mark.asyncio
async def test_sealed_manager_blocks_direct_persistent_mutations(
    plugin_manager: PluginManager,
) -> None:
    """Manager 单项兼容入口在封口后返回既有失败形状且不触发底层副作用。"""
    plugin_manager._plugin_config_store = MagicMock()
    plugin_manager._plugin_instance_store = MagicMock()
    plugin_manager._plugin_sync = MagicMock()
    plugin_manager._plugin_clone = MagicMock()
    plugin_manager._plugin_mutation_admission.seal()
    plugin_manager._plugin_runtime_closed = True

    assert plugin_manager.save_plugin_config("DemoPlugin", {}) is False
    assert await plugin_manager.async_save_plugin_config("DemoPlugin", {}) is False
    assert plugin_manager.delete_plugin_config("DemoPlugin") is False
    assert plugin_manager.delete_plugin_data("DemoPlugin") is False
    assert plugin_manager.delete_plugin_instance("DemoPlugin") is False
    clone_result = plugin_manager.clone_plugin("DemoPlugin", "Work", "", "")
    assert clone_result[0] is False
    assert "停机阶段" in clone_result[1]
    with pytest.raises(PluginMutationRejectedError):
        plugin_manager.sync()

    plugin_manager._plugin_config_store.write.assert_not_called()
    plugin_manager._plugin_config_store.async_write.assert_not_called()
    plugin_manager._plugin_config_store.delete.assert_not_called()
    plugin_manager._plugin_config_store.delete_data.assert_not_called()
    plugin_manager._plugin_instance_store.delete.assert_not_called()
    plugin_manager._plugin_sync.sync.assert_not_called()
    plugin_manager._plugin_clone.clone.assert_not_called()
