"""PRD §5.1 FRFA-CS016 — per-call-type required admin docs enforced at publish.
PRD §5.1 FRFA-CS020 — per-call submission settings (size + formats) enforced on applicant upload.
"""
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.rims.grants.funding import validate_call_publishable
from apps.rims.grants.models import EligibilityRule, GrantCall, GrantCallDocument
from apps.rims.grants.services import validate_grant_upload


# Valid PDF magic bytes — the upload validator runs a libmagic check, so
# placeholder content must look like a real PDF for the per-call CS020 size /
# format checks to be the ones that fire.
_PDF_HEAD = b"%PDF-1.4\n%\xc3\xa4\xc3\xb6\n"


def _pdf_bytes(filler_size: int = 0) -> bytes:
    return _PDF_HEAD + (b"0" * filler_size) + b"\n%%EOF\n"


def _published_ready_grant_call(**kwargs):
    """Build a grant call that's one document away from publishable."""
    call = GrantCall.objects.create(
        title="Test grant",
        slug="test-grant-cs016",
        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"]},
    )
    return call


def _add_doc(call, kind, label=None):
    GrantCallDocument.objects.create(
        call=call,
        label=label or kind.replace("_", " ").title(),
        doc_kind=kind,
        is_guideline=(kind == "guidelines"),
        file=SimpleUploadedFile(f"{kind}.pdf", b"dummy", content_type="application/pdf"),
    )


@pytest.mark.django_db
def test_publish_blocks_grant_call_missing_required_doc_kinds():
    """A Grant call needs guidelines + proposal_template + budget_template + tc."""
    call = _published_ready_grant_call()
    _add_doc(call, "guidelines")  # only one of four

    with pytest.raises(ValidationError) as excinfo:
        validate_call_publishable(call)
    msg = " ".join(excinfo.value.messages)
    assert "Proposal template" in msg
    assert "Budget template" in msg
    assert "Terms & conditions" in msg


@pytest.mark.django_db
def test_publish_allows_grant_call_with_all_required_doc_kinds():
    call = _published_ready_grant_call()
    _add_doc(call, "guidelines")
    _add_doc(call, "proposal_template")
    _add_doc(call, "budget_template")
    _add_doc(call, "tc")
    # Add budget_ceiling-less, no funding_record path — should now pass.
    validate_call_publishable(call)


@pytest.mark.django_db
def test_publish_blocks_fellowship_call_missing_required_doc_kinds():
    call = GrantCall.objects.create(
        title="Test fellowship",
        slug="test-fellowship-cs016",
        call_type=GrantCall.CallType.FELLOWSHIP,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=30),
    )
    EligibilityRule.objects.create(
        call=call,
        rule_type=EligibilityRule.RuleType.DEGREE,
        params={"levels": ["phd"]},
    )
    _add_doc(call, "guidelines")  # missing fellowship_terms + host_agreement

    with pytest.raises(ValidationError) as excinfo:
        validate_call_publishable(call)
    msg = " ".join(excinfo.value.messages)
    assert "Fellowship terms" in msg
    assert "Host institution agreement" in msg


@pytest.mark.django_db
def test_validate_grant_upload_rejects_oversize_for_call():
    call = GrantCall.objects.create(
        title="Tiny limit",
        slug="tiny-limit-cs020",
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=10),
        applicant_attachment_max_size_mb=1,  # 1 MB cap
        applicant_attachment_formats="pdf",
    )
    big_file = SimpleUploadedFile("big.pdf", _pdf_bytes(2 * 1024 * 1024), content_type="application/pdf")
    with pytest.raises(ValidationError) as excinfo:
        validate_grant_upload(big_file, call=call)
    assert "1 MB" in " ".join(excinfo.value.messages)


@pytest.mark.django_db
def test_validate_grant_upload_rejects_disallowed_format_for_call():
    call = GrantCall.objects.create(
        title="Pdf only",
        slug="pdf-only-cs020",
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=10),
        applicant_attachment_max_size_mb=50,
        applicant_attachment_formats="pdf",  # docx not allowed
    )
    docx = SimpleUploadedFile("essay.docx", b"docx-bytes", content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")
    with pytest.raises(ValidationError) as excinfo:
        validate_grant_upload(docx, call=call)
    assert ".docx" in " ".join(excinfo.value.messages)


@pytest.mark.django_db
def test_validate_grant_upload_accepts_allowed_format_for_call():
    call = GrantCall.objects.create(
        title="Pdf only ok",
        slug="pdf-only-ok-cs020",
        call_type=GrantCall.CallType.GRANT,
        opens_at=timezone.now(),
        closes_at=timezone.now() + timedelta(days=10),
        applicant_attachment_max_size_mb=50,
        applicant_attachment_formats="pdf,docx",
    )
    pdf = SimpleUploadedFile("essay.pdf", _pdf_bytes(), content_type="application/pdf")
    # No exception means acceptance.
    validate_grant_upload(pdf, call=call)


@pytest.mark.django_db
def test_validate_grant_upload_no_call_falls_back_to_defaults():
    """When called without a call (e.g. admin uploads), only the module-level
    UploadHandler validation runs — per-call caps don't apply."""
    pdf = SimpleUploadedFile("admin.pdf", _pdf_bytes(), content_type="application/pdf")
    validate_grant_upload(pdf)  # no call kwarg
