"""Agent API 路由与禁用响应的延迟加载合同。"""

from __future__ import annotations

import json
import os
import subprocess
import sys
from pathlib import Path


def _run_isolated(script: str, config_dir: Path) -> dict:
    """在隔离解释器中执行路由探针，并返回末行 JSON 结果。"""
    env = os.environ.copy()
    env.update(
        {
            "AI_AGENT_ENABLE": "false",
            "API_TOKEN": "test-agent-api-token-1234",
            "CONFIG_DIR": str(config_dir),
            "PYTHONDONTWRITEBYTECODE": "1",
        }
    )
    completed = subprocess.run(
        [sys.executable, "-c", script],
        check=True,
        capture_output=True,
        text=True,
        env=env,
    )
    lines = [line for line in completed.stdout.splitlines() if line.strip()]
    return json.loads(lines[-1])


def test_full_api_openapi_keeps_agent_runtime_cold(tmp_path: Path) -> None:
    """完整路由与 OpenAPI 注册不得物化 Agent、工具或模型运行时。"""
    result = _run_isolated(
        r'''
import json
import socket
import sys
import types

network_attempts = []

def block_network(*args, **kwargs):
    network_attempts.append(repr(args[:2]))
    raise AssertionError("router import attempted network access")

socket.create_connection = block_network
socket.getaddrinfo = block_network
socket.socket.connect = block_network

sites = types.ModuleType("app.application.site.sites")
sites.SitesHelper = type("SitesHelper", (), {})
sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites

from fastapi import FastAPI
from app.startup.initializers.routers import init_routers

app = FastAPI()
init_routers(app)
paths = set(app.openapi()["paths"])
required_paths = {
    "/api/v1/message/agent/stream",
    "/api/v1/message/agent/sessions",
    "/api/v1/openai/v1/chat/completions",
    "/api/v1/openai/v1/responses",
    "/api/v1/anthropic/v1/messages",
    "/api/v1/llm/manage",
    "/api/v1/mcp",
    "/api/v1/mcp/tools",
}
forbidden = (
    "app.agent.callback",
    "app.agent.llm.helper",
    "app.agent.orchestrator",
    "app.agent.tools.base",
    "app.agent.tools.catalog",
    "app.agent.tools.factory",
    "app.agent.tools.impl",
    "langgraph",
)
loaded = sorted(
    name
    for name in sys.modules
    if any(name == prefix or name.startswith(prefix + ".") for prefix in forbidden)
)
print(json.dumps({
    "loaded": loaded,
    "missing_paths": sorted(required_paths - paths),
    "network_attempts": network_attempts,
}))
''',
        tmp_path / "router-import",
    )

    assert result == {
        "loaded": [],
        "missing_paths": [],
        "network_attempts": [],
    }


def test_disabled_protocol_requests_preserve_503_without_runtime_load(
    tmp_path: Path,
) -> None:
    """禁用态兼容协议保持 503，并且不会因构造响应加载 Agent。"""
    result = _run_isolated(
        r'''
import asyncio
import json
import socket
import sys
import types
from types import SimpleNamespace

network_attempts = []

def block_network(*args, **kwargs):
    network_attempts.append(repr(args[:2]))
    raise AssertionError("disabled request attempted network access")

socket.create_connection = block_network
socket.getaddrinfo = block_network
socket.socket.connect = block_network

sites = types.ModuleType("app.application.site.sites")
sites.SitesHelper = type("SitesHelper", (), {})
sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites

from fastapi.security import HTTPAuthorizationCredentials
from app import schemas
from app.api.endpoints import anthropic, openai
from app.api.endpoints.anthropic import messages as anthropic_messages
from app.api.endpoints.openai import chat_completions, responses
from app.application.configuration import ApiRuntimeConfig
from app.runtime.config import settings

runtime_config = ApiRuntimeConfig(
    False, 60, False, settings.AI_AGENT_ENABLE,
    api_token=settings.API_TOKEN,
)
anthropic.get_api_runtime_config_snapshot = lambda: runtime_config
openai.get_api_runtime_config_snapshot = lambda: runtime_config

credentials = HTTPAuthorizationCredentials(
    scheme="Bearer",
    credentials=settings.API_TOKEN,
)
request = SimpleNamespace(headers={})

async def run_requests():
    chat_response = await chat_completions(
        payload=schemas.OpenAIChatCompletionsRequest(
            messages=[schemas.OpenAIChatMessage(role="user", content="hello")]
        ),
        request=request,
        credentials=credentials,
    )
    responses_response = await responses(
        payload=schemas.OpenAIResponsesRequest(input="hello"),
        credentials=credentials,
    )
    anthropic_response = await anthropic_messages(
        payload=schemas.AnthropicMessagesRequest(
            messages=[schemas.AnthropicMessage(role="user", content="hello")]
        ),
        x_api_key=settings.API_TOKEN,
    )
    return chat_response, responses_response, anthropic_response

protocol_responses = asyncio.run(run_requests())
forbidden = (
    "app.agent.callback",
    "app.agent.llm.helper",
    "app.agent.orchestrator",
    "app.agent.tools.base",
    "app.agent.tools.catalog",
    "app.agent.tools.factory",
    "app.agent.tools.impl",
    "langgraph",
)
loaded = sorted(
    name
    for name in sys.modules
    if any(name == prefix or name.startswith(prefix + ".") for prefix in forbidden)
)
print(json.dumps({
    "loaded": loaded,
    "network_attempts": network_attempts,
    "status_codes": [response.status_code for response in protocol_responses],
    "bodies": [json.loads(response.body) for response in protocol_responses],
}, ensure_ascii=False))
''',
        tmp_path / "disabled-requests",
    )

    assert result["loaded"] == []
    assert result["network_attempts"] == []
    assert result["status_codes"] == [503, 503, 503]
    assert result["bodies"][0]["error"]["code"] == "ai_agent_disabled"
    assert result["bodies"][1]["error"]["code"] == "ai_agent_disabled"
    assert result["bodies"][2]["error"]["type"] == "api_error"


