"""Tests for FRREP-AR007/AR008 — AI classification/metadata accept-or-override UI.

``AIDocumentInsight`` rows (kind CLASSIFICATION / METADATA) were computed and
persisted by the LangChain chains in ``apps.repository.analytics.ai_insights``
but never surfaced to a human reviewer. This slice adds a document-detail
card (manager/owner only, only while pending) plus an accept/dismiss endpoint
that applies the suggestion conservatively.
"""

from __future__ import annotations

import pytest
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.urls import reverse

from apps.core.notifications.models import Notification
from apps.repository.analytics.models import AIDocumentInsight
from apps.repository.documents import services
from apps.repository.documents.models import Collection, Document

User = get_user_model()

PDF_BYTES = (
    b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj<</Type/Catalog>>endobj\ntrailer<<>>\n%%EOF\n"
)


def _make_pdf(name: str) -> ContentFile:
    return ContentFile(PDF_BYTES + name.encode(), name=name)


def _ingest(title, submitter_user, collection, **extra):
    return services.ingest_document(
        file=_make_pdf(f"{title}.pdf"),
        title=title,
        collection=collection,
        document_type=Document.DocumentType.THESIS,
        abstract="Abstract text.",
        submitted_by=submitter_user,
        actor=submitter_user,
        **extra,
    )


@pytest.fixture
def datasets_collection(db, manager_user):
    col = Collection.objects.create(
        name="Datasets", slug="datasets", visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    col.managed_by.add(manager_user)
    return col


# ── Card visibility ──────────────────────────────────────────────────────────


@pytest.mark.django_db
def test_classification_card_renders_for_owner_not_outsider_or_anonymous(
    client, submitter_user, public_collection, datasets_collection
):
    doc = _ingest("AR007 visibility", submitter_user, public_collection)
    AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets", "keywords": ["maize", "drought"]},
        confidence=0.82,
        model_used="stub",
    )
    url = reverse("repo_documents:document_detail", kwargs={"uid": doc.document_uid})

    # Owner sees the card.
    client.force_login(submitter_user)
    resp = client.get(url)
    assert resp.status_code == 200
    assert b"AI classification suggestion" in resp.content

    # An unrelated authenticated user does not.
    outsider = User.objects.create_user(email="ar007.outsider@test.example", password="pw-test-1234")
    client.force_login(outsider)
    resp = client.get(url)
    assert resp.status_code == 200
    assert b"AI classification suggestion" not in resp.content

    # Anonymous does not either.
    client.logout()
    resp = client.get(url)
    assert resp.status_code == 200
    assert b"AI classification suggestion" not in resp.content


@pytest.mark.django_db
def test_classification_card_resolves_slug_to_collection_name_not_raw_slug(
    client, submitter_user, public_collection, datasets_collection
):
    """FRREP-AR007 — the AI classification card must never render the raw
    ``suggested_collection_slug`` to the user; it should resolve to the
    Collection's display name instead."""
    doc = _ingest("AR007 slug resolve", submitter_user, public_collection)
    AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets", "keywords": ["maize"]},
        confidence=0.8,
        model_used="stub",
    )
    url = reverse("repo_documents:document_detail", kwargs={"uid": doc.document_uid})

    client.force_login(submitter_user)
    resp = client.get(url)
    content = resp.content.decode()

    assert "Datasets" in content
    assert resp.context["ai_classification_suggested_collection"].pk == datasets_collection.pk
    # The raw slug string must not leak into the rendered card body.
    card_start = content.index("AI classification suggestion")
    card_snippet = content[card_start : card_start + 2000]
    assert "<strong>datasets</strong>" not in card_snippet


@pytest.mark.django_db
def test_classification_card_falls_back_to_raw_slug_when_collection_not_found(
    client, submitter_user, public_collection
):
    """If the suggested slug doesn't resolve (e.g. collection since renamed/deleted),
    fall back to showing the raw slug rather than crashing or showing nothing."""
    doc = _ingest("AR007 slug unresolved", submitter_user, public_collection)
    AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "no-such-collection-slug", "keywords": ["x"]},
        confidence=0.5,
        model_used="stub",
    )
    url = reverse("repo_documents:document_detail", kwargs={"uid": doc.document_uid})

    client.force_login(submitter_user)
    resp = client.get(url)

    assert resp.context["ai_classification_suggested_collection"] is None
    assert "no-such-collection-slug" in resp.content.decode()


@pytest.mark.django_db
def test_card_hidden_once_insight_already_decided(client, submitter_user, public_collection):
    doc = _ingest("AR007 decided", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": public_collection.slug, "keywords": ["a", "b", "c"]},
        confidence=0.5,
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="dismiss", actor=submitter_user)

    client.force_login(submitter_user)
    resp = client.get(reverse("repo_documents:document_detail", kwargs={"uid": doc.document_uid}))
    assert b"AI classification suggestion" not in resp.content


# ── Accept — classification ─────────────────────────────────────────────────


@pytest.mark.django_db
def test_accept_classification_moves_document_to_suggested_collection(
    submitter_user, public_collection, datasets_collection
):
    doc = _ingest("AR007 accept", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets", "keywords": ["maize", "drought", "yield"]},
        confidence=0.91,
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="accept", actor=submitter_user)

    insight.refresh_from_db()
    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    assert insight.accepted_by_user is True
    assert doc.collection_id == datasets_collection.pk


