"""Tests for Phase 21b — founder_metrics Cloud Function."""

from __future__ import annotations

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

import pytest

_PATCH_DB = "backend.ops.founder_metrics.get_db"
_PATCH_PADDLE_KEY = "backend.ops.founder_metrics._get_paddle_api_key"
_PATCH_POSTMARK_TOKEN = "backend.billing.email_sender._get_postmark_token"


# ---------------------------------------------------------------------------
# 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]) -> MagicMock:
    doc = MagicMock()
    doc.id = doc_id
    doc.to_dict.return_value = data
    doc.exists = True
    return doc


def _make_feedback_doc(doc_id: str, score: int) -> MagicMock:
    return _make_doc(doc_id, {"score": score})


def _build_metrics_db(
    feedback_docs: list[MagicMock] | None = None,
    lifecycle_docs: list[MagicMock] | None = None,
    scan_docs: list[MagicMock] | None = None,
    xero_data: dict[str, Any] | None = None,
) -> MagicMock:
    db = MagicMock()
    set_mock = AsyncMock()

    def collection_fn(name: str) -> Any:
        coll = MagicMock()
        if name == "customer_feedback":
            coll.stream = MagicMock(return_value=_FakeAsyncIter(feedback_docs or []))
        elif name == "customer_lifecycle":
            coll.where = MagicMock(return_value=_FakeQuery(lifecycle_docs or []))
        elif name == "scan_activity":
            coll.stream = MagicMock(return_value=_FakeAsyncIter(scan_docs or []))
        elif name == "integrations":
            doc_ref = MagicMock()
            doc_mock = MagicMock()
            if xero_data:
                doc_mock.exists = True
                doc_mock.to_dict.return_value = xero_data
            else:
                doc_mock.exists = False
                doc_mock.to_dict.return_value = None
            doc_ref.get = AsyncMock(return_value=doc_mock)
            coll.document = MagicMock(return_value=doc_ref)
        elif name == "founder_metrics":
            doc_ref = MagicMock()
            doc_ref.set = set_mock
            coll.document = MagicMock(return_value=doc_ref)
        return coll

    db.collection = MagicMock(side_effect=collection_fn)
    db._set_mock = set_mock  # Expose for assertion
    return db


def _mock_httpx_response(
    status_code: int = 200,
    json_data: Any = None,
) -> MagicMock:
    resp = MagicMock()
    resp.status_code = status_code
    resp.json.return_value = json_data or {}
    resp.raise_for_status = MagicMock()
    if status_code >= 400:
        resp.raise_for_status.side_effect = Exception(f"HTTP {status_code}")
    return resp


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