def test_runtime_agent_type_factories_are_single_flight(tmp_path: Path) -> None:
    """并发首次解析必须返回同一 class，避免会话复用误判构造器已变化。"""
    result = _run_isolated(
        r'''
import json
import sys
import threading
import time
import types

sites = types.ModuleType("app.application.site.sites")
sites.SitesHelper = type("SitesHelper", (), {})
sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites

from app.api.endpoints import agent, openai

def exercise(module, factory_name, getter_name):
    calls = []
    call_lock = threading.Lock()
    start = threading.Barrier(8)

    class RuntimeAgent:
        pass

    def get_runtime_type():
        with call_lock:
            calls.append(1)
        time.sleep(0.02)
        return RuntimeAgent

    setattr(module, getter_name, get_runtime_type)
    factory = getattr(module, factory_name)
    results = []

    def resolve():
        start.wait()
        results.append(factory())

    threads = [threading.Thread(target=resolve) for _ in range(8)]
    for thread in threads:
        thread.start()
    for thread in threads:
        thread.join()
    return len(calls), all(result is results[0] for result in results)

web_calls, web_identity = exercise(
    agent,
    "_get_web_agent_type",
    "get_moviepilot_agent_type",
)
collecting_calls, collecting_identity = exercise(
    openai,
    "_get_collecting_agent_type",
    "get_moviepilot_agent_type",
)
print(json.dumps({
    "web_calls": web_calls,
    "web_identity": web_identity,
    "collecting_calls": collecting_calls,
    "collecting_identity": collecting_identity,
}))
''',
        tmp_path / "agent-type-single-flight",
    )

    assert result == {
        "web_calls": 1,
        "web_identity": True,
        "collecting_calls": 1,
        "collecting_identity": True,
    }


def test_persistent_protocol_agent_rebinds_stream_queue_without_stale_output(
    tmp_path: Path,
) -> None:
    """稳定协议会话复用 Agent 时必须保留 handler identity 并切换请求队列。"""
    result = _run_isolated(
        r'''
import asyncio
import json
import sys
import types

sites = types.ModuleType("app.application.site.sites")
sites.SitesHelper = type("SitesHelper", (), {})
sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites

from app.api.endpoints import openai

class RuntimeAgent:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        self.stream_handler = object()
        self._compiled_agent_bundle = object()

class TestStreamingHandler(openai._OpenAIStreamingHandlerMixin):
    pass

openai._get_openai_streaming_handler_type = lambda: TestStreamingHandler
agent_type = openai._build_collecting_agent_type(RuntimeAgent)
agent = agent_type(session_id="stable", user_id="api")
first_queue = asyncio.Queue()
second_queue = asyncio.Queue()

agent.configure_protocol_request(stream_mode=True, event_queue=first_queue)
handler = agent.stream_handler
handler._event_queue.put_nowait("first")
compiled_bundle = object()
agent._compiled_agent_bundle = compiled_bundle

agent.configure_protocol_request(stream_mode=True, event_queue=second_queue)
agent.release_protocol_request(first_queue)
handler._event_queue.put_nowait("second")
agent.release_protocol_request(second_queue)

print(json.dumps({
    "same_handler": agent.stream_handler is handler,
    "same_bundle": agent._compiled_agent_bundle is compiled_bundle,
    "first": first_queue.get_nowait(),
    "first_empty": first_queue.empty(),
    "second": second_queue.get_nowait(),
    "second_empty": second_queue.empty(),
    "released": handler._event_queue is None,
}))
''',
        tmp_path / "protocol-stream-rebind",
    )

    assert result == {
        "same_handler": True,
        "same_bundle": True,
        "first": "first",
        "first_empty": True,
        "second": "second",
        "second_empty": True,
        "released": True,
    }


