"""Server-side SEP-990 Identity Assertion Authorization Grant (RFC 7523 jwt-bearer) handling."""

import secrets
import time

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

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

ID_JAG_GRANT_PROFILE = "urn:ietf:params:oauth:grant-profile:id-jag"
VALID_ASSERTION = "valid-id-jag"
CONFIDENTIAL_CLIENT_ID = "enterprise-client"
CONFIDENTIAL_CLIENT_SECRET = "enterprise-secret"


class IdentityAssertionProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
    """A provider that implements `exchange_identity_assertion`; everything else is unused here."""

    def __init__(self) -> None:
        self.clients: dict[str, OAuthClientInformationFull] = {}
        self.tokens: dict[str, AccessToken] = {}
        self.last_params: IdentityAssertionParams | None = None

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

    async def register_client(self, client_info: OAuthClientInformationFull) -> None:
        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:
        raise NotImplementedError

    async def load_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: str
    ) -> AuthorizationCode | None:
        raise NotImplementedError

    async def exchange_authorization_code(
        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
    ) -> OAuthToken:
        raise NotImplementedError

    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
        raise NotImplementedError

    async def exchange_refresh_token(
        self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
    ) -> OAuthToken:
        raise NotImplementedError

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

    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
        raise NotImplementedError

    async def exchange_identity_assertion(
        self, client: OAuthClientInformationFull, params: IdentityAssertionParams
    ) -> OAuthToken:
        self.last_params = params
        # Stand-in for RFC 7523 §3 / SEP-990 §5.1 assertion validation.
        if params.assertion != VALID_ASSERTION:
            raise TokenError(error="invalid_grant", error_description="assertion is not valid")
        assert client.client_id is not None
        scopes = params.scopes or ["mcp"]
        access = f"access_{secrets.token_hex(16)}"
        self.tokens[access] = AccessToken(
            token=access,
            client_id=client.client_id,
            scopes=scopes,
            expires_at=int(time.time()) + 3600,
            resource=params.resource,
            subject="assertion-user",
        )
        return OAuthToken(access_token=access, token_type="Bearer", expires_in=3600, scope=" ".join(scopes))


@pytest.fixture
def provider() -> IdentityAssertionProvider:
    prov = IdentityAssertionProvider()
    # Pre-register a confidential client (DCR refuses the grant; see the DCR test).
    prov.clients[CONFIDENTIAL_CLIENT_ID] = OAuthClientInformationFull(
        client_id=CONFIDENTIAL_CLIENT_ID,
        client_secret=CONFIDENTIAL_CLIENT_SECRET,
        redirect_uris=None,
        grant_types=[JWT_BEARER_GRANT_TYPE],
        token_endpoint_auth_method="client_secret_post",
        scope="mcp",
    )
    return prov


@pytest.fixture
def app(provider: IdentityAssertionProvider) -> Starlette:
    routes = create_auth_routes(
        provider,
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=["mcp"]),
        revocation_options=RevocationOptions(enabled=False),
        identity_assertion_enabled=True,
    )
    return Starlette(routes=routes)


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


def assertion_form(**overrides: str) -> dict[str, str]:
    form = {
        "grant_type": JWT_BEARER_GRANT_TYPE,
        "client_id": CONFIDENTIAL_CLIENT_ID,
        "client_secret": CONFIDENTIAL_CLIENT_SECRET,
        "assertion": VALID_ASSERTION,
    }
    form.update(overrides)
    return form


def test_build_metadata_advertises_id_jag_profile_when_enabled():
    enabled = build_metadata(
        AnyHttpUrl("https://auth.example.com"),
        None,
        ClientRegistrationOptions(),
        RevocationOptions(),
        supports_identity_assertion=True,
    )
    assert JWT_BEARER_GRANT_TYPE in (enabled.grant_types_supported or [])
    assert enabled.authorization_grant_profiles_supported == [ID_JAG_GRANT_PROFILE]
    # The grant is confidential-only, so the `none` auth method is NOT advertised.
    assert "none" not in (enabled.token_endpoint_auth_methods_supported or [])

    disabled = build_metadata(
        AnyHttpUrl("https://auth.example.com"),
        None,
        ClientRegistrationOptions(),
        RevocationOptions(),
    )
    assert JWT_BEARER_GRANT_TYPE not in (disabled.grant_types_supported or [])
    assert disabled.authorization_grant_profiles_supported is None


