"""Tests for Phase 21c — POST /v1/nps endpoint."""

from __future__ import annotations

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

from fastapi.testclient import TestClient

_PATCH_DB = "backend.api.routes.nps.get_db"
_PATCH_KEY = "backend.api.routes.nps._get_nps_signing_key"
_SIGNING_KEY = "test-nps-signing-key"


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


def _make_token(
    customer_id: str,
    token_type: str,
    signing_key: str = _SIGNING_KEY,
    expires_at: str | None = None,
) -> str:
    if expires_at is None:
        expires_at = (datetime.now(UTC) + timedelta(days=30)).isoformat()
    message = f"{customer_id}|{token_type}|{expires_at}"
    signature = hmac.new(
        signing_key.encode(),
        message.encode(),
        hashlib.sha256,
    ).hexdigest()
    return f"{customer_id}|{token_type}|{expires_at}|{signature}"


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, count: int = 0) -> None:
        self._count = count

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

    def stream(self) -> _FakeAsyncIter:
        return _FakeAsyncIter([MagicMock() for _ in range(self._count)])


class _FakeSubcollection:
    def __init__(self, existing_count: int = 0) -> None:
        self._query = _FakeQuery(existing_count)
        self._docs: dict[str, MagicMock] = {}

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

    def document(self, doc_id: str) -> MagicMock:
        if doc_id not in self._docs:
            ref = MagicMock()
            ref.set = AsyncMock()
            self._docs[doc_id] = ref
        return self._docs[doc_id]


def _setup_db(
    customer_data: dict[str, Any] | None = None,
    existing_feedback_count: int = 0,
) -> MagicMock:
    db = MagicMock()
    customer_data = customer_data or {"tier": "pro"}

    customer_snap = MagicMock()
    customer_snap.exists = True
    customer_snap.to_dict.return_value = customer_data

    customer_doc_ref = MagicMock()
    customer_doc_ref.get = AsyncMock(return_value=customer_snap)

    feedback_doc_ref = MagicMock()
    sub = _FakeSubcollection(existing_feedback_count)
    feedback_doc_ref.collection = MagicMock(return_value=sub)

    def _collection_router(name: str) -> Any:
        col = MagicMock()
        if name == "customers":
            col.document = MagicMock(return_value=customer_doc_ref)
        elif name == "customer_feedback":
            col.document = MagicMock(return_value=feedback_doc_ref)
        return col

    db.collection = _collection_router
    return db


def _get_client() -> TestClient:
    from backend.api.main import app

    return TestClient(app)


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


class TestNPSEndpoint:
    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_nps_score_collected(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """Valid token, score 8 → stored with 200."""
        mock_db.return_value = _setup_db()
        token = _make_token("cust1", "nps")

        client = _get_client()
        resp = client.post("/v1/nps", params={"token": token}, json={"score": 8})

        assert resp.status_code == 200
        data = resp.json()
        assert data["status"] == "ok"
        assert data["message"] == "Thank you for your feedback"

    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_cancellation_reason_collected(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """Cancellation reason "competitor" → stored."""
        mock_db.return_value = _setup_db()
        token = _make_token("cust1", "cancellation")

        client = _get_client()
        resp = client.post(
            "/v1/nps",
            params={"token": token},
            json={"reason": "competitor"},
        )

        assert resp.status_code == 200

    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_invalid_token(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """Tampered token → 403."""
        token = _make_token("cust1", "nps") + "tampered"

        client = _get_client()
        resp = client.post("/v1/nps", params={"token": token}, json={"score": 8})

        assert resp.status_code == 403

    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_expired_token(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """>30 days old token → 403."""
        expired_at = (datetime.now(UTC) - timedelta(days=1)).isoformat()
        token = _make_token("cust1", "nps", expires_at=expired_at)

        client = _get_client()
        resp = client.post("/v1/nps", params={"token": token}, json={"score": 8})

        assert resp.status_code == 403

    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_duplicate_submission(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """Same type within 24hr → 409."""
        mock_db.return_value = _setup_db(existing_feedback_count=1)
        token = _make_token("cust1", "nps")

        client = _get_client()
        resp = client.post("/v1/nps", params={"token": token}, json={"score": 8})

        assert resp.status_code == 409

    @patch(_PATCH_KEY, new_callable=AsyncMock, return_value=_SIGNING_KEY)
    @patch(_PATCH_DB)
    def test_nps_score_range(
        self,
        mock_db: MagicMock,
        mock_key: AsyncMock,
    ) -> None:
        """Score 11 → 422 validation error."""
        mock_db.return_value = _setup_db()
        token = _make_token("cust1", "nps")

        client = _get_client()
        resp = client.post("/v1/nps", params={"token": token}, json={"score": 11})

        assert resp.status_code == 422
