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

import pytest
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.utils import timezone

from apps.rims.grants.models import Application, EligibilityRule, GrantCall, GrantCallDocument, Review
from apps.rims.grants.services import (
    award_application,
    close_grant_call,
    publish_call,
    shortlist_application,
    submit_application,
    submit_review,
)


def _seed_publish_prereqs(call: GrantCall) -> None:
    if not call.eligibility_rules.exists():
        EligibilityRule.objects.create(
            call=call,
            rule_type=EligibilityRule.RuleType.NATIONALITY,
            params={"countries": []},
            is_blocking=True,
        )
    # PRD §5.1 FRFA-CS016 — required doc_kinds per call type. Grant requires
    # guidelines + proposal_template + budget_template + tc; cover all four.
    required = {
        GrantCall.CallType.GRANT: ("guidelines", "proposal_template", "budget_template", "tc"),
        GrantCall.CallType.SCHOLARSHIP: ("guidelines", "instructions", "tc"),
        GrantCall.CallType.FELLOWSHIP: ("guidelines", "fellowship_terms", "host_agreement"),
        GrantCall.CallType.CHALLENGE: ("guidelines", "innovation_brief", "judging_criteria", "tc"),
    }
    needed = required.get(call.call_type, ("guidelines",))
    present = set(call.documents.values_list("doc_kind", flat=True))
    for kind in needed:
        if kind in present:
            continue
        GrantCallDocument.objects.create(
            call=call,
            label=kind.replace("_", " ").title(),
            doc_kind=kind,
            file=ContentFile(b"%PDF-1.4\n", name=f"{kind}.pdf"),
            is_guideline=(kind == "guidelines"),
        )


@pytest.mark.django_db
def test_submit_application_transitions(user, institution):
    call = GrantCall.objects.create(
        title="Test call",
        slug="test-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=30),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=user, institution=institution)
    submit_application(app)
    assert Application.objects.get(pk=app.pk).status == Application.Status.UNDER_REVIEW


@pytest.mark.django_db
@patch("apps.rims.grants.tasks.notify_call_published.delay")
def test_publish_call_transition(_mock_delay, user):
    call = GrantCall.objects.create(
        title="Draft",
        slug="draft-call",
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=10),
        created_by=user,
    )
    _seed_publish_prereqs(call)
    publish_call(call, actor=user, auto_close_acknowledged=True)
    assert GrantCall.objects.get(pk=call.pk).status == GrantCall.Status.PUBLISHED


@pytest.mark.django_db
def test_publish_call_blocked_without_eligibility():
    call = GrantCall.objects.create(
        title="No rules",
        slug="no-rules",
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=10),
    )
    GrantCallDocument.objects.create(
        call=call,
        label="G",
        file=ContentFile(b"%PDF-1.4\n", name="g.pdf"),
        is_guideline=True,
    )
    with pytest.raises(ValidationError):
        publish_call(call)


@pytest.mark.django_db
def test_award_moves_call_to_awarding(user, institution):
    call = GrantCall.objects.create(
        title="Award flow",
        slug="award-flow",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=30),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    shortlist_application(app)
    award_application(app, Decimal("5000.00"), date.today(), narrative="")
    assert Application.objects.get(pk=app.pk).status == Application.Status.AWARDED
    assert GrantCall.objects.get(pk=call.pk).status == GrantCall.Status.AWARDING


@pytest.mark.django_db
def test_submit_review_scores_are_json_serializable(user, institution):
    call = GrantCall.objects.create(
        title="Rubric",
        slug="rubric-call",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=user, institution=institution)
    submit_application(app)
    rev = Review.objects.create(application=app, reviewer=user)
    submit_review(
        rev,
        {"Scientific merit": Decimal("10.5"), "Impact": Decimal("5")},
        "ok",
    )
    rev = Review.objects.get(pk=rev.pk)
    assert rev.submitted_at is not None
    assert rev.scores == {"Scientific merit": 10.5, "Impact": 5.0}


@pytest.mark.django_db
def test_duplicate_award_raises_validation_error(user, institution):
    """PRD NFRFA001 — one award per application. A second call to
    award_application for the same application must raise ValidationError
    rather than surfacing the raw IntegrityError from the OneToOneField
    uniqueness constraint or a django-fsm TransitionNotAllowed."""
    call = GrantCall.objects.create(
        title="Dup award",
        slug="dup-award",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=30),
        status=GrantCall.Status.PUBLISHED,
    )
    app = Application.objects.create(call=call, applicant=user, institution=institution)
    submit_application(app)
    app = Application.objects.get(pk=app.pk)
    shortlist_application(app)
    award_application(app, Decimal("1000.00"), date.today() + timedelta(days=30), narrative="")
    # The first call has transitioned the application to AWARDED. A concurrent
    # duplicate (simulated by re-entering the service with the original in-memory
    # object) is caught by the select_for_update+exists guard and surfaces a
    # clean ValidationError.
    app_stale = Application.objects.get(pk=app.pk)
    with pytest.raises(ValidationError):
        award_application(
            app_stale,
            Decimal("1000.00"),
            date.today() + timedelta(days=30),
            narrative="",
        )


@pytest.mark.django_db
def test_close_grant_call_requires_awarding(user, institution):
    call = GrantCall.objects.create(
        title="Complete",
        slug="complete-call",
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=5),
        status=GrantCall.Status.AWARDING,
    )
    close_grant_call(call)
    assert GrantCall.objects.get(pk=call.pk).status == GrantCall.Status.COMPLETED
