"""Tests for the resumable / chunked upload pipeline (FRREP-DCI004)."""
from __future__ import annotations

import hashlib
import json
from datetime import timedelta
from pathlib import Path

import pytest
from django.conf import settings
from django.test import Client
from django.urls import reverse
from django.utils import timezone

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


def _pdf_bytes(size_kb: int = 6) -> bytes:
    """Return at least ``size_kb`` KB of valid-PDF-prefixed bytes."""
    head = b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj<</Type/Catalog>>endobj\n"
    tail = b"\ntrailer<<>>\n%%EOF\n"
    pad = b"X" * max(0, size_kb * 1024 - len(head) - len(tail))
    return head + pad + tail


# ---------------------------------------------------------------------------
# Service-level happy path + edge cases
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_start_resumable_upload_creates_session_and_chunk_dir(submitter_user):
    upload = services.start_resumable_upload(
        user=submitter_user,
        filename="thesis.pdf",
        total_bytes=2_500_000,
        chunk_size=1_000_000,
    )
    assert upload.token
    assert upload.expected_chunks == 3
    assert upload.status == ResumableUpload.Status.IN_PROGRESS
    chunk_dir = Path(settings.MEDIA_ROOT) / "resumable" / upload.token
    assert chunk_dir.exists()


@pytest.mark.django_db
def test_start_rejects_unsupported_extension(submitter_user):
    from django.core.exceptions import ValidationError as DjValidationError

    with pytest.raises(DjValidationError):
        services.start_resumable_upload(
            user=submitter_user,
            filename="malware.exe",
            total_bytes=1024,
        )


@pytest.mark.django_db
def test_start_rejects_oversize_file(submitter_user):
    from django.core.exceptions import ValidationError as DjValidationError

    # Repository module cap is 200 MB; ask for 300 MB.
    with pytest.raises(DjValidationError):
        services.start_resumable_upload(
            user=submitter_user,
            filename="huge.pdf",
            total_bytes=300 * 1024 * 1024,
        )


@pytest.mark.django_db
def test_finalize_assembles_and_validates_pdf(submitter_user):
    payload = _pdf_bytes(size_kb=7)
    chunk_size = 2048
    upload = services.start_resumable_upload(
        user=submitter_user,
        filename="paper.pdf",
        total_bytes=len(payload),
        chunk_size=chunk_size,
    )
    expected_chunks = upload.expected_chunks
    for index in range(expected_chunks):
        chunk = payload[index * chunk_size : (index + 1) * chunk_size]
        services.append_resumable_chunk(
            token=upload.token, user=submitter_user, chunk_index=index, body=chunk,
        )
    digest = hashlib.sha256(payload).hexdigest()
    upload, entry = services.finalize_resumable_upload(
        token=upload.token, user=submitter_user, expected_sha256=digest,
    )
    assert upload.status == ResumableUpload.Status.COMPLETED
    assert upload.sha256_hex == digest
    assembled = Path(settings.MEDIA_ROOT) / entry["rel_path"]
    assert assembled.exists()
    # Chunk dir is wiped after successful finalize.
    chunk_dir = Path(settings.MEDIA_ROOT) / "resumable" / upload.token
    assert not chunk_dir.exists()


@pytest.mark.django_db
def test_re_uploading_same_chunk_index_is_idempotent(submitter_user):
    upload = services.start_resumable_upload(
        user=submitter_user, filename="data.csv", total_bytes=1000, chunk_size=500,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=b"a" * 500,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=b"a" * 500,
    )
    upload.refresh_from_db()
    assert upload.chunks_received == 1
    assert upload.received_chunk_indexes == [0]


