"""
Tests for Image Collections: unified files-backend storage, upload/delete
API flows, URL resolution and the R2→files backfill command.
"""

import io
import os
from unittest import mock

import pytest
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from PIL import Image as PILImage

from links.models import ImageCollection, Image, FileUpload


def _real_png(name="photo.png"):
    """A fully decodable 8x8 PNG (Pillow can open it → thumbnails work)."""
    buf = io.BytesIO()
    PILImage.new("RGB", (8, 8), (200, 60, 60)).save(buf, format="PNG")
    return SimpleUploadedFile(name, buf.getvalue(), content_type="image/png")


def _text_file():
    return SimpleUploadedFile("note.txt", b"hello", content_type="text/plain")


@pytest.mark.django_db
class TestImageFilesBackend:
    def _collection(self):
        return ImageCollection.objects.create(name="Test Collection")

    def test_upload_images_uses_files_backend(self, api_client):
        coll = self._collection()
        r = api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": _real_png()},
            format="multipart",
        )
        assert r.status_code == 201
        assert r.json()["status"] == "success"

        image = coll.images.get()
        assert image.file is not None
        # The image is served through the files backend URL, not R2
        assert image.file_key == ""
        assert image.file.mime_type == "image/png"
        assert image.file.size > 0
        assert os.path.exists(image.file.file_path)
        assert image.get_url() == image.file.download_url
        assert image.get_url().startswith("/ui/files/")
        assert image.get_thumbnail_url() == image.file.thumbnail_url

    def test_upload_rejects_non_image(self, api_client):
        coll = self._collection()
        r = api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": _text_file()},
            format="multipart",
        )
        assert r.status_code == 400
        assert coll.images.count() == 0
        assert FileUpload.objects.count() == 0

    def test_upload_multiple_images(self, api_client):
        coll = self._collection()
        r = api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": [_real_png("a.png"), _real_png("b.png")]},
            format="multipart",
        )
        assert r.status_code == 201
        assert coll.images.count() == 2
        assert FileUpload.objects.count() == 2

    def test_serializer_exposes_url_and_thumbnail(self, api_client):
        coll = self._collection()
        api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": _real_png()},
            format="multipart",
        )
        data = api_client.get(f"/api/collections/{coll.pk}").json()
        assert data["image_count"] == 1
        r = api_client.get(f"/api/images/{coll.images.get().pk}")
        assert r.status_code == 200
        body = r.json()
        assert body["url"].startswith("/ui/files/")
        assert body["thumbnail_url"] == "/ui/files/" + str(coll.images.get().file.pk) + "/thumb/"

    def test_delete_image_removes_file_and_record(self, api_client):
        coll = self._collection()
        api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": _real_png()},
            format="multipart",
        )
        image = coll.images.get()
        path = image.file.file_path
        assert os.path.exists(path)

        r = api_client.delete(f"/api/images/{image.pk}")
        assert r.status_code == 204 or r.status_code == 200
        assert not Image.objects.filter(pk=image.pk).exists()
        assert not FileUpload.objects.filter(pk=image.file.pk).exists()
        assert not os.path.exists(path)

    def test_delete_collection_cascades(self, api_client):
        coll = self._collection()
        api_client.post(
            f"/api/collections/{coll.pk}/upload_images",
            {"file": [_real_png("a.png"), _real_png("b.png")]},
            format="multipart",
        )
        paths = [img.file.file_path for img in coll.images.all()]
        assert all(os.path.exists(p) for p in paths)

        r = api_client.delete(f"/api/collections/{coll.pk}")
        assert r.status_code == 204 or r.status_code == 200
        assert not ImageCollection.objects.filter(pk=coll.pk).exists()
        assert not Image.objects.filter(collection=coll).exists()
        assert FileUpload.objects.count() == 0
        assert not any(os.path.exists(p) for p in paths)

    def test_legacy_r2_image_falls_back_to_r2_url(self):
        """Images not yet backfilled (file_key set, no file) still resolve via R2."""
        coll = self._collection()
        image = Image.objects.create(
            collection=coll, title="legacy", file_key="images/legacy/x.jpg",
            content_type="image/jpeg", size=123,
        )
        with mock.patch("links.models.R2Storage") as fake:
            fake.return_value.get_url.return_value = "https://r2.example/x.jpg?sig=1"
            assert image.get_url().startswith("https://r2.example")
            assert fake.return_value.get_url.call_count == 1
        # And thumbnails request the cdn-cgi transformation variant
        with mock.patch("links.models.R2Storage") as fake:
            fake.return_value.get_url.return_value = "https://cdn.example/x.jpg?w=200"
            image.get_thumbnail_url()
            kwargs = fake.return_value.get_url.call_args.kwargs
            assert kwargs.get("width") == 200


@pytest.mark.django_db
class TestMigrateImageStorageCommand:
    def _legacy_image(self, coll, key="images/old/cat.jpg", raw=None):
        return Image.objects.create(
            collection=coll, title="old.png", file_key=key,
            content_type="image/png", size=len(raw or b"data"),
        )

    @staticmethod
    def _patch_download(raw=None, exc=None):
        return mock.patch(
            "links.management.commands.migrate_image_storage._download_r2_object",
            return_value=raw if raw is not None else b"\x89PNGdata",
            side_effect=exc,
        )

    def test_dry_run_does_nothing(self, tmp_path):
        coll = ImageCollection.objects.create(name="C")
        self._legacy_image(coll, raw=b"\x89PNGdata")
        call_command("migrate_image_storage")
        img = Image.objects.get()
        assert img.file is None
        assert img.file_key == "images/old/cat.jpg"

    def test_backfill_creates_fileupload_and_links(self, tmp_path):
        coll = ImageCollection.objects.create(name="C")
        raw = io.BytesIO()
        PILImage.new("RGB", (8, 8)).save(raw, format="PNG")
        raw = raw.getvalue()
        self._legacy_image(coll, raw=raw)

        with self._patch_download(raw=raw):
            call_command("migrate_image_storage", "--commit")

        img = Image.objects.get()
        assert img.file is not None
        assert img.file_key == ""
        assert os.path.exists(img.file.file_path)
        assert img.file.size == len(raw)
        # Idempotent: second run has nothing to do
        with self._patch_download(raw=raw):
            call_command("migrate_image_storage", "--commit")
        assert Image.objects.get().file is not None

    def test_backfill_failure_leaves_record_for_retry(self, tmp_path):
        coll = ImageCollection.objects.create(name="C")
        self._legacy_image(coll, raw=b"\x89PNGdata")

        with self._patch_download(exc=Exception("network down")):
            call_command("migrate_image_storage", "--commit")

        img = Image.objects.get()
        assert img.file is None
        assert img.file_key == "images/old/cat.jpg"
        assert FileUpload.objects.count() == 0
