"""Restate Cloud bearer-token plumbing.

Restate Cloud authenticates every ingress + admin call with a bearer token;
self-hosted Restate listens unauthenticated. The backend MUST attach the
header on every Restate-bound request when the token is configured, and MUST
NOT send anything Authorization-shaped when it isn't (Cloud rejects callers
with stale tokens — being noisy about it would mask real bugs).

Coverage:

1. ``restate_auth_headers`` helper — both states.
2. ``restate_proxy`` — service mode + object mode each attach the header.
3. Discovery probe in ``app.main`` — attaches the header on the GET.
4. Cron registration — attaches the header on both its GET and POST.
5. WhatsApp inbound dispatch — attaches the header on its POST.

Every call site is exercised because each one was added by hand in a separate
hunk; a missed merge would silently keep an unauthenticated request alive.
"""

from __future__ import annotations

import importlib
from collections.abc import Iterator
from typing import Any
from unittest.mock import MagicMock

import httpx
import pytest

from app import cron_registration
from app.config import Settings
from app.dependencies import restate_auth_headers, restate_proxy

# ── helper unit tests ───────────────────────────────────────────────────────


class _SettingsStub:
    """Minimal stand-in for app.config.Settings.

    We don't want to instantiate the real ``Settings`` here because its
    ``model_config`` reads ``.env`` and validates required fields. We only
    need the two attrs ``restate_auth_headers`` touches.
    """

    def __init__(self, token: str = "") -> None:
        self.RESTATE_CLOUD_AUTH_TOKEN = token
        self.RESTATE_INGRESS_URL = "https://test.restate.cloud"


def _real_settings(token: str = "") -> Settings:
    """Construct a real ``Settings`` for tests that drive the helper directly.

    ty enforces the function's declared ``Settings | None`` signature, so the
    light-weight ``_SettingsStub`` only works at call sites where the type is
    already suppressed for unrelated reasons (e.g. mocking client transports).
    """
    return Settings(
        NEON_DATABASE_URL="postgresql://x:y@localhost:5432/z",
        REDIS_URL="redis://localhost:6379/0",
        RESTATE_CLOUD_AUTH_TOKEN=token,
    )


def test_restate_auth_headers_returns_empty_when_token_unset() -> None:
    assert restate_auth_headers(_real_settings(token="")) == {}


def test_restate_auth_headers_returns_bearer_when_token_set() -> None:
    assert restate_auth_headers(_real_settings(token="tok_xyz")) == {
        "Authorization": "Bearer tok_xyz"
    }


def test_restate_auth_headers_real_settings_default_is_empty() -> None:
    """Default Settings construction leaves the token empty → no header."""
    assert restate_auth_headers(_real_settings()) == {}


# ── restate_proxy: service-mode + object-mode both attach the header ────────


class _StubResponse:
    def __init__(self) -> None:
        self.status_code = 200
        self.text = "{}"

    def json(self) -> dict[str, Any]:
        return {"ok": True}


def _make_client(captured: list[dict[str, Any]]) -> httpx.AsyncClient:
    client = MagicMock(spec=httpx.AsyncClient)

    async def fake_post(
        url: str,
        *,
        json: dict[str, Any],
        headers: dict[str, str],
        timeout: float,
    ) -> _StubResponse:
        captured.append({"url": url, "json": json, "headers": headers, "timeout": timeout})
        return _StubResponse()

    client.post = fake_post
    return client


@pytest.mark.asyncio
async def test_restate_proxy_attaches_bearer_in_service_mode() -> None:
    settings = _SettingsStub(token="tok_service")
    captured: list[dict[str, Any]] = []
    client = _make_client(captured)

    await restate_proxy(
        client,
        "ReminderService",
        "daily_reminder_sweep",
        {"foo": "bar"},
        settings=settings,  # type: ignore[arg-type]
    )

    assert len(captured) == 1
    sent = captured[0]
    assert sent["url"] == "https://test.restate.cloud/ReminderService/daily_reminder_sweep"
    assert sent["headers"]["Authorization"] == "Bearer tok_service"
    assert sent["headers"]["Content-Type"] == "application/json"


@pytest.mark.asyncio
async def test_restate_proxy_attaches_bearer_in_object_mode() -> None:
    settings = _SettingsStub(token="tok_object")
    captured: list[dict[str, Any]] = []
    client = _make_client(captured)

    await restate_proxy(
        client,
        "ReservationObject",
        "res_42",
        "approve",
        {"actor": "owner"},
        mode="object",
        settings=settings,  # type: ignore[arg-type]
    )

    assert len(captured) == 1
    sent = captured[0]
    assert sent["url"] == "https://test.restate.cloud/ReservationObject/res_42/approve"
    assert sent["headers"]["Authorization"] == "Bearer tok_object"


@pytest.mark.asyncio
async def test_restate_proxy_omits_authorization_when_token_unset() -> None:
    settings = _SettingsStub(token="")
    captured: list[dict[str, Any]] = []
    client = _make_client(captured)

    await restate_proxy(
        client,
        "ReminderService",
        "daily_reminder_sweep",
        {},
        settings=settings,  # type: ignore[arg-type]
    )

    assert "Authorization" not in captured[0]["headers"]


# ── discovery probe: app.main._discover_restate_service_names ──────────────