@pytest.mark.django_db
def test_finalize_rejects_hash_mismatch_and_marks_abandoned(submitter_user):
    payload = _pdf_bytes()
    upload = services.start_resumable_upload(
        user=submitter_user,
        filename="paper.pdf",
        total_bytes=len(payload),
        chunk_size=4096,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=payload[:4096],
    )
    services.append_resumable_chunk(
        token=upload.token,
        user=submitter_user,
        chunk_index=1,
        body=payload[4096:],
    )
    with pytest.raises(services.ResumableUploadHashMismatch):
        services.finalize_resumable_upload(
            token=upload.token,
            user=submitter_user,
            expected_sha256="0" * 64,
        )
    upload.refresh_from_db()
    assert upload.status == ResumableUpload.Status.ABANDONED


@pytest.mark.django_db
def test_finalize_runs_validators_and_rejects_renamed_exe(submitter_user):
    # An ELF / PE binary renamed to .pdf will trip the magic-bytes check.
    elf = b"\x7fELF" + b"\x00" * 4092
    upload = services.start_resumable_upload(
        user=submitter_user,
        filename="trojan.pdf",
        total_bytes=len(elf),
        chunk_size=4096,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=elf,
    )
    with pytest.raises(services.ResumableUploadValidationFailed):
        services.finalize_resumable_upload(
            token=upload.token, user=submitter_user, expected_sha256=None,
        )
    upload.refresh_from_db()
    assert upload.status == ResumableUpload.Status.ABANDONED


@pytest.mark.django_db
def test_finalize_rejects_when_chunks_missing(submitter_user):
    from django.core.exceptions import ValidationError as DjValidationError

    upload = services.start_resumable_upload(
        user=submitter_user, filename="x.pdf", total_bytes=8000, chunk_size=4000,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=b"a" * 4000,
    )
    with pytest.raises(DjValidationError):
        services.finalize_resumable_upload(
            token=upload.token, user=submitter_user, expected_sha256=None,
        )


@pytest.mark.django_db
def test_abort_marks_session_abandoned_and_removes_chunks(submitter_user):
    upload = services.start_resumable_upload(
        user=submitter_user, filename="x.pdf", total_bytes=4000, chunk_size=4000,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=b"a" * 4000,
    )
    services.abort_resumable_upload(token=upload.token, user=submitter_user)
    upload.refresh_from_db()
    assert upload.status == ResumableUpload.Status.ABANDONED
    assert not (Path(settings.MEDIA_ROOT) / "resumable" / upload.token).exists()


@pytest.mark.django_db
def test_cleanup_expired_resumable_uploads_sweeps_stale_sessions(submitter_user):
    expired = services.start_resumable_upload(
        user=submitter_user, filename="a.pdf", total_bytes=4000, chunk_size=4000,
    )
    expired.expires_at = timezone.now() - timedelta(hours=1)
    expired.save(update_fields=["expires_at"])
    abandoned = services.start_resumable_upload(
        user=submitter_user, filename="b.pdf", total_bytes=4000, chunk_size=4000,
    )
    services.abort_resumable_upload(token=abandoned.token, user=submitter_user)
    fresh = services.start_resumable_upload(
        user=submitter_user, filename="c.pdf", total_bytes=4000, chunk_size=4000,
    )

    swept = services.cleanup_expired_resumable_uploads()
    assert swept == 2
    expired.refresh_from_db()
    fresh.refresh_from_db()
    assert expired.status == ResumableUpload.Status.ABANDONED
    assert fresh.status == ResumableUpload.Status.IN_PROGRESS


