"""Integration tests for MCP authorization components."""

import base64
import hashlib
import secrets
import time
import unittest.mock
from typing import Any
from urllib.parse import parse_qs, urlparse

import httpx
import pytest
from pydantic import AnyHttpUrl, AnyUrl
from starlette.applications import Starlette

from mcp.server.auth.provider import (
    AccessToken,
    AuthorizationCode,
    AuthorizationParams,
    OAuthAuthorizationServerProvider,
    RefreshToken,
    construct_redirect_uri,
)
from mcp.server.auth.routes import create_auth_routes
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken


# Mock OAuth provider for testing
class MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
    def __init__(self):
        self.clients: dict[str, OAuthClientInformationFull] = {}
        self.auth_codes: dict[str, AuthorizationCode] = {}  # code -> {client_id, code_challenge, redirect_uri}
        self.tokens: dict[str, AccessToken] = {}  # token -> {client_id, scopes, expires_at}
        self.refresh_tokens: dict[str, str] = {}  # refresh_token -> access_token

    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
        return self.clients.get(client_id)

    async def register_client(self, client_info: OAuthClientInformationFull):
        assert client_info.client_id is not None
        self.clients[client_info.client_id] = client_info

    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
        # toy authorize implementation which just immediately generates an authorization
        # code and completes the redirect
        assert client.client_id is not None
        code = AuthorizationCode(
            code=f"code_{int(time.time())}",
            client_id=client.client_id,
            code_challenge=params.code_challenge,
            redirect_uri=params.redirect_uri,
            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,
            expires_at=time.time() + 300,
            scopes=params.scopes or ["read", "write"],
            subject="test-user",
        )
        self.auth_codes[code.code] = code

        return construct_redirect_uri(str(params.redirect_uri), code=code.code, state=params.state)

    async def load_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: str
    ) -> AuthorizationCode | None:
        return self.auth_codes.get(authorization_code)

    async def exchange_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
    ) -> OAuthToken:
        assert authorization_code.code in self.auth_codes

        # Generate an access token and refresh token
        access_token = f"access_{secrets.token_hex(32)}"
        refresh_token = f"refresh_{secrets.token_hex(32)}"

        # Store the tokens
        assert client.client_id is not None
        self.tokens[access_token] = AccessToken(
            token=access_token,
            client_id=client.client_id,
            scopes=authorization_code.scopes,
            expires_at=int(time.time()) + 3600,
            subject=authorization_code.subject,
        )

        self.refresh_tokens[refresh_token] = access_token

        # Remove the used code
        del self.auth_codes[authorization_code.code]

        return OAuthToken(
            access_token=access_token,
            token_type="Bearer",
            expires_in=3600,
            scope="read write",
            refresh_token=refresh_token,
        )

    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
        old_access_token = self.refresh_tokens.get(refresh_token)
        if old_access_token is None:
            return None
        token_info = self.tokens.get(old_access_token)
        if token_info is None:  # pragma: no cover
            return None

        # Create a RefreshToken object that matches what is expected in later code
        refresh_obj = RefreshToken(
            token=refresh_token,
            client_id=token_info.client_id,
            scopes=token_info.scopes,
            expires_at=token_info.expires_at,
            subject=token_info.subject,
        )

        return refresh_obj

    async def exchange_refresh_token(
        self,
        client: OAuthClientInformationFull,
        refresh_token: RefreshToken,
        scopes: list[str],
    ) -> OAuthToken:
        # Check if refresh token exists
        assert refresh_token.token in self.refresh_tokens

        old_access_token = self.refresh_tokens[refresh_token.token]

        # Check if the access token exists
        assert old_access_token in self.tokens

        # Check if the token was issued to this client
        token_info = self.tokens[old_access_token]
        assert token_info.client_id == client.client_id

        # Generate a new access token and refresh token
        new_access_token = f"access_{secrets.token_hex(32)}"
        new_refresh_token = f"refresh_{secrets.token_hex(32)}"

        # Store the new tokens
        assert client.client_id is not None
        self.tokens[new_access_token] = AccessToken(
            token=new_access_token,
            client_id=client.client_id,
            scopes=scopes or token_info.scopes,
            expires_at=int(time.time()) + 3600,
            subject=refresh_token.subject,
        )

        self.refresh_tokens[new_refresh_token] = new_access_token

        # Remove the old tokens
        del self.refresh_tokens[refresh_token.token]
        del self.tokens[old_access_token]

        return OAuthToken(
            access_token=new_access_token,
            token_type="Bearer",
            expires_in=3600,
            scope=" ".join(scopes) if scopes else " ".join(token_info.scopes),
            refresh_token=new_refresh_token,
        )

    async def load_access_token(self, token: str) -> AccessToken | None:
        token_info = self.tokens.get(token)

        # Check if token is expired
        # if token_info.expires_at < int(time.time()):
        #     raise InvalidTokenError("Access token has expired")

        return token_info and AccessToken(
            token=token,
            client_id=token_info.client_id,
            scopes=token_info.scopes,
            expires_at=token_info.expires_at,
            subject=token_info.subject,
        )

    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
        match token:
            case RefreshToken():  # pragma: lax no cover
                # Remove the refresh token
                del self.refresh_tokens[token.token]

            case AccessToken():  # pragma: no branch
                # Remove the access token
                del self.tokens[token.token]

                # Also remove any refresh tokens that point to this access token
                for refresh_token, access_token in list(self.refresh_tokens.items()):
                    if access_token == token.token:  # pragma: no branch
                        del self.refresh_tokens[refresh_token]


