"""Tests for Phase 21c — postmark_webhook handler."""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

_PATCH_DB = "backend.ops.postmark_webhook.get_db"
_PATCH_POSTMARK_TOKEN = "backend.ops.postmark_webhook._get_postmark_token"
_PATCH_PAGERDUTY_KEY = "backend.ops.postmark_webhook._get_pagerduty_key"
_PATCH_ALERT = "backend.ops.postmark_webhook.send_alert_email"


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


class _FakeAsyncIter:
    def __init__(self, items: list[Any]) -> None:
        self._items = list(items)
        self._index = 0

    def __aiter__(self) -> _FakeAsyncIter:
        return self

    async def __anext__(self) -> Any:
        if self._index >= len(self._items):
            raise StopAsyncIteration
        item = self._items[self._index]
        self._index += 1
        return item


class _FakeQuery:
    def __init__(self, count: int = 0) -> None:
        self._count = count

    def where(self, *args: Any, **kwargs: Any) -> _FakeQuery:
        return self

    def stream(self) -> _FakeAsyncIter:
        return _FakeAsyncIter([MagicMock() for _ in range(self._count)])


class _FakeCollection:
    def __init__(
        self,
        query: _FakeQuery | None = None,
        counts_by_type: dict[str, int] | None = None,
    ) -> None:
        self._query = query or _FakeQuery()
        self._counts_by_type = counts_by_type or {}
        self._docs: dict[str, MagicMock] = {}

    def where(self, *args: Any, **kwargs: Any) -> _FakeQuery:
        # Check if filtering by record_type
        if args and self._counts_by_type:
            filter_field = args[0] if args else ""
            filter_value = args[2] if len(args) > 2 else ""
            if filter_field == "record_type" and filter_value in self._counts_by_type:
                return _FakeQuery(self._counts_by_type[filter_value])
        return self._query

    def document(self, doc_id: str) -> MagicMock:
        if doc_id not in self._docs:
            ref = MagicMock()
            ref.set = AsyncMock()
            self._docs[doc_id] = ref
        return self._docs[doc_id]