@pytest.fixture
def _captured_get() -> Iterator[list[dict[str, Any]]]:
    """Patch ``app.main._RESTATE_AUTH_HEADERS`` to a known value and capture
    every ``client.get`` invocation the discovery probe makes."""
    main_module = importlib.import_module("app.main")
    saved = dict(main_module._RESTATE_AUTH_HEADERS)
    main_module._RESTATE_AUTH_HEADERS.clear()
    main_module._RESTATE_AUTH_HEADERS.update({"Authorization": "Bearer tok_discover"})
    yield []  # capture list populated per-test via closure below
    main_module._RESTATE_AUTH_HEADERS.clear()
    main_module._RESTATE_AUTH_HEADERS.update(saved)


@pytest.mark.asyncio
async def test_discover_attaches_bearer_header(_captured_get: list[dict[str, Any]]) -> None:
    from app import main as main_module

    captured: list[dict[str, Any]] = []

    class _FakeResp:
        def json(self) -> dict[str, Any]:
            return {"services": []}

        def raise_for_status(self) -> None:
            return None

    async def fake_get(url: str, *, headers: dict[str, str], timeout: float) -> _FakeResp:
        captured.append({"url": url, "headers": headers, "timeout": timeout})
        return _FakeResp()

    client = MagicMock(spec=httpx.AsyncClient)
    client.get = fake_get

    await main_module._discover_restate_service_names(client, max_attempts=1)

    assert captured, "discovery probe issued no GET requests"
    for call in captured:
        assert call["headers"]["Authorization"] == "Bearer tok_discover"


# ── cron registration ──────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_register_cron_jobs_attaches_bearer_to_get_and_post(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    """register_cron_jobs builds a fresh ``Settings`` via ``get_settings``;
    monkeypatch that to inject the token without touching real env."""

    class _Get:
        def __init__(self, url: str, headers: dict[str, str], timeout: float) -> None:
            self.url = url
            self.headers = dict(headers)
            self.timeout = timeout

        @property
        def is_success(self) -> bool:
            return True

        def json(self) -> list[str]:
            return [job["key"] for job in cron_registration.CRON_JOBS]

    class _Post:
        def __init__(
            self,
            url: str,
            json: dict[str, Any],
            headers: dict[str, str],
            timeout: float,
        ) -> None:
            self.url = url
            self.payload = json
            self.headers = dict(headers)
            self.timeout = timeout
            self.status_code = 200

        @property
        def is_success(self) -> bool:
            return True

    gets: list[_Get] = []
    posts: list[_Post] = []

    async def fake_get(url: str, *, headers: dict[str, str], timeout: float) -> _Get:
        g = _Get(url, headers, timeout)
        gets.append(g)
        return g

    async def fake_post(
        url: str, *, json: dict[str, Any], headers: dict[str, str], timeout: float
    ) -> _Post:
        p = _Post(url, json, headers, timeout)
        posts.append(p)
        return p

    client = MagicMock(spec=httpx.AsyncClient)
    client.get = fake_get
    client.post = fake_post

    monkeypatch.setattr(
        cron_registration,
        "get_settings",
        lambda: _SettingsStub(token="tok_cron"),
        raising=False,
    )

    # Patch the late-bound import too: cron_registration imports get_settings
    # inside its function body via `from app.config import get_settings`. Cover
    # both forms.
    from app import config as config_module

    monkeypatch.setattr(config_module, "get_settings", lambda: _SettingsStub(token="tok_cron"))

    await cron_registration.register_cron_jobs(client)

    assert gets, "allowed_keys GET was not issued"
    for g in gets:
        assert g.headers["Authorization"] == "Bearer tok_cron"

    assert posts, "no cron POST was issued"
    for p in posts:
        assert p.headers["Authorization"] == "Bearer tok_cron"


# ── whatsapp router: process_inbound posts with auth header ────────────────


@pytest.mark.asyncio
async def test_whatsapp_inbound_post_attaches_bearer(monkeypatch: pytest.MonkeyPatch) -> None:
    """The fire-and-forget dispatch from /webhooks/whatsapp to the Restate
    WhatsAppInboundHandler MUST carry the bearer when configured."""
    from app.routers import whatsapp as whatsapp_module

    captured: list[dict[str, Any]] = []

    class _Resp:
        def raise_for_status(self) -> None:
            return None

    async def fake_post(url: str, *, json: dict[str, Any], headers: dict[str, str]) -> _Resp:
        captured.append({"url": url, "json": json, "headers": headers})
        return _Resp()

    client = MagicMock(spec=httpx.AsyncClient)
    client.post = fake_post

    request = MagicMock()
    request.app.state.http_client = client

    monkeypatch.setattr(
        whatsapp_module,
        "get_settings",
        lambda: _SettingsStub(token="tok_whatsapp"),
    )

    account = MagicMock()
    account.id = "acct_1"
    account.restaurant_id = "rest_1"

    await whatsapp_module._dispatch_inbound_message(
        request,
        account=account,
        phone_number_id="123",
        message={"id": "wamid.HBgM", "from": "456"},
        sender={"phone_number": "456"},
        metadata={"display_phone_number": "+1"},
    )

    assert len(captured) == 1
    sent = captured[0]
    assert "/WhatsAppInboundHandler/wamid.HBgM/process/send" in sent["url"]
    assert sent["headers"]["Authorization"] == "Bearer tok_whatsapp"
