"""WS4 / FRREP-MCL004 + FRREP-MCL012 — Author and taxonomy merge wizards.

Covers:
* `merge_authors` rewires DocumentAuthor rows + drops uniqueness collisions.
* `merge_collections` refuses on visibility mismatch + writes slug aliases.
* `rename_collection_slug` records the alias and the detail view honours it
  via 301 redirect.
"""

from __future__ import annotations

import pytest
from django.urls import reverse

from apps.repository.documents import services
from apps.repository.documents.models import (
    Author,
    AuthorAlias,
    Collection,
    CollectionSlugAlias,
    Document,
    DocumentAuthor,
)


# ── Author merge ──────────────────────────────────────────────────────────


@pytest.fixture
def primary_author(db):
    return Author.objects.create(
        first_name="Jane",
        last_name="Doe",
        orcid="0000-0001-0002-0003",
        email="jane@ruforum.org",
    )


@pytest.fixture
def duplicate_author(db):
    return Author.objects.create(
        first_name="Jane",
        last_name="Doe-Smith",
        affiliation_text="Makerere University",
    )


@pytest.mark.django_db
def test_merge_authors_rewires_document_author_rows(
    primary_author,
    duplicate_author,
    submitter_user,
    public_collection,
    sample_pdf,
):
    doc = services.ingest_document(
        file=sample_pdf,
        title="A study",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    DocumentAuthor.objects.create(document=doc, author=duplicate_author, order=1)

    services.merge_authors(primary_author, [duplicate_author], actor=submitter_user)

    # Soft-delete: duplicate is hidden from default manager, still in all_objects.
    assert not Author.objects.filter(pk=duplicate_author.pk).exists()
    assert Author.all_objects.filter(pk=duplicate_author.pk).exists()
    duplicate_author.refresh_from_db()
    assert duplicate_author.is_archived is True
    assert duplicate_author.merged_into_id == primary_author.pk
    assert DocumentAuthor.objects.filter(document=doc, author=primary_author).exists()


@pytest.mark.django_db
def test_merge_authors_creates_alias_for_each_duplicate(
    primary_author, submitter_user
):
    """FRREP-MCL004: every merged-in name becomes a discoverable alias."""
    dup_a = Author.objects.create(first_name="Jane", last_name="Smithson")
    dup_b = Author.objects.create(first_name="Janie", last_name="Doe")

    services.merge_authors(primary_author, [dup_a, dup_b], actor=submitter_user)

    aliases = set(primary_author.aliases.values_list("full_name", "source"))
    assert ("Jane Smithson", AuthorAlias.Source.MERGE) in aliases
    assert ("Janie Doe", AuthorAlias.Source.MERGE) in aliases


@pytest.mark.django_db
def test_merge_authors_alias_creation_is_idempotent(
    primary_author, duplicate_author, submitter_user
):
    """Re-running a merge with the same duplicate full_name does not duplicate aliases."""
    services.merge_authors(primary_author, [duplicate_author], actor=submitter_user)
    second_dup = Author.objects.create(
        first_name=duplicate_author.first_name,
        last_name=duplicate_author.last_name,
    )
    services.merge_authors(primary_author, [second_dup], actor=submitter_user)

    matching = primary_author.aliases.filter(full_name=duplicate_author.full_name).count()
    assert matching == 1


@pytest.mark.django_db
def test_register_author_links_to_canonical_via_alias(submitter_user):
    """FRREP-MCL004 name-change linking: a submission of an old name returns
    the canonical author so historical bylines stay mapped."""
    canonical = Author.objects.create(first_name="Mary", last_name="Smith")
    AuthorAlias.objects.create(
        author=canonical,
        full_name="Mary Jones",
        source=AuthorAlias.Source.RENAME,
    )

    looked_up = services.register_author(first_name="Mary", last_name="Jones")
    assert looked_up.pk == canonical.pk


@pytest.mark.django_db
def test_rename_author_snapshots_previous_name(primary_author, submitter_user):
    """Calling ``rename_author`` records the prior name as a RENAME alias."""
    previous = primary_author.full_name
    services.rename_author(
        primary_author,
        first_name="Janet",
        last_name="Doe-Smith",
        actor=submitter_user,
    )
    primary_author.refresh_from_db()
    assert primary_author.full_name == "Janet Doe-Smith"
    assert primary_author.aliases.filter(
        full_name=previous, source=AuthorAlias.Source.RENAME,
    ).exists()


@pytest.mark.django_db
def test_rename_author_noop_when_unchanged(primary_author, submitter_user):
    services.rename_author(
        primary_author,
        first_name=primary_author.first_name,
        last_name=primary_author.last_name,
        actor=submitter_user,
    )
    assert primary_author.aliases.count() == 0


@pytest.mark.django_db
def test_merge_authors_rejects_archived_target(primary_author, submitter_user):
    """An archived (already-merged-away) author cannot be the primary."""
    primary_author.is_archived = True
    primary_author.save(update_fields=["is_archived"])
    other = Author.objects.create(first_name="Test", last_name="Author")
    # Default manager hides archived; merge logic itself doesn't gate, but the
    # AuthorMergeView form's queryset will. Verifying the manager filter:
    assert not Author.objects.filter(pk=primary_author.pk).exists()
    assert Author.all_objects.filter(pk=primary_author.pk).exists()


@pytest.mark.django_db
def test_merge_authors_drops_collision_when_both_on_same_document(
    primary_author,
    duplicate_author,
    submitter_user,
    public_collection,
    sample_pdf,
):
    """If primary + duplicate co-authored the same doc, the duplicate row is
    dropped — uniqueness on (document, author) is preserved."""
    doc = services.ingest_document(
        file=sample_pdf,
        title="A study",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )
    DocumentAuthor.objects.create(document=doc, author=primary_author, order=1)
    DocumentAuthor.objects.create(document=doc, author=duplicate_author, order=2)

    services.merge_authors(primary_author, [duplicate_author], actor=submitter_user)

    rows = DocumentAuthor.objects.filter(document=doc)
    assert rows.count() == 1
    assert rows.first().author_id == primary_author.pk


@pytest.mark.django_db
def test_merge_authors_backfills_empty_primary_fields(
    primary_author, duplicate_author, submitter_user
):
    primary_author.email = ""
    primary_author.affiliation_text = ""
    primary_author.save(update_fields=["email", "affiliation_text"])
    duplicate_author.email = "fallback@ruforum.org"
    duplicate_author.affiliation_text = "Kenyatta University"
    duplicate_author.save(update_fields=["email", "affiliation_text"])

    services.merge_authors(primary_author, [duplicate_author], actor=submitter_user)

    primary_author.refresh_from_db()
    assert primary_author.email == "fallback@ruforum.org"
    assert "Kenyatta University" in primary_author.affiliation_text


@pytest.mark.django_db
def test_merge_authors_rejects_self_merge(primary_author, submitter_user):
    from django.core.exceptions import ValidationError

    with pytest.raises(ValidationError):
        services.merge_authors(primary_author, [primary_author], actor=submitter_user)


# ── Collection merge ─────────────────────────────────────────────────────


@pytest.mark.django_db
def test_merge_collections_moves_documents_and_writes_alias(
    submitter_user, manager_user, sample_pdf
):
    primary = Collection.objects.create(
        name="Theses (canonical)",
        slug="theses-canon",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    primary.managed_by.add(manager_user)
    duplicate = Collection.objects.create(
        name="Theses (legacy)",
        slug="theses-legacy",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    duplicate.managed_by.add(manager_user)

    doc = services.ingest_document(
        file=sample_pdf,
        title="Legacy thesis",
        collection=duplicate,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
    )

    services.merge_collections(primary, [duplicate], actor=manager_user)

    # Re-fetch — FSMField(protected=True) on Document.status forbids refresh_from_db.
    doc = Document.objects.get(pk=doc.pk)
    assert doc.collection_id == primary.pk
    # Soft-delete via deprecation: row still exists but is deprecated.
    duplicate.refresh_from_db()
    assert duplicate.is_deprecated is True
    assert duplicate.superseded_by_id == primary.pk
    assert duplicate.deprecated_at is not None
    assert CollectionSlugAlias.objects.filter(
        old_slug="theses-legacy", collection=primary
    ).exists()


# ── Collection deprecation (FRREP-MCL012, SRS use case A4) ───────────────


@pytest.fixture
def active_collection(db, manager_user):
    col = Collection.objects.create(
        name="Active Theses",
        slug="active-theses",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    col.managed_by.add(manager_user)
    return col


@pytest.mark.django_db
def test_deprecate_collection_sets_fields(active_collection, manager_user):
    services.deprecate_collection(
        active_collection,
        message="Folded into 'Theses (canonical)' — pick that instead.",
        actor=manager_user,
    )
    active_collection.refresh_from_db()
    assert active_collection.is_deprecated is True
    assert active_collection.deprecated_at is not None
    assert "Theses (canonical)" in active_collection.deprecation_message


@pytest.mark.django_db
def test_deprecate_requires_message(active_collection, manager_user):
    from django.core.exceptions import ValidationError
    with pytest.raises(ValidationError):
        services.deprecate_collection(active_collection, message="", actor=manager_user)


@pytest.mark.django_db
def test_deprecate_self_supersession_rejected(active_collection, manager_user):
    from django.core.exceptions import ValidationError
    with pytest.raises(ValidationError):
        services.deprecate_collection(
            active_collection,
            message="x",
            superseded_by=active_collection,
            actor=manager_user,
        )


@pytest.mark.django_db
def test_undeprecate_clears_state(active_collection, manager_user):
    services.deprecate_collection(active_collection, message="x", actor=manager_user)
    services.undeprecate_collection(active_collection, actor=manager_user)
    active_collection.refresh_from_db()
    assert active_collection.is_deprecated is False
    assert active_collection.deprecation_message == ""
    assert active_collection.deprecated_at is None
    assert active_collection.superseded_by_id is None


@pytest.mark.django_db
def test_submit_form_warns_when_picking_deprecated_with_replacement(
    public_collection, manager_user, sample_pdf
):
    from apps.repository.documents.forms import DocumentSubmitForm

    replacement = Collection.objects.create(
        name="New Active",
        slug="new-active",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    replacement.managed_by.add(manager_user)
    public_collection.is_deprecated = True
    public_collection.deprecation_message = "Folded into New Active."
    public_collection.superseded_by = replacement
    public_collection.save(update_fields=[
        "is_deprecated", "deprecation_message", "superseded_by",
    ])

    form = DocumentSubmitForm(
        data={
            "title": "X",
            "abstract": "x",
            "document_type": Document.DocumentType.THESIS,
            "license_type": Document.License.CC_BY,
            "language": "en",
            "year": "2026",
            "collection": public_collection.pk,
            "visibility": Document.Visibility.INHERIT,
            "authors_text": "Doe, Jane",
            "staged_token": "tok",
        },
    )
    # Trigger clean_collection
    form.is_valid()
    assert form.cleaned_data.get("collection") == public_collection
    assert form._deprecation_warning is not None
    assert "New Active" in form._deprecation_warning


@pytest.mark.django_db
def test_submit_form_rejects_deprecated_without_replacement(public_collection):
    from apps.repository.documents.forms import DocumentSubmitForm

    public_collection.is_deprecated = True
    public_collection.deprecation_message = "Retired with no replacement."
    public_collection.save(update_fields=["is_deprecated", "deprecation_message"])

    form = DocumentSubmitForm(
        data={
            "title": "X",
            "abstract": "x",
            "document_type": Document.DocumentType.THESIS,
            "license_type": Document.License.CC_BY,
            "language": "en",
            "year": "2026",
            "collection": public_collection.pk,
            "visibility": Document.Visibility.INHERIT,
            "authors_text": "Doe, Jane",
            "staged_token": "tok",
        },
    )
    assert form.is_valid() is False
    assert "collection" in form.errors


@pytest.mark.django_db
def test_merge_collections_deprecates_duplicate_instead_of_deleting(
    submitter_user, manager_user, sample_pdf
):
    primary = Collection.objects.create(
        name="Active Repo", slug="active-repo",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    primary.managed_by.add(manager_user)
    dup = Collection.objects.create(
        name="Old Repo", slug="old-repo",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    dup.managed_by.add(manager_user)

    services.merge_collections(primary, [dup], actor=manager_user)

    dup.refresh_from_db()
    assert dup.is_deprecated is True
    assert dup.superseded_by_id == primary.pk
    assert "Active Repo" in dup.deprecation_message


@pytest.mark.django_db
def test_merge_collections_refuses_on_visibility_mismatch(manager_user):
    primary = Collection.objects.create(
        name="Open",
        slug="open",
        visibility=Collection.Visibility.PUBLIC_OPEN,
    )
    restricted = Collection.objects.create(
        name="Restricted",
        slug="restricted-x",
        visibility=Collection.Visibility.RESTRICTED,
    )
    with pytest.raises(services.IncompatibleMergeError):
        services.merge_collections(primary, [restricted], actor=manager_user)
    # Both still exist.
    assert Collection.objects.filter(pk=primary.pk).exists()
    assert Collection.objects.filter(pk=restricted.pk).exists()


# ── Slug rename + 301 redirect ───────────────────────────────────────────


@pytest.mark.django_db
def test_rename_collection_slug_writes_alias(public_collection, manager_user):
    old_slug = public_collection.slug
    services.rename_collection_slug(
        public_collection, "theses-renamed", actor=manager_user
    )
    public_collection.refresh_from_db()
    assert public_collection.slug == "theses-renamed"
    assert CollectionSlugAlias.objects.filter(
        old_slug=old_slug, collection=public_collection
    ).exists()


@pytest.mark.django_db
def test_rename_rejects_collision_with_existing_slug(
    public_collection, restricted_collection, manager_user
):
    from django.core.exceptions import ValidationError

    with pytest.raises(ValidationError):
        services.rename_collection_slug(
            public_collection,
            restricted_collection.slug,
            actor=manager_user,
        )


@pytest.mark.django_db
def test_collection_detail_view_redirects_old_slug(
    client, public_collection, manager_user
):
    old_slug = public_collection.slug
    services.rename_collection_slug(
        public_collection, "theses-newname", actor=manager_user
    )
    resp = client.get(reverse("repo_documents:collection_detail", kwargs={"slug": old_slug}))
    assert resp.status_code == 301
    assert resp["Location"].endswith("/collections/theses-newname/")


# ── Authors edit on DocumentEditView (FRREP-MCL004 / FRREP-COL008) ──────


@pytest.mark.django_db
def test_render_authors_text_round_trips_format(
    submitter_user, public_collection, sample_pdf
):
    primary = Author.objects.create(
        first_name="Jane", last_name="Doe",
        orcid="0000-0001-0002-0003", email="jane@x.org",
    )
    coauth = Author.objects.create(first_name="Mary", last_name="Smith")

    doc = services.ingest_document(
        file=sample_pdf,
        title="X",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[primary, coauth],
    )
    text = services.render_authors_text(doc)
    lines = text.splitlines()
    assert lines[0].startswith("Doe, Jane | 0000-0001-0002-0003 | jane@x.org")
    assert lines[1].startswith("Smith, Mary")


@pytest.mark.django_db
def test_set_document_authors_replaces_roster(
    submitter_user, public_collection, sample_pdf
):
    a = Author.objects.create(first_name="Old", last_name="One")
    b = Author.objects.create(first_name="Old", last_name="Two")
    c = Author.objects.create(first_name="New", last_name="Three")

    doc = services.ingest_document(
        file=sample_pdf,
        title="X",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a, b],
    )
    services.set_document_authors(doc, [c, a], actor=submitter_user)

    rows = list(doc.documentauthor_set.order_by("order").values_list("author_id", "order", "role"))
    assert rows == [
        (c.pk, 0, DocumentAuthor.AuthorRole.PRIMARY),
        (a.pk, 1, DocumentAuthor.AuthorRole.CO_AUTHOR),
    ]
    # b is gone
    assert not doc.documentauthor_set.filter(author=b).exists()


@pytest.mark.django_db
def test_set_document_authors_rejects_empty_list(
    submitter_user, public_collection, sample_pdf
):
    from django.core.exceptions import ValidationError as DjValidationError

    a = Author.objects.create(first_name="Solo", last_name="Author")
    doc = services.ingest_document(
        file=sample_pdf,
        title="X",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a],
    )
    with pytest.raises(DjValidationError):
        services.set_document_authors(doc, [], actor=submitter_user)


@pytest.mark.django_db
def test_parse_authors_text_resolves_alias_to_canonical_author(submitter_user):
    canonical = Author.objects.create(first_name="Mary", last_name="Smith")
    AuthorAlias.objects.create(
        author=canonical,
        full_name="Mary Jones",
        source=AuthorAlias.Source.RENAME,
    )

    parsed = services.parse_authors_text("Jones, Mary")
    assert len(parsed) == 1
    assert parsed[0].pk == canonical.pk


@pytest.mark.django_db
def test_edit_view_persists_a_changed_scalar_field(
    submitter_user, public_collection, sample_pdf
):
    """Regression: ``update_document_metadata`` used to compare its "old"
    value via ``getattr(document, field_name)`` — but by the time the view
    calls it, Django's ModelForm ``_post_clean()``/``construct_instance()``
    has already ``setattr``'d every cleaned field onto that same instance, so
    the "old" value always equalled the "new" one and the change was silently
    dropped without ever calling ``.save()``. This locks in the fix (the old
    value is now re-read from the database)."""
    from django.test import Client

    a = Author.objects.create(first_name="A", last_name="One")
    doc = services.ingest_document(
        file=sample_pdf,
        title="Edit Me",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a],
    )
    client = Client()
    client.force_login(submitter_user)
    resp = client.post(
        reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}),
        data={
            "edit_token": services.document_edit_token(doc),
            "title": "A Brand New Title",
            "abstract": doc.abstract,
            "document_type": doc.document_type,
            "license_type": doc.license_type,
            "license_notes": doc.license_notes,
            "language": doc.language,
            "year": doc.year or "",
            "doi": doc.doi or "",
            "visibility": doc.visibility,
            "grant_reference": doc.grant_reference or "",
            "authors_text": "One, A",
        },
    )
    assert resp.status_code == 302
    doc = Document.objects.get(pk=doc.pk)
    assert doc.title == "A Brand New Title"


@pytest.mark.django_db
def test_edit_view_persists_author_roster_changes(
    submitter_user, public_collection, sample_pdf
):
    from django.test import Client

    a = Author.objects.create(first_name="A", last_name="One")
    doc = services.ingest_document(
        file=sample_pdf,
        title="Edit Me",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a],
    )
    client = Client()
    client.force_login(submitter_user)

    initial_token = services.document_edit_token(doc)
    resp = client.post(
        reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}),
        data={
            "edit_token": initial_token,
            "title": doc.title,
            "abstract": doc.abstract,
            "document_type": doc.document_type,
            "license_type": doc.license_type,
            "license_notes": doc.license_notes,
            "language": doc.language,
            "year": doc.year or "",
            "doi": doc.doi or "",
            "visibility": doc.visibility,
            "grant_reference": doc.grant_reference or "",
            "authors_text": "One, A\nTwo, B | 0000-0001-9999-9999",
        },
    )
    assert resp.status_code == 302
    rows = list(
        doc.documentauthor_set.order_by("order")
        .values_list("author__last_name", "author__first_name", "order")
    )
    assert rows == [("One", "A", 0), ("Two", "B", 1)]


