"""PRD §5.1 — cross-cutting items added in WS9.

  - FRFA-GPS026 FundingRecordVersion snapshot
  - FRFA-CS030  GrantCall withdrawal flow (status flip + drafts expired +
                applicant notifications)
  - FRFA-AA015  MilestoneEvidence model exists and accepts uploads
  - FRFA-GPS015 generate_reporting_calendar produces the right cadence
"""

from __future__ import annotations

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

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.rims.grants.models import (
    Application,
    AwardTranche,
    FundingRecord,
    FundingRecordVersion,
    GrantCall,
    MilestoneEvidence,
    ReportingCalendarEntry,
)
from apps.rims.grants.services import (
    generate_reporting_calendar,
    snapshot_funding_record_version,
    withdraw_call,
)


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


# ---------------------------------------------------------------------------
# FundingRecordVersion (FRFA-GPS026)
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_snapshot_funding_record_version_increments(grants_manager_user):
    fr = FundingRecord.objects.create(
        title="Versions",
        amount=Decimal("1000"),
        start_date=date.today(),
        end_date=date.today() + timedelta(days=365),
    )
    v1 = snapshot_funding_record_version(fr, actor=grants_manager_user)
    fr.title = "Versions v2"
    fr.save(update_fields=["title"])
    v2 = snapshot_funding_record_version(fr, actor=grants_manager_user)
    assert v1.version_number == 1
    assert v2.version_number == 2
    assert v1.snapshot["title"] == "Versions"
    assert v2.snapshot["title"] == "Versions v2"
    assert FundingRecordVersion.objects.filter(funding_record=fr).count() == 2


# ---------------------------------------------------------------------------
# Call withdrawal (FRFA-CS030)
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_withdraw_call_requires_reason(grants_manager_user):
    call = GrantCall.objects.create(
        title="To withdraw",
        slug="to-withdraw",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )
    with pytest.raises(ValidationError):
        withdraw_call(call, reason="   ", actor=grants_manager_user)


@pytest.mark.django_db
def test_withdraw_call_flips_status_expires_drafts_and_notifies(
    applicant_user, institution, grants_manager_user
):
    call = GrantCall.objects.create(
        title="Withdraw flow",
        slug="withdraw-flow",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )
    Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    Notification.objects.all().delete()
    withdraw_call(call, reason="Donor pulled funding.", actor=grants_manager_user)
    # Re-fetch via objects.get rather than refresh_from_db, because GrantCall
    # status is an FSMField with protected=True which the descriptor refuses
    # to set on an existing in-memory instance.
    call = GrantCall.objects.get(pk=call.pk)
    assert call.status == GrantCall.Status.WITHDRAWN
    assert call.withdrawal_reason == "Donor pulled funding."
    assert call.withdrawn_by_id == grants_manager_user.pk
    # Draft application is expired.
    app = Application.objects.get(call=call, applicant=applicant_user)
    assert app.status == Application.Status.EXPIRED_DRAFT
    # Applicant notified with the reason.
    assert Notification.objects.filter(
        recipient=applicant_user, message__icontains="Donor pulled funding"
    ).exists()


# ---------------------------------------------------------------------------
# MilestoneEvidence (FRFA-AA015)
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_milestone_evidence_links_to_tranche(applicant_user, institution):
    from django.core.files.uploadedfile import SimpleUploadedFile

    from apps.rims.grants.models import Award

    call = GrantCall.objects.create(
        title="Tranche call",
        slug="tranche-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    award = Award.objects.create(
        application=app,
        amount=Decimal("1000"),
        currency="USD",
        project_end_date=date.today() + timedelta(days=180),
    )
    tranche = AwardTranche.objects.create(award=award, label="T1", amount=Decimal("500"))
    ev = MilestoneEvidence.objects.create(
        tranche=tranche,
        file=SimpleUploadedFile("evidence.pdf", b"%PDF-1.4 fake"),
        summary="Q1 deliverables complete",
        submitted_by=applicant_user,
    )
    assert ev.review_status == MilestoneEvidence.ReviewStatus.SUBMITTED
    assert tranche.evidence.count() == 1


# ---------------------------------------------------------------------------
# Reporting calendar (FRFA-GPS015)
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_generate_reporting_calendar_produces_quarterly_entries():
    fr = FundingRecord.objects.create(
        title="Calendar test",
        amount=Decimal("10000"),
        start_date=date(2026, 1, 1),
        end_date=date(2026, 12, 31),
        reporting_financial_frequency=FundingRecord.ReportingFrequency.QUARTERLY,
        reporting_narrative_frequency=FundingRecord.ReportingFrequency.SEMI_ANNUAL,
    )
    created = generate_reporting_calendar(fr)
    # Quarterly over a year ≈ 4 entries; semi-annual ≈ 2. We allow some
    # rounding due to the day-step approximation but require both kinds.
    fin = ReportingCalendarEntry.objects.filter(
        funding_record=fr, kind=ReportingCalendarEntry.Kind.FINANCIAL
    )
    nar = ReportingCalendarEntry.objects.filter(
        funding_record=fr, kind=ReportingCalendarEntry.Kind.NARRATIVE
    )
    assert fin.count() >= 3
    assert nar.count() >= 1
    assert len(created) == fin.count() + nar.count()


@pytest.mark.django_db
def test_generate_reporting_calendar_is_idempotent():
    fr = FundingRecord.objects.create(
        title="Idem",
        amount=Decimal("10000"),
        start_date=date(2026, 1, 1),
        end_date=date(2026, 12, 31),
        reporting_financial_frequency=FundingRecord.ReportingFrequency.SEMI_ANNUAL,
        reporting_narrative_frequency=FundingRecord.ReportingFrequency.ANNUAL,
    )
    first = generate_reporting_calendar(fr)
    second = generate_reporting_calendar(fr)
    assert len(second) == 0  # All entries already exist; nothing new created.
    assert ReportingCalendarEntry.objects.filter(funding_record=fr).count() == len(first)


@pytest.mark.django_db
def test_generate_reporting_calendar_skips_ad_hoc():
    fr = FundingRecord.objects.create(
        title="Ad-hoc",
        amount=Decimal("10000"),
        start_date=date(2026, 1, 1),
        end_date=date(2026, 12, 31),
        reporting_financial_frequency=FundingRecord.ReportingFrequency.AD_HOC,
        reporting_narrative_frequency=FundingRecord.ReportingFrequency.AD_HOC,
    )
    created = generate_reporting_calendar(fr)
    assert created == []
    assert not ReportingCalendarEntry.objects.filter(funding_record=fr).exists()
