"""Behavioural tests for :class:`MockSessionVerifier` + :class:`MockWebhookVerifier`.

The mocks are real auth verifiers in test contexts; these tests pin
the contract that downstream Phase-3 service code relies on. The
webhook mock has two-mode coverage:

  * Permissive (no secret) — mirror-handler-shape tests use this.
  * Signature-mode — exercises the verification code paths so
    Phase-3 wiring against the real :class:`ClerkWebhookVerifier`
    can lean on identical assertions.
"""

from __future__ import annotations

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

import pytest
from tests.conftest import Principal

from mgf.auth import (
    InvalidSessionError,
    InvalidWebhookSignatureError,
    MockSessionVerifier,
    MockWebhookVerifier,
)


def _faraj_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=8),
    )


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


class TestVerifySession:
    @pytest.mark.asyncio
    async def test_pre_seeded_token_resolves(self) -> None:
        verifier = MockSessionVerifier()
        principal = _faraj_principal()
        verifier.add_session("faraj-token", principal)

        result = await verifier.verify_session("faraj-token")

        assert result is principal

    @pytest.mark.asyncio
    async def test_unknown_token_raises(self) -> None:
        verifier = MockSessionVerifier()
        with pytest.raises(InvalidSessionError):
            await verifier.verify_session("never-seeded")

    @pytest.mark.asyncio
    async def test_empty_token_raises(self) -> None:
        verifier = MockSessionVerifier()
        with pytest.raises(InvalidSessionError):
            await verifier.verify_session("")

    @pytest.mark.asyncio
    async def test_revoked_token_raises(self) -> None:
        verifier = MockSessionVerifier()
        verifier.add_session("token", _faraj_principal())
        verifier.remove_session("token")
        with pytest.raises(InvalidSessionError):
            await verifier.verify_session("token")

    def test_add_session_rejects_empty_token(self) -> None:
        verifier = MockSessionVerifier()
        with pytest.raises(ValueError, match="non-empty"):
            verifier.add_session("", _faraj_principal())

    def test_remove_session_idempotent(self) -> None:
        verifier = MockSessionVerifier()
        # Removing a never-added token must not raise — eases test
        # teardown ergonomics.
        verifier.remove_session("never-seeded")


# ----------------------------------------------------------------------
# verify_webhook — permissive mode
# ----------------------------------------------------------------------


class TestVerifyWebhookPermissive:
    @pytest.mark.asyncio
    async def test_well_formed_payload_yields_event(self) -> None:
        verifier = MockWebhookVerifier()
        body = json.dumps({"type": "user.created", "data": {"id": "user_abc"}}).encode()
        headers = {"svix-id": "msg_test_1"}

        event = await verifier.verify_webhook(headers, body)

        assert event.event_type == "user.created"
        assert event.event_id == "msg_test_1"
        assert event.payload["data"] == {"id": "user_abc"}
        assert event.received_at.tzinfo is UTC

    @pytest.mark.asyncio
    async def test_event_id_falls_back_to_payload_id(self) -> None:
        verifier = MockWebhookVerifier()
        body = json.dumps({"type": "user.created", "id": "evt_payload_id"}).encode()
        event = await verifier.verify_webhook({}, body)
        assert event.event_id == "evt_payload_id"

    @pytest.mark.asyncio
    async def test_invalid_json_raises(self) -> None:
        verifier = MockWebhookVerifier()
        with pytest.raises(InvalidWebhookSignatureError, match="not valid JSON"):
            await verifier.verify_webhook({}, b"{not json")

    @pytest.mark.asyncio
    async def test_non_object_payload_raises(self) -> None:
        verifier = MockWebhookVerifier()
        with pytest.raises(InvalidWebhookSignatureError, match="JSON object"):
            await verifier.verify_webhook({}, b"[1, 2, 3]")

    @pytest.mark.asyncio
    async def test_missing_type_raises(self) -> None:
        verifier = MockWebhookVerifier()
        with pytest.raises(InvalidWebhookSignatureError, match="'type'"):
            await verifier.verify_webhook({}, b'{"data": {}}')


# ----------------------------------------------------------------------
# verify_webhook — signature mode
# ----------------------------------------------------------------------


class TestVerifyWebhookSignatureMode:
    @pytest.mark.asyncio
    async def test_round_trip_with_helper_signs_and_verifies(self) -> None:
        secret = b"whsec_round_trip"
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret(secret)

        body = json.dumps({"type": "user.created"}).encode()
        headers = MockWebhookVerifier.sign(secret=secret, body=body, event_id="msg_1")

        event = await verifier.verify_webhook(headers, body)
        assert event.event_type == "user.created"
        assert event.event_id == "msg_1"

    @pytest.mark.asyncio
    async def test_wrong_secret_rejected(self) -> None:
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret(b"whsec_real")

        body = b'{"type": "user.created"}'
        headers = MockWebhookVerifier.sign(secret=b"whsec_attacker", body=body, event_id="msg_2")

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

    @pytest.mark.asyncio
    async def test_missing_signature_header_rejected(self) -> None:
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret(b"whsec_x")
        with pytest.raises(InvalidWebhookSignatureError, match="signature header"):
            await verifier.verify_webhook(
                {"svix-id": "1", "svix-timestamp": str(int(time.time()))}, b"{}"
            )

    @pytest.mark.asyncio
    async def test_stale_timestamp_rejected(self) -> None:
        secret = b"whsec_stale"
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret(secret)
        body = b'{"type": "user.created"}'
        # Outside the 5-minute Svix replay window.
        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_string_secret_accepted(self) -> None:
        # Tests typically pass the secret as a string; the verifier
        # must accept both forms transparently.
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret("whsec_string_form")
        body = b'{"type": "user.created"}'
        headers = MockWebhookVerifier.sign(
            secret=b"whsec_string_form", body=body, event_id="msg_str"
        )
        event = await verifier.verify_webhook(headers, body)
        assert event.event_type == "user.created"

    @pytest.mark.asyncio
    async def test_disable_signature_check_returns_to_permissive_mode(self) -> None:
        verifier = MockWebhookVerifier()
        verifier.configure_webhook_secret(b"whsec_x")
        verifier.configure_webhook_secret(None)

        # No signature headers; permissive mode accepts the body.
        body = b'{"type": "user.created"}'
        event = await verifier.verify_webhook({}, body)
        assert event.event_type == "user.created"
