"""Tests for the REPO-SP4/SP5/SP6 SRS gap-closure fixes (2026-07-06 audit).

Covers:
- AC003 — visibility field gated on DocumentEditView / DocumentMetadataEditForm.
- AC005 — in-app group creation + member management.
- AC006 — document-aware 403 on direct download/raw + access-denial notification.
- AC007 — restricted .docx downloads are watermarked (footer notice).
- VL005 — retention advance-notice, ahead of the cutoff.
- VL006 — retention_action actually branches (archive / withdraw / flag).
- VL009 — GrantClosureNotifyView.
- VL010 — audit_repository calls on grant-closure + retention actions.
- QA001 — pre-check failures (missing fields, invalid ORCID) surfaced to the submitter.
- QA006 — revise-and-resubmit loop + reviewer comments in the notification.
- QA011 — M&EL payload includes author institution.
- QA012 — CollectionUpdateView logs policy-field diffs.
"""

from __future__ import annotations

import io
from datetime import timedelta

import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client
from django.urls import reverse
from django.utils import timezone

from apps.core.audit import tasks as audit_tasks
from apps.core.audit.models import AuditLog
from apps.core.authentication.models import Institution
from apps.core.notifications.models import Notification
from apps.repository.documents import services, tasks
from apps.repository.documents.forms import DocumentMetadataEditForm
from apps.repository.documents.models import (
    Author,
    Collection,
    Document,
    IntegrationOutbox,
    QARecord,
)

User = get_user_model()


@pytest.fixture(autouse=True)
def sync_audit(monkeypatch):
    """Audit writes normally go through Celery — make them synchronous."""

    def _eager_delay(**kwargs):
        return audit_tasks.persist_audit_log(**kwargs)

    monkeypatch.setattr(audit_tasks.write_audit_log, "delay", _eager_delay)


@pytest.fixture
def outsider_user(db):
    return User.objects.create_user(email="ac006.outsider@test.example", password="pw-test-1234")


@pytest.fixture
def admin_user(db):
    u = User.objects.create_user(email="repo.admin@test.example", password="pw-test-1234")
    u.is_superuser = True
    u.save()
    return u


def _pdf_bytes() -> bytes:
    return b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj<</Type/Catalog>>endobj\ntrailer<<>>\n%%EOF\n"


def _docx_bytes() -> bytes:
    from docx import Document as DocxDocument

    d = DocxDocument()
    d.add_paragraph("Body text.")
    buf = io.BytesIO()
    d.save(buf)
    return buf.getvalue()


def _publish(doc, *, manager):
    services.submit_for_qa(doc, actor=doc.submitted_by)
    services.decide_qa(
        doc.qa_records.first(),
        decision="approve",
        actor=manager,
        checklist=services.full_qa_checklist(),
    )


# ── AC003 — visibility gating ────────────────────────────────────────────


@pytest.mark.django_db
def test_ac003_form_hides_visibility_when_gated():
    form = DocumentMetadataEditForm(can_set_visibility=False)
    assert "visibility" not in form.fields


@pytest.mark.django_db
def test_ac003_form_keeps_visibility_by_default():
    form = DocumentMetadataEditForm()
    assert "visibility" in form.fields


@pytest.mark.django_db(transaction=True)
def test_ac003_submitter_cannot_escalate_own_document_visibility(
    submitter_user, manager_user, restricted_collection, sample_pdf
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="AC003 submitter",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        authors=[Author.objects.create(first_name="A", last_name="Submitter")],
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    client = Client()
    client.force_login(submitter_user)
    resp = client.post(
        reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}),
        {
            "title": doc.title,
            "abstract": doc.abstract,
            "document_type": doc.document_type,
            "license_type": doc.license_type,
            "license_notes": "",
            "language": "en",
            "year": doc.year or "",
            "doi": "",
            # Tampered — this user has no `visibility` field in their form.
            "visibility": Document.Visibility.PUBLIC_OPEN,
            "grant_reference": "",
            "authors_text": services.render_authors_text(doc),
            "edit_token": services.document_edit_token(doc),
        },
    )
    assert resp.status_code == 302
    reloaded = Document.objects.get(pk=doc.pk)
    assert reloaded.visibility == Document.Visibility.INHERIT


