"""Tests for Phase 21c — rotation_reminder 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.rotation_reminder.get_db"
_PATCH_TOKEN = "backend.ops.rotation_reminder._get_github_token"
_PATCH_ALERT = "backend.ops.rotation_reminder.send_alert_email"


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


def _tracker_snap(secrets: dict[str, Any], exists: bool = True) -> MagicMock:
    snap = MagicMock()
    snap.exists = exists
    snap.to_dict.return_value = {"secrets": secrets} if exists else None
    return snap


def _mock_httpx_get(titles: list[str]) -> AsyncMock:
    """Return a mock httpx client whose GET returns issue titles."""
    items = [{"title": t} for t in titles]
    resp = MagicMock()
    resp.status_code = 200
    resp.json.return_value = items

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

    client = AsyncMock()
    client.get = AsyncMock(side_effect=[resp, empty_resp])
    client.post = AsyncMock(return_value=MagicMock(status_code=201))
    return client


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


class TestRotationReminder:
    @pytest.mark.asyncio
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch("backend.ops.rotation_reminder.httpx.AsyncClient")
    @patch(_PATCH_TOKEN, new_callable=AsyncMock, return_value="gh-token")
    @patch(_PATCH_DB)
    async def test_upcoming_rotation(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_httpx_cls: MagicMock,
        mock_alert: AsyncMock,
    ) -> None:
        """340 days since rotation → GitHub issue created."""
        rotated = (datetime.now(UTC) - timedelta(days=340)).isoformat()
        secrets = {
            "session-encryption-key": {
                "last_rotated_at": rotated,
                "rotated_by": "founder",
            },
        }

        db = MagicMock()
        snap = _tracker_snap(secrets)
        doc_ref = MagicMock()
        doc_ref.get = AsyncMock(return_value=snap)
        fm_collection = MagicMock()
        fm_collection.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=fm_collection)
        mock_db.return_value = db

        client = _mock_httpx_get([])
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from backend.ops.rotation_reminder import rotation_reminder

        result = await rotation_reminder(MagicMock())
        assert result == {"status": "ok", "function": "complyform-rotation-reminder"}

        # Issue should have been created
        assert client.post.called

    @pytest.mark.asyncio
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch("backend.ops.rotation_reminder.httpx.AsyncClient")
    @patch(_PATCH_TOKEN, new_callable=AsyncMock, return_value="gh-token")
    @patch(_PATCH_DB)
    async def test_overdue_rotation(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_httpx_cls: MagicMock,
        mock_alert: AsyncMock,
    ) -> None:
        """370 days since rotation → issue + email alert."""
        rotated = (datetime.now(UTC) - timedelta(days=370)).isoformat()
        secrets = {
            "session-encryption-key": {
                "last_rotated_at": rotated,
                "rotated_by": "founder",
            },
        }

        db = MagicMock()
        snap = _tracker_snap(secrets)
        doc_ref = MagicMock()
        doc_ref.get = AsyncMock(return_value=snap)
        fm_collection = MagicMock()
        fm_collection.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=fm_collection)
        mock_db.return_value = db

        client = _mock_httpx_get([])
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from backend.ops.rotation_reminder import rotation_reminder

        await rotation_reminder(MagicMock())

        # Issue created AND alert email sent
        assert client.post.called
        mock_alert.assert_called()

    @pytest.mark.asyncio
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch("backend.ops.rotation_reminder.httpx.AsyncClient")
    @patch(_PATCH_TOKEN, new_callable=AsyncMock, return_value="gh-token")
    @patch(_PATCH_DB)
    async def test_skip_existing_issue(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_httpx_cls: MagicMock,
        mock_alert: AsyncMock,
    ) -> None:
        """Duplicate issue not created if one already exists."""
        rotated = (datetime.now(UTC) - timedelta(days=340)).isoformat()
        secrets = {
            "session-encryption-key": {
                "last_rotated_at": rotated,
                "rotated_by": "founder",
            },
        }
        days_remaining = 365 - 340
        existing_title = f"Rotate session-encryption-key — due in {days_remaining} days"

        db = MagicMock()
        snap = _tracker_snap(secrets)
        doc_ref = MagicMock()
        doc_ref.get = AsyncMock(return_value=snap)
        fm_collection = MagicMock()
        fm_collection.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=fm_collection)
        mock_db.return_value = db

        client = _mock_httpx_get([existing_title])
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from backend.ops.rotation_reminder import rotation_reminder

        await rotation_reminder(MagicMock())

        # No POST to create issue (only GET calls for listing)
        assert not client.post.called

    @pytest.mark.asyncio
    @patch(_PATCH_ALERT, new_callable=AsyncMock)
    @patch("backend.ops.rotation_reminder.httpx.AsyncClient")
    @patch(_PATCH_TOKEN, new_callable=AsyncMock, return_value="gh-token")
    @patch(_PATCH_DB)
    async def test_recent_rotation(
        self,
        mock_db: MagicMock,
        mock_token: AsyncMock,
        mock_httpx_cls: MagicMock,
        mock_alert: AsyncMock,
    ) -> None:
        """100 days since rotation → no action."""
        rotated = (datetime.now(UTC) - timedelta(days=100)).isoformat()
        secrets = {
            "session-encryption-key": {
                "last_rotated_at": rotated,
                "rotated_by": "founder",
            },
        }

        db = MagicMock()
        snap = _tracker_snap(secrets)
        doc_ref = MagicMock()
        doc_ref.get = AsyncMock(return_value=snap)
        fm_collection = MagicMock()
        fm_collection.document = MagicMock(return_value=doc_ref)
        db.collection = MagicMock(return_value=fm_collection)
        mock_db.return_value = db

        client = _mock_httpx_get([])
        mock_httpx_cls.return_value.__aenter__ = AsyncMock(return_value=client)
        mock_httpx_cls.return_value.__aexit__ = AsyncMock(return_value=False)

        from backend.ops.rotation_reminder import rotation_reminder

        await rotation_reminder(MagicMock())

        # No issue creation
        assert not client.post.called
