"""Tests for MCPServerStreamableHttp httpx_client_factory functionality."""

from __future__ import annotations

import base64
from unittest.mock import MagicMock, patch

import httpx
import pytest
from anyio import create_memory_object_stream
from mcp.shared.message import SessionMessage
from mcp.types import JSONRPCMessage, JSONRPCNotification, JSONRPCRequest

from agents.mcp import MCPServerStreamableHttp
from agents.mcp.server import (
    _create_default_streamable_http_client,
    _InitializedNotificationTolerantStreamableHTTPTransport,
    _streamablehttp_client_with_transport,
)


class TestMCPServerStreamableHttpClientFactory:
    """Test cases for custom httpx_client_factory parameter."""

    @pytest.mark.asyncio
    async def test_default_httpx_client_factory(self):
        """Test that default behavior works when no custom factory is provided."""
        # Mock the streamablehttp_client to avoid actual network calls
        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "http://localhost:8000/mcp",
                    "headers": {"Authorization": "Bearer token"},
                    "timeout": 10,
                }
            )

            server.create_streams()

            # Verify streamablehttp_client was called with the hardened default factory.
            mock_client.assert_called_once_with(
                url="http://localhost:8000/mcp",
                headers={"Authorization": "Bearer token"},
                timeout=10,
                sse_read_timeout=300,  # Default value
                terminate_on_close=True,  # Default value
                httpx_client_factory=_create_default_streamable_http_client,
            )

    @pytest.mark.asyncio
    async def test_custom_httpx_client_factory(self):
        """Test that custom httpx_client_factory is passed correctly."""

        # Create a custom factory function
        def custom_factory(
            headers: dict[str, str] | None = None,
            timeout: httpx.Timeout | None = None,
            auth: httpx.Auth | None = None,
        ) -> httpx.AsyncClient:
            return httpx.AsyncClient(
                verify=False,  # Disable SSL verification for testing
                timeout=httpx.Timeout(60.0),
                headers={"X-Custom-Header": "test"},
            )

        # Mock the streamablehttp_client to avoid actual network calls
        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "http://localhost:8000/mcp",
                    "headers": {"Authorization": "Bearer token"},
                    "timeout": 10,
                    "httpx_client_factory": custom_factory,
                }
            )

            # Create streams should pass the custom factory
            server.create_streams()

            # Verify streamablehttp_client was called with the custom factory
            mock_client.assert_called_once_with(
                url="http://localhost:8000/mcp",
                headers={"Authorization": "Bearer token"},
                timeout=10,
                sse_read_timeout=300,  # Default value
                terminate_on_close=True,  # Default value
                httpx_client_factory=custom_factory,
            )

    @pytest.mark.asyncio
    async def test_custom_httpx_client_factory_with_ssl_cert(self):
        """Test custom factory with SSL certificate configuration."""

        def ssl_cert_factory(
            headers: dict[str, str] | None = None,
            timeout: httpx.Timeout | None = None,
            auth: httpx.Auth | None = None,
        ) -> httpx.AsyncClient:
            return httpx.AsyncClient(
                verify="/path/to/cert.pem",  # Custom SSL certificate
                timeout=httpx.Timeout(120.0),
            )

        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "https://secure-server.com/mcp",
                    "timeout": 30,
                    "httpx_client_factory": ssl_cert_factory,
                }
            )

            server.create_streams()

            mock_client.assert_called_once_with(
                url="https://secure-server.com/mcp",
                headers=None,
                timeout=30,
                sse_read_timeout=300,
                terminate_on_close=True,
                httpx_client_factory=ssl_cert_factory,
            )

    @pytest.mark.asyncio
    async def test_custom_httpx_client_factory_with_proxy(self):
        """Test custom factory with proxy configuration."""

        def proxy_factory(
            headers: dict[str, str] | None = None,
            timeout: httpx.Timeout | None = None,
            auth: httpx.Auth | None = None,
        ) -> httpx.AsyncClient:
            return httpx.AsyncClient(
                proxy="http://proxy.example.com:8080",
                timeout=httpx.Timeout(60.0),
            )

        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "http://localhost:8000/mcp",
                    "httpx_client_factory": proxy_factory,
                }
            )

            server.create_streams()

            mock_client.assert_called_once_with(
                url="http://localhost:8000/mcp",
                headers=None,
                timeout=5,  # Default value
                sse_read_timeout=300,
                terminate_on_close=True,
                httpx_client_factory=proxy_factory,
            )

    @pytest.mark.asyncio
    async def test_custom_httpx_client_factory_with_retry_logic(self):
        """Test custom factory with retry logic configuration."""

        def retry_factory(
            headers: dict[str, str] | None = None,
            timeout: httpx.Timeout | None = None,
            auth: httpx.Auth | None = None,
        ) -> httpx.AsyncClient:
            return httpx.AsyncClient(
                timeout=httpx.Timeout(30.0),
                # Note: httpx doesn't have built-in retry, but this shows how
                # a custom factory could be used to configure retry behavior
                # through middleware or other mechanisms
            )

        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "http://localhost:8000/mcp",
                    "httpx_client_factory": retry_factory,
                }
            )

            server.create_streams()

            mock_client.assert_called_once_with(
                url="http://localhost:8000/mcp",
                headers=None,
                timeout=5,
                sse_read_timeout=300,
                terminate_on_close=True,
                httpx_client_factory=retry_factory,
            )

    def test_httpx_client_factory_type_annotation(self):
        """Test that the type annotation is correct for httpx_client_factory."""
        from agents.mcp.server import MCPServerStreamableHttpParams

        # This test ensures the type annotation is properly set
        # We can't easily test the TypedDict at runtime, but we can verify
        # that the import works and the type is available
        assert hasattr(MCPServerStreamableHttpParams, "__annotations__")

        # Verify that the httpx_client_factory parameter is in the annotations
        annotations = MCPServerStreamableHttpParams.__annotations__
        assert "httpx_client_factory" in annotations

        # The annotation should contain the string representation of the type
        annotation_str = str(annotations["httpx_client_factory"])
        assert "HttpClientFactory" in annotation_str

    @pytest.mark.asyncio
    async def test_all_parameters_with_custom_factory(self):
        """Test that all parameters work together with custom factory."""

        def comprehensive_factory(
            headers: dict[str, str] | None = None,
            timeout: httpx.Timeout | None = None,
            auth: httpx.Auth | None = None,
        ) -> httpx.AsyncClient:
            return httpx.AsyncClient(
                verify=False,
                timeout=httpx.Timeout(90.0),
                headers={"X-Test": "value"},
            )

        with patch("agents.mcp.server.streamablehttp_client") as mock_client:
            mock_client.return_value = MagicMock()

            server = MCPServerStreamableHttp(
                params={
                    "url": "https://api.example.com/mcp",
                    "headers": {"Authorization": "Bearer token"},
                    "timeout": 45,
                    "sse_read_timeout": 600,
                    "terminate_on_close": False,
                    "httpx_client_factory": comprehensive_factory,
                }
            )

            server.create_streams()

            mock_client.assert_called_once_with(
                url="https://api.example.com/mcp",
                headers={"Authorization": "Bearer token"},
                timeout=45,
                sse_read_timeout=600,
                terminate_on_close=False,
                httpx_client_factory=comprehensive_factory,
            )


