"""PRD §5.1 FRFA-AA002 / FRFA-AA003 — award letter PDF generation.

The PDF generator renders the ``grants/award_letter.html`` template through
WeasyPrint and stores the bytes on ``Award.letter_pdf_file``. The Award
post_save signal generates the PDF on creation and attaches it to the
awardee notification via the ``attachment_from_model`` plumbing in
``apps.core.notifications.emailing``.
"""

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.models import (
    Application,
    Award,
    FundingRecord,
    GrantCall,
    StrategicObjective,
)
from apps.rims.grants.services import (
    award_application,
    generate_award_letter_pdf,
    shortlist_application,
    submit_application,
)


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


def _award(applicant_user, institution, grants_manager_user, *, suffix: str) -> Award:
    fr = FundingRecord.objects.create(
        title=f"PDF FR {suffix}",
        amount=Decimal("10000"),
        start_date=date.today() - timedelta(days=10),
        end_date=date.today() + timedelta(days=365),
        grant_manager=grants_manager_user,
    )
    obj = StrategicObjective.objects.create(name=f"Objective {suffix}", slug=f"obj-{suffix}")
    fr.strategic_objective_refs.add(obj)
    call = GrantCall.objects.create(
        title=f"PDF Call {suffix}",
        slug=f"pdf-{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=180))
    return Application.objects.get(pk=app.pk).award


@pytest.mark.django_db
def test_generate_award_letter_returns_non_empty_pdf_bytes(
    applicant_user, institution, grants_manager_user
):
    award = _award(applicant_user, institution, grants_manager_user, suffix="bytes")
    # Reset the field so the helper exercises the full code path.
    Award.objects.filter(pk=award.pk).update(letter_pdf_file=None, letter_generated_at=None)
    award = Award.objects.get(pk=award.pk)
    pdf = generate_award_letter_pdf(award)
    assert isinstance(pdf, bytes)
    assert pdf.startswith(b"%PDF-")
    assert len(pdf) > 1000
    award = Award.objects.get(pk=award.pk)
    assert award.letter_pdf_file.name
    assert award.letter_generated_at is not None


@pytest.mark.django_db
def test_award_creation_attaches_letter_to_notification_context(
    applicant_user, institution, grants_manager_user
):
    captured: list[tuple] = []

    def _fake(notification_id, channel, context):
        captured.append((notification_id, channel, dict(context)))

    # Override the autouse fixture's mock so we can capture context.
    with patch(
        "apps.core.notifications.services.dispatch_notification_channel.delay",
        side_effect=_fake,
    ):
        award = _award(applicant_user, institution, grants_manager_user, suffix="attach")
    # The award post_save dispatched the GRANT_AWARDED email with an
    # attachment spec pointing at letter_pdf_file.
    grant_awarded = [
        ctx for _, _, ctx in captured if ctx.get("subject", "").startswith("Award:")
    ]
    assert grant_awarded, f"no GRANT_AWARDED email queued: {[c.get('subject') for _,_,c in captured]}"
    spec = grant_awarded[-1].get("attachment_from_model")
    assert spec is not None
    assert spec["app_label"] == "rims_grants"
    assert spec["model_name"] == "Award"
    assert spec["pk"] == award.pk
    assert spec["filename"].endswith(".pdf")
    assert award.letter_pdf_file.name


@pytest.mark.django_db
def test_award_creation_dispatches_grant_awarded_notification(
    applicant_user, institution, grants_manager_user
):
    award = _award(applicant_user, institution, grants_manager_user, suffix="ntf")
    assert Notification.objects.filter(
        recipient=applicant_user,
        verb=Notification.Verb.GRANT_AWARDED,
        message__icontains="award",
    ).exists()
    award = Award.objects.get(pk=award.pk)
    assert award.letter_pdf_file.name


@pytest.mark.django_db
def test_letter_template_renders_for_award_without_strategic_objectives(
    applicant_user, institution, grants_manager_user
):
    """Funding records may have no strategic objectives linked yet — the
    letter template must still render without errors."""
    fr = FundingRecord.objects.create(
        title="No-obj FR",
        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="No-obj call",
        slug="pdf-no-obj",
        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=180))
    award = Application.objects.get(pk=app.pk).award
    assert award.letter_pdf_file.name
    assert award.letter_pdf_file.size > 0
