"""Phase 3 — bulk upload experience (FRREP-DCI002/003, #4/#8).

Covers:
  3.1 per-file staged bulk tokens → one Document each; a bad file doesn't sink
      the run; staged blobs are discarded on success.
  3.2 the batch summary report (created / duplicate / rejected rows) + retry.
  3.3 per-file metadata override with inheritance from the shared template.
  3.4 replace/swap a staged file at the view level (DELETE then re-POST).

All tests use ``force_login`` (django-axes rejects ``client.login()``).
"""
from __future__ import annotations

import json

import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import Client
from django.urls import reverse

from apps.repository.documents import services
from apps.repository.documents.models import Document

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


def _pdf(tag: str) -> bytes:
    """A small but byte-distinct PDF so files don't collide on hash."""
    return PDF_HEAD + f"% {tag}\n%%EOF\n".encode()


def _client(user) -> Client:
    c = Client()
    c.force_login(user)
    return c


def _stage(client, name: str, content: bytes) -> str:
    resp = client.post(
        reverse("repo_documents:staged_upload"),
        {"file": SimpleUploadedFile(name, content, content_type="application/pdf")},
    )
    assert resp.status_code == 200, resp.content[:200]
    return resp.json()["token"]


def _base_data(collection_pk, **extra):
    data = {
        "title": "Shared Title",
        "abstract": "A shared abstract describing the batch.",
        "document_type": Document.DocumentType.THESIS,
        "license_type": Document.License.CC_BY,
        "language": "en",
        "year": 2026,
        "collection": collection_pk,
        "authors_text": "Doe, Jane",
    }
    data.update(extra)
    return data


# ── 3.1 — per-file staged bulk tokens → one Document each ───────────────────


@pytest.mark.django_db
def test_bulk_tokens_create_one_document_each(public_collection, submitter_user):
    client = _client(submitter_user)
    tok1 = _stage(client, "extra1.pdf", _pdf("extra1"))
    tok2 = _stage(client, "extra2.pdf", _pdf("extra2"))

    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps(
                [
                    {"token": tok1, "name": "extra1.pdf", "metadata": {}},
                    {"token": tok2, "name": "extra2.pdf", "metadata": {}},
                ]
            ),
        ),
    )
    # A bulk run renders the batch report (200), not a redirect.
    assert resp.status_code == 200
    assert Document.objects.count() == 3
    # All three queued for review and listed in the report.
    body = resp.content.decode()
    assert "Batch upload report" in body
    assert body.count("Queued") >= 3  # column header word appears; rows too


@pytest.mark.django_db
def test_bulk_staged_blobs_discarded_on_success(public_collection, submitter_user):
    from django.conf import settings
    from pathlib import Path

    client = _client(submitter_user)
    tok = _stage(client, "extra.pdf", _pdf("extra"))
    staged_path = Path(settings.MEDIA_ROOT) / "staged_uploads" / tok / "extra.pdf"
    assert staged_path.exists()

    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps([{"token": tok, "name": "extra.pdf"}]),
        ),
    )
    assert resp.status_code == 200
    assert Document.objects.count() == 2
    # The staged copy is cleaned up after a successful ingest.
    assert not staged_path.exists()


@pytest.mark.django_db
def test_bulk_bad_token_does_not_block_the_rest(public_collection, submitter_user):
    client = _client(submitter_user)
    good = _stage(client, "good.pdf", _pdf("good"))

    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps(
                [
                    {"token": good, "name": "good.pdf"},
                    {"token": "deadbeefdeadbeef", "name": "missing.pdf"},
                ]
            ),
        ),
    )
    assert resp.status_code == 200
    # primary + good ingested; the missing-token file becomes a failure row.
    assert Document.objects.count() == 2
    body = resp.content.decode()
    assert "missing.pdf" in body
    assert "Rejected" in body


# ── 3.2 — batch report (mixed outcomes) + retry ─────────────────────────────