@pytest.mark.asyncio
async def test_initialized_notification_failure_returns_synthetic_success():
    async def handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(503, request=request)

    transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp")
    read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0)
    client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        ctx = MagicMock()
        ctx.client = client
        ctx.read_stream_writer = read_stream_writer
        ctx.session_message = SessionMessage(
            JSONRPCMessage(
                JSONRPCNotification(
                    jsonrpc="2.0",
                    method="notifications/initialized",
                    params={},
                )
            )
        )

        await transport._handle_post_request(ctx)
    finally:
        await client.aclose()
        await read_stream_writer.aclose()


@pytest.mark.asyncio
async def test_initialized_notification_transport_exception_returns_synthetic_success():
    async def handler(request: httpx.Request) -> httpx.Response:
        raise httpx.ConnectError("boom", request=request)

    transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp")
    read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0)
    client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        ctx = MagicMock()
        ctx.client = client
        ctx.read_stream_writer = read_stream_writer
        ctx.session_message = SessionMessage(
            JSONRPCMessage(
                JSONRPCNotification(
                    jsonrpc="2.0",
                    method="notifications/initialized",
                    params={},
                )
            )
        )

        await transport._handle_post_request(ctx)
    finally:
        await client.aclose()
        await read_stream_writer.aclose()


