"""Tests for the framework-neutral Svix HMAC primitive (PAPER-26)."""

from __future__ import annotations

import base64
import hashlib
import hmac

import pytest

from mgf.common.webhooks import (
    SVIX_SCHEMA,
    WebhookHeaderSchema,
    WebhookHmacError,
    decode_svix_secret,
    verify_svix_hmac,
)

_SECRET = "whsec_" + base64.b64encode(b"super-secret-key-0123456789").decode()
_TS = 1_700_000_000


def _sign(msg_id: str, ts: int, body: bytes, secret: str = _SECRET) -> str:
    signed = f"{msg_id}.{ts}.".encode() + body  # raw body bytes, matches Svix
    digest = hmac.new(decode_svix_secret(secret), signed, hashlib.sha256).digest()
    return "v1," + base64.b64encode(digest).decode("ascii")


def _headers(msg_id: str, ts: int, sig: str) -> dict[str, str]:
    return {"svix-id": msg_id, "svix-timestamp": str(ts), "svix-signature": sig}


class TestVerifySvixHmac:
    def test_valid_signature_returns_id_ts(self) -> None:
        body = b'{"event":"ok"}'
        sig = _sign("msg_1", _TS, body)
        assert verify_svix_hmac(_headers("msg_1", _TS, sig), body, _SECRET, now=lambda: _TS) == ("msg_1", _TS)

    @pytest.mark.parametrize("drop", ["svix-id", "svix-timestamp", "svix-signature"])
    def test_missing_header_fails_closed(self, drop: str) -> None:
        body = b"{}"
        h = _headers("m", _TS, _sign("m", _TS, body))
        del h[drop]
        with pytest.raises(WebhookHmacError, match="header missing"):
            verify_svix_hmac(h, body, _SECRET, now=lambda: _TS)

    def test_non_integer_timestamp(self) -> None:
        with pytest.raises(WebhookHmacError, match="not an integer"):
            verify_svix_hmac({"svix-id": "m", "svix-timestamp": "nope", "svix-signature": "v1,x"}, b"{}", _SECRET)

    def test_replay_window_enforced(self) -> None:
        body = b"{}"
        sig = _sign("m", _TS, body)
        with pytest.raises(WebhookHmacError, match="replay window"):
            verify_svix_hmac(_headers("m", _TS, sig), body, _SECRET, now=lambda: _TS + 10_000)

    def test_non_utf8_body_with_valid_sig_passes(self) -> None:
        # Svix signs raw bytes — a correctly-signed non-UTF-8 body must verify
        # (the primitive HMACs the bytes directly, no decode).
        body = b"\xff\xfe\x00binary"
        sig = _sign("m", _TS, body)
        assert verify_svix_hmac(_headers("m", _TS, sig), body, _SECRET, now=lambda: _TS) == ("m", _TS)

    def test_non_utf8_body_bad_sig_fails_closed(self) -> None:
        body = b"\xff\xfe\x00binary"
        with pytest.raises(WebhookHmacError, match="HMAC mismatch"):
            verify_svix_hmac(_headers("m", _TS, "v1,wrong"), body, _SECRET, now=lambda: _TS)

    def test_hmac_mismatch(self) -> None:
        body = b'{"a":1}'
        sig = _sign("m", _TS, body, secret="whsec_" + base64.b64encode(b"WRONG-key").decode())
        with pytest.raises(WebhookHmacError, match="HMAC mismatch"):
            verify_svix_hmac(_headers("m", _TS, sig), body, _SECRET, now=lambda: _TS)

    def test_key_rotation_space_separated_versions(self) -> None:
        body = b"{}"
        good = _sign("m", _TS, body)
        # multiple candidates incl. a stale one; whitespace-separated (RA-02)
        sig = f"v1,STALESTALESTALE   {good}"
        assert verify_svix_hmac(_headers("m", _TS, sig), body, _SECRET, now=lambda: _TS) == ("m", _TS)

    def test_headers_case_insensitive(self) -> None:
        body = b"{}"
        sig = _sign("m", _TS, body)
        h = {"Svix-Id": "m", "SVIX-TIMESTAMP": str(_TS), "Svix-Signature": sig}
        assert verify_svix_hmac(h, body, _SECRET, now=lambda: _TS) == ("m", _TS)

    def test_custom_schema(self) -> None:
        body = b"{}"
        sig = _sign("m", _TS, body)
        schema = WebhookHeaderSchema("x-id", "x-ts", "x-sig")
        h = {"x-id": "m", "x-ts": str(_TS), "x-sig": sig}
        assert verify_svix_hmac(h, body, _SECRET, schema=schema, now=lambda: _TS) == ("m", _TS)


class TestDecodeSvixSecret:
    def test_decodes_whsec_base64(self) -> None:
        raw = b"the-real-key"
        secret = "whsec_" + base64.b64encode(raw).decode()
        assert decode_svix_secret(secret) == raw

    def test_non_base64_falls_back_to_raw(self) -> None:
        # '!!!' is not in the base64 alphabet → validate=True rejects → raw fallback
        assert decode_svix_secret("whsec_!!!not-b64!!!") == b"!!!not-b64!!!"

    def test_accepts_bytes(self) -> None:
        raw = b"k"
        assert decode_svix_secret(("whsec_" + base64.b64encode(raw).decode()).encode()) == raw


def test_svix_schema_is_lowercased() -> None:
    assert SVIX_SCHEMA.id_header == "svix-id"
    assert WebhookHeaderSchema("X-Id", "X-Ts", "X-Sig").id_header == "x-id"
