"""Unit tests for :class:`ClerkSessionVerifier`.

Strategy: generate an ephemeral RSA-2048 keypair per test, hand the
public PEM to the verifier, and sign synthetic JWTs with the private
key. This exercises real cryptography without any Clerk API contact.
"""

from __future__ import annotations

import time
from datetime import UTC, datetime, timedelta
from uuid import uuid4

import jwt as pyjwt
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from tests.conftest import Principal

from mgf.auth import InvalidSessionError, SessionVerifier
from mgf.auth.clerk_session import (
    ClerkClaims,
    ClerkSessionVerifier,
)

_ISSUER = "https://clerk.test.example"


# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------


def _make_keypair() -> tuple[bytes, bytes]:
    """Return ``(private_pem, public_pem)`` as bytes."""
    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
    private_pem = private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption(),
    )
    public_pem = private_key.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo,
    )
    return private_pem, public_pem


def _sign_jwt(private_pem: bytes, claims: dict[str, object]) -> str:
    return pyjwt.encode(claims, private_pem, algorithm="RS256")


def _default_claims(now: int | None = None) -> dict[str, object]:
    now = now if now is not None else int(time.time())
    return {
        "iss": _ISSUER,
        "sub": "user_2_faraj",
        "org_id": "org_acme",
        "org_role": "operator",
        "email": "faraj@acme.example",
        "locale": "ar-IQ",
        "iat": now,
        "exp": now + 3600,
    }


def _seeded_principal() -> Principal:
    now = datetime.now(UTC)
    return Principal(
        user_id=uuid4(),
        tenant_id=uuid4(),
        role="operator",
        email="faraj@acme.example",
        locale="ar-IQ",
        issued_at=now,
        expires_at=now + timedelta(hours=1),
    )


def _build_verifier(
    public_pem: bytes,
) -> tuple[ClerkSessionVerifier, list[ClerkClaims]]:
    """Return the verifier plus a list capturing the claims passed to
    its principal_resolver — useful for asserting what got parsed."""
    captured: list[ClerkClaims] = []
    principal = _seeded_principal()

    async def resolver(claims: ClerkClaims) -> Principal:
        captured.append(claims)
        return principal

    verifier = ClerkSessionVerifier(
        jwt_public_key=public_pem.decode(),
        issuer=_ISSUER,
        principal_resolver=resolver,
    )
    return verifier, captured


# ---------------------------------------------------------------------
# Protocol conformance
# ---------------------------------------------------------------------


def test_clerk_session_verifier_satisfies_protocol() -> None:
    _, public_pem = _make_keypair()
    verifier, _ = _build_verifier(public_pem)
    assert isinstance(verifier, SessionVerifier)


# ---------------------------------------------------------------------
# verify_session
# ---------------------------------------------------------------------


class TestVerifySession:
    @pytest.mark.asyncio
    async def test_well_formed_token_resolves(self) -> None:
        private, public = _make_keypair()
        verifier, captured = _build_verifier(public)

        token = _sign_jwt(private, _default_claims())
        principal = await verifier.verify_session(token)

        assert principal.role == "operator"
        assert len(captured) == 1
        assert captured[0].sub == "user_2_faraj"
        assert captured[0].org_id == "org_acme"
        assert captured[0].locale == "ar-IQ"

    @pytest.mark.asyncio
    async def test_empty_token_raises(self) -> None:
        _, public = _make_keypair()
        verifier, _ = _build_verifier(public)
        with pytest.raises(InvalidSessionError, match="missing"):
            await verifier.verify_session("")

    @pytest.mark.asyncio
    async def test_expired_token_raises(self) -> None:
        private, public = _make_keypair()
        verifier, _ = _build_verifier(public)

        claims = _default_claims(now=int(time.time()) - 3 * 3600)
        token = _sign_jwt(private, claims)

        with pytest.raises(InvalidSessionError, match="expired"):
            await verifier.verify_session(token)

    @pytest.mark.asyncio
    async def test_wrong_issuer_raises(self) -> None:
        private, public = _make_keypair()
        verifier, _ = _build_verifier(public)

        claims = _default_claims()
        claims["iss"] = "https://clerk.attacker.example"
        token = _sign_jwt(private, claims)

        with pytest.raises(InvalidSessionError, match="issuer"):
            await verifier.verify_session(token)

    @pytest.mark.asyncio
    async def test_wrong_signing_key_raises(self) -> None:
        # Verifier configured with key A; token signed with key B.
        _, public_a = _make_keypair()
        private_b, _ = _make_keypair()
        verifier, _ = _build_verifier(public_a)

        token = _sign_jwt(private_b, _default_claims())

        with pytest.raises(InvalidSessionError, match="signature"):
            await verifier.verify_session(token)

    @pytest.mark.asyncio
    async def test_missing_required_claim_raises(self) -> None:
        private, public = _make_keypair()
        verifier, _ = _build_verifier(public)

        claims = _default_claims()
        del claims["sub"]
        token = _sign_jwt(private, claims)

        with pytest.raises(InvalidSessionError):
            await verifier.verify_session(token)

    @pytest.mark.asyncio
    async def test_missing_email_claim_raises(self) -> None:
        # ``email`` is non-standard for JWT but required by our
        # resolver pipeline; missing it surfaces at parse time.
        private, public = _make_keypair()
        verifier, _ = _build_verifier(public)

        claims = _default_claims()
        del claims["email"]
        token = _sign_jwt(private, claims)

        with pytest.raises(InvalidSessionError, match="email"):
            await verifier.verify_session(token)

    @pytest.mark.asyncio
    async def test_locale_defaults_to_en_us_when_missing(self) -> None:
        private, public = _make_keypair()
        verifier, captured = _build_verifier(public)

        claims = _default_claims()
        del claims["locale"]
        token = _sign_jwt(private, claims)

        await verifier.verify_session(token)
        assert captured[0].locale == "en-US"

    @pytest.mark.asyncio
    async def test_org_id_optional(self) -> None:
        private, public = _make_keypair()
        verifier, captured = _build_verifier(public)

        claims = _default_claims()
        del claims["org_id"]
        del claims["org_role"]
        token = _sign_jwt(private, claims)

        await verifier.verify_session(token)
        assert captured[0].org_id is None
        assert captured[0].org_role is None