@pytest.mark.django_db
def test_edit_view_rejects_empty_authors_text(
    submitter_user, public_collection, sample_pdf
):
    from django.test import Client

    a = Author.objects.create(first_name="A", last_name="One")
    doc = services.ingest_document(
        file=sample_pdf,
        title="Edit Me",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a],
    )
    client = Client()
    client.force_login(submitter_user)

    resp = client.post(
        reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}),
        data={
            "edit_token": services.document_edit_token(doc),
            "title": doc.title,
            "abstract": doc.abstract,
            "document_type": doc.document_type,
            "license_type": doc.license_type,
            "license_notes": doc.license_notes,
            "language": doc.language,
            "year": doc.year or "",
            "doi": doc.doi or "",
            "visibility": doc.visibility,
            "grant_reference": doc.grant_reference or "",
            "authors_text": "",
        },
    )
    assert resp.status_code == 200  # form rendered with errors
    assert "authors_text" in resp.context["form"].errors
    # Underlying roster is unchanged.
    assert doc.documentauthor_set.count() == 1


@pytest.mark.django_db
def test_edit_page_renders_author_search_box_not_raw_textarea_only(
    submitter_user, public_collection, sample_pdf
):
    """FRREP-MCL011 — the edit page must offer the same registry-search-box
    component as the submit wizard, not just a raw pipe-delimited textarea."""
    from django.test import Client

    a = Author.objects.create(first_name="A", last_name="One", orcid="0000-0001-2345-6789")
    doc = services.ingest_document(
        file=sample_pdf,
        title="Edit Me",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="x",
        submitted_by=submitter_user,
        actor=submitter_user,
        authors=[a],
    )
    client = Client()
    client.force_login(submitter_user)
    resp = client.get(reverse("repo_documents:document_edit", kwargs={"uid": doc.document_uid}))
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "editAuthorPicker" in body
    assert reverse("repo_documents:author_search") in body
    # Existing author still round-trips into the hidden textarea for no-JS
    # submission / the JS hydration step to parse.
    assert "One, A" in body
    assert "0000-0001-2345-6789" in body