@pytest.mark.django_db
def test_accept_classification_with_unknown_slug_is_a_safe_no_op_on_collection(
    submitter_user, public_collection,
):
    doc = _ingest("AR007 unknown slug", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "does-not-exist", "keywords": ["x"]},
        confidence=0.4,
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="accept", actor=submitter_user)
    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    insight.refresh_from_db()
    assert insight.accepted_by_user is True
    assert doc.collection_id == public_collection.pk  # unchanged


# ── Accept — metadata (conservative merge) ──────────────────────────────────


@pytest.mark.django_db
def test_accept_metadata_fills_blank_year_and_adds_authors(submitter_user, public_collection):
    doc = _ingest("AR008 accept blank", submitter_user, public_collection)
    assert doc.year is None
    assert not doc.authors.exists()

    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.METADATA,
        payload={
            "authors": ["Jane Doe"],
            "publication_date": "2021-06-01",
            "funding_sources": ["RUFORUM"],
            "key_themes": ["soil health"],
            "language": "en",
        },
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="accept", actor=submitter_user)

    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    insight.refresh_from_db()
    assert insight.accepted_by_user is True
    assert doc.year == 2021
    assert doc.authors.count() == 1
    author = doc.authors.first()
    assert author.first_name == "Jane"
    assert author.last_name == "Doe"


@pytest.mark.django_db
def test_accept_metadata_does_not_clobber_existing_year_or_authors(submitter_user, public_collection):
    doc = _ingest("AR008 accept no clobber", submitter_user, public_collection, year=1999)
    existing_author = services.register_author(first_name="Existing", last_name="Author")
    services.set_document_authors(doc, [existing_author], actor=submitter_user)

    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.METADATA,
        payload={
            "authors": ["Someone Else"],
            "publication_date": "2021-06-01",
            "funding_sources": [],
            "key_themes": [],
            "language": "en",
        },
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="accept", actor=submitter_user)

    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    assert doc.year == 1999  # untouched — was already set
    assert doc.authors.count() == 1
    assert doc.authors.first().pk == existing_author.pk  # untouched — was already set


# ── Dismiss ──────────────────────────────────────────────────────────────────


@pytest.mark.django_db
def test_dismiss_sets_accepted_false_without_mutating_document(
    submitter_user, public_collection, datasets_collection
):
    doc = _ingest("AR007 dismiss", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets", "keywords": ["a", "b", "c"]},
        confidence=0.7,
        model_used="stub",
    )
    services.decide_ai_insight(insight, decision="dismiss", actor=submitter_user)

    insight.refresh_from_db()
    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    assert insight.accepted_by_user is False
    assert doc.collection_id == public_collection.pk  # unchanged


@pytest.mark.django_db
def test_invalid_decision_raises(submitter_user, public_collection):
    doc = _ingest("AR007 invalid decision", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets"},
        model_used="stub",
    )
    with pytest.raises(ValidationError):
        services.decide_ai_insight(insight, decision="maybe", actor=submitter_user)


# ── View wiring + permissions + notification ────────────────────────────────


@pytest.mark.django_db
def test_view_accept_requires_manage_permission(client, submitter_user, public_collection):
    doc = _ingest("AR007 view perm", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": public_collection.slug},
        model_used="stub",
    )
    outsider = User.objects.create_user(email="ar007.viewperm@test.example", password="pw-test-1234")
    client.force_login(outsider)
    resp = client.post(
        reverse(
            "repo_documents:ai_insight_decide",
            kwargs={"uid": doc.document_uid, "pk": insight.pk},
        ),
        {"decision": "accept"},
    )
    assert resp.status_code == 403
    insight.refresh_from_db()
    assert insight.accepted_by_user is None


@pytest.mark.django_db
def test_view_accept_notifies_submitter_when_actor_differs(
    client, submitter_user, public_collection, datasets_collection
):
    doc = _ingest("AR007 notify", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.CLASSIFICATION,
        payload={"suggested_collection_slug": "datasets", "keywords": ["a", "b", "c"]},
        confidence=0.6,
        model_used="stub",
    )
    superuser = User.objects.create_superuser(email="ar007.super@test.example", password="pw-test-1234")
    Notification.objects.filter(verb=Notification.Verb.REPO_AI_INSIGHT_ACCEPTED).delete()

    client.force_login(superuser)
    resp = client.post(
        reverse(
            "repo_documents:ai_insight_decide",
            kwargs={"uid": doc.document_uid, "pk": insight.pk},
        ),
        {"decision": "accept"},
    )
    assert resp.status_code == 302
    doc = Document.objects.get(pk=doc.pk)  # Document.status is a protected FSMField — refresh_from_db() can't set it.
    assert doc.collection_id == datasets_collection.pk
    assert Notification.objects.filter(
        recipient=submitter_user, verb=Notification.Verb.REPO_AI_INSIGHT_ACCEPTED
    ).exists()


@pytest.mark.django_db
def test_view_dismiss_via_post(client, submitter_user, public_collection):
    doc = _ingest("AR007 view dismiss", submitter_user, public_collection)
    insight = AIDocumentInsight.objects.create(
        document=doc,
        kind=AIDocumentInsight.Kind.METADATA,
        payload={"authors": [], "publication_date": None, "funding_sources": [], "key_themes": []},
        model_used="stub",
    )
    client.force_login(submitter_user)
    resp = client.post(
        reverse(
            "repo_documents:ai_insight_decide",
            kwargs={"uid": doc.document_uid, "pk": insight.pk},
        ),
        {"decision": "dismiss"},
    )
    assert resp.status_code == 302
    insight.refresh_from_db()
    assert insight.accepted_by_user is False
