"""Tests for drift alert delivery."""

from __future__ import annotations

import hashlib
import hmac
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from backend.drift.alerter import (
    _send_email_alert,
    _send_webhook_alert,
    send_drift_alert,
)
from backend.drift.models import (
    DriftAlertPayload,
    DriftDetectionResult,
    DriftFinding,
)


def _make_result(**overrides: object) -> DriftDetectionResult:
    defaults = {
        "project_id": "proj-1",
        "project_label": "prod",
        "cloud": "gcp",
        "previous_score": 85.0,
        "new_score": 78.0,
        "score_delta": -7.0,
        "new_failures": 3,
        "resolved_failures": 0,
        "top_findings": [
            DriftFinding(
                control_id="CKV_GCP_1",
                framework="cis_gcp",
                resource_address="google_compute_instance.web",
                severity="high",
            )
        ],
        "findings_uri": "gs://bucket/findings.json",
        "alert_required": True,
    }
    defaults.update(overrides)
    return DriftDetectionResult(**defaults)  # type: ignore[arg-type]


def _make_payload() -> DriftAlertPayload:
    return DriftAlertPayload(
        project_label="prod",
        cloud="gcp",
        old_score=85.0,
        new_score=78.0,
        score_delta=-7.0,
        top_findings=[],
        dashboard_url="https://dashboard.complyform.dev/projects/proj-1",
        timestamp="2026-01-01T00:00:00Z",
    )


class TestEmailAlert:
    """Postmark email alert tests."""

    @pytest.mark.asyncio
    async def test_send_email_via_postmark(self) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 200

        with (
            patch(
                "backend.drift.alerter._get_postmark_token",
                new_callable=AsyncMock,
                return_value="test-token",
            ),
            patch("httpx.AsyncClient") as mock_client_cls,
        ):
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_resp
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            result = await _send_email_alert(
                result=_make_result(), to_email="test@example.com"
            )
            assert result is True
            mock_client.post.assert_called_once()

    @pytest.mark.asyncio
    async def test_email_failure_returns_false(self) -> None:
        with patch(
            "backend.drift.alerter._get_postmark_token",
            new_callable=AsyncMock,
            side_effect=RuntimeError("Secret Manager down"),
        ):
            result = await _send_email_alert(
                result=_make_result(), to_email="test@example.com"
            )
            assert result is False