@pytest.fixture
def mock_oauth_provider():
    return MockOAuthProvider()


@pytest.fixture
def auth_app(mock_oauth_provider: MockOAuthProvider):
    # Create auth router
    auth_routes = create_auth_routes(
        mock_oauth_provider,
        AnyHttpUrl("https://auth.example.com"),
        AnyHttpUrl("https://docs.example.com"),
        client_registration_options=ClientRegistrationOptions(
            enabled=True,
            valid_scopes=["read", "write", "profile"],
            default_scopes=["read", "write"],
        ),
        revocation_options=RevocationOptions(enabled=True),
    )

    # Create Starlette app
    app = Starlette(routes=auth_routes)

    return app


@pytest.fixture
async def test_client(auth_app: Starlette):
    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=auth_app), base_url="https://mcptest.com") as client:
        yield client


@pytest.fixture
async def registered_client(
    test_client: httpx.AsyncClient, request: pytest.FixtureRequest
) -> OAuthClientInformationFull:
    """Create and register a test client.

    Parameters can be customized via indirect parameterization:
    @pytest.mark.parametrize("registered_client",
                            [{"grant_types": ["authorization_code"]}],
                            indirect=True)
    """
    # Default client metadata
    client_metadata = {
        "redirect_uris": ["https://client.example.com/callback"],
        "client_name": "Test Client",
        "grant_types": ["authorization_code", "refresh_token"],
    }

    # Override with any parameters from the test
    if hasattr(request, "param") and request.param:
        client_metadata.update(request.param)

    response = await test_client.post("/register", json=client_metadata)
    assert response.status_code == 201, f"Failed to register client: {response.content}"

    client_info = response.json()
    return client_info


@pytest.fixture
def pkce_challenge():
    """Create a PKCE challenge with code_verifier and code_challenge."""
    code_verifier = "some_random_verifier_string"
    code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().rstrip("=")

    return {"code_verifier": code_verifier, "code_challenge": code_challenge}


@pytest.fixture
async def auth_code(
    test_client: httpx.AsyncClient,
    registered_client: dict[str, Any],
    pkce_challenge: dict[str, str],
    request: pytest.FixtureRequest,
):
    """Get an authorization code.

    Parameters can be customized via indirect parameterization:
    @pytest.mark.parametrize("auth_code",
                            [{"redirect_uri": "https://client.example.com/other-callback"}],
                            indirect=True)
    """
    # Default authorize params
    auth_params = {
        "response_type": "code",
        "client_id": registered_client["client_id"],
        "redirect_uri": "https://client.example.com/callback",
        "code_challenge": pkce_challenge["code_challenge"],
        "code_challenge_method": "S256",
        "state": "test_state",
    }

    # Override with any parameters from the test
    if hasattr(request, "param") and request.param:  # pragma: no cover
        auth_params.update(request.param)

    response = await test_client.get("/authorize", params=auth_params)
    assert response.status_code == 302, f"Failed to get auth code: {response.content}"

    # Extract the authorization code
    redirect_url = response.headers["location"]
    parsed_url = urlparse(redirect_url)
    query_params = parse_qs(parsed_url.query)

    assert "code" in query_params, f"No code in response: {query_params}"
    auth_code = query_params["code"][0]

    return {
        "code": auth_code,
        "redirect_uri": auth_params["redirect_uri"],
        "state": query_params.get("state", [None])[0],
    }