@pytest.mark.django_db
def test_bulk_report_shows_rejected_row_for_disallowed_format(
    public_collection, submitter_user
):
    """A disallowed format ridden in via the no-JS additional_files path is a
    rejected row; the good primary file is still created."""
    client = _client(submitter_user)
    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            additional_files=[
                SimpleUploadedFile("clip.mp4", b"\x00\x00\x00\x18ftypmp42", content_type="video/mp4")
            ],
        ),
    )
    assert resp.status_code == 200
    assert Document.objects.count() == 1  # only the primary pdf
    body = resp.content.decode()
    assert "clip.mp4" in body
    assert "Rejected" in body
    assert "Retry failed files" in body  # retry affordance present


@pytest.mark.django_db
def test_bulk_report_shows_duplicate_row(public_collection, submitter_user):
    existing = services.ingest_document(
        file=SimpleUploadedFile("orig.pdf", _pdf("dupe"), content_type="application/pdf"),
        title="Original",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="seed",
        submitted_by=submitter_user,
    )
    client = _client(submitter_user)
    dup_token = _stage(client, "again.pdf", _pdf("dupe"))

    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps([{"token": dup_token, "name": "again.pdf"}]),
        ),
    )
    assert resp.status_code == 200
    # seed + new primary only — the duplicate is reported, not re-ingested.
    assert Document.objects.count() == 2
    body = resp.content.decode()
    assert "Duplicate" in body
    assert existing.public_document_id in body


@pytest.mark.django_db
def test_retry_link_prefills_shared_metadata(public_collection, submitter_user):
    """FRREP-DCI002 — the batch report claims 'your metadata is kept' when
    retrying; DocumentSubmitView.get_initial() must actually restore it from
    the session stash `_render_batch_report` writes on a failed row."""
    from apps.repository.documents.models import Author

    author = Author.objects.create(first_name="Jane", last_name="Doe")
    client = _client(submitter_user)
    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            title="Shared Title",
            abstract="A shared abstract describing the batch.",
            authors_json=json.dumps([{"id": author.pk}]),
            authors_text="",
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            additional_files=[
                SimpleUploadedFile("clip.mp4", b"\x00\x00\x00\x18ftypmp42", content_type="video/mp4")
            ],
        ),
    )
    assert resp.status_code == 200
    assert "Retry failed files" in resp.content.decode()

    # A fresh GET to the submit page must come back pre-populated.
    retry_resp = client.get(reverse("repo_documents:submit"))
    assert retry_resp.status_code == 200
    form = retry_resp.context["form"]
    assert form.initial.get("title") == "Shared Title"
    assert form.initial.get("abstract") == "A shared abstract describing the batch."
    assert form.initial.get("collection") == public_collection.pk
    assert json.loads(form.initial.get("authors_json") or "[]") == [{"id": author.pk}]

    # One-shot: the stash is consumed, a second GET doesn't carry it forward.
    second_resp = client.get(reverse("repo_documents:submit"))
    assert not second_resp.context["form"].initial.get("title")


@pytest.mark.django_db
def test_retry_failed_file_as_separate_creates_it(public_collection, submitter_user):
    """Retry = re-submit just the failed file; choosing 'separate' lands it."""
    services.ingest_document(
        file=SimpleUploadedFile("orig.pdf", _pdf("dupe"), content_type="application/pdf"),
        title="Original",
        collection=public_collection,
        document_type=Document.DocumentType.THESIS,
        abstract="seed",
        submitted_by=submitter_user,
    )
    client = _client(submitter_user)
    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            title="Retried doc",
            duplicate_decision="separate",
            file=SimpleUploadedFile("again.pdf", _pdf("dupe"), content_type="application/pdf"),
        ),
    )
    # Single-file retry resolves the duplicate and redirects to the new detail.
    assert resp.status_code == 302
    assert Document.objects.count() == 2


