"""PRD §5.1 FRFA-CS028 — restrict edits on PUBLISHED calls to non-critical fields.
PRD §5.1 FRFA-CS029 — extend submission deadline service + view.
PRD §5.1 FRFA-CS033 — version history snapshot on every governed mid-flight edit.
"""
from datetime import timedelta

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.forms import GrantCallActiveEditForm, GrantCallForm
from apps.rims.grants.models import (
    Application,
    EligibilityRule,
    GrantCall,
    GrantCallDocument,
    GrantCallVersion,
)
from apps.rims.grants.services import extend_call_deadline, snapshot_call_version


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


def _draft_call(slug, closes_in_days=30):
    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=closes_in_days),
    )
    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


def _published_call(slug, **kwargs):
    call = _draft_call(slug, **kwargs)
    GrantCall.objects.filter(pk=call.pk).update(status=GrantCall.Status.PUBLISHED)
    return GrantCall.objects.get(pk=call.pk)


# ---------------------------------------------------------------------------
# CS028 — active-call edit restriction
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_active_edit_form_only_exposes_non_critical_fields():
    """PRD FRFA-CS028 — only description + guidelines are editable on a PUBLISHED call."""
    call = _published_call("cs028-fields")
    form = GrantCallActiveEditForm(instance=call)
    assert set(form.fields.keys()) == {"description", "guidelines"}


@pytest.mark.django_db
def test_active_edit_form_save_persists_description_change():
    call = _published_call("cs028-persist")
    form = GrantCallActiveEditForm(
        instance=call,
        data={"description": "Revised description.", "guidelines": call.guidelines},
    )
    assert form.is_valid(), form.errors
    form.save()
    call = GrantCall.objects.get(pk=call.pk)
    assert call.description == "Revised description."


@pytest.mark.django_db
def test_draft_calls_still_use_full_form():
    """A DRAFT call uses the full GrantCallForm — call_type, dates, etc., remain editable."""
    call = _draft_call("cs028-draft-full")
    form = GrantCallForm(instance=call)
    assert "call_type" in form.fields
    assert "closes_at" in form.fields


# ---------------------------------------------------------------------------
# CS029 — deadline extension
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_extend_call_deadline_happy_path(grants_manager_user):
    call = _published_call("cs029-happy")
    new_deadline = call.closes_at + timedelta(days=14)
    extend_call_deadline(call, new_deadline, actor=grants_manager_user)
    call = GrantCall.objects.get(pk=call.pk)
    assert call.closes_at == new_deadline


@pytest.mark.django_db
def test_extend_call_deadline_rejects_earlier_date(grants_manager_user):
    call = _published_call("cs029-earlier")
    earlier = call.closes_at - timedelta(days=1)
    with pytest.raises(ValidationError):
        extend_call_deadline(call, earlier, actor=grants_manager_user)


@pytest.mark.django_db
def test_extend_call_deadline_rejects_when_review_period_collides(grants_manager_user):
    call = _published_call("cs029-review-collision")
    GrantCall.objects.filter(pk=call.pk).update(
        review_starts_at=call.closes_at + timedelta(days=2)
    )
    call = GrantCall.objects.get(pk=call.pk)
    too_far = call.closes_at + timedelta(days=5)  # past review_starts_at
    with pytest.raises(ValidationError):
        extend_call_deadline(call, too_far, actor=grants_manager_user)


@pytest.mark.django_db
def test_extend_call_deadline_rejects_on_draft_call(grants_manager_user):
    call = _draft_call("cs029-draft-reject")
    with pytest.raises(ValidationError):
        extend_call_deadline(
            call, call.closes_at + timedelta(days=14), actor=grants_manager_user
        )


@pytest.mark.django_db
def test_extend_call_deadline_notifies_in_progress_applicants(
    grants_manager_user, applicant_user, institution
):
    call = _published_call("cs029-notify-applicant")
    Application.objects.create(
        call=call, applicant=applicant_user, institution=institution, status=Application.Status.DRAFT
    )
    extend_call_deadline(
        call, call.closes_at + timedelta(days=7), actor=grants_manager_user
    )
    assert Notification.objects.filter(
        recipient=applicant_user, verb=Notification.Verb.DEADLINE_REMINDER
    ).exists()


# ---------------------------------------------------------------------------
# CS033 — version history snapshot
# ---------------------------------------------------------------------------
@pytest.mark.django_db
def test_snapshot_call_version_writes_row_with_state(grants_manager_user):
    call = _published_call("cs033-snapshot")
    v = snapshot_call_version(
        call, actor=grants_manager_user, change_summary="Initial test snapshot"
    )
    assert v.version_number == 1
    assert v.snapshot["title"] == call.title
    assert v.snapshot["closes_at"] == call.closes_at.isoformat()
    assert v.change_summary == "Initial test snapshot"
    assert v.created_by == grants_manager_user


@pytest.mark.django_db
def test_extend_call_deadline_writes_a_version_row(grants_manager_user):
    call = _published_call("cs033-via-extend")
    extend_call_deadline(
        call, call.closes_at + timedelta(days=14), actor=grants_manager_user
    )
    assert GrantCallVersion.objects.filter(call=call).count() == 1
    v = GrantCallVersion.objects.get(call=call)
    assert "Deadline extended" in v.change_summary


@pytest.mark.django_db
def test_snapshot_call_version_numbers_increment_per_call(grants_manager_user):
    call = _published_call("cs033-increment")
    snapshot_call_version(call, actor=grants_manager_user, change_summary="first")
    snapshot_call_version(call, actor=grants_manager_user, change_summary="second")
    snapshot_call_version(call, actor=grants_manager_user, change_summary="third")
    nums = list(
        GrantCallVersion.objects.filter(call=call)
        .order_by("version_number")
        .values_list("version_number", flat=True)
    )
    assert nums == [1, 2, 3]
