"""Tests for MCPOAuthClient."""

import asyncio
from io import BytesIO
from unittest.mock import AsyncMock, Mock, patch

import httpx
import pytest

from chuk_mcp_client_oauth.mcp_oauth import (
    DynamicClientRegistration,
    MCPAuthorizationMetadata,
    MCPOAuthClient,
)


def setup_discovery_mocks(mock_client, discovery_response):
    """
    Helper to set up discovery mocks for PRM fallback to AS.

    Args:
        mock_client: The mocked httpx.AsyncClient
        discovery_response: The AS metadata response dict
    """
    # Mock PRM discovery to fail (404 not found), then AS discovery succeeds
    prm_response = Mock()
    prm_response.status_code = 404
    prm_response.raise_for_status.side_effect = httpx.HTTPStatusError(
        "Not found", request=Mock(), response=prm_response
    )

    as_response = Mock()
    as_response.raise_for_status = Mock()
    as_response.json = Mock(return_value=discovery_response)

    # Return list of responses for side_effect
    return [prm_response, as_response]


class TestMCPAuthorizationMetadata:
    """Test MCPAuthorizationMetadata dataclass."""

    def test_from_dict_minimal(self):
        """Test creating metadata from minimal dict."""
        data = {
            "authorization_endpoint": "https://auth.example.com/authorize",
            "token_endpoint": "https://auth.example.com/token",
        }

        metadata = MCPAuthorizationMetadata.model_validate(data)

        assert metadata.authorization_endpoint == data["authorization_endpoint"]
        assert metadata.token_endpoint == data["token_endpoint"]
        assert metadata.registration_endpoint is None
        assert metadata.scopes_supported == []
        assert metadata.response_types_supported == ["code"]
        assert metadata.grant_types_supported == ["authorization_code", "refresh_token"]
        assert metadata.code_challenge_methods_supported == ["S256"]

    def test_from_dict_complete(self):
        """Test creating metadata from complete dict."""
        data = {
            "authorization_endpoint": "https://auth.example.com/authorize",
            "token_endpoint": "https://auth.example.com/token",
            "registration_endpoint": "https://auth.example.com/register",
            "scopes_supported": ["read", "write"],
            "response_types_supported": ["code", "token"],
            "grant_types_supported": ["authorization_code"],
            "code_challenge_methods_supported": ["S256", "plain"],
        }

        metadata = MCPAuthorizationMetadata.model_validate(data)

        assert metadata.authorization_endpoint == data["authorization_endpoint"]
        assert metadata.token_endpoint == data["token_endpoint"]
        assert metadata.registration_endpoint == data["registration_endpoint"]
        assert metadata.scopes_supported == ["read", "write"]
        assert metadata.response_types_supported == ["code", "token"]
        assert metadata.grant_types_supported == ["authorization_code"]
        assert metadata.code_challenge_methods_supported == ["S256", "plain"]


class TestDynamicClientRegistration:
    """Test DynamicClientRegistration dataclass."""

    def test_from_dict_minimal(self):
        """Test creating registration from minimal dict."""
        data = {"client_id": "test-client-123"}

        registration = DynamicClientRegistration.model_validate(data)

        assert registration.client_id == "test-client-123"
        assert registration.client_secret is None
        assert registration.client_id_issued_at is None
        assert registration.client_secret_expires_at == 0

    def test_from_dict_complete(self):
        """Test creating registration from complete dict."""
        data = {
            "client_id": "test-client-123",
            "client_secret": "secret-456",
            "client_id_issued_at": 1234567890,
            "client_secret_expires_at": 1234567999,
        }

        registration = DynamicClientRegistration.model_validate(data)

        assert registration.client_id == "test-client-123"
        assert registration.client_secret == "secret-456"
        assert registration.client_id_issued_at == 1234567890
        assert registration.client_secret_expires_at == 1234567999

    def test_to_dict_minimal(self):
        """Test converting minimal registration to dict."""
        registration = DynamicClientRegistration(client_id="test-client-123")

        result = registration.model_dump(exclude_defaults=True, exclude_none=True)

        assert result == {"client_id": "test-client-123"}

    def test_to_dict_complete(self):
        """Test converting complete registration to dict."""
        registration = DynamicClientRegistration(
            client_id="test-client-123",
            client_secret="secret-456",
            client_id_issued_at=1234567890,
            client_secret_expires_at=1234567999,
        )

        result = registration.model_dump()

        assert result == {
            "client_id": "test-client-123",
            "client_secret": "secret-456",
            "client_id_issued_at": 1234567890,
            "client_secret_expires_at": 1234567999,
        }


