"""Tests for Phase 21a — usage_reconciler 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.usage_reconciler.get_db"
_PATCH_PADDLE_KEY = "backend.ops.usage_reconciler._get_paddle_api_key"
_PATCH_CHARGE = "backend.ops.usage_reconciler._charge_overage"


# ---------------------------------------------------------------------------
# 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.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_reconciler_db(
    customers: list[MagicMock],
    projects_per_customer: dict[str, list[MagicMock]] | None = None,
) -> MagicMock:
    db = MagicMock()
    projects_map = projects_per_customer or {}

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

            def coll_document(doc_id: str) -> Any:
                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)
                return doc_ref

            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, status: str = "active") -> MagicMock:
    return _make_doc(proj_id, {"status": status})


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


@pytest.mark.asyncio
@patch(_PATCH_CHARGE, new_callable=AsyncMock, return_value=True)
@patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test_key")
@patch(_PATCH_DB)
async def test_bill_overage(
    mock_db: MagicMock,
    mock_key: AsyncMock,
    mock_charge: AsyncMock,
) -> None:
    """8 projects, limit 5 → bills 3 × overage."""
    customer = _make_doc(
        "cus_bill",
        {
            "status": "active",
            "tier": "agency",
            "paddle_subscription_id": "sub_123",
            "last_overage_billed_at": None,
        },
    )
    projects = [_make_project(f"proj_{i}") for i in range(8)]
    db = _build_reconciler_db([customer], {"cus_bill": projects})
    mock_db.return_value = db

    from backend.ops.usage_reconciler import usage_reconciler

    result = await usage_reconciler.__wrapped__(None)
    assert result["customers_billed"] == 1
    assert result["total_overage"] == 3
    mock_charge.assert_called_once_with("test_key", "sub_123", "pri_agency_overage", 3)


@pytest.mark.asyncio
@patch(_PATCH_CHARGE, new_callable=AsyncMock, return_value=True)
@patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test_key")
@patch(_PATCH_DB)
async def test_skip_within_billing_period(
    mock_db: MagicMock,
    mock_key: AsyncMock,
    mock_charge: AsyncMock,
) -> None:
    """last_overage_billed_at < 30 days ago → skip."""
    recent = (datetime.now(UTC) - timedelta(days=10)).isoformat()
    customer = _make_doc(
        "cus_recent",
        {
            "status": "active",
            "tier": "agency",
            "paddle_subscription_id": "sub_456",
            "last_overage_billed_at": recent,
        },
    )
    projects = [_make_project(f"proj_{i}") for i in range(8)]
    db = _build_reconciler_db([customer], {"cus_recent": projects})
    mock_db.return_value = db

    from backend.ops.usage_reconciler import usage_reconciler

    result = await usage_reconciler.__wrapped__(None)
    assert result["customers_billed"] == 0
    mock_charge.assert_not_called()


@pytest.mark.asyncio
@patch(_PATCH_CHARGE, new_callable=AsyncMock, return_value=True)
@patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test_key")
@patch(_PATCH_DB)
async def test_no_overage(
    mock_db: MagicMock,
    mock_key: AsyncMock,
    mock_charge: AsyncMock,
) -> None:
    """3 projects, limit 5 → no charge."""
    customer = _make_doc(
        "cus_ok",
        {
            "status": "active",
            "tier": "agency",
            "paddle_subscription_id": "sub_789",
            "last_overage_billed_at": None,
        },
    )
    projects = [_make_project(f"proj_{i}") for i in range(3)]
    db = _build_reconciler_db([customer], {"cus_ok": projects})
    mock_db.return_value = db

    from backend.ops.usage_reconciler import usage_reconciler

    result = await usage_reconciler.__wrapped__(None)
    assert result["customers_billed"] == 0
    mock_charge.assert_not_called()


@pytest.mark.asyncio
async def test_paddle_api_call() -> None:
    """Verify POST body matches Paddle contract."""
    with patch("backend.ops.usage_reconciler.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 200
        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client_cls.return_value = mock_client

        from backend.ops.usage_reconciler import _charge_overage

        result = await _charge_overage("key123", "sub_abc", "pri_agency_overage", 3)
        assert result is True

        call_args = mock_client.post.call_args
        assert "subscriptions/sub_abc/charge" in call_args[0][0]
        body = call_args[1]["json"]
        assert body["effective_from"] == "immediately"
        assert body["items"] == [{"price_id": "pri_agency_overage", "quantity": 3}]


@pytest.mark.asyncio
async def test_paddle_api_error() -> None:
    """4xx from Paddle → return False, customer skipped."""
    with patch("backend.ops.usage_reconciler.httpx.AsyncClient") as mock_client_cls:
        mock_resp = MagicMock()
        mock_resp.status_code = 400
        mock_resp.text = "Bad Request"
        mock_client = AsyncMock()
        mock_client.post = AsyncMock(return_value=mock_resp)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)
        mock_client_cls.return_value = mock_client

        from backend.ops.usage_reconciler import _charge_overage

        result = await _charge_overage("key123", "sub_abc", "pri_agency_overage", 3)
        assert result is False


@pytest.mark.asyncio
@patch(_PATCH_CHARGE, new_callable=AsyncMock, return_value=True)
@patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test_key")
@patch(_PATCH_DB)
async def test_enterprise_unlimited_exempt(
    mock_db: MagicMock,
    mock_key: AsyncMock,
    mock_charge: AsyncMock,
) -> None:
    customer = _make_doc(
        "cus_eu",
        {
            "status": "active",
            "tier": "enterprise_unlimited",
            "paddle_subscription_id": "sub_eu",
            "last_overage_billed_at": None,
        },
    )
    projects = [_make_project(f"proj_{i}") for i in range(20)]
    db = _build_reconciler_db([customer], {"cus_eu": projects})
    mock_db.return_value = db

    from backend.ops.usage_reconciler import usage_reconciler

    result = await usage_reconciler.__wrapped__(None)
    assert result["customers_billed"] == 0
    mock_charge.assert_not_called()


@pytest.mark.asyncio
@patch(_PATCH_CHARGE, new_callable=AsyncMock, return_value=True)
@patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test_key")
@patch(_PATCH_DB)
async def test_community_pro_team_excluded(
    mock_db: MagicMock,
    mock_key: AsyncMock,
    mock_charge: AsyncMock,
) -> None:
    """Tiers without overage billing are skipped."""
    for tier in ("community", "pro", "team"):
        customer = _make_doc(
            f"cus_{tier}",
            {
                "status": "active",
                "tier": tier,
                "paddle_subscription_id": f"sub_{tier}",
                "last_overage_billed_at": None,
            },
        )
        db = _build_reconciler_db([customer])
        mock_db.return_value = db

        from backend.ops.usage_reconciler import usage_reconciler

        result = await usage_reconciler.__wrapped__(None)
        assert result["customers_billed"] == 0

    mock_charge.assert_not_called()
