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

from __future__ import annotations

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

import pytest

_PATCH_DB = "backend.ops.github_issues_sync.get_db"
_PATCH_GH_TOKEN = "backend.ops.github_issues_sync._get_github_token"


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


def _make_issue(
    number: int,
    state: str = "open",
    labels: list[str] | None = None,
    created_at: str = "2025-01-15T10:00:00Z",
    updated_at: str = "2025-01-16T10:00:00Z",
    closed_at: str | None = None,
    comments: int = 0,
) -> dict[str, Any]:
    label_objs = [{"name": lbl} for lbl in (labels or [])]
    issue: dict[str, Any] = {
        "number": number,
        "title": f"Issue #{number}",
        "state": state,
        "labels": label_objs,
        "created_at": created_at,
        "updated_at": updated_at,
        "comments": comments,
        "html_url": f"https://github.com/complyform/complyform/issues/{number}",
    }
    if closed_at:
        issue["closed_at"] = closed_at
    return issue


def _build_sync_db() -> MagicMock:
    db = MagicMock()
    set_mock = AsyncMock()

    def collection_fn(name: str) -> Any:
        coll = MagicMock()
        if 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
    return db


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


class TestGitHubIssuesSync:
    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_GH_TOKEN, new_callable=AsyncMock, return_value="ghp_test")
    async def test_sync_success(
        self, _mock_token: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Mock GitHub API — verify tier/severity distribution."""
        db = _build_sync_db()
        mock_db.return_value = db

        issues = [
            _make_issue(1, "open", ["tier/pro", "priority/p1"]),
            _make_issue(2, "open", ["tier/team", "priority/p2"]),
            _make_issue(3, "open", ["priority/p3"]),
            _make_issue(4, "closed", closed_at="2025-03-10T00:00:00Z"),
        ]

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = issues
        resp.raise_for_status = MagicMock()

        empty_resp = MagicMock()
        empty_resp.status_code = 200
        empty_resp.json.return_value = []
        empty_resp.raise_for_status = MagicMock()

        call_count = 0

        async def fake_get(url: str, **kwargs: Any) -> MagicMock:
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return resp
            return empty_resp

        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.github_issues_sync.httpx.AsyncClient",
            return_value=mock_client,
        ):
            from backend.ops.github_issues_sync import github_issues_sync

            result = await github_issues_sync.__wrapped__(None)

        assert result["open"] == 3
        # Verify Firestore write
        db._set_mock.assert_called_once()
        write_data = db._set_mock.call_args[0][0]
        assert write_data["open_count"] == 3
        assert write_data["tier_distribution"]["pro"] == 1
        assert write_data["tier_distribution"]["team"] == 1
        assert write_data["severity_distribution"]["p1"] == 1
        assert write_data["severity_distribution"]["p2"] == 1
        assert write_data["severity_distribution"]["p3"] == 1

    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_GH_TOKEN, new_callable=AsyncMock, return_value="ghp_test")
    async def test_oldest_unresolved(
        self, _mock_token: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Picks correct oldest open issue."""
        db = _build_sync_db()
        mock_db.return_value = db

        issues = [
            _make_issue(1, "open", created_at="2024-06-01T00:00:00Z"),
            _make_issue(2, "open", created_at="2025-01-01T00:00:00Z"),
            _make_issue(3, "open", created_at="2024-03-15T00:00:00Z"),
        ]

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = issues
        resp.raise_for_status = MagicMock()

        empty_resp = MagicMock()
        empty_resp.status_code = 200
        empty_resp.json.return_value = []
        empty_resp.raise_for_status = MagicMock()

        call_count = 0

        async def fake_get(url: str, **kwargs: Any) -> MagicMock:
            nonlocal call_count
            call_count += 1
            return resp if call_count == 1 else empty_resp

        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.github_issues_sync.httpx.AsyncClient",
            return_value=mock_client,
        ):
            from backend.ops.github_issues_sync import github_issues_sync

            await github_issues_sync.__wrapped__(None)

        write_data = db._set_mock.call_args[0][0]
        assert write_data["oldest_unresolved"]["number"] == 3
        assert write_data["oldest_unresolved"]["created_at"] == "2024-03-15T00:00:00Z"

    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(
        _PATCH_GH_TOKEN,
        new_callable=AsyncMock,
        side_effect=Exception("Secret Manager error"),
    )
    async def test_github_api_failure(
        self, _mock_token: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Error logged, wrapper returns 200."""
        db = _build_sync_db()
        mock_db.return_value = db

        from backend.ops.github_issues_sync import github_issues_sync

        # The wrapper catches exceptions and returns 200
        result = await github_issues_sync(None)
        assert result["status"] == "error"

    @pytest.mark.asyncio
    @patch(_PATCH_DB)
    @patch(_PATCH_GH_TOKEN, new_callable=AsyncMock, return_value="ghp_test")
    async def test_writes_singleton(
        self, _mock_token: AsyncMock, mock_db: MagicMock
    ) -> None:
        """Writes to founder_metrics/github_issues (overwritten each run)."""
        db = _build_sync_db()
        mock_db.return_value = db

        resp = MagicMock()
        resp.status_code = 200
        resp.json.return_value = []
        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.github_issues_sync.httpx.AsyncClient",
            return_value=mock_client,
        ):
            from backend.ops.github_issues_sync import github_issues_sync

            await github_issues_sync.__wrapped__(None)

        # Verify document name is "github_issues"
        coll_calls = [
            c for c in db.collection.call_args_list if c[0][0] == "founder_metrics"
        ]
        assert len(coll_calls) > 0
        doc_call = coll_calls[0].return_value  # noqa: F841
        # The set was called on the mock chain
        db._set_mock.assert_called_once()
