"""PRD §5.1 — P3 validation bundle:

- FRFA-AA011 AwardTranche.clean() sum cap
- FRFA-AA010 Milestone.clean() due_date within project
- FRFA-AM024 scoring rubric validation on submit_review
- FRFA-CS006 GrantCall.call_type immutability after publish
- FRFA-CO014 close-out checklist evidence enforcement
- Award.currency must match FundingRecord.currency
"""
from datetime import date, timedelta
from decimal import Decimal

import pytest
from django.core.exceptions import ValidationError
from django.utils import timezone

from apps.rims.grants.closeout_helpers import assert_checklist_items_have_evidence
from apps.rims.grants.models import (
    Application,
    Award,
    AwardTranche,
    FundingRecord,
    GrantCall,
    Review,
)
from apps.rims.grants.services import (
    award_application,
    shortlist_application,
    submit_application,
    submit_review,
)
from apps.rims.projects.models import Milestone, Project


def _awarded(applicant_user, institution, *, suffix, fund_record=None, currency="USD"):
    call = GrantCall.objects.create(
        title="P3 call",
        slug=f"p3-{suffix}-{int(timezone.now().timestamp() * 1000) % 10_000_000}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
        funding_record=fund_record,
    )
    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),
        narrative="",
        currency=currency,
    )
    return Application.objects.get(pk=app.pk).award


# ---- FRFA-AA011 tranche sum ----

@pytest.mark.django_db
def test_award_tranche_sum_cannot_exceed_award_amount(applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="tranche")
    t1 = AwardTranche.objects.create(award=award, label="A", amount=Decimal("700"))
    t1.full_clean()
    t2 = AwardTranche(award=award, label="B", amount=Decimal("400"))
    with pytest.raises(ValidationError):
        t2.full_clean()


# ---- FRFA-AA010 milestone date bounds ----

@pytest.mark.django_db
def test_milestone_due_date_must_be_within_project(applicant_user, institution):
    award = _awarded(applicant_user, institution, suffix="milestone")
    project = Project.objects.create(
        award=award,
        title="Proj",
        summary="",
        start_date=date.today(),
        end_date=date.today() + timedelta(days=60),
        status=Project.Status.ACTIVE,
    )
    m = Milestone(
        project=project,
        name="Too late",
        due_date=date.today() + timedelta(days=120),
        status=Milestone.Status.PLANNED,
    )
    with pytest.raises(ValidationError):
        m.full_clean()

    m2 = Milestone(
        project=project,
        name="Too early",
        due_date=date.today() - timedelta(days=1),
        status=Milestone.Status.PLANNED,
    )
    with pytest.raises(ValidationError):
        m2.full_clean()


# ---- FRFA-AM024 scoring rubric ----

@pytest.mark.django_db
def test_submit_review_rejects_unknown_criteria(applicant_user, institution):
    call = GrantCall.objects.create(
        title="Rubric call",
        slug=f"rubric-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
        scoring_rubric=[
            {"criterion": "Merit", "max_points": 10},
            {"criterion": "Impact", "max_points": 5},
        ],
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    review = Review.objects.create(application=Application.objects.get(pk=app.pk), reviewer=applicant_user)
    with pytest.raises(ValidationError):
        submit_review(review, {"Merit": 5, "Creativity": 3}, "ok")


@pytest.mark.django_db
def test_submit_review_rejects_score_exceeding_max_points(applicant_user, institution):
    call = GrantCall.objects.create(
        title="Rubric max",
        slug=f"rubric-max-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
        scoring_rubric=[{"criterion": "Merit", "max_points": 10}],
    )
    app = Application.objects.create(call=call, applicant=applicant_user, institution=institution)
    submit_application(app)
    review = Review.objects.create(application=Application.objects.get(pk=app.pk), reviewer=applicant_user)
    with pytest.raises(ValidationError):
        submit_review(review, {"Merit": 11}, "ok")


# ---- FRFA-CS006 call_type immutability ----

@pytest.mark.django_db
def test_call_type_cannot_change_after_publish():
    call = GrantCall.objects.create(
        title="Lockable",
        slug=f"lock-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.PUBLISHED,
        call_type=GrantCall.CallType.GRANT,
    )
    call.call_type = GrantCall.CallType.SCHOLARSHIP
    # `clean()` is called because `full_clean()` trips django-fsm's protected
    # status field during field-level validation; the invariant we care about
    # lives in our own clean().
    with pytest.raises(ValidationError):
        call.clean()


@pytest.mark.django_db
def test_call_type_can_change_while_draft():
    call = GrantCall.objects.create(
        title="Draft",
        slug=f"draft-{int(timezone.now().timestamp())}",
        opens_at=timezone.now() - timedelta(days=1),
        closes_at=timezone.now() + timedelta(days=10),
        status=GrantCall.Status.DRAFT,
        call_type=GrantCall.CallType.GRANT,
    )
    call.call_type = GrantCall.CallType.SCHOLARSHIP
    call.clean()  # Should not raise.


# ---- FRFA-CO014 checklist evidence ----

def test_assert_checklist_items_have_evidence_flags_missing():
    checklist = [
        {"key": "final_narrative", "done": True, "evidence_note": ""},
        {"key": "asset_verification", "done": False, "evidence_note": ""},
    ]
    with pytest.raises(ValidationError):
        assert_checklist_items_have_evidence(checklist)


def test_assert_checklist_items_have_evidence_passes_when_all_have_notes():
    checklist = [
        {"key": "final_narrative", "done": True, "evidence_note": "Report OK"},
    ]
    assert_checklist_items_have_evidence(checklist)  # no raise


# ---- Award.currency == FundingRecord.currency ----

@pytest.mark.django_db
def test_award_currency_must_match_funding_record(applicant_user, institution):
    fr = FundingRecord.objects.create(
        title="FR",
        amount=Decimal("5000"),
        start_date=date.today(),
        end_date=date.today() + timedelta(days=90),
        currency="USD",
    )
    # Build a separate award whose currency mismatches by directly editing.
    award = _awarded(applicant_user, institution, suffix="ccy-ok", fund_record=fr, currency="USD")
    award.currency = "UGX"
    with pytest.raises(ValidationError):
        award.clean()