@pytest.mark.anyio
async def test_metadata_endpoint_lists_id_jag_profile(client: httpx.AsyncClient):
    response = await client.get("/.well-known/oauth-authorization-server")
    assert response.status_code == 200
    body = response.json()
    assert JWT_BEARER_GRANT_TYPE in body["grant_types_supported"]
    assert body["authorization_grant_profiles_supported"] == [ID_JAG_GRANT_PROFILE]


@pytest.mark.anyio
async def test_identity_assertion_success(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
    response = await client.post("/token", data=assertion_form(scope="mcp", resource="https://mcp.example.com/mcp"))

    assert response.status_code == 200, response.content
    body = response.json()
    assert body["token_type"] == "Bearer"
    assert "issued_token_type" not in body  # plain RFC 6749 response under jwt-bearer

    issued = await provider.load_access_token(body["access_token"])
    assert issued is not None
    assert issued.scopes == ["mcp"]
    assert issued.subject == "assertion-user"

    assert provider.last_params is not None
    assert provider.last_params.assertion == VALID_ASSERTION
    assert provider.last_params.scopes == ["mcp"]
    assert provider.last_params.resource == "https://mcp.example.com/mcp"


@pytest.mark.anyio
async def test_identity_assertion_invalid_assertion(client: httpx.AsyncClient):
    response = await client.post("/token", data=assertion_form(assertion="forged"))

    assert response.status_code == 400
    assert response.json() == {"error": "invalid_grant", "error_description": "assertion is not valid"}


@pytest.mark.anyio
async def test_identity_assertion_rejected_when_disabled(provider: IdentityAssertionProvider):
    routes = create_auth_routes(
        provider,
        issuer_url=AnyHttpUrl("https://auth.example.com"),
        client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=["mcp"]),
        revocation_options=RevocationOptions(enabled=False),
        identity_assertion_enabled=False,
    )
    app = Starlette(routes=routes)
    transport = ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="https://auth.example.com") as http:
        response = await http.post("/token", data=assertion_form())

    assert response.status_code == 400
    assert response.json()["error"] == "unsupported_grant_type"
    assert provider.last_params is None


@pytest.mark.anyio
async def test_identity_assertion_rejects_public_client(client: httpx.AsyncClient, provider: IdentityAssertionProvider):
    """A public (auth method 'none') client cannot use the grant, even if it presents a valid assertion."""
    provider.clients["public-client"] = OAuthClientInformationFull(
        client_id="public-client",
        redirect_uris=None,
        grant_types=[JWT_BEARER_GRANT_TYPE],
        token_endpoint_auth_method="none",
        scope="mcp",
    )

    response = await client.post(
        "/token",
        data={"grant_type": JWT_BEARER_GRANT_TYPE, "client_id": "public-client", "assertion": VALID_ASSERTION},
    )

    assert response.status_code == 400
    assert response.json()["error"] == "unauthorized_client"
    assert provider.last_params is None


@pytest.mark.anyio
async def test_identity_assertion_rejects_secretless_confidential_client(
    client: httpx.AsyncClient, provider: IdentityAssertionProvider
):
    """A client registered with a secret-based method but no stored secret fails authentication.

    `ClientAuthenticator` rejects this misconfiguration as `invalid_client`, so the request never
    reaches the jwt-bearer handler or the provider hook.
    """
    provider.clients["secretless-client"] = OAuthClientInformationFull(
        client_id="secretless-client",
        client_secret=None,
        redirect_uris=None,
        grant_types=[JWT_BEARER_GRANT_TYPE],
        token_endpoint_auth_method="client_secret_post",
        scope="mcp",
    )

    response = await client.post(
        "/token",
        data={"grant_type": JWT_BEARER_GRANT_TYPE, "client_id": "secretless-client", "assertion": VALID_ASSERTION},
    )

    assert response.status_code == 401
    body = response.json()
    assert body["error"] == "invalid_client"
    assert "no stored secret" in body["error_description"]
    assert provider.last_params is None


