"""
Tests for OAuth error handling in the auth handlers.
"""

import unittest.mock
from typing import TYPE_CHECKING, Any
from urllib.parse import parse_qs, urlparse

import httpx
import pytest
from httpx import ASGITransport
from pydantic import AnyHttpUrl
from starlette.applications import Starlette

from mcp.server.auth.provider import AuthorizeError, RegistrationError, TokenError
from mcp.server.auth.routes import create_auth_routes

# TODO(Marcelo): This TYPE_CHECKING shouldn't be here, but pytest doesn't seem to get the module correctly.
if TYPE_CHECKING:
    from ...server.fastmcp.auth.test_auth_integration import MockOAuthProvider
else:
    from tests.server.fastmcp.auth.test_auth_integration import MockOAuthProvider


@pytest.fixture
def oauth_provider():
    """Return a MockOAuthProvider instance that can be configured to raise errors."""
    return MockOAuthProvider()


@pytest.fixture
def app(oauth_provider: MockOAuthProvider):
    from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions

    # Enable client registration
    client_registration_options = ClientRegistrationOptions(enabled=True)
    revocation_options = RevocationOptions(enabled=True)

    # Create auth routes
    auth_routes = create_auth_routes(
        oauth_provider,
        issuer_url=AnyHttpUrl("http://localhost"),
        client_registration_options=client_registration_options,
        revocation_options=revocation_options,
    )

    # Create Starlette app with routes directly
    return Starlette(routes=auth_routes)


@pytest.fixture
def client(app: Starlette):
    transport = ASGITransport(app=app)
    # Use base_url without a path since routes are directly on the app
    return httpx.AsyncClient(transport=transport, base_url="http://localhost")


@pytest.fixture
def pkce_challenge():
    """Create a PKCE challenge with code_verifier and code_challenge."""
    import base64
    import hashlib
    import secrets

    # Generate a code verifier
    code_verifier = secrets.token_urlsafe(64)[:128]

    # Create code challenge using S256 method
    code_verifier_bytes = code_verifier.encode("ascii")
    sha256 = hashlib.sha256(code_verifier_bytes).digest()
    code_challenge = base64.urlsafe_b64encode(sha256).decode().rstrip("=")

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


@pytest.fixture
async def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:
    """Create and register a test client."""
    # Default client metadata
    client_metadata = {
        "redirect_uris": ["https://client.example.com/callback"],
        "token_endpoint_auth_method": "client_secret_post",
        "grant_types": ["authorization_code", "refresh_token"],
        "response_types": ["code"],
        "client_name": "Test Client",
    }

    response = await 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


class TestRegistrationErrorHandling:
    @pytest.mark.anyio
    async def test_registration_error_handling(self, client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):
        # Mock the register_client method to raise a registration error
        with unittest.mock.patch.object(
            oauth_provider,
            "register_client",
            side_effect=RegistrationError(
                error="invalid_redirect_uri",
                error_description="The redirect URI is invalid",
            ),
        ):
            # Prepare a client registration request
            client_data = {
                "redirect_uris": ["https://client.example.com/callback"],
                "token_endpoint_auth_method": "client_secret_post",
                "grant_types": ["authorization_code", "refresh_token"],
                "response_types": ["code"],
                "client_name": "Test Client",
            }

            # Send the registration request
            response = await client.post(
                "/register",
                json=client_data,
            )

            # Verify the response
            assert response.status_code == 400, response.content
            data = response.json()
            assert data["error"] == "invalid_redirect_uri"
            assert data["error_description"] == "The redirect URI is invalid"


class TestAuthorizeErrorHandling:
    @pytest.mark.anyio
    async def test_authorize_error_handling(
        self,
        client: httpx.AsyncClient,
        oauth_provider: MockOAuthProvider,
        registered_client: dict[str, Any],
        pkce_challenge: dict[str, str],
    ):
        # Mock the authorize method to raise an authorize error
        with unittest.mock.patch.object(
            oauth_provider,
            "authorize",
            side_effect=AuthorizeError(error="access_denied", error_description="The user denied the request"),
        ):
            # Register the client
            client_id = registered_client["client_id"]
            redirect_uri = registered_client["redirect_uris"][0]

            # Prepare an authorization request
            params = {
                "client_id": client_id,
                "redirect_uri": redirect_uri,
                "response_type": "code",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            }

            # Send the authorization request
            response = await client.get("/authorize", params=params)

            # Verify the response is a 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 query_params["error"][0] == "access_denied"
            assert "error_description" in query_params
            assert query_params["state"][0] == "test_state"