# ---------------------------------------------------------------------------
# View-level
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_resumable_start_view_returns_token(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    resp = client.post(
        reverse("repo_documents:resumable_start"),
        data=json.dumps({"filename": "thesis.pdf", "total_bytes": 5000, "chunk_size": 2000}),
        content_type="application/json",
    )
    assert resp.status_code == 201
    body = resp.json()
    assert body["expected_chunks"] == 3
    assert ResumableUpload.objects.filter(token=body["token"]).exists()


@pytest.mark.django_db
def test_resumable_start_view_returns_413_on_oversize(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    resp = client.post(
        reverse("repo_documents:resumable_start"),
        data=json.dumps({"filename": "huge.pdf", "total_bytes": 300 * 1024 * 1024}),
        content_type="application/json",
    )
    assert resp.status_code == 413


@pytest.mark.django_db
def test_full_resumable_flow_via_views_registers_staged_token(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    payload = _pdf_bytes(size_kb=7)
    chunk_size = 2048

    start = client.post(
        reverse("repo_documents:resumable_start"),
        data=json.dumps({
            "filename": "thesis.pdf",
            "total_bytes": len(payload),
            "chunk_size": chunk_size,
        }),
        content_type="application/json",
    )
    assert start.status_code == 201
    token = start.json()["token"]
    expected_chunks = start.json()["expected_chunks"]

    for index in range(expected_chunks):
        chunk = payload[index * chunk_size : (index + 1) * chunk_size]
        resp = client.patch(
            reverse("repo_documents:resumable_chunk"),
            data=chunk,
            content_type="application/octet-stream",
            HTTP_X_RESUMABLE_TOKEN=token,
            HTTP_X_RESUMABLE_CHUNK_INDEX=str(index),
        )
        assert resp.status_code == 200

    digest = hashlib.sha256(payload).hexdigest()
    finalize = client.post(
        reverse("repo_documents:resumable_finalize"),
        data=json.dumps({"token": token, "sha256_hex": digest}),
        content_type="application/json",
    )
    assert finalize.status_code == 201
    body = finalize.json()
    assert body["sha256_hex"] == digest

    # Staged-upload session should now contain the same token, so the
    # existing submission wizard can consume it.
    session = client.session
    store = session.get("repo_staged_uploads") or {}
    assert token in store
    rel = store[token]["rel_path"]
    assembled = Path(settings.MEDIA_ROOT) / rel
    assert assembled.exists()


@pytest.mark.django_db
def test_resumable_chunk_view_returns_410_for_unknown_token(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    resp = client.patch(
        reverse("repo_documents:resumable_chunk"),
        data=b"x",
        content_type="application/octet-stream",
        HTTP_X_RESUMABLE_TOKEN="does-not-exist",
        HTTP_X_RESUMABLE_CHUNK_INDEX="0",
    )
    assert resp.status_code == 410


@pytest.mark.django_db
def test_resumable_finalize_view_returns_422_on_validation_failure(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    elf = b"\x7fELF" + b"\x00" * 4092

    start = client.post(
        reverse("repo_documents:resumable_start"),
        data=json.dumps({
            "filename": "trojan.pdf",
            "total_bytes": len(elf),
            "chunk_size": 4096,
        }),
        content_type="application/json",
    )
    assert start.status_code == 201
    token = start.json()["token"]

    chunk_resp = client.patch(
        reverse("repo_documents:resumable_chunk"),
        data=elf,
        content_type="application/octet-stream",
        HTTP_X_RESUMABLE_TOKEN=token,
        HTTP_X_RESUMABLE_CHUNK_INDEX="0",
    )
    assert chunk_resp.status_code == 200

    finalize = client.post(
        reverse("repo_documents:resumable_finalize"),
        data=json.dumps({"token": token}),
        content_type="application/json",
    )
    assert finalize.status_code == 422
    assert ResumableUpload.objects.get(token=token).status == ResumableUpload.Status.ABANDONED


@pytest.mark.django_db
def test_resumable_abort_view_clears_chunk_dir(submitter_user):
    client = Client()
    client.force_login(submitter_user)
    upload = services.start_resumable_upload(
        user=submitter_user, filename="x.pdf", total_bytes=4000, chunk_size=4000,
    )
    services.append_resumable_chunk(
        token=upload.token, user=submitter_user, chunk_index=0, body=b"a" * 4000,
    )

    resp = client.delete(
        reverse("repo_documents:resumable_abort"),
        data=json.dumps({"token": upload.token}),
        content_type="application/json",
    )
    assert resp.status_code == 200
    upload.refresh_from_db()
    assert upload.status == ResumableUpload.Status.ABANDONED
    assert not (Path(settings.MEDIA_ROOT) / "resumable" / upload.token).exists()
