"""Tests for server graph MCP loading behavior."""

from __future__ import annotations

import importlib
import os
import sys
from types import ModuleType, SimpleNamespace
from typing import TYPE_CHECKING
from unittest.mock import AsyncMock, MagicMock, patch

from deepagents_code._env_vars import SERVER_ENV_PREFIX
from deepagents_code._server_config import ServerConfig

if TYPE_CHECKING:
    import pytest


def _import_fresh_server_graph() -> ModuleType:
    """Import `deepagents_code.server_graph` from a clean module state."""
    sys.modules.pop("deepagents_code.server_graph", None)
    return importlib.import_module("deepagents_code.server_graph")


def _module_with_attrs(name: str, **attrs: object) -> ModuleType:
    """Create a module stub with dynamically assigned attributes."""
    module = ModuleType(name)
    for key, value in attrs.items():
        setattr(module, key, value)
    return module


class TestServerGraph:
    """Tests for server-mode graph bootstrap."""

    def test_auto_discovery_loads_mcp_without_explicit_config(self) -> None:
        """Server mode should auto-discover MCP configs when no path is passed."""
        graph_obj = object()
        model_obj = object()
        fetch_tool = object()
        thread_tool = object()
        mcp_tool = object()
        mcp_server_info = [SimpleNamespace(name="docs")]
        create_cli_agent = MagicMock(return_value=(graph_obj, object()))
        agent_module = _module_with_attrs(
            "deepagents_code.agent",
            DEFAULT_AGENT_NAME="agent",
            create_cli_agent=create_cli_agent,
            load_async_subagents=MagicMock(return_value=None),
        )

        model_result = SimpleNamespace(
            model=model_obj,
            apply_to_settings=MagicMock(),
        )
        config_module = _module_with_attrs(
            "deepagents_code.config",
            create_model=MagicMock(return_value=model_result),
            settings=SimpleNamespace(
                has_tavily=False,
                reload_from_environment=MagicMock(),
            ),
        )

        tools_module = _module_with_attrs(
            "deepagents_code.tools",
            fetch_url=fetch_tool,
            get_current_thread_id=thread_tool,
            web_search=object(),
        )

        class FakeSessionManager:
            async def cleanup(self) -> None:
                return None

        resolve_mcp_tools = AsyncMock(return_value=([mcp_tool], None, mcp_server_info))
        mcp_module = _module_with_attrs(
            "deepagents_code.mcp_tools",
            MCPSessionManager=FakeSessionManager,
            resolve_and_load_mcp_tools=resolve_mcp_tools,
        )

        # Build env from ServerConfig to exercise the same serialization
        # path the real CLI uses.
        config = ServerConfig(no_mcp=False)
        env_overrides = {}
        for suffix, value in config.to_env().items():
            if value is not None:
                env_overrides[f"{SERVER_ENV_PREFIX}{suffix}"] = value

        with (
            patch.dict(os.environ, env_overrides, clear=False),
            patch.dict(
                sys.modules,
                {
                    "deepagents_code.agent": agent_module,
                    "deepagents_code.config": config_module,
                    "deepagents_code.tools": tools_module,
                    "deepagents_code.mcp_tools": mcp_module,
                },
            ),
            patch(
                "deepagents_code.project_utils.get_server_project_context",
                return_value=None,
            ),
        ):
            for suffix in (
                "MCP_CONFIG_PATH",
                "TRUST_PROJECT_MCP",
                "CWD",
                "PROJECT_ROOT",
            ):
                os.environ.pop(f"{SERVER_ENV_PREFIX}{suffix}", None)

            module = _import_fresh_server_graph()

        resolve_mcp_tools.assert_awaited_once()
        kwargs = resolve_mcp_tools.await_args_list[0].kwargs
        assert kwargs["explicit_config_path"] is None
        assert kwargs["no_mcp"] is False
        assert kwargs["trust_project_mcp"] is None
        assert kwargs["project_context"] is None
        assert kwargs["stateless"] is True
        assert isinstance(kwargs["session_manager"], FakeSessionManager)
        create_cli_agent.assert_called_once_with(
            model=model_obj,
            assistant_id="agent",
            tools=[fetch_tool, thread_tool, mcp_tool],
            sandbox=None,
            sandbox_type=None,
            system_prompt=None,
            interactive=True,
            auto_approve=False,
            interrupt_shell_only=False,
            shell_allow_list=None,
            enable_ask_user=False,
            enable_memory=True,
            enable_skills=True,
            enable_shell=True,
            enable_interpreter=False,
            mcp_server_info=mcp_server_info,
            cwd=None,
            project_context=None,
            async_subagents=None,
        )
        assert module.graph is graph_obj


class TestStartupErrorMarker:
    """`emit_startup_failure` must produce the parser marker on stderr.

    The marker is the contract `wait_for_server_healthy` parses to surface
    a one-line summary instead of "Server process exited with code N".
    """

    def test_emits_marker_with_type_and_summary(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        from deepagents_code._startup_error import (
            STARTUP_ERROR_MARKER,
            emit_startup_failure,
        )

        emit_startup_failure(ValueError("boom: details"))
        captured = capsys.readouterr()
        assert f"{STARTUP_ERROR_MARKER}ValueError: boom: details" in captured.err
        assert "Failed to initialize server graph: boom: details" in captured.err

    def test_marker_collapses_multiline_exception(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        from deepagents_code._startup_error import (
            STARTUP_ERROR_MARKER,
            emit_startup_failure,
        )

        emit_startup_failure(ValueError("first line\nsecond line"))
        captured = capsys.readouterr()
        marker_line = next(
            line
            for line in captured.err.splitlines()
            if line.startswith(STARTUP_ERROR_MARKER)
        )
        assert marker_line == f"{STARTUP_ERROR_MARKER}ValueError: first line"

    def test_marker_handles_empty_exception_message(
        self,
        capsys: pytest.CaptureFixture[str],
    ) -> None:
        from deepagents_code._startup_error import (
            STARTUP_ERROR_MARKER,
            emit_startup_failure,
        )

        emit_startup_failure(RuntimeError())
        captured = capsys.readouterr()
        assert f"{STARTUP_ERROR_MARKER}RuntimeError: <no message>" in captured.err
