"""Tests for Phase 21a — overage_enforcement Cloud Function."""

from __future__ import annotations

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

import pytest

_PATCH_DB = "backend.ops.overage_enforcement.get_db"


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


def _make_doc(doc_id: str, data: dict[str, Any]) -> MagicMock:
    doc = MagicMock()
    doc.id = doc_id
    doc.to_dict.return_value = data
    doc.reference = MagicMock()
    doc.reference.delete = AsyncMock()
    doc.reference.update = AsyncMock()
    return doc


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)


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


def _build_enforcement_db(
    customers: list[MagicMock],
    projects_per_customer: dict[str, list[MagicMock]] | None = None,
) -> MagicMock:
    db = MagicMock()
    projects_map = projects_per_customer or {}
    # Cache doc refs so tests can inspect them
    doc_refs: dict[str, MagicMock] = {}
    db._doc_refs = doc_refs

    def collection_fn(name: str) -> Any:
        if name == "customers":
            mock_coll = MagicMock()

            def coll_where(field: str, op: str, value: Any) -> Any:
                return _FakeQuery(customers)

            mock_coll.where = coll_where

            def coll_document(doc_id: str) -> Any:
                if doc_id not in doc_refs:
                    doc_ref = MagicMock()
                    doc_ref.update = AsyncMock()

                    def sub_collection(sub_name: str) -> Any:
                        if sub_name == "projects":
                            projs = projects_map.get(doc_id, [])
                            return _FakeQuery(projs)
                        return _FakeQuery([])

                    doc_ref.collection = MagicMock(side_effect=sub_collection)
                    doc_refs[doc_id] = doc_ref
                return doc_refs[doc_id]

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

    db.collection = MagicMock(side_effect=collection_fn)
    return db


def _make_project(proj_id: str, last_scan: str, status: str = "active") -> MagicMock:
    doc = _make_doc(proj_id, {"last_scan": last_scan, "status": status})
    return doc


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


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_suspend_overage_projects(mock_db: MagicMock) -> None:
    """8 projects, limit 5 → suspends 3 least-recently-scanned."""
    overdue = (datetime.now(UTC) - timedelta(days=45)).isoformat()
    customer = _make_doc(
        "cus_over",
        {
            "status": "active",
            "tier": "agency",
            "overage_past_due_since": overdue,
        },
    )

    projects = [
        _make_project(f"proj_{i}", f"2025-01-{i + 1:02d}T00:00:00Z") for i in range(8)
    ]
    db = _build_enforcement_db([customer], {"cus_over": projects})
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    result = await overage_enforcement.__wrapped__(None)
    assert result["projects_suspended"] == 3
    assert result["customers_enforced"] == 1


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_skip_within_30_days(mock_db: MagicMock) -> None:
    """overage_past_due_since < 30 days ago → no action (filtered by query)."""
    db = _build_enforcement_db([])
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    result = await overage_enforcement.__wrapped__(None)
    assert result["projects_suspended"] == 0


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_skip_cancelled(mock_db: MagicMock) -> None:
    overdue = (datetime.now(UTC) - timedelta(days=45)).isoformat()
    customer = _make_doc(
        "cus_cancel",
        {
            "status": "cancelled",
            "tier": "agency",
            "overage_past_due_since": overdue,
        },
    )
    db = _build_enforcement_db([customer])
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    result = await overage_enforcement.__wrapped__(None)
    assert result["customers_enforced"] == 0


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_enterprise_unlimited_exempt(mock_db: MagicMock) -> None:
    overdue = (datetime.now(UTC) - timedelta(days=45)).isoformat()
    customer = _make_doc(
        "cus_eu",
        {
            "status": "active",
            "tier": "enterprise_unlimited",
            "overage_past_due_since": overdue,
        },
    )
    db = _build_enforcement_db([customer])
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    result = await overage_enforcement.__wrapped__(None)
    assert result["customers_enforced"] == 0


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_base_projects_untouched(mock_db: MagicMock) -> None:
    """First 5 (most recently scanned) projects are never suspended."""
    overdue = (datetime.now(UTC) - timedelta(days=45)).isoformat()
    customer = _make_doc(
        "cus_base",
        {
            "status": "active",
            "tier": "agency",
            "overage_past_due_since": overdue,
        },
    )

    projects = [
        _make_project(f"proj_{i}", f"2025-06-{i + 1:02d}T00:00:00Z") for i in range(8)
    ]
    db = _build_enforcement_db([customer], {"cus_base": projects})
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    result = await overage_enforcement.__wrapped__(None)
    # 3 suspended, 5 untouched
    assert result["projects_suspended"] == 3
    # The 5 most recently scanned (proj_3..proj_7) should NOT have update called
    for proj in projects[5:]:
        proj.reference.update.assert_not_called()


@pytest.mark.asyncio
@patch(_PATCH_DB)
async def test_sets_customer_past_due(mock_db: MagicMock) -> None:
    overdue = (datetime.now(UTC) - timedelta(days=45)).isoformat()
    customer = _make_doc(
        "cus_pd",
        {
            "status": "active",
            "tier": "agency",
            "overage_past_due_since": overdue,
        },
    )
    projects = [
        _make_project(f"proj_{i}", f"2025-01-{i + 1:02d}T00:00:00Z") for i in range(8)
    ]
    db = _build_enforcement_db([customer], {"cus_pd": projects})
    mock_db.return_value = db

    from backend.ops.overage_enforcement import overage_enforcement

    await overage_enforcement.__wrapped__(None)
    # Customer doc should be updated to past_due
    doc_ref = db._doc_refs["cus_pd"]
    doc_ref.update.assert_called_once_with({"status": "past_due"})
