"""PRD §5.1 — periodic chase tasks added to clear audit gaps.

Coverage:
  - FRFA-CO003 expire_awards: ACTIVE awards past project_end_date flip to
    CLOSEOUT_IN_PROGRESS and create an AwardCloseoutRecord. Idempotent.
  - FRFA-AA009 chase_agreements: AGREEMENT_PENDING awards past the grace
    window get reminders, capped at max_reminders.
  - FRFA-CO011 remind_closeout_reports: awards in close-out missing final
    reports get reminded; awards with both reports submitted are skipped.
"""

from __future__ import annotations

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

import pytest
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.rims.grants.closeout_helpers import get_or_create_closeout_record
from apps.rims.grants.models import (
    Application,
    Award,
    AwardCloseoutRecord,
    FinalFinancialReport,
    FinalNarrativeReport,
    FundingRecord,
    GrantCall,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
)
from apps.rims.grants.tasks import (
    chase_agreements,
    expire_awards,
    remind_closeout_reports,
)


@pytest.fixture(autouse=True)
def _no_async_dispatch():
    with patch(
        "apps.core.notifications.services.dispatch_notification_channel.delay"
    ) as fake:
        yield fake


def _funded_award(applicant_user, institution, grants_manager_user, *, suffix: str, end_in_days: int = 180) -> Award:
    fr = FundingRecord.objects.create(
        title=f"FR {suffix}",
        amount=Decimal("10000"),
        start_date=date.today() - timedelta(days=10),
        end_date=date.today() + timedelta(days=365),
        grant_manager=grants_manager_user,
    )
    call = GrantCall.objects.create(
        title=f"Call {suffix}",
        slug=f"chase-{suffix}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
        funding_record=fr,
    )
    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("1000"), date.today() + timedelta(days=end_in_days))
    return Application.objects.get(pk=app.pk).award


# ---------------------------------------------------------------------------
# FRFA-CO003 — expire_awards
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_expire_awards_flips_status_and_creates_closeout(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="exp1")
    # Backdate the project end so the award is past expiry.
    Award.objects.filter(pk=award.pk).update(project_end_date=date.today() - timedelta(days=1))

    flipped = expire_awards()
    assert flipped == 1

    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.CLOSEOUT_IN_PROGRESS
    assert award.closeout_ready_at is not None
    assert AwardCloseoutRecord.objects.filter(award=award).exists()


@pytest.mark.django_db
def test_expire_awards_is_idempotent(applicant_user, institution, grants_manager_user):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="exp2")
    Award.objects.filter(pk=award.pk).update(project_end_date=date.today() - timedelta(days=1))

    flipped_first = expire_awards()
    flipped_second = expire_awards()
    assert flipped_first == 1
    assert flipped_second == 0


@pytest.mark.django_db
def test_expire_awards_skips_active_awards_inside_window(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="exp3")
    # End date is 180 days in the future — should not be touched.
    flipped = expire_awards()
    assert flipped == 0
    award = Award.objects.get(pk=award.pk)
    assert award.status == Award.Status.ACTIVE


# ---------------------------------------------------------------------------
# FRFA-AA009 — chase_agreements
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_chase_agreements_skips_inside_grace(applicant_user, institution, grants_manager_user):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="ca-grace")
    Award.objects.filter(pk=award.pk).update(status=Award.Status.AGREEMENT_PENDING)
    sent = chase_agreements(grace_days=14)
    assert sent == 0


@pytest.mark.django_db
def test_chase_agreements_sends_after_grace(applicant_user, institution, grants_manager_user):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="ca-after")
    Award.objects.filter(pk=award.pk).update(
        status=Award.Status.AGREEMENT_PENDING,
        awarded_at=timezone.now() - timedelta(days=20),
    )
    Notification.objects.all().delete()
    sent = chase_agreements(grace_days=14, max_reminders=3)
    assert sent == 1
    assert Notification.objects.filter(
        recipient=applicant_user, message__icontains="signed agreement"
    ).exists()
    award = Award.objects.get(pk=award.pk)
    assert award.acknowledgement_reminders_sent == 1


@pytest.mark.django_db
def test_chase_agreements_caps_at_max_reminders(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="ca-cap")
    Award.objects.filter(pk=award.pk).update(
        status=Award.Status.AGREEMENT_PENDING,
        awarded_at=timezone.now() - timedelta(days=30),
        acknowledgement_reminders_sent=3,
    )
    sent = chase_agreements(grace_days=14, max_reminders=3)
    assert sent == 0


# ---------------------------------------------------------------------------
# FRFA-CO011 — remind_closeout_reports
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_remind_closeout_reports_targets_awards_missing_reports(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co-r1")
    Award.objects.filter(pk=award.pk).update(
        status=Award.Status.CLOSEOUT_IN_PROGRESS,
        closeout_ready_at=timezone.now() - timedelta(days=10),
    )
    get_or_create_closeout_record(Award.objects.get(pk=award.pk))
    Notification.objects.all().delete()
    sent = remind_closeout_reports(warn_days_before=7)
    assert sent == 1
    assert Notification.objects.filter(
        recipient=applicant_user, message__icontains="final"
    ).exists()


@pytest.mark.django_db
def test_remind_closeout_reports_skips_when_both_reports_submitted(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co-r2")
    Award.objects.filter(pk=award.pk).update(
        status=Award.Status.CLOSEOUT_IN_PROGRESS,
        closeout_ready_at=timezone.now() - timedelta(days=10),
    )
    award = Award.objects.get(pk=award.pk)
    closeout = get_or_create_closeout_record(award)
    FinalNarrativeReport.objects.create(closeout=closeout)
    FinalFinancialReport.objects.create(closeout=closeout)
    Notification.objects.all().delete()
    sent = remind_closeout_reports(warn_days_before=7)
    assert sent == 0


@pytest.mark.django_db
def test_remind_closeout_reports_respects_warn_window(
    applicant_user, institution, grants_manager_user
):
    award = _funded_award(applicant_user, institution, grants_manager_user, suffix="co-r3")
    Award.objects.filter(pk=award.pk).update(
        status=Award.Status.CLOSEOUT_IN_PROGRESS,
        closeout_ready_at=timezone.now() - timedelta(days=2),  # inside warn window
    )
    get_or_create_closeout_record(Award.objects.get(pk=award.pk))
    sent = remind_closeout_reports(warn_days_before=7)
    assert sent == 0