def test_protocol_routes_follow_agent_service_lifecycle(tmp_path: Path) -> None:
    """服务未运行时返回 503，运行态仍执行原有兼容协议响应流程。"""
    result = _run_isolated(
        r'''
import asyncio
import json
import socket
import sys
import types
from types import SimpleNamespace

network_attempts = []

def block_network(*args, **kwargs):
    network_attempts.append(repr(args[:2]))
    raise AssertionError("protocol lifecycle test attempted network access")

socket.create_connection = block_network
socket.getaddrinfo = block_network
socket.socket.connect = block_network

sites = types.ModuleType("app.application.site.sites")
sites.SitesHelper = type("SitesHelper", (), {})
sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites

from fastapi.security import HTTPAuthorizationCredentials
from app import schemas
from app.api.endpoints import anthropic, openai
from app.application.configuration import ApiRuntimeConfig
from app.runtime.config import settings

settings.AI_AGENT_ENABLE = True
runtime_config = ApiRuntimeConfig(
    False, 60, False, True,
    api_token=settings.API_TOKEN,
)
anthropic.get_api_runtime_config_snapshot = lambda: runtime_config
openai.get_api_runtime_config_snapshot = lambda: runtime_config
credentials = HTTPAuthorizationCredentials(
    scheme="Bearer",
    credentials=settings.API_TOKEN,
)
request = SimpleNamespace(headers={})
chat_payload = schemas.OpenAIChatCompletionsRequest(
    messages=[schemas.OpenAIChatMessage(role="user", content="hello")]
)
anthropic_payload = schemas.AnthropicMessagesRequest(
    messages=[schemas.AnthropicMessage(role="user", content="hello")]
)

async def run_unavailable():
    return (
        await openai.chat_completions(chat_payload, request, credentials),
        await anthropic.messages(
            anthropic_payload,
            x_api_key=settings.API_TOKEN,
        ),
    )

unavailable = asyncio.run(run_unavailable())

class RuntimeAgent:
    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)
        self.stream_handler = object()
        self._compiled_agent_bundle = None

    async def process(self, _prompt, **_kwargs):
        return "runtime reply"

class RunningManager:
    async def process_message(self, **kwargs):
        agent = kwargs["agent_factory"](
            session_id=kwargs["session_id"],
            user_id=kwargs["user_id"],
            channel=kwargs["channel"],
            source=kwargs["source"],
            username=kwargs["username"],
        )
        kwargs["agent_setup"](agent)
        return await agent.process(
            kwargs["message"],
            images=kwargs["images"],
            files=kwargs["files"],
        )

    async def clear_session(self, **_kwargs):
        return None

running_manager = RunningManager()
openai.get_running_agent_manager = lambda: running_manager
anthropic.get_running_agent_manager = lambda: running_manager
openai.get_moviepilot_agent_type = lambda: RuntimeAgent

async def run_available():
    return (
        await openai.chat_completions(chat_payload, request, credentials),
        await anthropic.messages(
            anthropic_payload,
            x_api_key=settings.API_TOKEN,
        ),
    )

available = asyncio.run(run_available())
openai_body = json.loads(available[0].body)
print(json.dumps({
    "unavailable_status": [response.status_code for response in unavailable],
    "unavailable_codes": [
        json.loads(unavailable[0].body)["error"]["code"],
        json.loads(unavailable[1].body)["error"]["type"],
    ],
    "available_openai": openai_body["choices"][0]["message"]["content"],
    "available_anthropic": available[1].content[0].text,
    "network_attempts": network_attempts,
}, ensure_ascii=False))
''',
        tmp_path / "protocol-service-lifecycle",
    )

    assert result == {
        "unavailable_status": [503, 503],
        "unavailable_codes": ["ai_agent_unavailable", "api_error"],
        "available_openai": "runtime reply",
        "available_anthropic": "runtime reply",
        "network_attempts": [],
    }
