"""Transport-level composed flows: multi-client isolation, reconnection, and dual-transport hosting.

These scenarios are about how the transport layer holds together across more than one connection
or more than one transport, so they connect real `Client`s against one mounted server rather than
running over the matrix.
"""

import anyio
import httpx
import pytest
from inline_snapshot import snapshot
from mcp_types import CallToolResult, LoggingMessageNotificationParams, TextContent

from mcp.client.session import LoggingFnT
from mcp.server.mcpserver import Context, MCPServer
from tests.interaction._connect import client_via_http, connect_over_sse, mounted_app
from tests.interaction._requirements import requirement

pytestmark = pytest.mark.anyio


@requirement("flow:multi-client:stateful-isolation")
async def test_concurrent_clients_on_one_stateful_server_receive_only_their_own_notifications() -> None:
    """Two clients on one stateful manager each receive only the notifications their own request produced.

    Complements `test_terminating_one_session_leaves_others_working` (which proves session
    independence under termination) with the notification-isolation dimension: a notification
    emitted by one session's handler does not leak to another session's client.
    """
    mcp = MCPServer("multi")

    @mcp.tool()
    async def announce(label: str, ctx: Context) -> str:
        """Emit one info-level log carrying the caller's label, then return it."""
        await ctx.info(label)  # pyright: ignore[reportDeprecated]
        return label

    received_a: list[object] = []
    received_b: list[object] = []

    async def collect_a(params: LoggingMessageNotificationParams) -> None:
        received_a.append(params.data)

    async def collect_b(params: LoggingMessageNotificationParams) -> None:
        received_b.append(params.data)

    async with mounted_app(mcp) as (http, _):
        with anyio.fail_after(5):
            async with anyio.create_task_group() as tg:  # pragma: no branch

                async def call(label: str, collect: LoggingFnT) -> None:
                    async with client_via_http(http, logging_callback=collect) as client:
                        await client.call_tool("announce", {"label": label})

                tg.start_soon(call, "a", collect_a)
                tg.start_soon(call, "b", collect_b)

    assert received_a == ["a"]
    assert received_b == ["b"]


@requirement("flow:session:terminate-then-reconnect")
async def test_a_fresh_connection_after_termination_obtains_a_new_session_and_operates() -> None:
    """After a client terminates, a fresh connection to the same manager gets a distinct session.

    Steps: (1) connect a client and call list_tools, (2) the client exits (its DELETE fires),
    (3) connect a second client to the same mounted app, (4) the second client's call_tool
    succeeds and the recorded session ids show two distinct sessions were issued.
    """
    mcp = MCPServer("reconnectable")

    @mcp.tool()
    def echo(text: str) -> str:
        """Return the input unchanged."""
        return text

    session_ids: list[str] = []

    async def record(request: httpx.Request) -> None:
        session_id = request.headers.get("mcp-session-id")
        if session_id is not None:
            session_ids.append(session_id)

    async with mounted_app(mcp, on_request=record) as (http, _):
        async with client_via_http(http) as first:
            first_result = await first.list_tools()
        async with client_via_http(http) as second:
            second_result = await second.call_tool("echo", {"text": "again"})

    assert {tool.name for tool in first_result.tools} == {"echo"}
    assert second_result == snapshot(
        CallToolResult(content=[TextContent(text="again")], structured_content={"result": "again"})
    )
    distinct = set(session_ids)
    assert len(distinct) == 2, f"expected two distinct session ids across the two connections, saw {distinct}"


@requirement("flow:compat:dual-transport-server")
async def test_one_server_serves_streamable_http_and_sse_clients_concurrently() -> None:
    """One MCPServer instance serves a streamable-HTTP client and a legacy-SSE client at the same time.

    The two transports have independent connection management (the streamable-HTTP session manager
    versus a per-connection SSE handler), but both dispatch into the same server's request
    handlers. The test connects one client over each transport against the same instance and
    proves both reach the same tool. Uses MCPServer because the low-level Server has no SSE
    convenience; the entry is about hosting composition, not the low-level API.
    """
    mcp = MCPServer("dual")

    @mcp.tool()
    def echo(text: str) -> str:
        """Return the input unchanged."""
        return text

    async with (
        mounted_app(mcp) as (http, _),
        connect_over_sse(mcp) as sse_client,
        client_via_http(http) as shttp_client,
    ):
        with anyio.fail_after(5):
            shttp_result = await shttp_client.call_tool("echo", {"text": "via http"})
            sse_result = await sse_client.call_tool("echo", {"text": "via sse"})

    assert shttp_result == snapshot(
        CallToolResult(content=[TextContent(text="via http")], structured_content={"result": "via http"})
    )
    assert sse_result == snapshot(
        CallToolResult(content=[TextContent(text="via sse")], structured_content={"result": "via sse"})
    )
