"""Tests for the server bootstrap path that doesn't require running stdio."""

from __future__ import annotations

from unittest.mock import patch

import pytest
import respx

from accela_mcp.auth.refresher import RefreshTokenExpiredError
from accela_mcp.auth.token_store import Tokens, TokenStore
from accela_mcp.capabilities import LoadedConfig
from accela_mcp.server import StartupError, build_context, register_enabled_tools
from accela_mcp.settings import Settings
from accela_mcp.tools._base import ToolContext


@pytest.mark.asyncio
async def test_build_context_requires_tokens(
    settings: Settings, loaded_config: LoadedConfig
) -> None:
    with pytest.raises(StartupError) as exc:
        await build_context(settings, loaded_config)
    assert "accela-mcp auth" in str(exc.value)


@pytest.mark.asyncio
async def test_build_context_agency_mismatch(
    settings: Settings,
    loaded_config: LoadedConfig,
    fresh_tokens: Tokens,
    token_store: TokenStore,
) -> None:
    other = fresh_tokens.model_copy(update={"agency": "ELSEWHERE"})
    token_store.save(other)
    with pytest.raises(StartupError) as exc:
        await build_context(settings, loaded_config)
    assert "agency" in str(exc.value).lower()


@pytest.mark.asyncio
async def test_build_context_environment_mismatch(
    settings: Settings,
    loaded_config: LoadedConfig,
    fresh_tokens: Tokens,
    token_store: TokenStore,
) -> None:
    other = fresh_tokens.model_copy(update={"environment": "PROD"})
    token_store.save(other)
    with pytest.raises(StartupError) as exc:
        await build_context(settings, loaded_config)
    assert "environment" in str(exc.value).lower()


@pytest.mark.asyncio
async def test_build_context_refresh_expired(
    settings: Settings,
    loaded_config: LoadedConfig,
    expired_refresh_tokens: Tokens,
    token_store: TokenStore,
) -> None:
    token_store.save(expired_refresh_tokens)
    with pytest.raises(RefreshTokenExpiredError):
        await build_context(settings, loaded_config)


@pytest.mark.asyncio
async def test_build_context_corrupt_tokens_routes_to_bootstrap(
    settings: Settings,
    loaded_config: LoadedConfig,
) -> None:
    """A tokens file encrypted under a different key (or otherwise corrupt)
    must surface as StartupError so serve_async falls into bootstrap mode,
    not as an uncaught RuntimeError that crashes the subprocess.

    Regression for v0.3.0: the MCPB extension would die mid-`initialize` if
    a leftover tokens.json from a previous CLI install couldn't be decrypted
    under the autogenerated key.
    """
    settings.token_path.parent.mkdir(parents=True, exist_ok=True)
    # Write garbage that Fernet can't decrypt with our key.
    settings.token_path.write_bytes(b"definitely-not-a-fernet-ciphertext")

    with pytest.raises(StartupError) as exc:
        await build_context(settings, loaded_config)
    assert "decrypt" in str(exc.value).lower()
    assert "accela_login" in str(exc.value)


@pytest.mark.asyncio
async def test_serve_async_bootstraps_on_corrupt_tokens(
    settings: Settings, loaded_config: LoadedConfig
) -> None:
    """End-to-end: serve_async should register only auth tools (not crash)
    when tokens.json exists but cannot be decrypted under the active key."""
    from unittest.mock import patch

    from accela_mcp.server import serve_async

    settings.token_path.parent.mkdir(parents=True, exist_ok=True)
    settings.token_path.write_bytes(b"corrupt-token-file-from-an-old-key")

    captured: dict[str, object] = {}

    async def fake_stdio(self) -> None:  # type: ignore[no-untyped-def]
        captured["tool_names"] = set(self._tool_manager._tools.keys())

    with patch("mcp.server.fastmcp.FastMCP.run_stdio_async", new=fake_stdio):
        await serve_async(settings=settings, config=loaded_config)

    names = captured["tool_names"]
    assert "accela_login" in names  # type: ignore[operator]
    assert "accela_search_records" not in names  # type: ignore[operator]


