"""PRD §5.1 FRFA-CS022/CS023/CS024/CS025 — auto-close confirmation, notification config,
publication dispatch, and configurable deadline reminders.
"""
from datetime import timedelta
from unittest.mock import patch

import pytest
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils import timezone

from apps.core.notifications.models import Notification
from apps.rims.grants.models import (
    Application,
    CallNotificationConfig,
    EligibilityRule,
    GrantCall,
    GrantCallDocument,
)
from apps.rims.grants.services import publish_call, submit_application
from apps.rims.grants.tasks import remind_grant_call_deadlines


_PDF = b"%PDF-1.4\n0\n%%EOF\n"


def _publish_ready_call(slug, **kwargs):
    call = GrantCall.objects.create(
        title=f"Call {slug}",
        slug=slug,
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=30),
        **kwargs,
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.DEGREE,
        params={"levels": ["masters"]},
    )
    for kind in ("guidelines", "proposal_template", "budget_template", "tc"):
        GrantCallDocument.objects.create(
            call=call,
            label=kind,
            doc_kind=kind,
            is_guideline=(kind == "guidelines"),
            file=SimpleUploadedFile(f"{kind}.pdf", _PDF, content_type="application/pdf"),
        )
    return call


@pytest.mark.django_db
@patch("apps.rims.grants.tasks.notify_call_published.delay")
def test_publish_blocks_without_auto_close_acknowledgement(_mock, grants_manager_user):
    """PRD FRFA-CS022 — publish blocked when the admin did not tick the acknowledge box."""
    call = _publish_ready_call("publish-cs022-block")
    with pytest.raises(ValidationError) as excinfo:
        publish_call(call, actor=grants_manager_user, auto_close_acknowledged=False)
    assert "auto-close" in " ".join(excinfo.value.messages).lower()
    call = GrantCall.objects.get(pk=call.pk)
    assert call.status == GrantCall.Status.DRAFT


@pytest.mark.django_db
@patch("apps.rims.grants.tasks.notify_call_published.delay")
def test_publish_succeeds_when_acknowledged_and_stamps_timestamp(mock_notify, grants_manager_user):
    call = _publish_ready_call("publish-cs022-ok")
    publish_call(call, actor=grants_manager_user, auto_close_acknowledged=True)
    call = GrantCall.objects.get(pk=call.pk)
    assert call.status == GrantCall.Status.PUBLISHED
    assert call.auto_close_confirmed_at is not None
    mock_notify.assert_called_once_with(call.pk)


@pytest.mark.django_db
@patch("apps.rims.grants.tasks.notify_call_published.delay")
def test_publish_skips_notification_when_disabled_in_config(mock_notify, grants_manager_user):
    """PRD FRFA-CS023/CS024 — publication notifications honour per-call config."""
    call = _publish_ready_call("publish-cs023-nonotify")
    CallNotificationConfig.objects.create(call=call, publication_notify_enabled=False)
    publish_call(call, actor=grants_manager_user, auto_close_acknowledged=True)
    call = GrantCall.objects.get(pk=call.pk)
    assert call.status == GrantCall.Status.PUBLISHED
    mock_notify.assert_not_called()


@pytest.mark.django_db
def test_call_notification_config_default_intervals():
    call = _publish_ready_call("cfg-default-intervals")
    cfg = CallNotificationConfig.get_for_call(call)
    # Returned virtual config falls back to defaults.
    assert cfg.get_reminder_intervals() == [7, 1]


@pytest.mark.django_db
def test_call_notification_config_custom_intervals_sorted_desc_deduped():
    call = _publish_ready_call("cfg-custom-intervals")
    cfg = CallNotificationConfig.objects.create(
        call=call,
        reminder_intervals_days=[3, 7, 3, "1", 0, -2, "bad"],
    )
    # Coerce, dedup, drop non-positive / non-int, sort descending.
    assert cfg.get_reminder_intervals() == [7, 3, 1]


@pytest.mark.django_db
def test_acknowledgement_disabled_skips_submission_notification(applicant_user, institution):
    call = _publish_ready_call("ack-disabled-cs023")
    publish_call(call, auto_close_acknowledged=True)
    CallNotificationConfig.objects.create(call=call, acknowledgement_enabled=False)
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    # No APPLICATION_RECEIVED_ACK notification fires when the toggle is off.
    assert not Notification.objects.filter(
        recipient=applicant_user,
        verb=Notification.Verb.APPLICATION_RECEIVED_ACK,
    ).exists()


@pytest.mark.django_db
def test_acknowledgement_enabled_fires_with_dedicated_verb(applicant_user, institution):
    call = _publish_ready_call("ack-enabled-cs023")
    publish_call(call, auto_close_acknowledged=True)
    # Default config (no row) → acknowledgement_enabled is True virtually.
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    assert Notification.objects.filter(
        recipient=applicant_user,
        verb=Notification.Verb.APPLICATION_RECEIVED_ACK,
        message__icontains="received",
    ).exists()


@pytest.mark.django_db
def test_reminder_uses_configured_intervals_and_dedups_within_24h(
    applicant_user, institution
):
    """PRD FRFA-CS025 — reminders fire at configured intervals; same-day dupes are skipped."""
    # Call closes in ~7 days; configured interval [7] should match.
    closes_at = timezone.now() + timedelta(days=7)
    call = GrantCall.objects.create(
        title="Cs025 reminder",
        slug="cs025-reminder",
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=closes_at,
    )
    EligibilityRule.objects.create(
        call=call, rule_type=EligibilityRule.RuleType.DEGREE, params={"levels": ["masters"]}
    )
    GrantCall.objects.filter(pk=call.pk).update(status=GrantCall.Status.PUBLISHED)
    call = GrantCall.objects.get(pk=call.pk)
    CallNotificationConfig.objects.create(call=call, reminder_intervals_days=[7])

    Application.objects.create(
        call=call, applicant=applicant_user, institution=institution, status=Application.Status.DRAFT
    )

    remind_grant_call_deadlines()
    first = Notification.objects.filter(
        recipient=applicant_user, verb=Notification.Verb.DEADLINE_REMINDER
    ).count()
    assert first == 1

    # Re-run the same task within the dedup window — count must not increase.
    remind_grant_call_deadlines()
    second = Notification.objects.filter(
        recipient=applicant_user, verb=Notification.Verb.DEADLINE_REMINDER
    ).count()
    assert second == 1


@pytest.mark.django_db
def test_reminder_skipped_when_interval_does_not_match(applicant_user, institution):
    """Closes in 30 days, intervals [7,1] → no reminder fires today."""
    call = GrantCall.objects.create(
        title="Cs025 nomatch",
        slug="cs025-nomatch",
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=30),
    )
    EligibilityRule.objects.create(
        call=call, rule_type=EligibilityRule.RuleType.DEGREE, params={"levels": ["masters"]}
    )
    GrantCall.objects.filter(pk=call.pk).update(status=GrantCall.Status.PUBLISHED)
    call = GrantCall.objects.get(pk=call.pk)
    CallNotificationConfig.objects.create(call=call, reminder_intervals_days=[7, 1])

    Application.objects.create(
        call=call, applicant=applicant_user, institution=institution, status=Application.Status.DRAFT
    )

    remind_grant_call_deadlines()
    assert not Notification.objects.filter(
        recipient=applicant_user, verb=Notification.Verb.DEADLINE_REMINDER
    ).exists()
