"""PRD §5.1 FRFA-AM004 — application form auto-save endpoint.

Covers:
  - Persists partial fields on a draft Application.
  - Owner-only writes (non-owners get 403).
  - Locked / submitted applications reject auto-save with 409.
  - Calls past their deadline reject auto-save with 409.
"""

from __future__ import annotations

import json
from datetime import timedelta
from unittest.mock import patch

import pytest
from django.urls import reverse
from django.utils import timezone

from apps.rims.grants.models import Application, GrantCall


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


@pytest.fixture
def published_call(db):
    return GrantCall.objects.create(
        title="Auto-save call",
        slug="autosave",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=14),
        status=GrantCall.Status.PUBLISHED,
    )


@pytest.fixture
def closed_call(db):
    return GrantCall.objects.create(
        title="Closed call",
        slug="autosave-closed",
        opens_at=timezone.now() - timedelta(days=20),
        closes_at=timezone.now() - timedelta(days=1),
        status=GrantCall.Status.PUBLISHED,
    )


@pytest.mark.django_db
def test_autosave_creates_draft_and_persists_proposal(client, published_call, applicant_user):
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    resp = client.post(
        url,
        data=json.dumps({"proposal": "First draft sentence."}),
        content_type="application/json",
    )
    assert resp.status_code == 200
    body = resp.json()
    assert body["ok"] is True
    app = Application.objects.get(call=published_call, applicant=applicant_user)
    assert app.proposal == "First draft sentence."
    assert app.status == Application.Status.DRAFT


@pytest.mark.django_db
def test_autosave_updates_existing_draft(client, published_call, applicant_user, institution):
    Application.objects.create(call=published_call, applicant=applicant_user, institution=institution, proposal="v1")
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    resp = client.post(url, data=json.dumps({"proposal": "v2"}), content_type="application/json")
    assert resp.status_code == 200
    app = Application.objects.get(call=published_call, applicant=applicant_user)
    assert app.proposal == "v2"


@pytest.mark.django_db
def test_autosave_rejects_anonymous(client, published_call):
    url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    resp = client.post(url, data="{}", content_type="application/json")
    # LoginRequiredMixin redirects to login for unauthenticated users.
    assert resp.status_code in (302, 403)


@pytest.mark.django_db
def test_autosave_blocks_after_deadline(client, closed_call, applicant_user):
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_autosave", kwargs={"slug": closed_call.slug})
    resp = client.post(url, data=json.dumps({"proposal": "x"}), content_type="application/json")
    assert resp.status_code == 409
    assert resp.json()["error"] == "deadline_passed"


@pytest.mark.django_db
def test_autosave_blocks_locked_application(client, published_call, applicant_user, institution):
    app = Application.objects.create(
        call=published_call,
        applicant=applicant_user,
        institution=institution,
    )
    # Move out of DRAFT via a direct transition.
    app.submit()
    app.save(update_fields=["status", "submitted_at"])
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    resp = client.post(url, data=json.dumps({"proposal": "later edit"}), content_type="application/json")
    assert resp.status_code == 409
    assert resp.json()["error"] == "locked"


@pytest.mark.django_db
def test_autosave_rejects_invalid_json(client, published_call, applicant_user):
    client.force_login(applicant_user)
    url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    resp = client.post(url, data="not-json", content_type="application/json")
    assert resp.status_code == 400


@pytest.mark.django_db
def test_full_submit_reuses_autosave_draft(client, published_call, applicant_user, institution):
    """Regression: a full submit must reuse the draft that auto-save created.

    Auto-save persists a DRAFT Application for (call, applicant). The subsequent
    full-form POST to ApplyCallView used to build a *new* Application and hit the
    ``uniq_application_per_call_user`` constraint — an unhandled IntegrityError
    surfaced to the applicant as a 500 on the primary submit path.
    """
    client.force_login(applicant_user)

    autosave_url = reverse("rims_grants:application_autosave", kwargs={"slug": published_call.slug})
    client.post(
        autosave_url,
        data=json.dumps({"proposal": "Working draft."}),
        content_type="application/json",
    )
    assert Application.objects.filter(call=published_call, applicant=applicant_user).count() == 1

    apply_url = reverse("rims_grants:call_apply", kwargs={"slug": published_call.slug})
    proposal = "Climate-smart cassava intensification pilot across three cooperatives. " * 4
    assert len(proposal) >= 200
    resp = client.post(apply_url, data={"institution": institution.pk, "proposal": proposal})

    # Redirects to the application record — must NOT 500 on the duplicate key.
    assert resp.status_code == 302
    apps = Application.objects.filter(call=published_call, applicant=applicant_user)
    assert apps.count() == 1
    assert apps.first().status != Application.Status.DRAFT


@pytest.mark.django_db
def test_apply_get_reopens_draft_form(client, published_call, applicant_user, institution):
    """A DRAFT (e.g. from auto-save) must re-open the apply form, not redirect to
    the detail page — which offers no way to finish submitting a draft."""
    Application.objects.create(
        call=published_call,
        applicant=applicant_user,
        institution=institution,
        proposal="work in progress",
        status=Application.Status.DRAFT,
    )
    client.force_login(applicant_user)
    apply_url = reverse("rims_grants:call_apply", kwargs={"slug": published_call.slug})
    resp = client.get(apply_url)
    assert resp.status_code == 200