# ── 3.3 — per-file metadata override + inheritance ──────────────────────────


@pytest.mark.django_db
def test_per_file_title_override_with_inheritance(public_collection, submitter_user):
    client = _client(submitter_user)
    tok_override = _stage(client, "a.pdf", _pdf("a"))
    tok_inherit = _stage(client, "b.pdf", _pdf("b"))

    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            title="Shared Title",
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps(
                [
                    {"token": tok_override, "name": "a.pdf", "metadata": {"title": "Override Title"}},
                    {"token": tok_inherit, "name": "b.pdf", "metadata": {}},
                ]
            ),
        ),
    )
    assert resp.status_code == 200
    assert Document.objects.count() == 3
    # The overridden file carries its own title…
    assert Document.objects.filter(title="Override Title").count() == 1
    # …while the primary + the un-overridden bulk file inherit the shared title.
    assert Document.objects.filter(title="Shared Title").count() == 2


@pytest.mark.django_db
def test_per_file_override_inherits_mandatory_fields(public_collection, submitter_user):
    """A per-file override that only sets the title must still inherit the
    shared abstract/licence — the mandatory set can't be silently emptied."""
    client = _client(submitter_user)
    tok = _stage(client, "a.pdf", _pdf("a"))
    resp = client.post(
        reverse("repo_documents:submit"),
        data=_base_data(
            public_collection.pk,
            abstract="A shared abstract describing the batch.",
            file=SimpleUploadedFile("primary.pdf", _pdf("primary"), content_type="application/pdf"),
            bulk_files=json.dumps(
                [{"token": tok, "name": "a.pdf", "metadata": {"title": "Only Title Set"}}]
            ),
        ),
    )
    assert resp.status_code == 200
    overridden = Document.objects.get(title="Only Title Set")
    assert overridden.abstract == "A shared abstract describing the batch."
    assert overridden.license_type == Document.License.CC_BY


# ── 3.4 — replace/swap a staged file at the view level ──────────────────────


@pytest.mark.django_db
def test_replace_staged_file_discards_then_restages(submitter_user):
    """The Replace control = StagedUploadView DELETE of the old token, then a
    fresh POST for the new file. Exercise that round-trip at the view level."""
    from django.conf import settings
    from pathlib import Path

    client = _client(submitter_user)
    old = _stage(client, "wrong.pdf", _pdf("wrong"))
    old_path = Path(settings.MEDIA_ROOT) / "staged_uploads" / old / "wrong.pdf"
    assert old_path.exists()

    # Discard the mistaken file.
    resp = client.delete(
        reverse("repo_documents:staged_upload"),
        data=json.dumps({"token": old}),
        content_type="application/json",
    )
    assert resp.status_code == 200
    assert not old_path.exists()

    # Re-stage the corrected file in its place — a new token, blob present.
    new = _stage(client, "right.pdf", _pdf("right"))
    assert new != old
    new_path = Path(settings.MEDIA_ROOT) / "staged_uploads" / new / "right.pdf"
    assert new_path.exists()


@pytest.mark.django_db
def test_replace_control_renders_on_wizard(public_collection, submitter_user):
    client = _client(submitter_user)
    resp = client.get(reverse("repo_documents:submit"))
    assert resp.status_code == 200
    body = resp.content.decode()
    # Both the primary-file Replace affordance and the bulk staging hook render.
    assert "replaceBulkFile" in body
    assert "onBulkChange" in body
    assert 'name="bulk_files"' in body


@pytest.mark.django_db
def test_bulk_rows_carry_speed_and_eta_wiring(public_collection, submitter_user):
    """FRREP-DCI003 — bulk rows must compute speed/ETA like the primary
    uploader, not just a bare percentage."""
    client = _client(submitter_user)
    resp = client.get(reverse("repo_documents:submit"))
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "entry.speed" in body
    assert "entry.eta" in body
    assert "b.speed" in body and "b.eta" in body
