"""Tests for Phase 21b — backup_verify, license_audit, cost_review, dr_test."""

from __future__ import annotations

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

import pytest

_PATCH_ALERT = "backend.ops._shared.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, docs: list[MagicMock]) -> None:
        self._docs = docs

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

    def stream(self) -> _FakeAsyncIter:
        return _FakeAsyncIter(self._docs)


def _make_doc(doc_id: str, data: dict[str, Any], exists: bool = True) -> MagicMock:
    doc = MagicMock()
    doc.id = doc_id
    doc.exists = exists
    doc.to_dict.return_value = data if exists else None
    return doc


# ---------------------------------------------------------------------------
# Backup Verify
# ---------------------------------------------------------------------------


class TestBackupVerify:
    @pytest.mark.asyncio
    async def test_backup_verify_success(self) -> None:
        """Recent export, correct size → pass."""
        blob1 = MagicMock()
        blob1.name = "firestore-exports/2025-03-01/export.bak"
        blob1.size = 1_000_000
        blob1.time_created = "2025-03-01T00:00:00Z"

        blob2 = MagicMock()
        blob2.name = "firestore-exports/2025-03-08/export.bak"
        blob2.size = 1_100_000
        blob2.time_created = "2025-03-08T00:00:00Z"

        mock_bucket = MagicMock()
        mock_bucket.list_blobs.return_value = [blob1, blob2]

        mock_storage_client = MagicMock()
        mock_storage_client.bucket.return_value = mock_bucket

        mock_storage_mod = MagicMock()
        mock_storage_mod.Client.return_value = mock_storage_client

        db = MagicMock()
        set_mock = AsyncMock()
        doc_ref = MagicMock()
        doc_ref.set = set_mock
        coll = MagicMock()
        coll.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=coll)

        with (
            patch("backend.ops.backup_verify.get_db", return_value=db),
            patch(
                "backend.ops.backup_verify.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
            patch.dict(
                "sys.modules",
                {"google.cloud.storage": mock_storage_mod},
            ),
        ):
            from backend.ops.backup_verify import backup_verify

            result = await backup_verify.__wrapped__(None)

        assert result["status"] == "pass"
        assert result["size"] == 1_100_000
        # Pass email sent
        mock_alert.assert_called_once()
        assert "PASSED" in mock_alert.call_args[1]["subject"]

    @pytest.mark.asyncio
    async def test_backup_verify_missing(self) -> None:
        """No export → fail email."""
        mock_bucket = MagicMock()
        mock_bucket.list_blobs.return_value = []

        mock_storage_client = MagicMock()
        mock_storage_client.bucket.return_value = mock_bucket

        mock_storage_mod = MagicMock()
        mock_storage_mod.Client.return_value = mock_storage_client

        db = MagicMock()

        with (
            patch("backend.ops.backup_verify.get_db", return_value=db),
            patch(
                "backend.ops.backup_verify.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
            patch.dict(
                "sys.modules",
                {"google.cloud.storage": mock_storage_mod},
            ),
        ):
            from backend.ops.backup_verify import backup_verify

            result = await backup_verify.__wrapped__(None)

        assert result["status"] == "fail"
        mock_alert.assert_called_once()
        assert "FAILED" in mock_alert.call_args[1]["subject"]

    @pytest.mark.asyncio
    async def test_backup_verify_size_anomaly(self) -> None:
        """90% smaller → warn."""
        blob1 = MagicMock()
        blob1.name = "firestore-exports/2025-03-01/export.bak"
        blob1.size = 10_000_000
        blob1.time_created = "2025-03-01T00:00:00Z"

        blob2 = MagicMock()
        blob2.name = "firestore-exports/2025-03-08/export.bak"
        blob2.size = 500_000  # 95% smaller
        blob2.time_created = "2025-03-08T00:00:00Z"

        mock_bucket = MagicMock()
        mock_bucket.list_blobs.return_value = [blob1, blob2]

        mock_storage_client = MagicMock()
        mock_storage_client.bucket.return_value = mock_bucket

        mock_storage_mod = MagicMock()
        mock_storage_mod.Client.return_value = mock_storage_client

        db = MagicMock()
        set_mock = AsyncMock()
        doc_ref = MagicMock()
        doc_ref.set = set_mock
        coll = MagicMock()
        coll.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=coll)

        with (
            patch("backend.ops.backup_verify.get_db", return_value=db),
            patch(
                "backend.ops.backup_verify.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
            patch.dict(
                "sys.modules",
                {"google.cloud.storage": mock_storage_mod},
            ),
        ):
            from backend.ops.backup_verify import backup_verify

            result = await backup_verify.__wrapped__(None)

        assert result["status"] == "warn"
        # Warning email sent
        mock_alert.assert_called_once()
        assert "WARNING" in mock_alert.call_args[1]["subject"]


# ---------------------------------------------------------------------------
# License Audit
# ---------------------------------------------------------------------------


class TestLicenseAudit:
    @pytest.mark.asyncio
    async def test_license_audit_mismatch(self) -> None:
        """Firestore vs Paddle mismatch detected."""
        # Firestore: cus_1, cus_2 active
        # Paddle: cus_2, cus_3 active
        firestore_docs = [
            _make_doc("cus_1", {"status": "active"}),
            _make_doc("cus_2", {"status": "active"}),
        ]

        paddle_subs = [
            {"custom_data": {"customer_id": "cus_2"}},
            {"custom_data": {"customer_id": "cus_3"}},
        ]

        db = MagicMock()

        def collection_fn(name: str) -> Any:
            coll = MagicMock()
            if name == "customers":
                coll.where = MagicMock(return_value=_FakeQuery(firestore_docs))

                # consent subcollection
                def coll_document(doc_id: str) -> Any:
                    doc_ref = MagicMock()
                    consent_coll = MagicMock()
                    consent_doc = _make_doc("latest", {"tos_accepted_at": "2025-01-01"})
                    consent_doc_ref = MagicMock()
                    consent_doc_ref.get = AsyncMock(return_value=consent_doc)
                    consent_coll.document = MagicMock(return_value=consent_doc_ref)
                    doc_ref.collection = MagicMock(return_value=consent_coll)
                    return doc_ref

                coll.document = MagicMock(side_effect=coll_document)
            return coll

        db.collection = MagicMock(side_effect=collection_fn)

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {
            "data": paddle_subs,
            "meta": {"pagination": {"next": ""}},
        }
        resp.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("backend.ops.license_audit.get_db", return_value=db),
            patch(
                "backend.ops.license_audit._get_paddle_api_key",
                new_callable=AsyncMock,
                return_value="test-key",
            ),
            patch(
                "backend.ops.license_audit.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(
                "backend.ops.license_audit.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
        ):
            from backend.ops.license_audit import license_audit

            result = await license_audit.__wrapped__(None)

        assert result["in_firestore_not_paddle"] == 1  # cus_1
        assert result["in_paddle_not_firestore"] == 1  # cus_3
        mock_alert.assert_called_once()
        assert "MISMATCH" in mock_alert.call_args[1]["subject"]

    @pytest.mark.asyncio
    async def test_license_audit_consent_check(self) -> None:
        """Missing consent flagged."""
        firestore_docs = [_make_doc("cus_1", {"status": "active"})]
        paddle_subs = [{"custom_data": {"customer_id": "cus_1"}}]

        db = MagicMock()

        def collection_fn(name: str) -> Any:
            coll = MagicMock()
            if name == "customers":
                coll.where = MagicMock(return_value=_FakeQuery(firestore_docs))

                def coll_document(doc_id: str) -> Any:
                    doc_ref = MagicMock()
                    consent_coll = MagicMock()
                    # Missing consent
                    consent_doc = _make_doc("latest", {}, exists=False)
                    consent_doc_ref = MagicMock()
                    consent_doc_ref.get = AsyncMock(return_value=consent_doc)
                    consent_coll.document = MagicMock(return_value=consent_doc_ref)
                    doc_ref.collection = MagicMock(return_value=consent_coll)
                    return doc_ref

                coll.document = MagicMock(side_effect=coll_document)
            return coll

        db.collection = MagicMock(side_effect=collection_fn)

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = {
            "data": paddle_subs,
            "meta": {"pagination": {"next": ""}},
        }
        resp.raise_for_status = MagicMock()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(return_value=resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch("backend.ops.license_audit.get_db", return_value=db),
            patch(
                "backend.ops.license_audit._get_paddle_api_key",
                new_callable=AsyncMock,
                return_value="test-key",
            ),
            patch(
                "backend.ops.license_audit.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(
                "backend.ops.license_audit.send_alert_email",
                new_callable=AsyncMock,
            ),
        ):
            from backend.ops.license_audit import license_audit

            result = await license_audit.__wrapped__(None)

        assert result["missing_consent"] == 1


# ---------------------------------------------------------------------------
# Cost Review
# ---------------------------------------------------------------------------


class TestCostReview:
    @pytest.mark.asyncio
    async def test_cost_review_alert(self) -> None:
        """Ratio >2% triggers alert."""
        db = MagicMock()
        set_mock = AsyncMock()

        def collection_fn(name: str) -> Any:
            coll = MagicMock()
            doc_ref = MagicMock()
            doc_ref.set = set_mock
            if name == "founder_metrics":
                latest_doc = MagicMock()
                latest_doc.exists = True
                latest_doc.to_dict.return_value = {"mrr": 1000.0}
                doc_ref.get = AsyncMock(return_value=latest_doc)
            coll.document = MagicMock(return_value=doc_ref)
            return coll

        db.collection = MagicMock(side_effect=collection_fn)

        with (
            patch("backend.ops.cost_review.get_db", return_value=db),
            patch(
                "backend.ops.cost_review._fetch_gcp_spend",
                new_callable=AsyncMock,
                return_value=50.0,  # $50 / $1000 = 5%
            ),
            patch(
                "backend.ops.cost_review.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
        ):
            from backend.ops.cost_review import cost_review

            result = await cost_review.__wrapped__(None)

        assert result["ratio"] == 5.0
        assert result["alert"] is True
        mock_alert.assert_called_once()
        assert "ALERT" in mock_alert.call_args[1]["subject"]

    @pytest.mark.asyncio
    async def test_cost_review_normal(self) -> None:
        """Ratio <2% sends summary only."""
        db = MagicMock()
        set_mock = AsyncMock()

        def collection_fn(name: str) -> Any:
            coll = MagicMock()
            doc_ref = MagicMock()
            doc_ref.set = set_mock
            if name == "founder_metrics":
                latest_doc = MagicMock()
                latest_doc.exists = True
                latest_doc.to_dict.return_value = {"mrr": 10000.0}
                doc_ref.get = AsyncMock(return_value=latest_doc)
            coll.document = MagicMock(return_value=doc_ref)
            return coll

        db.collection = MagicMock(side_effect=collection_fn)

        with (
            patch("backend.ops.cost_review.get_db", return_value=db),
            patch(
                "backend.ops.cost_review._fetch_gcp_spend",
                new_callable=AsyncMock,
                return_value=50.0,  # $50 / $10,000 = 0.5%
            ),
            patch(
                "backend.ops.cost_review.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
        ):
            from backend.ops.cost_review import cost_review

            result = await cost_review.__wrapped__(None)

        assert result["ratio"] == 0.5
        assert result["alert"] is False
        mock_alert.assert_called_once()
        assert "OK" in mock_alert.call_args[1]["subject"]


# ---------------------------------------------------------------------------
# DR Test
# ---------------------------------------------------------------------------


class TestDRTest:
    @pytest.mark.asyncio
    async def test_dr_test_success(self) -> None:
        """Restore + count match → pass."""
        db = MagicMock()
        set_mock = AsyncMock()

        verify_doc = _make_doc(
            "backup_verify",
            {
                "latest_blob": "firestore-exports/2025-03-08/export.bak",
                "latest_size": 1_000_000,
            },
        )

        def collection_fn(name: str) -> Any:
            coll = MagicMock()
            doc_ref = MagicMock()
            doc_ref.set = set_mock
            doc_ref.get = AsyncMock(return_value=verify_doc)
            coll.document = MagicMock(return_value=doc_ref)
            return coll

        db.collection = MagicMock(side_effect=collection_fn)

        # Mock admin client
        mock_operation = MagicMock()
        mock_operation.done.return_value = True
        mock_operation.operation = MagicMock()
        mock_operation.operation.name = "ops/test-op"

        mock_admin = MagicMock()
        mock_admin.import_documents.return_value = mock_operation

        mock_admin_mod = MagicMock()
        mock_admin_mod.FirestoreAdminClient.return_value = mock_admin

        # Mock staging Firestore
        fake_staging_doc = MagicMock()
        fake_staging_collection = MagicMock()
        fake_staging_collection.stream.return_value = _FakeAsyncIter(
            [fake_staging_doc] * 10
        )

        mock_staging_db = MagicMock()
        mock_staging_db.collections.return_value = _FakeAsyncIter(
            [fake_staging_collection]
        )

        mock_firestore_mod = MagicMock()
        mock_firestore_mod.AsyncClient.return_value = mock_staging_db

        with (
            patch("backend.ops.dr_test.get_db", return_value=db),
            patch(
                "backend.ops.dr_test.send_alert_email",
                new_callable=AsyncMock,
            ) as mock_alert,
            patch.dict(
                "sys.modules",
                {
                    "google.cloud.firestore_admin_v1": mock_admin_mod,
                    "google.cloud.firestore_v1": mock_firestore_mod,
                },
            ),
        ):
            from backend.ops.dr_test import dr_test

            result = await dr_test.__wrapped__(None)

        assert result["status"] == "pass"
        assert result["doc_count"] == 10
        mock_alert.assert_called_once()
        assert "PASSED" in mock_alert.call_args[1]["subject"]
