"""Svix webhook tamper / replay regressions for :class:`ClerkWebhookVerifier`.

Marked ``regression``: pins the HMAC-signature class of attacks against
the Svix verifier. The verifier signs ``{svix-id}.{svix-timestamp}.{body}``
with HMAC-SHA256 (clerk_webhook.py:122-134), enforces a 5-minute replay
window (clerk_webhook.py:38, 116-120), and is **stateless** — it does
NOT dedupe svix-id values.

Existing ``tests/test_clerk_webhook.py`` covers wrong-secret and
missing-svix-id; the mock's stale-timestamp path is covered in
``tests/test_mock.py``. The tests below pin the concrete verifier's
single-bit tamper, its own stale-timestamp rejection, and document the
stateless-replay contract.
"""

from __future__ import annotations

import json
import time

import pytest

from mgf.auth import InvalidWebhookSignatureError, MockWebhookVerifier
from mgf.auth.clerk_webhook import ClerkWebhookVerifier

pytestmark = pytest.mark.regression


# ---------------------------------------------------------------------
# Attack 1 — single-bit signature tamper
# ---------------------------------------------------------------------


class TestSignatureTamper:
    @pytest.mark.asyncio
    async def test_flipped_signature_char_rejected(self) -> None:
        secret = b"whsec_tamper"
        verifier = ClerkWebhookVerifier(webhook_secret=secret)
        body = json.dumps({"type": "user.created"}).encode()
        headers = MockWebhookVerifier.sign(secret=secret, body=body, event_id="msg_tamper")

        # Flip one character inside the base64 signature (after "v1,").
        sig = headers["svix-signature"]
        prefix, b64 = sig.split(",", 1)
        flipped_char = "A" if b64[0] != "A" else "B"
        headers["svix-signature"] = f"{prefix},{flipped_char}{b64[1:]}"

        with pytest.raises(InvalidWebhookSignatureError, match="HMAC mismatch"):
            await verifier.verify_webhook(headers, body)


# ---------------------------------------------------------------------
# Attack 2 — stale timestamp (concrete verifier; mock path covered in
# test_mock.py, not duplicated there).
# ---------------------------------------------------------------------


class TestStaleTimestamp:
    @pytest.mark.asyncio
    async def test_stale_timestamp_rejected(self) -> None:
        secret = b"whsec_stale"
        verifier = ClerkWebhookVerifier(webhook_secret=secret)
        body = json.dumps({"type": "user.created"}).encode()
        # 10 minutes old — outside the 5-minute Svix replay window. The
        # signature itself is valid for this (id, ts, body) tuple, so
        # the rejection is purely the freshness check.
        stale_ts = int(time.time()) - 10 * 60
        headers = MockWebhookVerifier.sign(
            secret=secret, body=body, event_id="msg_stale", timestamp=stale_ts
        )

        with pytest.raises(InvalidWebhookSignatureError, match="replay window"):
            await verifier.verify_webhook(headers, body)

    @pytest.mark.asyncio
    async def test_future_timestamp_rejected(self) -> None:
        # Skew is absolute (clerk_webhook.py:116), so a far-future
        # timestamp is rejected the same way a stale one is.
        secret = b"whsec_future"
        verifier = ClerkWebhookVerifier(webhook_secret=secret)
        body = json.dumps({"type": "user.created"}).encode()
        future_ts = int(time.time()) + 10 * 60
        headers = MockWebhookVerifier.sign(
            secret=secret, body=body, event_id="msg_future", timestamp=future_ts
        )

        with pytest.raises(InvalidWebhookSignatureError, match="replay window"):
            await verifier.verify_webhook(headers, body)


# ---------------------------------------------------------------------
# Attack 3 — replay (duplicate svix-id)
# ---------------------------------------------------------------------


class TestReplay:
    @pytest.mark.asyncio
    async def test_duplicate_svix_id_both_verify(self) -> None:
        # CONTRACT: ClerkWebhookVerifier is STATELESS (clerk_webhook.py
        # docstring + no svix-id store). It does NOT dedupe replays —
        # replay defence is the consumer app's job (e.g. a unique
        # constraint / processed-event ledger keyed on event_id in the
        # mirror handler). So sending the SAME signed event twice
        # verifies BOTH times. This test pins that intentional
        # behaviour; it must NOT be "fixed" to expect a rejection the
        # verifier cannot and should not deliver.
        secret = b"whsec_replay"
        verifier = ClerkWebhookVerifier(webhook_secret=secret)
        body = json.dumps({"type": "organizationMembership.created"}).encode()
        headers = MockWebhookVerifier.sign(secret=secret, body=body, event_id="msg_replay")

        first = await verifier.verify_webhook(headers, body)
        second = await verifier.verify_webhook(headers, body)

        assert first.event_id == second.event_id == "msg_replay"
        assert first.event_type == second.event_type
        # Same identity surfaced twice: the dedup key the consumer keys on.