class TestFounderMetrics:
    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test-key")
    async def test_aggregate_all_sources(
        self, _mock_key: AsyncMock, mock_db: MagicMock
    ) -> None:
        """All data sources mocked — metrics written to Firestore."""
        feedback = [
            _make_feedback_doc("f1", 10),
            _make_feedback_doc("f2", 8),
            _make_feedback_doc("f3", 3),
        ]
        db = _build_metrics_db(feedback_docs=feedback)
        mock_db.return_value = db

        # Mock httpx client
        paddle_resp = _mock_httpx_response(
            json_data={
                "data": [
                    {"custom_data": {"tier": "pro"}, "items": []},
                    {"custom_data": {"tier": "team"}, "items": []},
                ],
                "meta": {"pagination": {"next": ""}},
            }
        )
        pypi_resp = _mock_httpx_response(json_data={"data": [{"downloads": 1000}]})
        homebrew_resp = _mock_httpx_response(
            json_data={"items": [{"formula": "complyform", "count": 500}]}
        )
        # GCP billing, postmark responses
        billing_resp = _mock_httpx_response(json_data={"billingAccounts": []})
        postmark_resp = _mock_httpx_response(
            json_data={"Sent": 100, "Bounced": 2, "SpamComplaints": 0}
        )
        cancelled_resp = _mock_httpx_response(
            json_data={"data": [], "meta": {"pagination": {"next": ""}}}
        )

        call_count = 0

        async def fake_get(url: str, **kwargs: Any) -> MagicMock:
            nonlocal call_count
            call_count += 1
            if "paddle.com/subscriptions" in url:
                if "canceled" in url:
                    return cancelled_resp
                return paddle_resp
            if "pypistats.org" in url:
                return pypi_resp
            if "formulae.brew.sh" in url:
                return homebrew_resp
            if "cloudbilling" in url:
                return billing_resp
            if "postmarkapp.com" in url:
                return postmark_resp
            return _mock_httpx_response()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(side_effect=fake_get)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch(
                "backend.ops.founder_metrics.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="tok"),
        ):
            from backend.ops.founder_metrics import founder_metrics

            result = await founder_metrics.__wrapped__(None)

        assert result["date"] is not None
        assert result["fields_written"] > 0
        # Verify Firestore write was called
        db._set_mock.assert_called_once()

    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(
        _PATCH_PADDLE_KEY,
        new_callable=AsyncMock,
        side_effect=Exception("Secret Manager unavailable"),
    )
    async def test_paddle_api_failure_writes_null(
        self, _mock_key: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Paddle down → null fields, no crash."""
        db = _build_metrics_db()
        mock_db.return_value = db

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

        with (
            patch(
                "backend.ops.founder_metrics.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="tok"),
        ):
            from backend.ops.founder_metrics import founder_metrics

            result = await founder_metrics.__wrapped__(None)

        # Function should still succeed
        assert result["date"] is not None
        # Verify null paddle fields were written
        write_call = db._set_mock.call_args[0][0]
        assert write_call["mrr"] is None
        assert write_call["arr"] is None

    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test-key")
    async def test_pypi_failure_non_fatal(
        self, _mock_key: AsyncMock, mock_db: MagicMock
    ) -> None:
        """PyPI down → null, continues."""
        db = _build_metrics_db()
        mock_db.return_value = db

        async def fake_get(url: str, **kwargs: Any) -> MagicMock:
            if "pypistats.org" in url:
                raise Exception("PyPI timeout")
            if "paddle.com" in url:
                return _mock_httpx_response(
                    json_data={
                        "data": [],
                        "meta": {"pagination": {"next": ""}},
                    }
                )
            return _mock_httpx_response()

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(side_effect=fake_get)
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch(
                "backend.ops.founder_metrics.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="tok"),
        ):
            from backend.ops.founder_metrics import founder_metrics

            result = await founder_metrics.__wrapped__(None)

        assert result["date"] is not None
        write_call = db._set_mock.call_args[0][0]
        assert write_call["pypi_installs_total"] is None


class TestNPSCalculation:
    def test_nps_standard(self) -> None:
        """promoters=3, passives=2, detractors=1 → NPS ≈ 33."""
        from backend.ops.founder_metrics import compute_nps

        result = compute_nps(promoters=3, passives=2, detractors=1)
        assert result is not None
        # (3/6 - 1/6) × 100 = 33.3
        assert abs(result - 33.3) < 0.2

    def test_nps_all_promoters(self) -> None:
        from backend.ops.founder_metrics import compute_nps

        result = compute_nps(promoters=10, passives=0, detractors=0)
        assert result == 100.0

    def test_nps_all_detractors(self) -> None:
        from backend.ops.founder_metrics import compute_nps

        result = compute_nps(promoters=0, passives=0, detractors=10)
        assert result == -100.0

    def test_nps_no_responses(self) -> None:
        from backend.ops.founder_metrics import compute_nps

        result = compute_nps(promoters=0, passives=0, detractors=0)
        assert result is None


class TestBurnMultiple:
    def test_burn_multiple_normal(self) -> None:
        from backend.ops.founder_metrics import compute_burn_multiple

        result = compute_burn_multiple(net_burn=100_000, net_new_arr=50_000)
        assert result == 2.0

    def test_burn_multiple_no_growth(self) -> None:
        """net_new_ARR=0 → infinity."""
        from backend.ops.founder_metrics import compute_burn_multiple

        result = compute_burn_multiple(net_burn=100_000, net_new_arr=0)
        assert math.isinf(result)


class TestFirestorePath:
    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_PADDLE_KEY, new_callable=AsyncMock, return_value="test-key")
    async def test_writes_to_correct_firestore_path(
        self, _mock_key: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Verify write goes to founder_metrics/{YYYY-MM-DD}."""
        db = _build_metrics_db()
        mock_db.return_value = db

        mock_client = AsyncMock()
        mock_client.get = AsyncMock(
            return_value=_mock_httpx_response(
                json_data={"data": [], "meta": {"pagination": {"next": ""}}}
            )
        )
        mock_client.__aenter__ = AsyncMock(return_value=mock_client)
        mock_client.__aexit__ = AsyncMock(return_value=False)

        with (
            patch(
                "backend.ops.founder_metrics.httpx.AsyncClient",
                return_value=mock_client,
            ),
            patch(_PATCH_POSTMARK_TOKEN, new_callable=AsyncMock, return_value="tok"),
        ):
            from backend.ops.founder_metrics import founder_metrics

            await founder_metrics.__wrapped__(None)

        # Check that collection("founder_metrics").document(date) was called
        coll_calls = [
            c for c in db.collection.call_args_list if c[0][0] == "founder_metrics"
        ]
        assert len(coll_calls) > 0
