"""Guard against 2026-era protocol vocabulary leaking onto legacy (2025-era) exchanges.

The 2026-07-28 spec revision introduces wire vocabulary that did not exist before it --
result-envelope fields (`resultType`, `ttlMs`, `cacheScope`), namespaced
`io.modelcontextprotocol/*` `_meta` keys, the version literal itself, and the per-request HTTP
headers `Mcp-Method` / `Mcp-Name` / `Mcp-Param-*`. None of that may appear on a connection
negotiated at an earlier protocol version: a test that records a plain legacy round trip and
runs it through :func:`assert_no_modern_vocabulary` will start failing the moment a 2026 change
leaks onto the existing wire.

Tests construct a :class:`RecordedExchange` from whatever instrumentation they have to hand --
the `on_request` / `on_response` hooks on :func:`tests.interaction._connect.mounted_app` for the
HTTP seam, and :class:`tests.interaction._helpers.RecordingTransport` for the JSON-RPC frames --
and pass it to the assertion. The helper scans header names and serialised bodies; it makes no
assumptions about which side produced what.
"""

from dataclasses import dataclass

import httpx
from mcp_types import JSONRPCMessage, jsonrpc_message_adapter

#: Substrings that must not appear anywhere in a request body or JSON-RPC frame on a legacy
#: exchange. Matching is by raw substring against the by-alias JSON serialisation, so a leaked
#: field name, `_meta` key prefix, or version literal is caught regardless of where in the
#: payload it sits.
MODERN_BODY_TOKENS: frozenset[str] = frozenset(
    {
        "resultType",
        "ttlMs",
        "cacheScope",
        "io.modelcontextprotocol/",
        "2026-07-28",
    }
)

#: Lower-cased HTTP header names introduced by the 2026-07-28 transport.
MODERN_HEADER_NAMES: frozenset[str] = frozenset({"mcp-method", "mcp-name"})

#: Lower-cased prefix for the 2026-07-28 per-parameter header family.
MODERN_HEADER_PREFIX = "mcp-param-"


@dataclass
class RecordedExchange:
    """Everything a test captured from one streamable-HTTP conversation, for vocabulary scanning.

    `requests` and `responses` are inspected for header names and (for requests) body bytes;
    `frames` are re-serialised to their wire JSON and scanned as body text. Response bodies are
    not read here -- streamable-HTTP responses are SSE streams that are consumed elsewhere -- so
    the server-to-client body content must be supplied via `frames`.
    """

    requests: list[httpx.Request]
    responses: list[httpx.Response]
    frames: list[JSONRPCMessage]


def assert_no_modern_vocabulary(recorded: RecordedExchange) -> None:
    """Fail if any 2026-era header name or body token appears anywhere in `recorded`.

    All findings are collected before asserting so a single failure reports every leak.
    """
    header_names = [name.lower() for request in recorded.requests for name in request.headers]
    header_names += [name.lower() for response in recorded.responses for name in response.headers]
    leaked = [
        f"header {name!r}"
        for name in header_names
        if name in MODERN_HEADER_NAMES or name.startswith(MODERN_HEADER_PREFIX)
    ]

    corpus = b"".join(request.content for request in recorded.requests).decode()
    corpus += "".join(
        jsonrpc_message_adapter.dump_json(frame, by_alias=True, exclude_none=True).decode() for frame in recorded.frames
    )
    leaked.extend(f"body token {token!r}" for token in MODERN_BODY_TOKENS if token in corpus)

    assert not leaked, f"Modern (2026-07-28) protocol vocabulary on a legacy exchange: {leaked}"