class TestMCPOAuthClientInit:
    """Test MCPOAuthClient initialization."""

    def test_init_default_redirect(self):
        """Test initialization with default redirect URI."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        assert client.server_url == "https://mcp.example.com/mcp"
        assert client.redirect_uri == "http://localhost:8080/callback"
        assert client._auth_metadata is None
        assert client._client_registration is None
        assert client._code_verifier is None
        assert client._auth_result is None

    def test_init_custom_redirect(self):
        """Test initialization with custom redirect URI."""
        client = MCPOAuthClient(
            "https://mcp.example.com/mcp", redirect_uri="http://localhost:9000/cb"
        )

        assert client.redirect_uri == "http://localhost:9000/cb"

    def test_init_strips_trailing_slash(self):
        """Test that trailing slash is removed from server URL."""
        client = MCPOAuthClient("https://mcp.example.com/mcp/")

        assert client.server_url == "https://mcp.example.com/mcp"


class TestMCPOAuthClientDiscovery:
    """Test authorization server discovery."""

    @pytest.mark.asyncio
    async def test_discover_authorization_server(self):
        """Test successful discovery with fallback to direct AS discovery."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery to fail (404 not found), then AS discovery succeeds
            prm_response = Mock()
            prm_response.status_code = 404
            prm_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "Not found", request=Mock(), response=prm_response
            )

            as_response = Mock()
            as_response.raise_for_status = Mock()
            as_response.json = Mock(return_value=discovery_response)

            # First call (PRM) fails, second call (AS) succeeds
            mock_client.get = AsyncMock(side_effect=[prm_response, as_response])

            metadata = await client.discover_authorization_server()

            assert (
                metadata.authorization_endpoint
                == discovery_response["authorization_endpoint"]
            )
            assert metadata.token_endpoint == discovery_response["token_endpoint"]
            assert client._auth_metadata == metadata

            # Should have called both PRM and AS endpoints
            assert mock_client.get.call_count == 2
            # First call: PRM
            assert (
                mock_client.get.call_args_list[0][0][0]
                == "https://mcp.example.com/.well-known/oauth-protected-resource"
            )
            # Second call: AS (fallback)
            assert (
                mock_client.get.call_args_list[1][0][0]
                == "https://mcp.example.com/.well-known/oauth-authorization-server"
            )

    @pytest.mark.asyncio
    async def test_discover_authorization_server_http_error(self):
        """Test discovery with HTTP error on both PRM and AS."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.raise_for_status.side_effect = Exception("Not found")
            # Both PRM and AS discovery fail
            mock_client.get = AsyncMock(return_value=mock_response)

            with pytest.raises(Exception, match="Not found"):
                await client.discover_authorization_server()

    @pytest.mark.asyncio
    async def test_discover_authorization_server_prm_with_base_url(self):
        """Test PRM discovery when authorization_servers contains base URL (not well-known URL)."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        prm_response_data = {
            "resource": "https://mcp.example.com",
            "authorization_servers": [
                "https://mcp.example.com"
            ],  # Base URL, not well-known
            "scopes_supported": ["read", "write"],
        }

        as_metadata_response = {
            "authorization_endpoint": "https://mcp.example.com/authorize",
            "token_endpoint": "https://mcp.example.com/token",
            "registration_endpoint": "https://mcp.example.com/register",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery to succeed
            prm_response = Mock()
            prm_response.raise_for_status = Mock()
            prm_response.json = Mock(return_value=prm_response_data)

            # Mock AS discovery to succeed
            as_response = Mock()
            as_response.raise_for_status = Mock()
            as_response.json = Mock(return_value=as_metadata_response)

            # First call: PRM succeeds, second call: AS succeeds
            mock_client.get = AsyncMock(side_effect=[prm_response, as_response])

            metadata = await client.discover_authorization_server()

            # Verify metadata is correct
            assert (
                metadata.authorization_endpoint == "https://mcp.example.com/authorize"
            )
            assert metadata.token_endpoint == "https://mcp.example.com/token"

            # Verify PRM was stored
            assert client._prm_metadata is not None
            assert client._prm_metadata.resource == "https://mcp.example.com"

            # Verify resource indicator was set
            assert client._resource_indicator == "https://mcp.example.com"

            # Should have called both endpoints
            assert mock_client.get.call_count == 2
            # First call: PRM
            assert (
                mock_client.get.call_args_list[0][0][0]
                == "https://mcp.example.com/.well-known/oauth-protected-resource"
            )
            # Second call: AS with well-known path appended to base URL
            assert (
                mock_client.get.call_args_list[1][0][0]
                == "https://mcp.example.com/.well-known/oauth-authorization-server"
            )

    @pytest.mark.asyncio
    async def test_discover_authorization_server_prm_with_wellknown_url(self):
        """Test PRM discovery when authorization_servers contains full well-known URL."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        prm_response_data = {
            "resource": "https://api.example.com",
            "authorization_servers": [
                "https://auth.example.com/.well-known/oauth-authorization-server"
            ],  # Full well-known URL
        }

        as_metadata_response = {
            "authorization_endpoint": "https://auth.example.com/authorize",
            "token_endpoint": "https://auth.example.com/token",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery to succeed
            prm_response = Mock()
            prm_response.raise_for_status = Mock()
            prm_response.json = Mock(return_value=prm_response_data)

            # Mock AS discovery to succeed
            as_response = Mock()
            as_response.raise_for_status = Mock()
            as_response.json = Mock(return_value=as_metadata_response)

            # First call: PRM succeeds, second call: AS succeeds
            mock_client.get = AsyncMock(side_effect=[prm_response, as_response])

            metadata = await client.discover_authorization_server()

            # Verify metadata is correct
            assert (
                metadata.authorization_endpoint == "https://auth.example.com/authorize"
            )
            assert metadata.token_endpoint == "https://auth.example.com/token"

            # Should have called both endpoints
            assert mock_client.get.call_count == 2
            # First call: PRM
            assert (
                mock_client.get.call_args_list[0][0][0]
                == "https://mcp.example.com/.well-known/oauth-protected-resource"
            )
            # Second call: AS using exact URL from PRM (no modification needed)
            assert (
                mock_client.get.call_args_list[1][0][0]
                == "https://auth.example.com/.well-known/oauth-authorization-server"
            )


class TestMCPOAuthClientRegistration:
    """Test dynamic client registration."""

    @pytest.mark.asyncio
    async def test_register_client_default(self):
        """Test client registration with defaults."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            registration_endpoint="https://example.com/register",
        )

        registration_response = {"client_id": "test-client-123"}

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = registration_response
            mock_client.post = AsyncMock(return_value=mock_response)

            registration = await client.register_client()

            assert registration.client_id == "test-client-123"
            assert client._client_registration == registration

            # Verify the post call
            call_args = mock_client.post.call_args
            assert call_args[0][0] == "https://example.com/register"
            assert call_args[1]["json"]["client_name"] == "MCP CLI"
            assert call_args[1]["json"]["redirect_uris"] == [
                "http://localhost:8080/callback"
            ]

    @pytest.mark.asyncio
    async def test_register_client_custom(self):
        """Test client registration with custom parameters."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            registration_endpoint="https://example.com/register",
        )

        registration_response = {"client_id": "custom-client"}

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = registration_response
            mock_client.post = AsyncMock(return_value=mock_response)

            registration = await client.register_client(
                client_name="Custom App",
                redirect_uris=["http://localhost:9000/callback"],
            )

            assert registration.client_id == "custom-client"

            call_args = mock_client.post.call_args
            assert call_args[1]["json"]["client_name"] == "Custom App"
            assert call_args[1]["json"]["redirect_uris"] == [
                "http://localhost:9000/callback"
            ]

    @pytest.mark.asyncio
    async def test_register_client_discovers_if_needed(self):
        """Test that registration discovers metadata if not already done."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        registration_response = {"client_id": "test-client"}

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Setup discovery mocks (PRM fails, then AS succeeds)
            discovery_responses = setup_discovery_mocks(mock_client, discovery_response)

            mock_post_response = Mock()
            mock_post_response.json = Mock(return_value=registration_response)
            mock_post_response.raise_for_status = Mock()

            # PRM fails (404), AS succeeds
            mock_client.get = AsyncMock(side_effect=discovery_responses)
            mock_client.post = AsyncMock(return_value=mock_post_response)

            await client.register_client()

            # Should have called discovery (PRM + AS fallback) and registration
            assert mock_client.get.call_count == 2  # PRM + AS
            mock_client.post.assert_called_once()  # Registration

    @pytest.mark.asyncio
    async def test_register_client_no_registration_endpoint(self):
        """Test registration when server doesn't support it."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            registration_endpoint=None,
        )

        with pytest.raises(
            ValueError, match="does not support dynamic client registration"
        ):
            await client.register_client()


class TestMCPOAuthClientPKCE:
    """Test PKCE generation."""

    def test_generate_pkce_pair(self):
        """Test PKCE generation produces valid pair."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        verifier, challenge = client._generate_pkce_pair()

        assert len(verifier) > 0
        assert len(challenge) > 0
        assert verifier != challenge
        assert "=" not in verifier  # Base64 padding should be stripped
        assert "=" not in challenge