def _make_request(body: dict[str, Any]) -> MagicMock:
    """Create a fake request with JSON body."""
    request = MagicMock()
    request.json = body
    del request.get_json
    del request.body
    return request


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestPostmarkWebhook:
    @pytest.mark.asyncio
    @patch("backend.ops.postmark_webhook.httpx.AsyncClient")
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="pm-token")
    @patch(_PATCH_DB)
    async def test_bounce_recorded(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_alert: AsyncMock,
        mock_httpx_cls: MagicMock,
    ) -> None:
        """Bounce event written to Firestore postmark_events."""
        events_col = _FakeCollection(_FakeQuery(count=1))

        db = MagicMock()
        db.collection = MagicMock(return_value=events_col)
        mock_db.return_value = db

        # Mock httpx for stats call — low bounce rate
        stats_resp = MagicMock()
        stats_resp.status_code = 200
        stats_resp.json.return_value = {"Sent": 1000}
        client = AsyncMock()
        client.get = AsyncMock(return_value=stats_resp)
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        request = _make_request(
            {
                "RecordType": "Bounce",
                "Email": "user@example.com",
                "ID": "bounce-123",
                "BouncedAt": "2026-03-17T10:00:00Z",
            }
        )

        from backend.ops.postmark_webhook import postmark_webhook

        result = await postmark_webhook(request)
        assert result["status"] == "ok"
        assert result["record_type"] == "Bounce"

        # Document written
        doc_ref = events_col.document("bounce-123")
        assert doc_ref.set.called
        data = doc_ref.set.call_args.args[0]
        assert data["type"] == "bounce"
        assert data["email"] == "user@example.com"

    @pytest.mark.asyncio
    @patch("backend.ops.postmark_webhook.httpx.AsyncClient")
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="pm-token")
    @patch(_PATCH_DB)
    async def test_spam_complaint_recorded(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_alert: AsyncMock,
        mock_httpx_cls: MagicMock,
    ) -> None:
        """SpamComplaint event written with correct type."""
        events_col = _FakeCollection(_FakeQuery(count=0))

        db = MagicMock()
        db.collection = MagicMock(return_value=events_col)
        mock_db.return_value = db

        stats_resp = MagicMock()
        stats_resp.status_code = 200
        stats_resp.json.return_value = {"Sent": 10000}
        client = AsyncMock()
        client.get = AsyncMock(return_value=stats_resp)
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        request = _make_request(
            {
                "RecordType": "SpamComplaint",
                "Email": "spammer@example.com",
                "ID": "spam-456",
                "InactiveSince": "2026-03-17T10:00:00Z",
            }
        )

        from backend.ops.postmark_webhook import postmark_webhook

        result = await postmark_webhook(request)
        assert result["status"] == "ok"
        assert result["record_type"] == "SpamComplaint"

        doc_ref = events_col.document("spam-456")
        assert doc_ref.set.called
        data = doc_ref.set.call_args.args[0]
        assert data["type"] == "spamcomplaint"

    @pytest.mark.asyncio
    @patch("backend.ops.postmark_webhook.httpx.AsyncClient")
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="pm-token")
    @patch(_PATCH_DB)
    async def test_bounce_rate_alert(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_alert: AsyncMock,
        mock_httpx_cls: MagicMock,
    ) -> None:
        """>2% bounce rate triggers email alert."""
        # 30 bounces, 0 spam out of 100 = 30% bounce
        events_col = _FakeCollection(
            counts_by_type={"Bounce": 30, "SpamComplaint": 0},
        )

        db = MagicMock()
        db.collection = MagicMock(return_value=events_col)
        mock_db.return_value = db

        stats_resp = MagicMock()
        stats_resp.status_code = 200
        stats_resp.json.return_value = {"Sent": 100}
        client = AsyncMock()
        client.get = AsyncMock(return_value=stats_resp)
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        request = _make_request(
            {
                "RecordType": "Bounce",
                "Email": "user@example.com",
                "ID": "bounce-999",
                "BouncedAt": "2026-03-17T10:00:00Z",
            }
        )

        from backend.ops.postmark_webhook import postmark_webhook

        await postmark_webhook(request)

        # Alert email sent
        mock_alert.assert_called()
        call_args = mock_alert.call_args
        subj = call_args.kwargs.get(
            "subject",
            call_args.args[0] if call_args.args else "",
        )
        assert "Bounce rate" in subj

    @pytest.mark.asyncio
    @patch("backend.ops.postmark_webhook.httpx.AsyncClient")
    @patch(_PATCH_PAGERDUTY_KEY, new_callable=AsyncMock, return_value="pd-key")
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="pm-token")
    @patch(_PATCH_DB)
    async def test_spam_rate_pagerduty(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_alert: AsyncMock,
        mock_pd_key: AsyncMock,
        mock_httpx_cls: MagicMock,
    ) -> None:
        """>0.1% spam rate triggers PagerDuty + email."""
        # 5 spam complaints out of 100 = 5%
        events_col = _FakeCollection(_FakeQuery(count=5))

        db = MagicMock()
        db.collection = MagicMock(return_value=events_col)
        mock_db.return_value = db

        stats_resp = MagicMock()
        stats_resp.status_code = 200
        stats_resp.json.return_value = {"Sent": 100}

        pd_resp = MagicMock()
        pd_resp.status_code = 202

        client = AsyncMock()
        client.get = AsyncMock(return_value=stats_resp)
        client.post = AsyncMock(return_value=pd_resp)
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        request = _make_request(
            {
                "RecordType": "SpamComplaint",
                "Email": "spam@example.com",
                "ID": "spam-critical",
                "InactiveSince": "2026-03-17T10:00:00Z",
            }
        )

        from backend.ops.postmark_webhook import postmark_webhook

        await postmark_webhook(request)

        # Both PagerDuty (via httpx post) and email alert
        mock_alert.assert_called()
        # PagerDuty call made through httpx
        assert client.post.called

    @pytest.mark.asyncio
    @patch("backend.ops.postmark_webhook.httpx.AsyncClient")
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="pm-token")
    @patch(_PATCH_DB)
    async def test_expires_at_set(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_alert: AsyncMock,
        mock_httpx_cls: MagicMock,
    ) -> None:
        """Event document has expires_at 30 days from now."""
        events_col = _FakeCollection(_FakeQuery(count=0))

        db = MagicMock()
        db.collection = MagicMock(return_value=events_col)
        mock_db.return_value = db

        stats_resp = MagicMock()
        stats_resp.status_code = 200
        stats_resp.json.return_value = {"Sent": 10000}
        client = AsyncMock()
        client.get = AsyncMock(return_value=stats_resp)
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        request = _make_request(
            {
                "RecordType": "Bounce",
                "Email": "user@example.com",
                "ID": "bounce-ttl",
                "BouncedAt": "2026-03-17T10:00:00Z",
            }
        )

        from backend.ops.postmark_webhook import postmark_webhook

        await postmark_webhook(request)

        doc_ref = events_col.document("bounce-ttl")
        data = doc_ref.set.call_args.args[0]
        expires_at = datetime.fromisoformat(data["expires_at"])
        now = datetime.now(UTC)
        # Should be approximately 30 days from now
        diff = (expires_at - now).days
        assert 29 <= diff <= 30