@pytest.mark.asyncio
async def test_streamable_http_server_passes_ignore_initialized_notification_failure():
    with patch("agents.mcp.server._streamablehttp_client_with_transport") as mock_client:
        mock_client.return_value = MagicMock()

        server = MCPServerStreamableHttp(
            params={
                "url": "http://localhost:8000/mcp",
                "ignore_initialized_notification_failure": True,
            }
        )

        server.create_streams()

        kwargs = mock_client.call_args.kwargs
        assert kwargs["url"] == "http://localhost:8000/mcp"
        assert kwargs["headers"] is None
        assert kwargs["timeout"] == 5
        assert kwargs["sse_read_timeout"] == 300
        assert kwargs["terminate_on_close"] is True
        assert kwargs["httpx_client_factory"] is _create_default_streamable_http_client
        assert (
            kwargs["transport_factory"] is _InitializedNotificationTolerantStreamableHTTPTransport
        )


@pytest.mark.asyncio
async def test_transport_preserves_non_initialized_failures():
    async def handler(request: httpx.Request) -> httpx.Response:
        raise httpx.ConnectError("boom", request=request)

    transport = _InitializedNotificationTolerantStreamableHTTPTransport("https://example.test/mcp")
    read_stream_writer, _ = create_memory_object_stream[SessionMessage | Exception](0)
    client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
    try:
        ctx = MagicMock()
        ctx.client = client
        ctx.read_stream_writer = read_stream_writer
        ctx.session_message = SessionMessage(
            JSONRPCMessage(
                JSONRPCRequest(
                    jsonrpc="2.0",
                    id=1,
                    method="tools/list",
                    params={},
                )
            )
        )

        with pytest.raises(httpx.ConnectError):
            await transport._handle_post_request(ctx)
    finally:
        await client.aclose()
        await read_stream_writer.aclose()


@pytest.mark.asyncio
async def test_stream_client_preserves_custom_factory_headers_timeout_and_auth():
    seen: dict[str, object] = {}

    class RecordingAuth(httpx.Auth):
        def auth_flow(self, request: httpx.Request):
            request.headers["Authorization"] = f"Basic {base64.b64encode(b'user:pass').decode()}"
            yield request

    async def handler(request: httpx.Request) -> httpx.Response:
        seen["request_headers"] = dict(request.headers)
        return httpx.Response(200, request=request)

    def base_factory(
        headers: dict[str, str] | None = None,
        timeout: httpx.Timeout | None = None,
        auth: httpx.Auth | None = None,
    ) -> httpx.AsyncClient:
        seen["factory_headers"] = headers
        seen["factory_timeout"] = timeout
        seen["factory_auth"] = auth
        return httpx.AsyncClient(
            headers=headers,
            timeout=timeout,
            auth=auth,
            transport=httpx.MockTransport(handler),
        )

    timeout = httpx.Timeout(12.0)
    auth = RecordingAuth()
    async with _streamablehttp_client_with_transport(
        "https://example.test/mcp",
        headers={"X-Test": "value"},
        timeout=12.0,
        sse_read_timeout=30.0,
        httpx_client_factory=base_factory,
        auth=auth,
        transport_factory=_InitializedNotificationTolerantStreamableHTTPTransport,
    ):
        pass

    assert seen["factory_headers"] == {"X-Test": "value"}
    seen_timeout = seen["factory_timeout"]
    assert isinstance(seen_timeout, httpx.Timeout)
    assert seen_timeout.connect == timeout.connect
    assert seen_timeout.read == 30.0
    assert seen_timeout.write == timeout.write
    assert seen_timeout.pool == timeout.pool
    assert seen["factory_auth"] is auth


@pytest.mark.asyncio
async def test_default_streamable_http_client_matches_expected_defaults():
    timeout = httpx.Timeout(12.0)
    auth = httpx.BasicAuth("user", "pass")

    client = _create_default_streamable_http_client(
        headers={"X-Test": "value"},
        timeout=timeout,
        auth=auth,
    )
    try:
        assert client.headers["X-Test"] == "value"
        assert client.timeout.connect == timeout.connect
        assert client.timeout.read == timeout.read
        assert client.timeout.write == timeout.write
        assert client.timeout.pool == timeout.pool
        assert client.auth is auth
        assert client.follow_redirects is False
    finally:
        await client.aclose()