class TestMCPOAuthClientAuthorization:
    """Test authorization URL generation."""

    def test_get_authorization_url_no_scopes(self):
        """Test authorization URL without scopes."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        url = client.get_authorization_url()

        assert url.startswith("https://example.com/authorize?")
        assert "client_id=test-client" in url
        assert "response_type=code" in url
        assert "redirect_uri=http" in url
        assert "code_challenge=" in url
        assert "code_challenge_method=S256" in url
        assert "state=" in url
        assert client._code_verifier is not None

    def test_get_authorization_url_with_scopes(self):
        """Test authorization URL with scopes."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        url = client.get_authorization_url(scopes=["read", "write"])

        assert "scope=read+write" in url

    def test_get_authorization_url_not_ready(self):
        """Test error when metadata or registration not set."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        with pytest.raises(ValueError, match="Must discover and register"):
            client.get_authorization_url()


class TestMCPOAuthClientTokenExchange:
    """Test token exchange."""

    @pytest.mark.asyncio
    async def test_exchange_code_for_token(self):
        """Test successful token exchange."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")
        client._code_verifier = "test-verifier"

        token_response = {
            "access_token": "test-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            tokens = await client.exchange_code_for_token("test-code")

            assert tokens.access_token == "test-access-token"

            # Verify post data
            call_args = mock_client.post.call_args
            assert call_args[0][0] == "https://example.com/token"
            assert call_args[1]["data"]["grant_type"] == "authorization_code"
            assert call_args[1]["data"]["code"] == "test-code"
            assert call_args[1]["data"]["code_verifier"] == "test-verifier"

    @pytest.mark.asyncio
    async def test_exchange_code_for_token_with_secret(self):
        """Test token exchange with client secret."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(
            client_id="test-client", client_secret="test-secret"
        )
        client._code_verifier = "test-verifier"

        token_response = {
            "access_token": "test-access-token",
            "token_type": "Bearer",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            await client.exchange_code_for_token("test-code")

            # Verify client_secret was included
            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["client_secret"] == "test-secret"

    @pytest.mark.asyncio
    async def test_exchange_code_for_token_not_ready(self):
        """Test error when not ready for exchange."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        with pytest.raises(ValueError, match="Must discover and register"):
            await client.exchange_code_for_token("test-code")


class TestMCPOAuthClientRefresh:
    """Test token refresh."""

    @pytest.mark.asyncio
    async def test_refresh_token(self):
        """Test successful token refresh."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        token_response = {
            "access_token": "new-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            tokens = await client.refresh_token("test-refresh-token")

            assert tokens.access_token == "new-access-token"

            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["grant_type"] == "refresh_token"
            assert call_args[1]["data"]["refresh_token"] == "test-refresh-token"

    @pytest.mark.asyncio
    async def test_refresh_token_with_secret(self):
        """Test token refresh with client secret."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(
            client_id="test-client", client_secret="test-secret"
        )

        token_response = {
            "access_token": "new-access-token",
            "token_type": "Bearer",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            await client.refresh_token("test-refresh-token")

            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["client_secret"] == "test-secret"

    @pytest.mark.asyncio
    async def test_refresh_token_not_ready(self):
        """Test error when not ready for refresh."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        with pytest.raises(ValueError, match="Must discover and register"):
            await client.refresh_token("test-refresh-token")


class TestMCPOAuthClientCallbackHandler:
    """Test callback handler."""

    def test_create_callback_handler(self):
        """Test callback handler creation."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        handler_class = client._create_callback_handler()

        assert handler_class is not None
        assert hasattr(handler_class, "do_GET")
        assert hasattr(handler_class, "log_message")

    def test_callback_handler_success(self):
        """Test callback handler with successful authorization."""

        client = MCPOAuthClient("https://mcp.example.com/mcp")
        handler_class = client._create_callback_handler()

        # Create mock request
        class MockSocket:
            def makefile(self, mode, buffsize=-1):
                if "r" in mode:
                    return BytesIO(
                        b"GET /callback?code=test-code&state=test-state HTTP/1.1\r\nHost: localhost\r\n\r\n"
                    )
                else:
                    return BytesIO()

            def sendall(self, data):
                pass  # Mock socket write

            def settimeout(self, timeout):
                pass

            def setsockopt(self, level, optname, value):
                pass

        # Create handler instance
        mock_request = MockSocket()
        mock_server = Mock()
        mock_server.server_name = "localhost"
        mock_server.server_port = 8080

        _ = handler_class(mock_request, ("127.0.0.1", 12345), mock_server)

        # Check that auth result was set
        assert client._auth_result is not None
        assert "code" in client._auth_result
        assert client._auth_result["code"] == "test-code"

    def test_callback_handler_error(self):
        """Test callback handler with error."""

        client = MCPOAuthClient("https://mcp.example.com/mcp")
        handler_class = client._create_callback_handler()

        # Create mock request with error
        class MockSocket:
            def makefile(self, mode, buffsize=-1):
                if "r" in mode:
                    return BytesIO(
                        b"GET /callback?error=access_denied&error_description=User+denied HTTP/1.1\r\nHost: localhost\r\n\r\n"
                    )
                else:
                    return BytesIO()

            def sendall(self, data):
                pass  # Mock socket write

            def settimeout(self, timeout):
                pass

            def setsockopt(self, level, optname, value):
                pass

        mock_request = MockSocket()
        mock_server = Mock()
        mock_server.server_name = "localhost"
        mock_server.server_port = 8080

        _ = handler_class(mock_request, ("127.0.0.1", 12345), mock_server)

        # Check that error was captured
        assert client._auth_result is not None
        assert "error" in client._auth_result

    def test_callback_handler_no_code(self):
        """Test callback handler without code or error."""

        client = MCPOAuthClient("https://mcp.example.com/mcp")
        handler_class = client._create_callback_handler()

        # Create mock request without code
        class MockSocket:
            def makefile(self, mode, buffsize=-1):
                if "r" in mode:
                    return BytesIO(
                        b"GET /callback?state=test-state HTTP/1.1\r\nHost: localhost\r\n\r\n"
                    )
                else:
                    return BytesIO()

            def sendall(self, data):
                pass  # Mock socket write

            def settimeout(self, timeout):
                pass

            def setsockopt(self, level, optname, value):
                pass

        mock_request = MockSocket()
        mock_server = Mock()
        mock_server.server_name = "localhost"
        mock_server.server_port = 8080

        # Clear any previous result
        client._auth_result = None

        _ = handler_class(mock_request, ("127.0.0.1", 12345), mock_server)

        # Should handle gracefully (either set error or no result)
        # The actual behavior depends on implementation

    def test_callback_handler_non_callback_path(self):
        """Test callback handler with non-callback path."""

        client = MCPOAuthClient("https://mcp.example.com/mcp")
        handler_class = client._create_callback_handler()

        # Create mock request for favicon
        class MockSocket:
            def makefile(self, mode, buffsize=-1):
                if "r" in mode:
                    return BytesIO(
                        b"GET /favicon.ico HTTP/1.1\r\nHost: localhost\r\n\r\n"
                    )
                else:
                    return BytesIO()

            def sendall(self, data):
                pass  # Mock socket write

            def settimeout(self, timeout):
                pass

            def setsockopt(self, level, optname, value):
                pass

        mock_request = MockSocket()
        mock_server = Mock()
        mock_server.server_name = "localhost"
        mock_server.server_port = 8080

        # Should not crash
        try:
            _ = handler_class(mock_request, ("127.0.0.1", 12345), mock_server)
        except Exception:
            pass  # Some errors are expected with mock sockets

    def test_callback_handler_already_got_result(self):
        """Test callback handler when result already received."""

        client = MCPOAuthClient("https://mcp.example.com/mcp")
        # Set a result first
        client._auth_result = {"code": "already-have-code"}

        handler_class = client._create_callback_handler()

        # Create another request
        class MockSocket:
            def makefile(self, mode, buffsize=-1):
                if "r" in mode:
                    return BytesIO(
                        b"GET /callback?code=new-code HTTP/1.1\r\nHost: localhost\r\n\r\n"
                    )
                else:
                    return BytesIO()

            def sendall(self, data):
                pass  # Mock socket write

            def settimeout(self, timeout):
                pass

            def setsockopt(self, level, optname, value):
                pass

        mock_request = MockSocket()
        mock_server = Mock()
        mock_server.server_name = "localhost"
        mock_server.server_port = 8080

        try:
            _ = handler_class(mock_request, ("127.0.0.1", 12345), mock_server)
        except Exception:
            pass

        # Should keep original result
        assert client._auth_result["code"] == "already-have-code"


class TestMCPOAuthClientCallbackServer:
    """Test callback server."""

    @pytest.mark.asyncio
    async def test_run_callback_server_with_result(self):
        """Test callback server receives result."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        # Simulate receiving auth result immediately
        async def simulate_callback():
            await asyncio.sleep(0.1)
            client._auth_result = {"code": "test-code"}

        # Run both tasks
        callback_task = asyncio.create_task(simulate_callback())
        server_task = asyncio.create_task(client._run_callback_server(18888))

        await asyncio.gather(callback_task, server_task)

        assert client._auth_result is not None

    @pytest.mark.asyncio
    async def test_run_callback_server_timeout(self):
        """Test callback server timeout."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        # Don't set _auth_result, let it timeout
        # Use short timeout by mocking asyncio.sleep
        call_count = {"count": 0}

        original_sleep = asyncio.sleep

        async def mock_sleep(duration):
            call_count["count"] += 1
            if call_count["count"] > 5:  # Exit after 5 iterations
                client._auth_result = {}  # Set something to exit
            await original_sleep(0.01)  # Very short sleep

        with patch("asyncio.sleep", mock_sleep):
            await client._run_callback_server(18889)

        # Should have timed out or exited
        assert call_count["count"] > 0

    @pytest.mark.asyncio
    async def test_run_callback_server_progress_messages(self):
        """Test callback server shows progress messages."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        # Test progress message at 30 second intervals
        call_count = {"count": 0}
        original_sleep = asyncio.sleep

        async def mock_sleep(duration):
            call_count["count"] += 1
            # Simulate 31 iterations to trigger progress message at i=30
            if call_count["count"] > 31:
                client._auth_result = {"code": "test-code"}
            await original_sleep(0.01)

        with patch("asyncio.sleep", mock_sleep):
            await client._run_callback_server(18890)

        # Should have completed and shown progress
        assert call_count["count"] > 30
        assert client._auth_result is not None

    @pytest.mark.asyncio
    async def test_run_callback_server_actual_timeout(self):
        """Test callback server with actual timeout (no result)."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        # Test that timeout path is covered
        call_count = {"count": 0}
        original_sleep = asyncio.sleep

        async def mock_sleep(duration):
            call_count["count"] += 1
            # Exit loop after a few iterations without setting result
            if call_count["count"] >= 6:
                # This will cause the loop to exit with _auth_result still None
                pass
            await original_sleep(0.01)

        with patch("asyncio.sleep", mock_sleep):
            # Reduce timeout for testing by modifying the loop range
            await client._run_callback_server(18891)

        # Result should still be None (timeout occurred)
        # This covers line 266 which prints timeout message
        assert client._auth_result is None


class TestMCPOAuthClientAuthorize:
    """Test full authorization flow."""

    @pytest.mark.asyncio
    async def test_authorize_success(self):
        """Test successful authorization flow."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        registration_response = {"client_id": "test-client"}

        token_response = {
            "access_token": "test-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Setup discovery mocks (PRM fails, then AS succeeds)
            discovery_responses = setup_discovery_mocks(mock_client, discovery_response)

            # Mock registration
            mock_reg_response = Mock()
            mock_reg_response.json = Mock(return_value=registration_response)
            mock_reg_response.raise_for_status = Mock()

            # Mock token exchange
            mock_token_response = Mock()
            mock_token_response.json = Mock(return_value=token_response)
            mock_token_response.raise_for_status = Mock()

            # PRM fails (404), AS succeeds
            mock_client.get = AsyncMock(side_effect=discovery_responses)
            mock_client.post = AsyncMock(
                side_effect=[mock_reg_response, mock_token_response]
            )

            # Mock browser opening
            with patch("webbrowser.open"):
                # Mock callback server
                async def mock_run_callback_server(port):
                    # Simulate successful callback
                    await asyncio.sleep(0.1)
                    client._auth_result = {"code": "test-code", "state": "test-state"}

                with patch.object(
                    client, "_run_callback_server", mock_run_callback_server
                ):
                    tokens = await client.authorize()

                    assert tokens.access_token == "test-access-token"

    @pytest.mark.asyncio
    async def test_authorize_timeout(self):
        """Test authorization timeout."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        registration_response = {"client_id": "test-client"}

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Setup discovery mocks (PRM fails, then AS succeeds)
            discovery_responses = setup_discovery_mocks(mock_client, discovery_response)

            mock_reg_response = Mock()
            mock_reg_response.json = Mock(return_value=registration_response)
            mock_reg_response.raise_for_status = Mock()

            # PRM fails (404), AS succeeds
            mock_client.get = AsyncMock(side_effect=discovery_responses)
            mock_client.post = AsyncMock(return_value=mock_reg_response)

            with patch("webbrowser.open"):

                async def mock_run_callback_server_timeout(port):
                    await asyncio.sleep(0.1)
                    # Don't set _auth_result - simulate timeout

                with patch.object(
                    client, "_run_callback_server", mock_run_callback_server_timeout
                ):
                    with pytest.raises(Exception, match="Authorization timed out"):
                        await client.authorize()

    @pytest.mark.asyncio
    async def test_authorize_error(self):
        """Test authorization with error."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        registration_response = {"client_id": "test-client"}

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Setup discovery mocks (PRM fails, then AS succeeds)
            discovery_responses = setup_discovery_mocks(mock_client, discovery_response)

            mock_reg_response = Mock()
            mock_reg_response.json = Mock(return_value=registration_response)
            mock_reg_response.raise_for_status = Mock()

            # PRM fails (404), AS succeeds
            mock_client.get = AsyncMock(side_effect=discovery_responses)
            mock_client.post = AsyncMock(return_value=mock_reg_response)

            with patch("webbrowser.open"):

                async def mock_run_callback_server_error(port):
                    await asyncio.sleep(0.1)
                    client._auth_result = {"error": "access_denied"}

                with patch.object(
                    client, "_run_callback_server", mock_run_callback_server_error
                ):
                    with pytest.raises(Exception, match="Authorization failed"):
                        await client.authorize()

    @pytest.mark.asyncio
    async def test_authorize_with_scopes(self):
        """Test authorization with custom scopes."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        discovery_response = {
            "authorization_endpoint": "https://example.com/authorize",
            "token_endpoint": "https://example.com/token",
            "registration_endpoint": "https://example.com/register",
        }

        registration_response = {"client_id": "test-client"}

        token_response = {
            "access_token": "test-access-token",
            "token_type": "Bearer",
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Setup discovery mocks (PRM fails, then AS succeeds)
            discovery_responses = setup_discovery_mocks(mock_client, discovery_response)

            mock_reg_response = Mock()
            mock_reg_response.json = Mock(return_value=registration_response)
            mock_reg_response.raise_for_status = Mock()

            mock_token_response = Mock()
            mock_token_response.json = Mock(return_value=token_response)
            mock_token_response.raise_for_status = Mock()

            # PRM fails (404), AS succeeds
            mock_client.get = AsyncMock(side_effect=discovery_responses)
            mock_client.post = AsyncMock(
                side_effect=[mock_reg_response, mock_token_response]
            )

            with patch("webbrowser.open") as mock_browser:

                async def mock_run_callback_server(port):
                    await asyncio.sleep(0.1)
                    client._auth_result = {"code": "test-code"}

                with patch.object(
                    client, "_run_callback_server", mock_run_callback_server
                ):
                    await client.authorize(scopes=["read", "write"])

                    # Verify browser was opened with scopes
                    assert mock_browser.called


class TestMCPOAuthClientWWWAuthenticate:
    """Test WWW-Authenticate header parsing and error response discovery."""

    def test_parse_www_authenticate_header_double_quotes(self):
        """Test parsing WWW-Authenticate header with double quotes."""
        header = 'Bearer error="invalid_token", resource_metadata="https://example.com/.well-known/oauth-protected-resource"'
        url = MCPOAuthClient.parse_www_authenticate_header(header)
        assert url == "https://example.com/.well-known/oauth-protected-resource"

    def test_parse_www_authenticate_header_single_quotes(self):
        """Test parsing WWW-Authenticate header with single quotes."""
        header = "Bearer error='invalid_token', resource_metadata='https://example.com/.well-known/oauth-protected-resource'"
        url = MCPOAuthClient.parse_www_authenticate_header(header)
        assert url == "https://example.com/.well-known/oauth-protected-resource"

    def test_parse_www_authenticate_header_no_resource_metadata(self):
        """Test parsing WWW-Authenticate header without resource_metadata."""
        header = 'Bearer error="invalid_token"'
        url = MCPOAuthClient.parse_www_authenticate_header(header)
        assert url is None

    def test_parse_www_authenticate_header_malformed(self):
        """Test parsing malformed WWW-Authenticate header."""
        header = "Bearer resource_metadata=no-quotes"
        url = MCPOAuthClient.parse_www_authenticate_header(header)
        # Should return None for malformed header
        assert url is None

    @pytest.mark.asyncio
    async def test_discover_from_error_response_401_with_www_authenticate(self):
        """Test discovering PRM from 401 response with WWW-Authenticate header."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        prm_data = {
            "resource": "https://api.example.com",
            "authorization_servers": ["https://auth.example.com"],
        }

        # Mock error response with WWW-Authenticate header
        error_response = Mock()
        error_response.status_code = 401
        error_response.headers = {
            "WWW-Authenticate": 'Bearer error="invalid_token", resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery
            prm_response = Mock()
            prm_response.raise_for_status = Mock()
            prm_response.json.return_value = prm_data
            mock_client.get = AsyncMock(return_value=prm_response)

            prm = await client.discover_from_error_response(error_response)

            assert prm is not None
            assert prm.resource == "https://api.example.com"
            assert mock_client.get.called

    @pytest.mark.asyncio
    async def test_discover_from_error_response_403_with_www_authenticate(self):
        """Test discovering PRM from 403 response with WWW-Authenticate header."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        prm_data = {
            "resource": "https://api.example.com",
            "authorization_servers": ["https://auth.example.com"],
        }

        # Mock error response with WWW-Authenticate header
        error_response = Mock()
        error_response.status_code = 403
        error_response.headers = {
            "WWW-Authenticate": 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery
            prm_response = Mock()
            prm_response.raise_for_status = Mock()
            prm_response.json.return_value = prm_data
            mock_client.get = AsyncMock(return_value=prm_response)

            prm = await client.discover_from_error_response(error_response)

            assert prm is not None
            assert prm.resource == "https://api.example.com"

    @pytest.mark.asyncio
    async def test_discover_from_error_response_200_returns_none(self):
        """Test that non-error status codes return None."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        success_response = Mock()
        success_response.status_code = 200

        prm = await client.discover_from_error_response(success_response)
        assert prm is None

    @pytest.mark.asyncio
    async def test_discover_from_error_response_no_www_authenticate(self):
        """Test that 401 without WWW-Authenticate returns None."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        error_response = Mock()
        error_response.status_code = 401
        error_response.headers = {}

        prm = await client.discover_from_error_response(error_response)
        assert prm is None

    @pytest.mark.asyncio
    async def test_discover_from_error_response_prm_discovery_fails(self):
        """Test that PRM discovery failure from WWW-Authenticate returns None."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")

        error_response = Mock()
        error_response.status_code = 401
        error_response.headers = {
            "WWW-Authenticate": 'Bearer resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"'
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value

            # Mock PRM discovery to fail
            mock_response = Mock()
            mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
                "Not found", request=Mock(), response=Mock()
            )
            mock_client.get = AsyncMock(return_value=mock_response)

            prm = await client.discover_from_error_response(error_response)
            assert prm is None


class TestMCPOAuthClientResourceIndicators:
    """Test RFC 8707 Resource Indicators in token operations."""

    @pytest.mark.asyncio
    async def test_exchange_code_with_resource_indicator(self):
        """Test token exchange includes resource indicator when available."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")
        client._code_verifier = "test-verifier"
        client._resource_indicator = "https://api.example.com"  # Set resource indicator

        token_response = {
            "access_token": "test-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            await client.exchange_code_for_token("test-code")

            # Verify resource indicator was included in request
            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["resource"] == "https://api.example.com"

    @pytest.mark.asyncio
    async def test_refresh_token_with_resource_indicator(self):
        """Test token refresh includes resource indicator when available."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")
        client._resource_indicator = "https://api.example.com"  # Set resource indicator

        token_response = {
            "access_token": "new-access-token",
            "token_type": "Bearer",
            "expires_in": 3600,
        }

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.raise_for_status = Mock()
            mock_response.json.return_value = token_response
            mock_client.post = AsyncMock(return_value=mock_response)

            await client.refresh_token("old-refresh-token")

            # Verify resource indicator was included in refresh request
            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["resource"] == "https://api.example.com"


class TestMCPOAuthClientRevoke:
    """Test token revocation (RFC 7009)."""

    @pytest.mark.asyncio
    async def test_revoke_token_success(self):
        """Test successful token revocation."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint="https://example.com/revoke",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.status_code = 200
            mock_client.post = AsyncMock(return_value=mock_response)

            result = await client.revoke_token("test-token")

            assert result is True
            call_args = mock_client.post.call_args
            assert call_args[0][0] == "https://example.com/revoke"
            assert call_args[1]["data"]["token"] == "test-token"

    @pytest.mark.asyncio
    async def test_revoke_token_with_type_hint(self):
        """Test token revocation with token_type_hint."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint="https://example.com/revoke",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.status_code = 200
            mock_client.post = AsyncMock(return_value=mock_response)

            result = await client.revoke_token(
                "test-refresh-token", token_type_hint="refresh_token"
            )

            assert result is True
            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["token"] == "test-refresh-token"
            assert call_args[1]["data"]["token_type_hint"] == "refresh_token"

    @pytest.mark.asyncio
    async def test_revoke_token_with_client_secret(self):
        """Test token revocation includes client secret when available."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint="https://example.com/revoke",
        )
        client._client_registration = DynamicClientRegistration(
            client_id="test-client", client_secret="test-secret"
        )

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.status_code = 200
            mock_client.post = AsyncMock(return_value=mock_response)

            await client.revoke_token("test-token")

            call_args = mock_client.post.call_args
            assert call_args[1]["data"]["client_id"] == "test-client"
            assert call_args[1]["data"]["client_secret"] == "test-secret"

    @pytest.mark.asyncio
    async def test_revoke_token_no_revocation_endpoint(self):
        """Test revocation when server doesn't support it."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint=None,  # No revocation support
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        # Should return True without making any requests
        result = await client.revoke_token("test-token")

        assert result is True

    @pytest.mark.asyncio
    async def test_revoke_token_http_error(self):
        """Test token revocation with HTTP error."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint="https://example.com/revoke",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_client.post = AsyncMock(side_effect=Exception("Network error"))

            # Should return False on error, not raise
            result = await client.revoke_token("test-token")

            assert result is False

    @pytest.mark.asyncio
    async def test_revoke_token_non_200_response(self):
        """Test token revocation with non-200 response."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        client._auth_metadata = MCPAuthorizationMetadata(
            authorization_endpoint="https://example.com/authorize",
            token_endpoint="https://example.com/token",
            revocation_endpoint="https://example.com/revoke",
        )
        client._client_registration = DynamicClientRegistration(client_id="test-client")

        with patch("httpx.AsyncClient") as mock_client_class:
            mock_client = mock_client_class.return_value.__aenter__.return_value
            mock_response = Mock()
            mock_response.status_code = 400  # Bad request
            mock_client.post = AsyncMock(return_value=mock_response)

            result = await client.revoke_token("test-token")

            # Should return False for non-200 status
            assert result is False

    @pytest.mark.asyncio
    async def test_revoke_token_without_metadata(self):
        """Test revocation fails when metadata not discovered."""
        client = MCPOAuthClient("https://mcp.example.com/mcp")
        # Don't set _auth_metadata

        with pytest.raises(ValueError, match="Must discover authorization server"):
            await client.revoke_token("test-token")