class TestTokenErrorHandling:
    @pytest.mark.anyio
    async def test_token_error_handling_auth_code(
        self,
        client: httpx.AsyncClient,
        oauth_provider: MockOAuthProvider,
        registered_client: dict[str, Any],
        pkce_challenge: dict[str, str],
    ):
        # Register the client and get an auth code
        client_id = registered_client["client_id"]
        client_secret = registered_client["client_secret"]
        redirect_uri = registered_client["redirect_uris"][0]

        # First get an authorization code
        auth_response = await client.get(
            "/authorize",
            params={
                "client_id": client_id,
                "redirect_uri": redirect_uri,
                "response_type": "code",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )

        redirect_url = auth_response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)
        code = query_params["code"][0]

        # Mock the exchange_authorization_code method to raise a token error
        with unittest.mock.patch.object(
            oauth_provider,
            "exchange_authorization_code",
            side_effect=TokenError(
                error="invalid_grant",
                error_description="The authorization code is invalid",
            ),
        ):
            # Try to exchange the code for tokens
            token_response = await client.post(
                "/token",
                data={
                    "grant_type": "authorization_code",
                    "code": code,
                    "redirect_uri": redirect_uri,
                    "client_id": client_id,
                    "client_secret": client_secret,
                    "code_verifier": pkce_challenge["code_verifier"],
                },
            )

            # Verify the response
            assert token_response.status_code == 400
            data = token_response.json()
            assert data["error"] == "invalid_grant"
            assert data["error_description"] == "The authorization code is invalid"

    @pytest.mark.anyio
    async def test_token_error_handling_refresh_token(
        self,
        client: httpx.AsyncClient,
        oauth_provider: MockOAuthProvider,
        registered_client: dict[str, Any],
        pkce_challenge: dict[str, str],
    ):
        # Register the client and get tokens
        client_id = registered_client["client_id"]
        client_secret = registered_client["client_secret"]
        redirect_uri = registered_client["redirect_uris"][0]

        # First get an authorization code
        auth_response = await client.get(
            "/authorize",
            params={
                "client_id": client_id,
                "redirect_uri": redirect_uri,
                "response_type": "code",
                "code_challenge": pkce_challenge["code_challenge"],
                "code_challenge_method": "S256",
                "state": "test_state",
            },
        )
        assert auth_response.status_code == 302, auth_response.content

        redirect_url = auth_response.headers["location"]
        parsed_url = urlparse(redirect_url)
        query_params = parse_qs(parsed_url.query)
        code = query_params["code"][0]

        # Exchange the code for tokens
        token_response = await client.post(
            "/token",
            data={
                "grant_type": "authorization_code",
                "code": code,
                "redirect_uri": redirect_uri,
                "client_id": client_id,
                "client_secret": client_secret,
                "code_verifier": pkce_challenge["code_verifier"],
            },
        )

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

        # Mock the exchange_refresh_token method to raise a token error
        with unittest.mock.patch.object(
            oauth_provider,
            "exchange_refresh_token",
            side_effect=TokenError(
                error="invalid_scope",
                error_description="The requested scope is invalid",
            ),
        ):
            # Try to use the refresh token
            refresh_response = await client.post(
                "/token",
                data={
                    "grant_type": "refresh_token",
                    "refresh_token": refresh_token,
                    "client_id": client_id,
                    "client_secret": client_secret,
                },
            )

            # Verify the response
            assert refresh_response.status_code == 400
            data = refresh_response.json()
            assert data["error"] == "invalid_scope"
            assert data["error_description"] == "The requested scope is invalid"