@pytest.mark.django_db(transaction=True)
def test_ac003_admin_can_change_visibility(admin_user, manager_user, restricted_collection, sample_pdf):
    doc = services.ingest_document(
        file=sample_pdf,
        title="AC003 admin",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        authors=[Author.objects.create(first_name="A", last_name="Admin")],
        submitted_by=admin_user,
        actor=admin_user,
    )
    _publish(doc, manager=manager_user)

    client = Client()
    client.force_login(admin_user)
    resp = client.post(
        reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}),
        {
            "title": doc.title,
            "abstract": doc.abstract,
            "document_type": doc.document_type,
            "license_type": doc.license_type,
            "license_notes": "",
            "language": "en",
            "year": doc.year or "",
            "doi": "",
            "visibility": Document.Visibility.PUBLIC_OPEN,
            "grant_reference": "",
            "authors_text": services.render_authors_text(doc),
            "edit_token": services.document_edit_token(doc),
        },
    )
    assert resp.status_code == 302
    reloaded = Document.objects.get(pk=doc.pk)
    assert reloaded.visibility == Document.Visibility.PUBLIC_OPEN


# ── AC005 — in-app group creation + member management ───────────────────


@pytest.mark.django_db
def test_ac005_group_manage_requires_repo_manager_role(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    resp = client.get(reverse("repo_documents:group_manage"))
    assert resp.status_code in (302, 403)


@pytest.mark.django_db
def test_ac005_manager_can_create_group_and_manage_members(manager_user, submitter_user):
    client = Client()
    client.force_login(manager_user)

    create_resp = client.post(reverse("repo_documents:group_manage"), {"name": "Health Researchers XYZ"})
    assert create_resp.status_code == 302
    group = Group.objects.get(name="Health Researchers XYZ")

    add_resp = client.post(
        reverse("repo_documents:group_member_add", kwargs={"pk": group.pk}),
        {"user": submitter_user.pk},
    )
    assert add_resp.status_code == 302
    assert submitter_user in group.user_set.all()

    remove_resp = client.post(
        reverse(
            "repo_documents:group_member_remove",
            kwargs={"pk": group.pk, "user_id": submitter_user.pk},
        )
    )
    assert remove_resp.status_code == 302
    assert submitter_user not in group.user_set.all()


# ── AC006 — document-aware 403 + access-denial notification ─────────────


@pytest.mark.django_db
def test_ac006_direct_download_shows_document_aware_403(
    submitter_user, manager_user, outsider_user, restricted_collection, sample_pdf
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="AC006 restricted",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    client = Client()
    client.force_login(outsider_user)
    for url_name in ("repo_documents:document_download", "repo_documents:document_raw"):
        resp = client.get(reverse(url_name, kwargs={"uid": doc.document_uid}))
        assert resp.status_code == 403
        body = resp.content.decode()
        assert "AC006 restricted" in body
        assert reverse("repo_documents:access_request_create", kwargs={"uid": doc.document_uid}) in body


@pytest.mark.django_db(transaction=True)
def test_ac006_access_denial_notifies_requester(
    submitter_user, manager_user, outsider_user, restricted_collection, sample_pdf
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="AC006 denial",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    ar = services.request_access(doc, outsider_user, "Need it for my research.")
    services.decide_access_request(ar, approve=False, actor=manager_user)

    assert Notification.objects.filter(
        recipient=outsider_user, verb=Notification.Verb.REPO_ACCESS_DENIED
    ).exists()


# ── AC007 — restricted .docx downloads are watermarked ───────────────────


@pytest.mark.django_db(transaction=True)
def test_ac007_restricted_docx_download_is_watermarked(submitter_user, manager_user, restricted_collection):
    docx_file = ContentFile(_docx_bytes(), name="report.docx")
    doc = services.ingest_document(
        file=docx_file,
        title="AC007 docx",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    client = Client()
    client.force_login(manager_user)
    resp = client.get(reverse("repo_documents:document_download", kwargs={"uid": doc.document_uid}))
    assert resp.status_code == 200

    from docx import Document as DocxDocument

    watermarked = DocxDocument(io.BytesIO(resp.content))
    footer_text = watermarked.sections[0].footer.paragraphs[0].text
    assert "Restricted document" in footer_text


# ── VL005 — retention advance notice ──────────────────────────────────────


@pytest.mark.django_db(transaction=True)
def test_vl005_advance_notice_sent_ahead_of_cutoff(manager_user, submitter_user, restricted_collection, sample_pdf):
    restricted_collection.retention_period_days = 30
    restricted_collection.retention_notice_days = 14
    restricted_collection.retention_action = Collection.RetentionAction.FLAG
    restricted_collection.save()

    doc = services.ingest_document(
        file=sample_pdf,
        title="VL005",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    # 16.5 days old: inside the [16, 17) day window that maps to "14 days
    # before the 30-day cutoff".
    Document.objects.filter(pk=doc.pk).update(
        published_at=timezone.now() - timedelta(days=16, hours=12)
    )

    tasks.enforce_retention_policies()

    assert Notification.objects.filter(
        recipient=manager_user, verb=Notification.Verb.REPO_RETENTION_NOTICE
    ).exists()
    # Day-of action must not have fired yet.
    assert Document.objects.get(pk=doc.pk).status == Document.Status.PUBLISHED


# ── VL006 — retention_action branching ────────────────────────────────────


@pytest.mark.django_db(transaction=True)
def test_vl006_archive_action_hides_document_without_touching_fsm(
    manager_user, submitter_user, restricted_collection, sample_pdf
):
    restricted_collection.retention_period_days = 10
    restricted_collection.retention_notice_days = 0
    restricted_collection.retention_action = Collection.RetentionAction.ARCHIVE
    restricted_collection.save()

    doc = services.ingest_document(
        file=sample_pdf,
        title="VL006 archive",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    Document.objects.filter(pk=doc.pk).update(published_at=timezone.now() - timedelta(days=20))

    tasks.enforce_retention_policies()

    reloaded = Document.objects.get(pk=doc.pk)
    assert reloaded.is_archived is True
    assert reloaded.status == Document.Status.PUBLISHED


@pytest.mark.django_db(transaction=True)
def test_vl006_withdraw_action_withdraws_document(manager_user, submitter_user, restricted_collection, sample_pdf):
    restricted_collection.retention_period_days = 10
    restricted_collection.retention_notice_days = 0
    restricted_collection.retention_action = Collection.RetentionAction.WITHDRAW
    restricted_collection.save()

    doc = services.ingest_document(
        file=sample_pdf,
        title="VL006 withdraw",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    Document.objects.filter(pk=doc.pk).update(published_at=timezone.now() - timedelta(days=20))

    tasks.enforce_retention_policies()

    assert Document.objects.get(pk=doc.pk).status == Document.Status.WITHDRAWN


@pytest.mark.django_db(transaction=True)
def test_vl006_flag_action_only_notifies(manager_user, submitter_user, restricted_collection, sample_pdf):
    restricted_collection.retention_period_days = 10
    restricted_collection.retention_notice_days = 0
    restricted_collection.retention_action = Collection.RetentionAction.FLAG
    restricted_collection.save()

    doc = services.ingest_document(
        file=sample_pdf,
        title="VL006 flag",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    Document.objects.filter(pk=doc.pk).update(published_at=timezone.now() - timedelta(days=20))

    tasks.enforce_retention_policies()

    reloaded = Document.objects.get(pk=doc.pk)
    assert reloaded.status == Document.Status.PUBLISHED
    assert reloaded.is_archived is False
    assert Notification.objects.filter(
        recipient=manager_user, verb=Notification.Verb.REPO_RETENTION_DUE
    ).exists()


# ── VL009 — GrantClosureNotifyView ────────────────────────────────────────


@pytest.mark.django_db
def test_vl009_grant_closure_notify_requires_permission(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    resp = client.post(reverse("repo_integration:grant_closure_notify"), {"grant_reference": "RU/X/1"})
    assert resp.status_code == 403


@pytest.mark.django_db(transaction=True)
def test_vl009_grant_closure_notify_flags_documents(admin_user, public_collection, sample_pdf):
    doc = services.ingest_document(
        file=sample_pdf,
        title="VL009",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        grant_reference="RU/2026/GRG/99",
        submitted_by=admin_user,
        actor=admin_user,
    )
    client = Client()
    client.force_login(admin_user)
    resp = client.post(
        reverse("repo_integration:grant_closure_notify"), {"grant_reference": "RU/2026/GRG/99"}
    )
    assert resp.status_code == 200
    assert resp.json()["documents_flagged"] == 1
    assert Document.objects.get(pk=doc.pk).grant_closed_flag is True


# ── VL010 — audit logging on grant-closure + retention actions ──────────


@pytest.mark.django_db(transaction=True)
def test_vl010_grant_closure_flag_is_audited(admin_user, public_collection, sample_pdf):
    services.ingest_document(
        file=sample_pdf,
        title="VL010",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        grant_reference="RU/2026/GRG/77",
        submitted_by=admin_user,
        actor=admin_user,
    )
    services.apply_grant_closure_flag("RU/2026/GRG/77", actor=admin_user)
    assert AuditLog.objects.filter(
        target_app="repository_documents",
        target_model="Document",
        object_repr="RU/2026/GRG/77",
    ).exists()


@pytest.mark.django_db(transaction=True)
def test_vl010_retention_task_audits_collection(manager_user, submitter_user, restricted_collection, sample_pdf):
    restricted_collection.retention_period_days = 10
    restricted_collection.retention_notice_days = 0
    restricted_collection.retention_action = Collection.RetentionAction.ARCHIVE
    restricted_collection.save()
    doc = services.ingest_document(
        file=sample_pdf,
        title="VL010 retention",
        collection=restricted_collection,
        document_type=Document.DocumentType.PROJECT_REPORT,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)
    Document.objects.filter(pk=doc.pk).update(published_at=timezone.now() - timedelta(days=20))

    tasks.enforce_retention_policies()

    assert AuditLog.objects.filter(
        target_app="repository_documents",
        target_model="Collection",
        object_id=str(restricted_collection.pk),
    ).exists()


# ── QA001 — pre-check failures surfaced to the submitter ────────────────


@pytest.mark.django_db(transaction=True)
def test_qa001_invalid_orcid_blocks_submission_with_specific_reason(submitter_user, public_collection, sample_pdf):
    bad_author = Author.objects.create(first_name="Bad", last_name="Orcid", orcid="1234-BAD-0000-000X")
    doc = services.ingest_document(
        file=sample_pdf,
        title="QA001",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="A sufficiently descriptive abstract.",
        authors=[bad_author],
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    with pytest.raises(services.PreCheckFailedError) as excinfo:
        services.submit_for_qa(doc, actor=submitter_user)
    assert any("ORCID" in f for f in excinfo.value.failures)
    # Rolled back to DRAFT — never silently queued for manual QA.
    assert Document.objects.get(pk=doc.pk).status == Document.Status.DRAFT
    assert not QARecord.objects.filter(document=doc).exists()


@pytest.mark.django_db(transaction=True)
def test_qa001_submit_view_surfaces_precheck_failures(public_collection, submitter_user):
    client = Client()
    client.force_login(submitter_user)
    upload = SimpleUploadedFile("thesis.pdf", _pdf_bytes(), content_type="application/pdf")
    resp = client.post(
        reverse("repo_documents:submit"),
        data={
            "title": "QA001 view submission",
            "abstract": "Body long enough to pass validation checks.",
            "document_type": Document.DocumentType.THESIS,
            "license_type": Document.License.CC_BY,
            "license_notes": "",
            "language": "en",
            "year": 2026,
            "doi": "",
            "collection": public_collection.pk,
            "grant_reference": "",
            "authors_text": "Orcid, Bad | 1234-BAD-0000-000X",
            "keywords_text": "",
            "file": upload,
        },
    )
    assert resp.status_code == 200  # re-rendered with errors, not redirected
    assert b"ORCID" in resp.content
    doc = Document.objects.filter(title="QA001 view submission").first()
    assert doc is not None
    assert doc.status == Document.Status.DRAFT


# ── QA006 — revise-and-resubmit loop ──────────────────────────────────────


@pytest.mark.django_db(transaction=True)
def test_qa006_edit_after_revision_resubmits_and_carries_comments(
    submitter_user, manager_user, public_collection, sample_pdf
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="QA006",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        authors=[Author.objects.create(first_name="A", last_name="Author")],
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    services.submit_for_qa(doc, actor=submitter_user)
    services.decide_qa(
        doc.qa_records.first(),
        decision="revision",
        comments="Please expand the abstract with methodology.",
        actor=manager_user,
    )
    reloaded = Document.objects.get(pk=doc.pk)
    assert reloaded.status == Document.Status.REVISION_REQUIRED

    notif = Notification.objects.filter(
        recipient=submitter_user, verb=Notification.Verb.REPO_DOC_REVISION_REQUIRED
    ).latest("created_at")
    assert "Please expand the abstract" in notif.message

    client = Client()
    client.force_login(submitter_user)
    edit_url = reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid})

    get_resp = client.get(edit_url)
    assert get_resp.status_code == 200
    assert b"Please expand the abstract" in get_resp.content
    assert b"resubmit" in get_resp.content.lower()

    resp = client.post(
        edit_url,
        {
            "title": reloaded.title,
            "abstract": "A properly expanded abstract that covers the methodology in depth.",
            "document_type": reloaded.document_type,
            "license_type": reloaded.license_type,
            "license_notes": "",
            "language": "en",
            "year": reloaded.year or "",
            "doi": "",
            "grant_reference": "",
            "authors_text": services.render_authors_text(reloaded),
            "edit_token": services.document_edit_token(reloaded),
        },
    )
    assert resp.status_code == 302
    final = Document.objects.get(pk=doc.pk)
    assert final.status == Document.Status.UNDER_QA
    assert final.qa_records.filter(decision=QARecord.Decision.PENDING).exists()


# ── QA011 — M&EL payload includes author institution ─────────────────────


@pytest.mark.django_db(transaction=True)
def test_qa011_mel_payload_includes_author_institution(submitter_user, manager_user, public_collection, sample_pdf):
    inst = Institution.objects.create(name="QA011 Test University")
    author = Author.objects.create(first_name="Jane", last_name="Doe", institution=inst)
    doc = services.ingest_document(
        file=sample_pdf,
        title="QA011",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        authors=[author],
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    _publish(doc, manager=manager_user)

    row = IntegrationOutbox.objects.filter(
        target=IntegrationOutbox.Target.MEL, event_type="document_published"
    ).latest("created_at")
    authors_payload = row.payload["authors"]
    assert authors_payload
    assert authors_payload[0]["institution"] == "QA011 Test University"


@pytest.mark.django_db
def test_qa011_mel_feed_builder_includes_institution():
    from apps.repository.integration.mel_feed import build_publication_event_payload

    inst = Institution.objects.create(name="QA011 Builder University")
    author = Author.objects.create(first_name="Amos", last_name="K", institution=inst)

    class _FakeDocument:
        pk = 1
        public_document_id = "REPO-2026-TEST"
        title = "t"
        document_type = "thesis"
        license_type = "cc_by"
        is_open_access = True
        grant_reference = ""
        published_at = None

        class collection:
            slug = "theses"

        def authors_all(self):
            return [author]

    fake = _FakeDocument()
    fake.authors = type("Rel", (), {"all": lambda self: [author]})()
    payload = build_publication_event_payload(fake)
    assert payload["authors"][0]["institution"] == "QA011 Builder University"


# ── QA012 — CollectionUpdateView audits policy diffs ─────────────────────


@pytest.mark.django_db(transaction=True)
def test_qa012_collection_update_audits_policy_diff(manager_user, public_collection):
    client = Client()
    client.force_login(manager_user)
    resp = client.post(
        reverse("repo_documents:collection_update", kwargs={"slug": public_collection.slug}),
        data={
            "name": public_collection.name,
            "slug": public_collection.slug,
            "description": public_collection.description,
            "visibility": public_collection.visibility,
            "qa_turnaround_days": 21,
            "retention_action": Collection.RetentionAction.FLAG,
            "retention_notice_days": 7,
        },
    )
    assert resp.status_code == 302
    reloaded = Collection.objects.get(pk=public_collection.pk)
    assert reloaded.qa_turnaround_days == 21

    entry = (
        AuditLog.objects.filter(
            target_app="repository_documents",
            target_model="Collection",
            object_id=str(public_collection.pk),
        )
        .latest("timestamp")
    )
    assert "qa_turnaround_days" in entry.changes
    assert entry.changes["qa_turnaround_days"]["new"] == 21