@pytest.mark.anyio
async def test_malformed_request_missing_assertion_is_invalid_request(client: httpx.AsyncClient):
    """A jwt-bearer request without the required `assertion` fails validation with invalid_request."""
    response = await client.post(
        "/token",
        data={
            "grant_type": JWT_BEARER_GRANT_TYPE,
            "client_id": CONFIDENTIAL_CLIENT_ID,
            "client_secret": CONFIDENTIAL_CLIENT_SECRET,
        },
    )

    assert response.status_code == 400
    assert response.json()["error"] == "invalid_request"


@pytest.mark.anyio
async def test_client_without_the_grant_registered_is_rejected(
    client: httpx.AsyncClient, provider: IdentityAssertionProvider
):
    """A confidential client whose registration omits the jwt-bearer grant is refused the grant."""
    provider.clients["no-grant-client"] = OAuthClientInformationFull(
        client_id="no-grant-client",
        client_secret="s",
        redirect_uris=None,
        grant_types=["authorization_code"],
        token_endpoint_auth_method="client_secret_post",
        scope="mcp",
    )

    response = await client.post(
        "/token",
        data={
            "grant_type": JWT_BEARER_GRANT_TYPE,
            "client_id": "no-grant-client",
            "client_secret": "s",
            "assertion": VALID_ASSERTION,
        },
    )

    assert response.status_code == 400
    assert response.json()["error"] == "unsupported_grant_type"
    assert provider.last_params is None


@pytest.mark.anyio
async def test_dcr_refuses_to_register_the_jwt_bearer_grant(
    client: httpx.AsyncClient, provider: IdentityAssertionProvider
):
    """Dynamic client registration rejects the jwt-bearer grant; the ID-JAG flow needs pre-registration."""
    response = await client.post(
        "/register",
        json={
            "redirect_uris": ["https://client.example.com/callback"],
            "token_endpoint_auth_method": "client_secret_post",
            "grant_types": ["authorization_code", JWT_BEARER_GRANT_TYPE],
            "response_types": ["code"],
        },
    )

    assert response.status_code == 400
    body = response.json()
    assert body["error"] == "invalid_client_metadata"
    assert JWT_BEARER_GRANT_TYPE in body["error_description"]

    # A registration without the jwt-bearer grant still succeeds and is stored.
    ok = await client.post(
        "/register",
        json={
            "redirect_uris": ["https://client.example.com/callback"],
            "token_endpoint_auth_method": "client_secret_post",
            "grant_types": ["authorization_code"],
            "response_types": ["code"],
        },
    )
    assert ok.status_code == 201
    assert ok.json()["client_id"] in provider.clients


@pytest.mark.anyio
async def test_default_provider_rejects_identity_assertion():
    """A provider that does not override `exchange_identity_assertion` rejects with unsupported_grant_type."""

    class BareProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
        async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
            raise NotImplementedError

        async def register_client(self, client_info: OAuthClientInformationFull) -> None:
            raise NotImplementedError

        async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
            raise NotImplementedError

        async def load_authorization_code(
            self, client: OAuthClientInformationFull, authorization_code: str
        ) -> AuthorizationCode | None:
            raise NotImplementedError

        async def exchange_authorization_code(
            self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode
        ) -> OAuthToken:
            raise NotImplementedError

        async def load_refresh_token(
            self, client: OAuthClientInformationFull, refresh_token: str
        ) -> RefreshToken | None:
            raise NotImplementedError

        async def exchange_refresh_token(
            self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]
        ) -> OAuthToken:
            raise NotImplementedError

        async def load_access_token(self, token: str) -> AccessToken | None:
            raise NotImplementedError

        async def revoke_token(self, token: AccessToken | RefreshToken) -> None:
            raise NotImplementedError

    bare = BareProvider()
    client_info = OAuthClientInformationFull(
        redirect_uris=None,
        client_id="c",
        grant_types=[JWT_BEARER_GRANT_TYPE],
    )
    params = IdentityAssertionParams(assertion=VALID_ASSERTION)
    with pytest.raises(TokenError) as excinfo:
        await bare.exchange_identity_assertion(client_info, params)
    assert excinfo.value.error == "unsupported_grant_type"