class TestAuthEndpoints:
    @pytest.mark.anyio
    async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):
        """Test the OAuth 2.0 metadata endpoint."""

        response = await test_client.get("/.well-known/oauth-authorization-server")
        assert response.status_code == 200

        metadata = response.json()
        assert metadata["issuer"] == "https://auth.example.com/"
        assert metadata["authorization_endpoint"] == "https://auth.example.com/authorize"
        assert metadata["token_endpoint"] == "https://auth.example.com/token"
        assert metadata["registration_endpoint"] == "https://auth.example.com/register"
        assert metadata["revocation_endpoint"] == "https://auth.example.com/revoke"
        assert metadata["response_types_supported"] == ["code"]
        assert metadata["code_challenge_methods_supported"] == ["S256"]
        assert metadata["token_endpoint_auth_methods_supported"] == ["client_secret_post", "client_secret_basic"]
        assert metadata["grant_types_supported"] == [
            "authorization_code",
            "refresh_token",
        ]
        assert metadata["service_documentation"] == "https://docs.example.com/"

    @pytest.mark.anyio
    async def test_token_validation_error(self, test_client: httpx.AsyncClient):
        """Test token endpoint error - validation error."""
        # Missing required fields
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                # Missing code, code_verifier, client_id, etc.
            },
        )
        error_response = response.json()
        # Per RFC 6749 Section 5.2, authentication failures (missing client_id)
        # must return "invalid_client", not "unauthorized_client"
        assert error_response["error"] == "invalid_client"
        assert "error_description" in error_response  # Contains error message

    @pytest.mark.anyio
    async def test_token_invalid_client_secret_returns_invalid_client(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        pkce_challenge: dict[str, str],
        mock_oauth_provider: MockOAuthProvider,
    ):
        """Test token endpoint returns 'invalid_client' for wrong client_secret per RFC 6749.

        RFC 6749 Section 5.2 defines:
        - invalid_client: Client authentication failed (wrong credentials, unknown client)
        - unauthorized_client: Authenticated client not authorized for grant type

        When client_secret is wrong, this is an authentication failure, so the
        error code MUST be 'invalid_client'.
        """
        # Create an auth code for the registered client
        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=registered_client["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Try to exchange the auth code with a WRONG client_secret
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": "wrong_secret_that_does_not_match",
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )

        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749 Section 5.2: authentication failures MUST return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Invalid client_secret" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_invalid_auth_code(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        pkce_challenge: dict[str, str],
    ):
        """Test token endpoint error - authorization code does not exist."""
        # Try to use a non-existent authorization code
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "code": "non_existent_auth_code",
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )

        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_grant"
        assert "authorization code does not exist" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_expired_auth_code(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        auth_code: dict[str, str],
        pkce_challenge: dict[str, str],
        mock_oauth_provider: MockOAuthProvider,
    ):
        """Test token endpoint error - authorization code has expired."""
        # Get the current time for our time mocking
        current_time = time.time()

        # Find the auth code object
        code_value = auth_code["code"]
        found_code = None
        for code_obj in mock_oauth_provider.auth_codes.values():  # pragma: no branch
            if code_obj.code == code_value:  # pragma: no branch
                found_code = code_obj
                break

        assert found_code is not None

        # Authorization codes are typically short-lived (5 minutes = 300 seconds)
        # So we'll mock time to be 10 minutes (600 seconds) in the future
        with unittest.mock.patch("time.time", return_value=current_time + 600):
            # Try to use the expired authorization code
            response = await test_client.post(
                "/token",
                data={
                    "grant_type": "authorization_code",
                    "client_id": registered_client["client_id"],
                    "client_secret": registered_client["client_secret"],
                    "code": code_value,
                    "code_verifier": pkce_challenge["code_verifier"],
                    "redirect_uri": auth_code["redirect_uri"],
                },
            )
            assert response.status_code == 400
            error_response = response.json()
            assert error_response["error"] == "invalid_grant"
            assert "authorization code has expired" in error_response["error_description"]

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "registered_client",
        [
            {
                "redirect_uris": [
                    "https://client.example.com/callback",
                    "https://client.example.com/other-callback",
                ]
            }
        ],
        indirect=True,
    )
    async def test_token_redirect_uri_mismatch(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        auth_code: dict[str, str],
        pkce_challenge: dict[str, str],
    ):
        """Test token endpoint error - redirect URI mismatch."""
        # Try to use the code with a different redirect URI
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "code": auth_code["code"],
                "code_verifier": pkce_challenge["code_verifier"],
                # Different from the one used in /authorize
                "redirect_uri": "https://client.example.com/other-callback",
            },
        )
        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_request"
        assert "redirect_uri did not match" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_code_verifier_mismatch(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], auth_code: dict[str, str]
    ):
        """Test token endpoint error - PKCE code verifier mismatch."""
        # Try to use the code with an incorrect code verifier
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "code": auth_code["code"],
                # Different from the one used to create challenge
                "code_verifier": "incorrect_code_verifier",
                "redirect_uri": auth_code["redirect_uri"],
            },
        )
        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_grant"
        assert "incorrect code_verifier" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_invalid_refresh_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
        """Test token endpoint error - refresh token does not exist."""
        # Try to use a non-existent refresh token
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "refresh_token",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "refresh_token": "non_existent_refresh_token",
            },
        )
        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_grant"
        assert "refresh token does not exist" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_expired_refresh_token(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        auth_code: dict[str, str],
        pkce_challenge: dict[str, str],
    ):
        """Test token endpoint error - refresh token has expired."""
        # Step 1: First, let's create a token and refresh token at the current time
        current_time = time.time()

        # Exchange authorization code for tokens normally
        token_response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "code": auth_code["code"],
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": auth_code["redirect_uri"],
            },
        )
        assert token_response.status_code == 200
        tokens = token_response.json()
        refresh_token = tokens["refresh_token"]

        # Step 2: Time travel forward 4 hours (tokens expire in 1 hour by default)
        # Mock the time.time() function to return a value 4 hours in the future
        with unittest.mock.patch("time.time", return_value=current_time + 14400):  # 4 hours = 14400 seconds
            # Try to use the refresh token which should now be considered expired
            response = await test_client.post(
                "/token",
                data={
                    "grant_type": "refresh_token",
                    "client_id": registered_client["client_id"],
                    "client_secret": registered_client["client_secret"],
                    "refresh_token": refresh_token,
                },
            )

            # In the "future", the token should be considered expired
            assert response.status_code == 400
            error_response = response.json()
            assert error_response["error"] == "invalid_grant"
            assert "refresh token has expired" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_token_invalid_scope(
        self,
        test_client: httpx.AsyncClient,
        registered_client: dict[str, Any],
        auth_code: dict[str, str],
        pkce_challenge: dict[str, str],
    ):
        """Test token endpoint error - invalid scope in refresh token request."""
        # Exchange authorization code for tokens
        token_response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "code": auth_code["code"],
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": auth_code["redirect_uri"],
            },
        )
        assert token_response.status_code == 200

        tokens = token_response.json()
        refresh_token = tokens["refresh_token"]

        # Try to use refresh token with an invalid scope
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "refresh_token",
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "refresh_token": refresh_token,
                "scope": "read write invalid_scope",  # Adding an invalid scope
            },
        )
        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_scope"
        assert "cannot request scope" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_client_registration(self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider):
        """Test client registration."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "client_uri": "https://client.example.com",
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 201, response.content

        client_info = response.json()
        assert "client_id" in client_info
        assert "client_secret" in client_info
        assert client_info["client_name"] == "Test Client"
        assert client_info["redirect_uris"] == ["https://client.example.com/callback"]

        # Verify that the client was registered
        # assert await mock_oauth_provider.clients_store.get_client(
        #     client_info["client_id"]
        # ) is not None

    @pytest.mark.anyio
    async def test_client_registration_missing_required_fields(self, test_client: httpx.AsyncClient):
        """Test client registration with missing required fields."""
        # Missing redirect_uris which is a required field
        client_metadata = {
            "client_name": "Test Client",
            "client_uri": "https://client.example.com",
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert error_data["error_description"] == "redirect_uris: Field required"

    @pytest.mark.anyio
    async def test_client_registration_invalid_uri(self, test_client: httpx.AsyncClient):
        """Test client registration with invalid URIs."""
        # Invalid redirect_uri format
        client_metadata = {
            "redirect_uris": ["not-a-valid-uri"],
            "client_name": "Test Client",
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert error_data["error_description"] == (
            "redirect_uris.0: Input should be a valid URL, relative URL without a base"
        )

    @pytest.mark.anyio
    async def test_client_registration_empty_redirect_uris(self, test_client: httpx.AsyncClient):
        """Test client registration with empty redirect_uris array."""
        redirect_uris: list[str] = []
        client_metadata = {
            "redirect_uris": redirect_uris,  # Empty array
            "client_name": "Test Client",
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert (
            error_data["error_description"] == "redirect_uris: List should have at least 1 item after validation, not 0"
        )

    @pytest.mark.anyio
    async def test_authorize_form_post(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
        """Test the authorization endpoint using POST with form-encoded data."""
        # Register a client
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 201
        client_info = response.json()

        # Use POST with form-encoded data for authorization
        response = await test_client.post(
            "/authorize",
            data={
                "response_type": "code",
                "client_id": client_info["client_id"],
                "redirect_uri": "https://client.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_form_state",
            },
        )
        assert response.status_code == 302

        # Extract the authorization code from the redirect URL
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "code" in query_params
        assert query_params["state"][0] == "test_form_state"

    @pytest.mark.anyio
    async def test_authorization_get(
        self,
        test_client: httpx.AsyncClient,
        mock_oauth_provider: MockOAuthProvider,
        pkce_challenge: dict[str, str],
    ):
        """Test the full authorization flow."""
        # 1. Register a client
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post(
            "/register",
            json=client_metadata,
        )
        assert response.status_code == 201
        client_info = response.json()

        # 2. Request authorization using GET with query params
        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": client_info["client_id"],
                "redirect_uri": "https://client.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )
        assert response.status_code == 302

        # 3. Extract the authorization code from the redirect URL
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "code" in query_params
        assert query_params["state"][0] == "test_state"
        auth_code = query_params["code"][0]

        # 4. Exchange the authorization code for tokens
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "client_secret": client_info["client_secret"],
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 200

        token_response = response.json()
        assert "access_token" in token_response
        assert "token_type" in token_response
        assert "refresh_token" in token_response
        assert "expires_in" in token_response
        assert token_response["token_type"] == "Bearer"

        # 5. Verify the access token
        access_token = token_response["access_token"]
        refresh_token = token_response["refresh_token"]

        # Create a test client with the token
        auth_info = await mock_oauth_provider.load_access_token(access_token)
        assert auth_info
        assert auth_info.client_id == client_info["client_id"]
        assert "read" in auth_info.scopes
        assert "write" in auth_info.scopes
        assert auth_info.subject == "test-user"

        # 6. Refresh the token
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "refresh_token",
                "client_id": client_info["client_id"],
                "client_secret": client_info["client_secret"],
                "refresh_token": refresh_token,
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 200

        new_token_response = response.json()
        assert "access_token" in new_token_response
        assert "refresh_token" in new_token_response
        assert new_token_response["access_token"] != access_token
        assert new_token_response["refresh_token"] != refresh_token

        refreshed_auth_info = await mock_oauth_provider.load_access_token(new_token_response["access_token"])
        assert refreshed_auth_info
        assert refreshed_auth_info.subject == "test-user"

        # 7. Revoke the token
        response = await test_client.post(
            "/revoke",
            data={
                "client_id": client_info["client_id"],
                "client_secret": client_info["client_secret"],
                "token": new_token_response["access_token"],
            },
        )
        assert response.status_code == 200

        # Verify that the token was revoked
        assert await mock_oauth_provider.load_access_token(new_token_response["access_token"]) is None

    @pytest.mark.anyio
    async def test_revoke_invalid_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
        """Test revoking an invalid token."""
        response = await test_client.post(
            "/revoke",
            data={
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "token": "invalid_token",
            },
        )
        # per RFC, this should return 200 even if the token is invalid
        assert response.status_code == 200

    @pytest.mark.anyio
    async def test_revoke_with_malformed_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):
        response = await test_client.post(
            "/revoke",
            data={
                "client_id": registered_client["client_id"],
                "client_secret": registered_client["client_secret"],
                "token": 123,
                "token_type_hint": "asdf",
            },
        )
        assert response.status_code == 400
        error_response = response.json()
        assert error_response["error"] == "invalid_request"
        assert "token_type_hint" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_client_registration_disallowed_scopes(self, test_client: httpx.AsyncClient):
        """Test client registration with scopes that are not allowed."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "scope": "read write profile admin",  # 'admin' is not in valid_scopes
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert "scope" in error_data["error_description"]
        assert "admin" in error_data["error_description"]

    @pytest.mark.anyio
    async def test_client_registration_default_scopes(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
    ):
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            # No scope specified
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()

        # Verify client was registered successfully
        assert client_info["scope"] == "read write"

        # Retrieve the client from the store to verify default scopes
        registered_client = await mock_oauth_provider.get_client(client_info["client_id"])
        assert registered_client is not None

        # Check that default scopes were applied
        assert registered_client.scope == "read write"

    @pytest.mark.anyio
    async def test_client_registration_with_authorization_code_only(self, test_client: httpx.AsyncClient):
        """Test that registration succeeds with only authorization_code (refresh_token is optional per RFC 7591)."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()
        assert "client_id" in client_info
        assert client_info["grant_types"] == ["authorization_code"]

    @pytest.mark.anyio
    async def test_client_registration_missing_authorization_code(self, test_client: httpx.AsyncClient):
        """Test that registration fails when authorization_code grant type is missing."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert error_data["error_description"] == "grant_types must include 'authorization_code'"

    @pytest.mark.anyio
    async def test_client_registration_with_additional_grant_type(self, test_client: httpx.AsyncClient):
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()

        # Verify client was registered successfully
        assert "client_id" in client_info
        assert "client_secret" in client_info
        assert client_info["client_name"] == "Test Client"

    @pytest.mark.anyio
    async def test_client_registration_with_additional_response_types(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
    ):
        """Test that registration accepts additional response_types values alongside 'code'."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token"],
            "response_types": ["code", "none"],  # Keycloak-style response with additional value
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        data = response.json()

        client = await mock_oauth_provider.get_client(data["client_id"])
        assert client is not None
        assert "code" in client.response_types

    @pytest.mark.anyio
    async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):
        """Test that registration rejects response_types that don't include 'code'."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token"],
            "response_types": ["token", "none", "nonsense-string"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 400
        error_data = response.json()
        assert "error" in error_data
        assert error_data["error"] == "invalid_client_metadata"
        assert "response_types must include 'code'" in error_data["error_description"]

    @pytest.mark.anyio
    async def test_client_registration_default_response_types(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider
    ):
        """Test that registration uses default response_types of ['code'] when not specified."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Test Client",
            "grant_types": ["authorization_code", "refresh_token"],
            # response_types not specified, should default to ["code"]
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        data = response.json()

        assert "response_types" in data
        assert data["response_types"] == ["code"]

    @pytest.mark.anyio
    async def test_client_secret_basic_authentication(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that client_secret_basic authentication works correctly."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Basic Auth Client",
            "token_endpoint_auth_method": "client_secret_basic",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()
        assert client_info["token_endpoint_auth_method"] == "client_secret_basic"

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        credentials = f"{client_info['client_id']}:{client_info['client_secret']}"
        encoded_credentials = base64.b64encode(credentials.encode()).decode()

        response = await test_client.post(
            "/token",
            headers={"Authorization": f"Basic {encoded_credentials}"},
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 200
        token_response = response.json()
        assert "access_token" in token_response

    @pytest.mark.anyio
    async def test_wrong_auth_method_without_valid_credentials_fails(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that using the wrong authentication method fails when credentials are missing."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Post Auth Client",
            "token_endpoint_auth_method": "client_secret_post",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()
        assert client_info["token_endpoint_auth_method"] == "client_secret_post"

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Try to use Basic auth when client_secret_post is registered (without secret in body)
        # This should fail because the secret is missing from the expected location

        credentials = f"{client_info['client_id']}:{client_info['client_secret']}"
        encoded_credentials = base64.b64encode(credentials.encode()).decode()

        response = await test_client.post(
            "/token",
            headers={"Authorization": f"Basic {encoded_credentials}"},
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                # client_secret NOT in body where it should be
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749: authentication failures return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Client secret is required" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_basic_auth_without_header_fails(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that omitting Basic auth when client_secret_basic is registered fails."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Basic Auth Client",
            "token_endpoint_auth_method": "client_secret_basic",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()
        assert client_info["token_endpoint_auth_method"] == "client_secret_basic"

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "client_secret": client_info["client_secret"],  # Secret in body (ignored)
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749: authentication failures return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Missing or invalid Basic authentication" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_basic_auth_invalid_base64_fails(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that invalid base64 in Basic auth header fails."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Basic Auth Client",
            "token_endpoint_auth_method": "client_secret_basic",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Send invalid base64
        response = await test_client.post(
            "/token",
            headers={"Authorization": "Basic !!!invalid-base64!!!"},
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749: authentication failures return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Invalid Basic authentication header" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_basic_auth_no_colon_fails(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that Basic auth without colon separator fails."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Basic Auth Client",
            "token_endpoint_auth_method": "client_secret_basic",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Send base64 without colon (invalid format)
        invalid_creds = base64.b64encode(b"no-colon-here").decode()
        response = await test_client.post(
            "/token",
            headers={"Authorization": f"Basic {invalid_creds}"},
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749: authentication failures return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Invalid Basic authentication header" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_basic_auth_client_id_mismatch_fails(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that client_id mismatch between body and Basic auth fails."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Basic Auth Client",
            "token_endpoint_auth_method": "client_secret_basic",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Send different client_id in Basic auth header
        wrong_creds = base64.b64encode(f"wrong-client-id:{client_info['client_secret']}".encode()).decode()
        response = await test_client.post(
            "/token",
            headers={"Authorization": f"Basic {wrong_creds}"},
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],  # Correct client_id in body
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 401
        error_response = response.json()
        # RFC 6749: authentication failures return "invalid_client"
        assert error_response["error"] == "invalid_client"
        assert "Client ID mismatch" in error_response["error_description"]

    @pytest.mark.anyio
    async def test_none_auth_method_public_client(
        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]
    ):
        """Test that 'none' authentication method works for public clients."""
        client_metadata = {
            "redirect_uris": ["https://client.example.com/callback"],
            "client_name": "Public Client",
            "token_endpoint_auth_method": "none",
            "grant_types": ["authorization_code", "refresh_token"],
        }

        response = await test_client.post("/register", json=client_metadata)
        assert response.status_code == 201
        client_info = response.json()
        assert client_info["token_endpoint_auth_method"] == "none"
        # Public clients should not have a client_secret
        assert "client_secret" not in client_info or client_info.get("client_secret") is None

        auth_code = f"code_{int(time.time())}"
        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(
            code=auth_code,
            client_id=client_info["client_id"],
            code_challenge=pkce_challenge["code_challenge"],
            redirect_uri=AnyUrl("https://client.example.com/callback"),
            redirect_uri_provided_explicitly=True,
            scopes=["read", "write"],
            expires_at=time.time() + 600,
        )

        # Token request without any client secret
        response = await test_client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "client_id": client_info["client_id"],
                "code": auth_code,
                "code_verifier": pkce_challenge["code_verifier"],
                "redirect_uri": "https://client.example.com/callback",
            },
        )
        assert response.status_code == 200
        token_response = response.json()
        assert "access_token" in token_response


class TestAuthorizeEndpointErrors:
    """Test error handling in the OAuth authorization endpoint."""

    @pytest.mark.anyio
    async def test_authorize_missing_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
        """Test authorization endpoint with missing client_id.

        According to the OAuth2.0 spec, if client_id is missing, the server should
        inform the resource owner and NOT redirect.
        """
        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                # Missing client_id
                "redirect_uri": "https://client.example.com/callback",
                "state": "test_state",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
            },
        )

        # Should NOT redirect, should show an error page
        assert response.status_code == 400
        # The response should include an error message about missing client_id
        assert "client_id" in response.text.lower()

    @pytest.mark.anyio
    async def test_authorize_invalid_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):
        """Test authorization endpoint with invalid client_id.

        According to the OAuth2.0 spec, if client_id is invalid, the server should
        inform the resource owner and NOT redirect.
        """
        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": "invalid_client_id_that_does_not_exist",
                "redirect_uri": "https://client.example.com/callback",
                "state": "test_state",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
            },
        )

        # Should NOT redirect, should show an error page
        assert response.status_code == 400
        # The response should include an error message about invalid client_id
        assert "client" in response.text.lower()

    @pytest.mark.anyio
    async def test_authorize_missing_redirect_uri(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test authorization endpoint with missing redirect_uri.

        If client has only one registered redirect_uri, it can be omitted.
        """

        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": registered_client["client_id"],
                # Missing redirect_uri
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        # Should redirect to the registered redirect_uri
        assert response.status_code == 302, response.content
        redirect_url = response.headers["location"]
        assert redirect_url.startswith("https://client.example.com/callback")

    @pytest.mark.anyio
    async def test_authorize_invalid_redirect_uri(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test authorization endpoint with invalid redirect_uri.

        According to the OAuth2.0 spec, if redirect_uri is invalid or doesn't match,
        the server should inform the resource owner and NOT redirect.
        """

        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": registered_client["client_id"],
                # Non-matching URI
                "redirect_uri": "https://attacker.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        # Should NOT redirect, should show an error page
        assert response.status_code == 400, response.content
        # The response should include an error message about redirect_uri mismatch
        assert "redirect" in response.text.lower()

    @pytest.mark.anyio
    @pytest.mark.parametrize(
        "registered_client",
        [
            {
                "redirect_uris": [
                    "https://client.example.com/callback",
                    "https://client.example.com/other-callback",
                ]
            }
        ],
        indirect=True,
    )
    async def test_authorize_missing_redirect_uri_multiple_registered(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test endpoint with missing redirect_uri with multiple registered URIs.

        If client has multiple registered redirect_uris, redirect_uri must be provided.
        """

        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": registered_client["client_id"],
                # Missing redirect_uri
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        # Should NOT redirect, should return a 400 error
        assert response.status_code == 400
        # The response should include an error message about missing redirect_uri
        assert "redirect_uri" in response.text.lower()

    @pytest.mark.anyio
    async def test_authorize_unsupported_response_type(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test authorization endpoint with unsupported response_type.

        According to the OAuth2.0 spec, for other errors like unsupported_response_type,
        the server should redirect with error parameters.
        """

        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "token",  # Unsupported (we only support "code")
                "client_id": registered_client["client_id"],
                "redirect_uri": "https://client.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        # Should redirect with error parameters
        assert response.status_code == 302
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "error" in query_params
        assert query_params["error"][0] == "unsupported_response_type"
        # State should be preserved
        assert "state" in query_params
        assert query_params["state"][0] == "test_state"

    @pytest.mark.anyio
    async def test_authorize_missing_response_type(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test authorization endpoint with missing response_type.

        Missing required parameter should result in invalid_request error.
        """

        response = await test_client.get(
            "/authorize",
            params={
                # Missing response_type
                "client_id": registered_client["client_id"],
                "redirect_uri": "https://client.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        # Should redirect with error parameters
        assert response.status_code == 302
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "error" in query_params
        assert query_params["error"][0] == "invalid_request"
        # State should be preserved
        assert "state" in query_params
        assert query_params["state"][0] == "test_state"

    @pytest.mark.anyio
    async def test_authorize_missing_pkce_challenge(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]
    ):
        """Test authorization endpoint with missing PKCE code_challenge.

        Missing PKCE parameters should result in invalid_request error.
        """
        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": registered_client["client_id"],
                # Missing code_challenge
                "state": "test_state",
                # using default URL
            },
        )

        # Should redirect with error parameters
        assert response.status_code == 302
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "error" in query_params
        assert query_params["error"][0] == "invalid_request"
        # State should be preserved
        assert "state" in query_params
        assert query_params["state"][0] == "test_state"

    @pytest.mark.anyio
    async def test_authorize_invalid_scope(
        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]
    ):
        """Test authorization endpoint with invalid scope.

        Invalid scope should redirect with invalid_scope error.
        """

        response = await test_client.get(
            "/authorize",
            params={
                "response_type": "code",
                "client_id": registered_client["client_id"],
                "redirect_uri": "https://client.example.com/callback",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "scope": "invalid_scope_that_does_not_exist",
                "state": "test_state",
            },
        )

        # Should redirect with error parameters
        assert response.status_code == 302
        redirect_url = response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)

        assert "error" in query_params
        assert query_params["error"][0] == "invalid_scope"
        # State should be preserved
        assert "state" in query_params
        assert query_params["state"][0] == "test_state"
