"""PRD §5.1 FRFA-CO015/016/017 — archive_closed_awards Celery beat task."""
from __future__ import annotations

from datetime import timedelta
from decimal import Decimal
from unittest.mock import patch

import pytest
from django.utils import timezone

from apps.core.audit.models import AuditLog
from apps.core.audit.tasks import persist_audit_log
from apps.rims.grants.models import Application, Award, GrantCall
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)
from apps.rims.grants.tasks import archive_closed_awards
from apps.rims.operations.models import RetentionPolicy


@pytest.fixture
def retention_policies(db):
    """Seed the four-policy retention table the task reads."""
    RetentionPolicy.objects.get_or_create(
        funding_type="grant", defaults={"retention_years": 7, "archive_after_days": 30}
    )
    RetentionPolicy.objects.get_or_create(
        funding_type="scholarship", defaults={"retention_years": 5, "archive_after_days": 60}
    )
    RetentionPolicy.objects.get_or_create(
        funding_type="fellowship", defaults={"retention_years": 5, "archive_after_days": 60}
    )
    RetentionPolicy.objects.get_or_create(
        funding_type="challenge", defaults={"retention_years": 3, "archive_after_days": 30}
    )
    return None


@pytest.fixture(autouse=True)
def _sync_audit():
    """Make AuditLog writes synchronous in tests so assertions can see rows."""
    with patch("apps.core.audit.tasks.write_audit_log.delay", side_effect=lambda **kw: persist_audit_log(**kw)):
        with patch(
            "apps.core.audit.mixins.transaction.on_commit",
            side_effect=lambda fn: fn(),
        ):
            yield


def _build_closed_award(applicant_user, institution, *, call_type: str = "grant", closed_days_ago: int) -> Award:
    call = GrantCall.objects.create(
        title=f"CO015 archive call {call_type} -{closed_days_ago}d",
        slug=f"co015-{call_type}-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        call_type=call_type,
        opens_at=timezone.now() - timedelta(days=400),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    shortlist_application(app)
    award_application(app, Decimal("100"), timezone.now().date() - timedelta(days=closed_days_ago + 1), narrative="")
    award = Application.objects.get(pk=app.pk).award
    award.status = Award.Status.CLOSED
    award.closed_at = timezone.now() - timedelta(days=closed_days_ago)
    award.save(update_fields=["status", "closed_at"])
    return award


@pytest.mark.django_db
def test_grant_older_than_30_days_archives(retention_policies, applicant_user, institution):
    """Grant closed 31 days ago is past the 30-day archive window."""
    award = _build_closed_award(applicant_user, institution, call_type="grant", closed_days_ago=31)
    archived = archive_closed_awards()
    assert archived == 1
    award.refresh_from_db()
    assert award.archived_at is not None


@pytest.mark.django_db
def test_grant_younger_than_30_days_does_not_archive(retention_policies, applicant_user, institution):
    """Grant closed 5 days ago is well within the 30-day window — must not archive."""
    award = _build_closed_award(applicant_user, institution, call_type="grant", closed_days_ago=5)
    archived = archive_closed_awards()
    assert archived == 0
    award.refresh_from_db()
    assert award.archived_at is None


@pytest.mark.django_db
def test_scholarship_uses_60_day_cutoff(retention_policies, applicant_user, institution):
    """Scholarship at 45 days closed is still within the 60-day window — must not archive."""
    award = _build_closed_award(applicant_user, institution, call_type="scholarship", closed_days_ago=45)
    archived = archive_closed_awards()
    assert archived == 0
    award.refresh_from_db()
    assert award.archived_at is None


@pytest.mark.django_db
def test_scholarship_past_60_days_archives(retention_policies, applicant_user, institution):
    """Scholarship at 61 days closed crosses the 60-day window."""
    award = _build_closed_award(applicant_user, institution, call_type="scholarship", closed_days_ago=61)
    archived = archive_closed_awards()
    assert archived == 1
    award.refresh_from_db()
    assert award.archived_at is not None


@pytest.mark.django_db
def test_beat_is_idempotent(retention_policies, applicant_user, institution):
    """Re-running the task is a no-op for already-archived awards."""
    award = _build_closed_award(applicant_user, institution, call_type="grant", closed_days_ago=31)
    first = archive_closed_awards()
    second = archive_closed_awards()
    assert first == 1
    assert second == 0
    award.refresh_from_db()
    assert award.archived_at is not None


@pytest.mark.django_db
def test_archive_writes_audit_row(retention_policies, applicant_user, institution):
    """PRD §5.1 — every archival writes an AWARD_ARCHIVED audit row."""
    award = _build_closed_award(applicant_user, institution, call_type="grant", closed_days_ago=45)
    archive_closed_awards()
    audit = AuditLog.objects.filter(
        action="AWARD_ARCHIVED",
        target_app="rims_grants",
        target_model="Award",
        object_id=str(award.pk),
    ).first()
    assert audit is not None
    assert audit.changes["archive_after_days"] == 30


@pytest.mark.django_db
def test_active_award_is_not_archived(retention_policies, applicant_user, institution):
    """Only CLOSED awards are eligible — ACTIVE / CANCELLED skipped."""
    award = _build_closed_award(applicant_user, institution, call_type="grant", closed_days_ago=31)
    award.status = Award.Status.ACTIVE
    award.save(update_fields=["status"])
    archived = archive_closed_awards()
    assert archived == 0
    award.refresh_from_db()
    assert award.archived_at is None
