"""Drives the server as a real subprocess over stdio, using mcp's own ClientSession -- exactly
how an agent would talk to it."""

import os
import sys
import time

import anyio
import mcp.client.stdio as stdio_mod
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

SRC = os.path.join(os.path.dirname(os.path.dirname(__file__)), "src")
PARAMS = StdioServerParameters(
    command=sys.executable,
    args=["-m", "mcp_notes.server"],
    env={**os.environ, "PYTHONPATH": SRC},
)


async def _drive(fn):
    async with stdio_client(PARAMS) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            return await fn(session)


def test_tools_list_has_both():
    result = anyio.run(_drive, lambda s: s.list_tools())
    got = {t.name for t in result.tools}
    assert got == {"search_notes", "add_note"}, f"expected exactly search_notes and add_note, got {got}"


def test_invalid_args_get_schema_error_not_a_crash():
    async def go(session):
        return await session.call_tool("search_notes", {"query": "x", "limit": "five"})

    result = anyio.run(_drive, go)
    assert result.isError, "a non-integer limit must be rejected, not silently accepted"
    text = result.content[0].text.lower()
    assert "input validation error" in text, (
        f"a bad argument must be caught by JSON-schema validation before it reaches the "
        f"handler -- expected an 'Input validation error' message, got {text!r}"
    )


def test_note_round_trips():
    async def go(session):
        added = await session.call_tool("add_note", {"title": "hello", "body": "world"})
        found = await session.call_tool("search_notes", {"query": "hello"})
        return added, found

    added, found = anyio.run(_drive, go)
    assert not added.isError, f"add_note must succeed on valid input, got {added.content}"
    notes = found.structuredContent["results"]
    assert any(n["title"] == "hello" and n["body"] == "world" for n in notes), (
        f"the note just added must come back from a search matching its title; got {notes}"
    )


def test_limit_is_honoured():
    async def go(session):
        for i in range(5):
            await session.call_tool("add_note", {"title": f"widget {i}", "body": "a widget"})
        return await session.call_tool("search_notes", {"query": "widget", "limit": 2})

    result = anyio.run(_drive, go)
    notes = result.structuredContent["results"]
    assert len(notes) == 2, f"limit=2 must cap the results to 2 even though 5 notes match; got {len(notes)}"


def test_unknown_tool_gets_a_proper_error():
    result = anyio.run(_drive, lambda s: s.call_tool("no_such_tool", {}))
    assert result.isError, "calling a tool that was never listed must be an error, not a silent no-op"
    text = result.content[0].text.lower()
    assert "unknown tool" in text, (
        f"the error should name the problem clearly ('unknown tool: ...'), not leak an internal "
        f"lookup exception; got {text!r}"
    )


def test_server_exits_cleanly_on_eof():
    # On exit, mcp's client closes the server's stdin and waits PROCESS_TERMINATION_TIMEOUT (2.0s in
    # the pinned SDK) for the process to go away on its own; only then does it fall back to SIGTERM.
    # A server that reads EOF and returns is gone long before that deadline, so the shutdown alone
    # is timed here -- process spawn and `initialize` are excluded, since a slow machine must not
    # turn a well-behaved server into a red row.
    deadline = stdio_mod.PROCESS_TERMINATION_TIMEOUT
    marks: dict[str, float] = {}

    async def go():
        async with stdio_client(PARAMS) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()
                await session.list_tools()
            marks["closed"] = time.monotonic()
        marks["exited"] = time.monotonic()

    anyio.run(go)
    elapsed = marks["exited"] - marks["closed"]
    assert elapsed < deadline / 2, (
        f"closing stdin should let the server notice EOF and exit on its own, well under the "
        f"{deadline:.1f}s force-kill fallback; took {elapsed:.2f}s -- it was likely force-terminated"
    )