class TestWebhookAlert:
    """Outgoing webhook alert tests."""

    @pytest.mark.asyncio
    async def test_webhook_hmac_signature_correct(self) -> None:
        secret = b"test-secret-32-bytes"

        mock_resp = MagicMock()
        mock_resp.status_code = 200

        captured_headers: dict[str, str] = {}

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()

            async def capture_post(url: str, **kwargs: object) -> MagicMock:
                captured_headers.update(kwargs.get("headers", {}))  # type: ignore[arg-type]
                return mock_resp

            mock_client.post = capture_post
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            payload = _make_payload()
            result = await _send_webhook_alert(
                payload=payload,
                webhook_url="https://example.com/webhook",
                webhook_secret=secret,
            )
            assert result is True

            # Verify HMAC
            ts = captured_headers["X-ComplyForm-Timestamp"]
            sig = captured_headers["X-ComplyForm-Signature"]
            body_json = payload.model_dump_json()
            expected = hmac.new(
                secret,
                f"{ts}.{body_json}".encode(),
                hashlib.sha256,
            ).hexdigest()
            assert sig == expected

    @pytest.mark.asyncio
    async def test_webhook_retry_on_5xx(self) -> None:
        mock_resp_500 = MagicMock()
        mock_resp_500.status_code = 500
        mock_resp_200 = MagicMock()
        mock_resp_200.status_code = 200

        call_count = 0

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()

            async def retry_post(url: str, **kwargs: object) -> MagicMock:
                nonlocal call_count
                call_count += 1
                if call_count == 1:
                    return mock_resp_500
                return mock_resp_200

            mock_client.post = retry_post
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            with patch("backend.drift.alerter.asyncio.sleep", new_callable=AsyncMock):
                result = await _send_webhook_alert(
                    payload=_make_payload(),
                    webhook_url="https://example.com/webhook",
                    webhook_secret=b"secret",
                )
                assert result is True
                assert call_count == 2

    @pytest.mark.asyncio
    async def test_webhook_all_retries_exhausted_returns_false(self) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 500

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()
            mock_client.post.return_value = mock_resp
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            with patch("backend.drift.alerter.asyncio.sleep", new_callable=AsyncMock):
                result = await _send_webhook_alert(
                    payload=_make_payload(),
                    webhook_url="https://example.com/webhook",
                    webhook_secret=b"secret",
                )
                assert result is False

    @pytest.mark.asyncio
    async def test_webhook_headers_correct(self) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        captured: dict[str, str] = {}

        with patch("httpx.AsyncClient") as mock_client_cls:
            mock_client = AsyncMock()

            async def capture(url: str, **kwargs: object) -> MagicMock:
                captured.update(kwargs.get("headers", {}))  # type: ignore[arg-type]
                return mock_resp

            mock_client.post = capture
            mock_client.__aenter__ = AsyncMock(return_value=mock_client)
            mock_client.__aexit__ = AsyncMock(return_value=None)
            mock_client_cls.return_value = mock_client

            await _send_webhook_alert(
                payload=_make_payload(),
                webhook_url="https://example.com/webhook",
                webhook_secret=b"secret",
            )

            assert "X-ComplyForm-Signature" in captured
            assert "X-ComplyForm-Timestamp" in captured
            assert captured["Content-Type"] == "application/json"
            assert captured["User-Agent"] == "ComplyForm-Drift/1.0"


class TestSendDriftAlert:
    """Integration tests for send_drift_alert."""

    @pytest.mark.asyncio
    async def test_email_failure_does_not_block_webhook(self) -> None:
        mock_resp = MagicMock()
        mock_resp.status_code = 200

        with (
            patch(
                "backend.drift.alerter._send_email_alert",
                new_callable=AsyncMock,
                return_value=False,
            ) as mock_email,
            patch(
                "backend.drift.alerter._send_webhook_alert",
                new_callable=AsyncMock,
                return_value=True,
            ) as mock_webhook,
        ):
            await send_drift_alert(
                result=_make_result(),
                alert_config={"webhook_url": "https://example.com/wh"},
                customer_email="test@example.com",
                webhook_secret=b"secret",
            )
            mock_email.assert_called_once()
            mock_webhook.assert_called_once()

    @pytest.mark.asyncio
    async def test_webhook_failure_does_not_block_email(self) -> None:
        with (
            patch(
                "backend.drift.alerter._send_email_alert",
                new_callable=AsyncMock,
                return_value=True,
            ) as mock_email,
            patch(
                "backend.drift.alerter._send_webhook_alert",
                new_callable=AsyncMock,
                return_value=False,
            ) as mock_webhook,
        ):
            await send_drift_alert(
                result=_make_result(),
                alert_config={"webhook_url": "https://example.com/wh"},
                customer_email="test@example.com",
                webhook_secret=b"secret",
            )
            mock_email.assert_called_once()
            mock_webhook.assert_called_once()

    @pytest.mark.asyncio
    async def test_no_webhook_configured_email_only(self) -> None:
        with (
            patch(
                "backend.drift.alerter._send_email_alert",
                new_callable=AsyncMock,
                return_value=True,
            ) as mock_email,
            patch(
                "backend.drift.alerter._send_webhook_alert",
                new_callable=AsyncMock,
            ) as mock_webhook,
        ):
            await send_drift_alert(
                result=_make_result(),
                alert_config={},
                customer_email="test@example.com",
                webhook_secret=None,
            )
            mock_email.assert_called_once()
            mock_webhook.assert_not_called()
