"""Delivery webhook signature verification (task 8.4, spec messaging FR-6.11).

`/api/webhooks/email` is the only unauthenticated write path in the application, and what
it writes is consequential: a `email.bounced` event sets `email_invalid`, which excludes
that guest from every future send. Silently. Nobody notices until they ask why they never
got the reminder.

So the endpoint is tested from the attacker's side: forged signatures, replayed captures,
truncated headers, and a rotated secret.
"""

import base64
import hashlib
import hmac
import json
import time

import pytest

from app.config import Settings, get_settings
from app.services.providers import ResendEmailProvider

SECRET = "whsec_" + base64.b64encode(b"a-test-webhook-signing-secret").decode()
PAYLOAD = {"type": "email.bounced", "data": {"email_id": "msg_123"}}


@pytest.fixture(autouse=True)
def _configured(monkeypatch: pytest.MonkeyPatch) -> None:
    """Point the provider at a known secret. `get_settings` is lru_cached, so the cache is
    cleared rather than the environment mutated."""
    monkeypatch.setenv("EMAIL_WEBHOOK_SECRET", SECRET)
    get_settings.cache_clear()
    yield
    get_settings.cache_clear()


def sign(
    body: bytes, *, message_id: str = "msg_abc", timestamp: int | None = None, secret: str = SECRET
) -> dict[str, str]:
    """Produce the headers Svix would send, so the test signs the way Resend really does."""
    ts = timestamp if timestamp is not None else int(time.time())
    key = base64.b64decode(secret.removeprefix("whsec_"))
    digest = hmac.new(key, f"{message_id}.{ts}.".encode() + body, hashlib.sha256).digest()
    return {
        "svix-id": message_id,
        "svix-timestamp": str(ts),
        "svix-signature": f"v1,{base64.b64encode(digest).decode()}",
    }


@pytest.fixture
def provider() -> ResendEmailProvider:
    return ResendEmailProvider(api_key="unused", sender="noreply@example.test")


def body_bytes() -> bytes:
    return json.dumps(PAYLOAD).encode()


# ------------------------------------------------------------------ the happy path


def test_a_correctly_signed_webhook_is_accepted(provider: ResendEmailProvider) -> None:
    body = body_bytes()
    assert provider.verify_signature(body, sign(body)) is True


def test_header_casing_does_not_matter(provider: ResendEmailProvider) -> None:
    """Starlette lower-cases headers; a direct caller may not."""
    body = body_bytes()
    headers = {k.upper(): v for k, v in sign(body).items()}
    assert provider.verify_signature(body, headers) is True


# ------------------------------------------------------------------ forgery


def test_an_unsigned_webhook_is_rejected(provider: ResendEmailProvider) -> None:
    assert provider.verify_signature(body_bytes(), {}) is False


def test_a_forged_signature_is_rejected(provider: ResendEmailProvider) -> None:
    body = body_bytes()
    headers = sign(body)
    headers["svix-signature"] = "v1," + base64.b64encode(b"0" * 32).decode()
    assert provider.verify_signature(body, headers) is False


def test_a_signature_from_the_wrong_secret_is_rejected(provider: ResendEmailProvider) -> None:
    body = body_bytes()
    other = "whsec_" + base64.b64encode(b"someone-elses-secret").decode()
    assert provider.verify_signature(body, sign(body, secret=other)) is False


def test_tampering_with_the_body_invalidates_the_signature(
    provider: ResendEmailProvider,
) -> None:
    """The realistic attack: capture a real `delivered` receipt, change it to a bounce for
    a different guest, and re-post it."""
    headers = sign(body_bytes())
    tampered = json.dumps({"type": "email.bounced", "data": {"email_id": "msg_999"}}).encode()
    assert provider.verify_signature(tampered, headers) is False


def test_swapping_the_message_id_invalidates_the_signature(
    provider: ResendEmailProvider,
) -> None:
    """The id is inside the signed payload, so it cannot be rewritten independently."""
    body = body_bytes()
    headers = sign(body)
    headers["svix-id"] = "msg_somethingelse"
    assert provider.verify_signature(body, headers) is False


# ------------------------------------------------------------------ replay


def test_a_replayed_webhook_is_rejected(provider: ResendEmailProvider) -> None:
    """A signature with no time bound stays valid forever. This is the test that fails if
    someone simplifies the check back to signing the body alone."""
    body = body_bytes()
    stale = int(time.time()) - 3600
    assert provider.verify_signature(body, sign(body, timestamp=stale)) is False


def test_a_future_dated_webhook_is_rejected(provider: ResendEmailProvider) -> None:
    """Clock skew cuts both ways; a far-future timestamp is equally not a live delivery."""
    body = body_bytes()
    ahead = int(time.time()) + 3600
    assert provider.verify_signature(body, sign(body, timestamp=ahead)) is False


def test_a_webhook_just_inside_the_window_is_accepted(provider: ResendEmailProvider) -> None:
    """Real clocks drift. Rejecting a 60-second-old webhook would drop live deliveries."""
    body = body_bytes()
    recent = int(time.time()) - 60
    assert provider.verify_signature(body, sign(body, timestamp=recent)) is True


@pytest.mark.parametrize("timestamp", ["", "not-a-number", "12.5"])
def test_a_malformed_timestamp_is_rejected(provider: ResendEmailProvider, timestamp: str) -> None:
    body = body_bytes()
    headers = sign(body)
    headers["svix-timestamp"] = timestamp
    assert provider.verify_signature(body, headers) is False


# ------------------------------------------------------------------ configuration


def test_no_configured_secret_rejects_everything(
    provider: ResendEmailProvider, monkeypatch: pytest.MonkeyPatch
) -> None:
    """Fail closed. An unset secret must not degrade to accepting unsigned webhooks —
    that is precisely how an endpoint ends up open in production."""
    monkeypatch.setenv("EMAIL_WEBHOOK_SECRET", "")
    get_settings.cache_clear()
    body = body_bytes()
    assert provider.verify_signature(body, sign(body)) is False


def test_rotation_accepts_either_signature_in_the_header(
    provider: ResendEmailProvider,
) -> None:
    """Svix sends every active signature space-separated during a rotation, so a valid one
    alongside a stale one must still pass — otherwise rotating the secret drops receipts."""
    body = body_bytes()
    headers = sign(body)
    stale = "v1," + base64.b64encode(b"1" * 32).decode()
    headers["svix-signature"] = f"{stale} {headers['svix-signature']}"
    assert provider.verify_signature(body, headers) is True


def test_the_webhook_secret_is_not_the_send_api_key() -> None:
    """Separate settings, so rotating one does not silently break the other — and a leaked
    send key does not also let the leaker forge delivery receipts."""
    fields = Settings.model_fields
    assert "email_webhook_secret" in fields
    assert "email_provider_api_key" in fields