@pytest.mark.asyncio
@respx.mock
async def test_build_context_happy_path(
    settings: Settings,
    loaded_config: LoadedConfig,
    fresh_tokens: Tokens,
    token_store: TokenStore,
) -> None:
    token_store.save(fresh_tokens)
    ctx = await build_context(settings, loaded_config)
    assert ctx.client.agency == "NULLISLAND"
    assert ctx.client.environment == "TEST"
    await ctx.client.aclose()


def test_register_enabled_tools_creates_groups(tool_context: ToolContext) -> None:
    from mcp.server.fastmcp import FastMCP

    mcp = FastMCP("test-server")
    registered = register_enabled_tools(mcp, tool_context)
    # All default-on groups should have registered.
    assert "discovery" in registered
    assert "records_read" in registered
    # And a sampling of the actual tools should be present.
    assert mcp._tool_manager.get_tool("accela_get_agency") is not None
    assert mcp._tool_manager.get_tool("accela_search_records") is not None


def test_register_enabled_tools_skips_auth_group(tool_context: ToolContext) -> None:
    """`register_enabled_tools` must not double-register auth — `serve_async`
    handles auth separately so it works in bootstrap mode."""
    from mcp.server.fastmcp import FastMCP

    mcp = FastMCP("test-server")
    # Force auth into the enabled set so we'd notice if it leaked through.
    tool_context.config.enabled_groups.add("auth")

    registered = register_enabled_tools(mcp, tool_context)
    assert "auth" not in registered
    assert mcp._tool_manager.get_tool("accela_login") is None


@pytest.mark.asyncio
async def test_serve_async_bootstrap_mode_when_no_tokens(
    settings: Settings, loaded_config: LoadedConfig
) -> None:
    """When tokens are missing, serve_async should register auth tools and
    skip the rest. We patch run_stdio_async so we don't actually block."""
    from accela_mcp.server import serve_async

    captured: dict[str, object] = {}

    async def fake_stdio(self) -> None:  # type: ignore[no-untyped-def]
        # Snapshot the registered tool names then return so serve_async exits.
        captured["tool_names"] = set(self._tool_manager._tools.keys())

    with patch("mcp.server.fastmcp.FastMCP.run_stdio_async", new=fake_stdio):
        await serve_async(settings=settings, config=loaded_config)

    names = captured["tool_names"]
    assert "accela_login" in names  # type: ignore[operator]
    assert "accela_auth_status" in names  # type: ignore[operator]
    # Other groups must NOT have been registered (no tokens means no client).
    assert "accela_search_records" not in names  # type: ignore[operator]


@pytest.mark.asyncio
@respx.mock
async def test_serve_async_full_mode_with_tokens(
    settings: Settings,
    loaded_config: LoadedConfig,
    fresh_tokens: Tokens,
    token_store: TokenStore,
) -> None:
    """With valid tokens, serve_async should register both auth and the rest."""
    from accela_mcp.server import serve_async

    token_store.save(fresh_tokens)
    # Mock token introspection so the best-effort check doesn't hit the network.
    respx.get("https://auth.test.example/oauth2/tokeninfo").mock(
        return_value=__import__("httpx").Response(200, json={"userId": "u1"})
    )

    captured: dict[str, object] = {}

    async def fake_stdio(self) -> None:  # type: ignore[no-untyped-def]
        captured["tool_names"] = set(self._tool_manager._tools.keys())

    with patch("mcp.server.fastmcp.FastMCP.run_stdio_async", new=fake_stdio):
        await serve_async(settings=settings, config=loaded_config)

    names = captured["tool_names"]
    assert "accela_login" in names  # type: ignore[operator]
    assert "accela_search_records" in names  # type: ignore[operator]
    assert "accela_get_agency" in names  # type: ignore[operator]
