import unittest

from django.conf import settings
from django.contrib.auth.models import Group, Permission
from django.core.cache import caches
from django.core.files import File
from django.core.files.storage import Storage, default_storage, storages
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db.models import Prefetch
from django.db.utils import IntegrityError
from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings
from django.urls import reverse
from willow.image import Image as WillowImage

from wagtail.images.models import (
    Filter,
    Picture,
    Rendition,
    ResponsiveImage,
    SourceImageIOError,
    get_rendition_storage,
)
from wagtail.images.rect import Rect
from wagtail.models import Collection, GroupCollectionPermission, Page, ReferenceIndex
from wagtail.test.testapp.models import (
    EventPage,
    EventPageCarouselItem,
    ReimportedImageModel,
)
from wagtail.test.utils import WagtailTestUtils

from .utils import (
    Image,
    get_test_image_file,
    get_test_image_file_svg,
    get_test_image_filename,
)


class CustomStorage(Storage):
    pass


class TestImage(TestCase):
    def setUp(self):
        # Create an image for running tests on
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(colour="white"),
        )

    def test_get_image_model_at_import_time(self):
        self.assertEqual(ReimportedImageModel, Image)

    def test_is_portrait(self):
        self.assertFalse(self.image.is_portrait())

    def test_is_landscape(self):
        self.assertTrue(self.image.is_landscape())

    def test_get_rect(self):
        self.assertEqual(self.image.get_rect(), Rect(0, 0, 640, 480))

    def test_get_focal_point(self):
        self.assertIsNone(self.image.get_focal_point())

        # Add a focal point to the image
        self.image.focal_point_x = 100
        self.image.focal_point_y = 200
        self.image.focal_point_width = 50
        self.image.focal_point_height = 20

        # Get it
        self.assertEqual(self.image.get_focal_point(), Rect(75, 190, 125, 210))

    def test_has_focal_point(self):
        self.assertFalse(self.image.has_focal_point())

        # Add a focal point to the image
        self.image.focal_point_x = 100
        self.image.focal_point_y = 200
        self.image.focal_point_width = 50
        self.image.focal_point_height = 20

        self.assertTrue(self.image.has_focal_point())

    def test_set_focal_point(self):
        self.assertIsNone(self.image.focal_point_x)
        self.assertIsNone(self.image.focal_point_y)
        self.assertIsNone(self.image.focal_point_width)
        self.assertIsNone(self.image.focal_point_height)

        self.image.set_focal_point(Rect(100, 150, 200, 350))

        self.assertEqual(self.image.focal_point_x, 150)
        self.assertEqual(self.image.focal_point_y, 250)
        self.assertEqual(self.image.focal_point_width, 100)
        self.assertEqual(self.image.focal_point_height, 200)

        self.image.set_focal_point(None)

        self.assertIsNone(self.image.focal_point_x)
        self.assertIsNone(self.image.focal_point_y)
        self.assertIsNone(self.image.focal_point_width)
        self.assertIsNone(self.image.focal_point_height)

    def test_is_stored_locally(self):
        self.assertTrue(self.image.is_stored_locally())

    @override_settings(
        STORAGES={
            **settings.STORAGES,
            "default": {
                "BACKEND": "wagtail.test.dummy_external_storage.DummyExternalStorage"
            },
        },
    )
    def test_is_stored_locally_with_external_storage(self):
        self.assertFalse(self.image.is_stored_locally())

    def test_get_file_size(self):
        file_size = self.image.get_file_size()
        self.assertIsInstance(file_size, int)
        self.assertGreater(file_size, 0)

    def test_get_file_size_on_missing_file_raises_sourceimageioerror(self):
        self.image.file.delete(save=False)
        with self.assertRaises(SourceImageIOError):
            self.image.get_file_size()

    def test_file_hash(self):
        self.assertEqual(
            self.image.get_file_hash(), "4dd0211870e130b7e1690d2ec53c499a54a48fef"
        )

    def test_get_suggested_focal_point_svg(self):
        """
        Feature detection should not be run on SVGs.

        https://github.com/wagtail/wagtail/issues/11172
        """
        image = Image.objects.create(
            title="Test SVG",
            file=get_test_image_file_svg(),
        )
        self.assertIsNone(image.get_suggested_focal_point())

    def test_default_with_description(self):
        # Primary default should be description
        image = Image.objects.create(
            title="Test Image",
            description="This is a test description",
            file=get_test_image_file(),
        )
        self.assertEqual(image.default_alt_text, image.description)

    def test_default_without_description(self):
        # Secondary default should be title
        image = Image.objects.create(
            title="Test Image",
            file=get_test_image_file(),
        )
        self.assertEqual(image.default_alt_text, image.title)


class TestImageQuerySet(TransactionTestCase):
    fixtures = ["test_empty.json"]

    def test_search_method(self):
        # Create an image for running tests on
        image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

        # Search for it
        results = Image.objects.search("Test")
        self.assertEqual(list(results), [image])

    def test_operators(self):
        aaa_image = Image.objects.create(
            title="AAA Test image",
            file=get_test_image_file(),
        )
        zzz_image = Image.objects.create(
            title="ZZZ Test image",
            file=get_test_image_file(),
        )

        results = Image.objects.search("aaa test", operator="and")
        self.assertEqual(list(results), [aaa_image])

        results = Image.objects.search("aaa test", operator="or")
        sorted_results = sorted(results, key=lambda img: img.title)
        self.assertEqual(sorted_results, [aaa_image, zzz_image])

    def test_custom_ordering(self):
        aaa_image = Image.objects.create(
            title="AAA Test image",
            file=get_test_image_file(),
        )
        zzz_image = Image.objects.create(
            title="ZZZ Test image",
            file=get_test_image_file(),
        )

        results = Image.objects.order_by("title").search(
            "Test", order_by_relevance=False
        )
        self.assertEqual(list(results), [aaa_image, zzz_image])
        results = Image.objects.order_by("-title").search(
            "Test", order_by_relevance=False
        )
        self.assertEqual(list(results), [zzz_image, aaa_image])

    def test_search_indexing_prefetches_tags(self):
        for i in range(0, 10):
            image = Image.objects.create(
                title="Test image %d" % i,
                file=get_test_image_file(),
            )
            image.tags.add("aardvark", "artichoke", "armadillo")

        with self.assertNumQueries(2):
            results = {
                image.title: [tag.name for tag in image.tags.all()]
                for image in Image.get_indexed_objects()
            }
            self.assertIn("aardvark", results["Test image 0"])


class TestImagePermissions(WagtailTestUtils, TestCase):
    def setUp(self):
        # Create some user accounts for testing permissions
        self.user = self.create_user(
            username="user", email="user@email.com", password="password"
        )
        self.owner = self.create_user(
            username="owner", email="owner@email.com", password="password"
        )
        self.editor = self.create_user(
            username="editor", email="editor@email.com", password="password"
        )
        self.editor.groups.add(Group.objects.get(name="Editors"))
        self.administrator = self.create_superuser(
            username="administrator",
            email="administrator@email.com",
            password="password",
        )

        # Owner user must have the add_image permission
        image_adders_group = Group.objects.create(name="Image adders")
        GroupCollectionPermission.objects.create(
            group=image_adders_group,
            collection=Collection.get_first_root_node(),
            permission=Permission.objects.get(codename="add_image"),
        )
        self.owner.groups.add(image_adders_group)

        # Create an image for running tests on
        self.image = Image.objects.create(
            title="Test image",
            uploaded_by_user=self.owner,
            file=get_test_image_file(),
        )

    def test_administrator_can_edit(self):
        self.assertTrue(self.image.is_editable_by_user(self.administrator))

    def test_editor_can_edit(self):
        self.assertTrue(self.image.is_editable_by_user(self.editor))

    def test_owner_can_edit(self):
        self.assertTrue(self.image.is_editable_by_user(self.owner))

    def test_user_cant_edit(self):
        self.assertFalse(self.image.is_editable_by_user(self.user))


class TestFilters(SimpleTestCase):
    def test_expand_spec_single(self):
        self.assertEqual(Filter.expand_spec("width-100"), ["width-100"])

    def test_expand_spec_flat(self):
        self.assertEqual(
            Filter.expand_spec("width-100 jpegquality-20"), ["width-100|jpegquality-20"]
        )

    def test_expand_spec_pipe(self):
        self.assertEqual(
            Filter.expand_spec("width-100|jpegquality-20"), ["width-100|jpegquality-20"]
        )

    def test_expand_spec_list(self):
        self.assertEqual(
            Filter.expand_spec(["width-100", "jpegquality-20"]),
            ["width-100|jpegquality-20"],
        )

    def test_expand_spec_braced(self):
        self.assertEqual(
            Filter.expand_spec("width-{100,200}"), ["width-100", "width-200"]
        )

    def test_expand_spec_mixed(self):
        self.assertEqual(
            Filter.expand_spec("width-{100,200} jpegquality-40"),
            ["width-100|jpegquality-40", "width-200|jpegquality-40"],
        )

    def test_expand_spec_mixed_pipe(self):
        self.assertEqual(
            Filter.expand_spec("width-{100,200}|jpegquality-40"),
            ["width-100|jpegquality-40", "width-200|jpegquality-40"],
        )

    def test_expand_spec_multiple_braces(self):
        self.assertEqual(
            Filter.expand_spec("width-{100,200} jpegquality-{40,80} grayscale"),
            [
                "width-100|jpegquality-40|grayscale",
                "width-100|jpegquality-80|grayscale",
                "width-200|jpegquality-40|grayscale",
                "width-200|jpegquality-80|grayscale",
            ],
        )


class TestResponsiveImage(TestCase):
    def setUp(self):
        # Create an image for running tests on
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )
        self.rendition_10 = self.image.get_rendition("width-10")

    def test_construct_empty(self):
        img = ResponsiveImage({})
        self.assertEqual(img.renditions, [])
        self.assertEqual(img.attrs, None)

    def test_construct_with_renditions(self):
        renditions = {"a": self.rendition_10}
        img = ResponsiveImage(renditions)
        self.assertEqual(img.renditions, [self.rendition_10])

    def test_evaluate_value(self):
        self.assertFalse(ResponsiveImage({}))
        self.assertFalse(ResponsiveImage({}, {"sizes": "100vw"}))

        renditions = {"a": self.rendition_10}
        self.assertTrue(ResponsiveImage(renditions))

    def test_compare_value(self):
        renditions = {"a": self.rendition_10}
        value1 = ResponsiveImage(renditions)
        value2 = ResponsiveImage(renditions)
        value3 = ResponsiveImage({"a": self.image.get_rendition("width-15")})
        value4 = ResponsiveImage(renditions, {"sizes": "100vw"})
        self.assertNotEqual(value1, value3)
        self.assertNotEqual(value1, 12345)
        self.assertEqual(value1, value2)
        self.assertNotEqual(value1, value4)

    def test_get_width_srcset(self):
        renditions = {
            "width-10": self.rendition_10,
            "width-90": self.image.get_rendition("width-90"),
        }
        filenames = [
            get_test_image_filename(self.image, "width-10"),
            get_test_image_filename(self.image, "width-90"),
        ]
        self.assertEqual(
            ResponsiveImage.get_width_srcset(list(renditions.values())),
            f"{filenames[0]} 10w, {filenames[1]} 90w",
        )

    def test_get_width_srcset_single_rendition(self):
        renditions = {"width-10": self.rendition_10}
        self.assertEqual(
            ResponsiveImage.get_width_srcset(list(renditions.values())),
            get_test_image_filename(self.image, "width-10"),
        )

    def test_render(self):
        renditions = {
            "width-10": self.rendition_10,
            "width-90": self.image.get_rendition("width-90"),
        }
        img = ResponsiveImage(renditions)
        filenames = [
            get_test_image_filename(self.image, "width-10"),
            get_test_image_filename(self.image, "width-90"),
        ]
        self.assertHTMLEqual(
            img.__html__(),
            f"""
                <img
                    alt="Test image"
                    src="{filenames[0]}"
                    srcset="{filenames[0]} 10w, {filenames[1]} 90w"
                    width="10"
                    height="7"
                >
            """,
        )

    def test_render_single_image_same_as_img_tag(self):
        img = ResponsiveImage({"width-10": self.rendition_10})
        self.assertHTMLEqual(img.__html__(), self.rendition_10.img_tag())


class TestPicture(TestCase):
    def setUp(self):
        # Create an image for running tests on
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )
        self.rendition_10 = self.image.get_rendition("width-10")

    def test_formats(self):
        renditions = {
            "format-jpeg": self.rendition_10,
            "format-webp": self.rendition_10,
        }
        img = Picture(renditions)
        self.assertEqual(
            img.formats, {"jpeg": [self.rendition_10], "webp": [self.rendition_10]}
        )

    def test_single_format(self):
        renditions = {"format-jpeg": self.rendition_10}
        img = Picture(renditions)
        self.assertEqual(img.formats, {})

    def test_mixed_format(self):
        renditions = {
            "format-jpeg": self.rendition_10,
            "format-webp": self.rendition_10,
            "format-webp-lossless": self.rendition_10,
        }
        img = Picture(renditions)
        self.assertEqual(
            img.formats,
            {
                "jpeg": [self.rendition_10],
                "webp": [self.rendition_10, self.rendition_10],
            },
        )

    def test_fallback_format(self):
        avif = {"format-avif": self.rendition_10}
        webp = {"format-webp": self.rendition_10}
        jpeg = {"format-jpeg": self.rendition_10}
        png = {"format-png": self.rendition_10}
        gif = {"format-gif": self.rendition_10}
        fallbacks = {
            "gif": {**avif, **webp, **jpeg, **png, **gif},
            "png": {**avif, **webp, **jpeg, **png},
            "jpeg": {**avif, **webp, **jpeg},
            "webp": {**avif, **webp},
        }
        for fmt, renditions in fallbacks.items():
            self.assertEqual(Picture(renditions).get_fallback_format(), fmt)

    def test_render_multi_format_sizes(self):
        renditions = {
            "format-jpeg|width-10": self.image.get_rendition("format-jpeg|width-10"),
            "format-jpeg|width-90": self.image.get_rendition("format-jpeg|width-90"),
            "format-webp|width-10": self.image.get_rendition("format-webp|width-10"),
            "format-webp|width-90": self.image.get_rendition("format-webp|width-90"),
        }
        img = Picture(renditions, {"sizes": "100vw"})
        filenames = [
            get_test_image_filename(self.image, "format-jpeg.width-10"),
            get_test_image_filename(self.image, "format-jpeg.width-90"),
            get_test_image_filename(self.image, "format-webp.width-10"),
            get_test_image_filename(self.image, "format-webp.width-90"),
        ]
        self.assertHTMLEqual(
            img.__html__(),
            f"""
                <picture>
                    <source srcset="{filenames[2]} 10w, {filenames[3]} 90w" sizes="100vw" type="image/webp">
                    <img
                        alt="Test image"
                        sizes="100vw"
                        src="{filenames[0]}"
                        srcset="{filenames[0]} 10w, {filenames[1]} 90w"
                        width="10"
                        height="7"
                    >
                </picture>
            """,
        )

    def test_render_single_image_same_as_img_tag(self):
        img = Picture({"width-10": self.rendition_10})
        self.assertHTMLEqual(
            img.__html__(), f"<picture>{self.rendition_10.img_tag()}</picture>"
        )


@override_settings(
    CACHES={"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
)
class TestRenditions(TestCase):
    SPECS = ("height-66", "width-100", "width-400")

    def setUp(self):
        # Create an image for running tests on
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

    def test_get_rendition_model(self):
        self.assertIs(Image.get_rendition_model(), Rendition)

    def test_minification(self):
        rendition = self.image.get_rendition("width-400")

        # Check size
        self.assertEqual(rendition.width, 400)
        self.assertEqual(rendition.height, 300)

        # check that the rendition has been recorded under the correct filter,
        # via the Rendition.filter_spec attribute (in active use as of Wagtail 1.8)
        self.assertEqual(rendition.filter_spec, "width-400")

    def test_resize_to_max(self):
        rendition = self.image.get_rendition("max-100x100")

        # Check size
        self.assertEqual(rendition.width, 100)
        self.assertEqual(rendition.height, 75)

    def test_resize_to_min(self):
        rendition = self.image.get_rendition("min-120x120")

        # Check size
        self.assertEqual(rendition.width, 160)
        self.assertEqual(rendition.height, 120)

    def test_resize_to_original(self):
        rendition = self.image.get_rendition("original")

        # Check size
        self.assertEqual(rendition.width, 640)
        self.assertEqual(rendition.height, 480)

    def test_cache(self):
        # Get two renditions with the same filter
        first_rendition = self.image.get_rendition("width-400")
        second_rendition = self.image.get_rendition("width-400")

        # Check that they are the same object
        self.assertEqual(first_rendition, second_rendition)

    def test_get_with_filter_instance(self):
        # Get two renditions with the same filter
        first_rendition = self.image.get_rendition("width-400")
        second_rendition = self.image.get_rendition(Filter("width-400"))

        # Check that they are the same object
        self.assertEqual(first_rendition, second_rendition)

    def test_prefetched_rendition_found(self):
        # Request a rendition that does not exist yet
        with self.assertNumQueries(5):
            original_rendition = self.image.get_rendition("width-100")

        # Refetch the same image with all renditions prefetched
        image = Image.objects.prefetch_related("renditions").get(pk=self.image.pk)

        # get_rendition() should return the prefetched rendition
        with self.assertNumQueries(0):
            second_rendition = image.get_rendition("width-100")

        # The renditions should match
        self.assertEqual(original_rendition, second_rendition)

    def test_prefetch_renditions_found(self):
        # Same test as above but uses the `prefetch_renditions` method on the manager instead.
        with self.assertNumQueries(5):
            original_rendition = self.image.get_rendition("width-100")

        image = Image.objects.prefetch_renditions("width-100").get(pk=self.image.pk)

        with self.assertNumQueries(0):
            second_rendition = image.get_rendition("width-100")

        self.assertEqual(original_rendition, second_rendition)

    def test_prefetched_rendition_not_found(self):
        # Request a rendition that does not exist yet
        with self.assertNumQueries(5):
            original_rendition = self.image.get_rendition("width-100")

        # Refetch the same image with all renditions prefetched
        image = Image.objects.prefetch_related("renditions").get(pk=self.image.pk)

        # Request a different rendition from this object
        with self.assertNumQueries(4):
            # The number of queries is fewer than before, because checks for
            # an existing rendition (in cache and db) are skipped
            second_rendition = image.get_rendition("height-66")

        # The renditions should NOT match
        self.assertNotEqual(original_rendition, second_rendition)

        # Request the same rendition again
        with self.assertNumQueries(0):
            # Newly created renditions are appended to the prefetched
            # rendition queryset, so that multiple requests for the same
            # rendition can reuse the same value
            third_rendition = image.get_rendition("height-66")

        # The second and third renditions should be references to the
        # exact same in-memory object
        self.assertIs(second_rendition, third_rendition)

    def test_prefetch_renditions_not_found(self):
        # Same test as above but uses the `prefetch_renditions` method on the manager instead.
        with self.assertNumQueries(5):
            original_rendition = self.image.get_rendition("width-100")

        image = Image.objects.prefetch_renditions("width-100").get(pk=self.image.pk)

        with self.assertNumQueries(4):
            second_rendition = image.get_rendition("height-66")

        self.assertNotEqual(original_rendition, second_rendition)

        with self.assertNumQueries(0):
            third_rendition = image.get_rendition("height-66")

        self.assertIs(second_rendition, third_rendition)

    def test_get_renditions_with_filter_instance(self):
        # Get two renditions with the same filter
        first = list(self.image.get_renditions("width-400").values())
        second = list(self.image.get_renditions(Filter("width-400")).values())

        # Check that they are the same object
        self.assertEqual(first[0], second[0])

    def test_get_renditions_key_order(self):
        # Fetch one of the renditions so it exists before the other two.
        self.image.get_rendition("width-40")
        specs = ["width-30", "width-40", "width-50"]
        renditions_keys = list(self.image.get_renditions(*specs).keys())
        self.assertEqual(renditions_keys, specs)

    def _test_get_renditions_performance(
        self,
        db_queries_expected: int,
        prefetch_restricted: bool = False,
        prefetch_all: bool = False,
    ):
        queryset = Image.objects.all()
        if prefetch_all:
            queryset = queryset.prefetch_related("renditions")
        elif prefetch_restricted:
            queryset = queryset.prefetch_renditions(*self.SPECS)

        image = queryset.get(id=self.image.id)
        with self.assertNumQueries(db_queries_expected):
            image.get_renditions(*self.SPECS)

    def test_get_renditions_performance_with_rendition_caching_disabled(self):
        # ATTEMPT 1
        # 1) An initial lookup for rendition from the DB
        # 2) A check for clashes before bulk saving new renditions
        # 3) A bulk_create() to save new renditions
        self._test_get_renditions_performance(3)

        # ATTEMPT 2
        # With all renditions already created, we should just see
        # 1) An initial lookup for rendition from the DB
        self._test_get_renditions_performance(1)

        # ATTEMPT 3
        # If the existing renditions are prefetched, no further queries should
        # be needed, whether that's with prefetch_related("renditions") or
        # prefetch_renditions()
        self._test_get_renditions_performance(0, prefetch_all=True)
        self._test_get_renditions_performance(0, prefetch_restricted=True)

    @override_settings(
        CACHES={
            "renditions": {
                "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            },
        },
    )
    def test_get_renditions_performance_with_rendition_caching_enabled(self):
        # ATTEMPT 1
        # 1) An initial lookup for rendition from the DB
        # 2) A check for clashes before bulk saving new renditions
        # 3) A bulk_create() to save new renditions
        self._test_get_renditions_performance(3)

        # ATTEMPT 2
        # Any renditions created in the first attempt should have
        # been added to the cache, so a second request should bypass
        # the database completely
        self._test_get_renditions_performance(0)

        # ATTEMPT 3
        # Prefetching renditions should mean that no queries are
        # required, and no cache hits are made
        self._test_get_renditions_performance(0, prefetch_all=True)

    def test_create_renditions(self):
        filter_list = [Filter(spec) for spec in self.SPECS]
        # When no renditions exist, there should be one query for
        # 'clash identification', and another for bulk creation
        with self.assertNumQueries(2):
            result = self.image.create_renditions(*filter_list)

        # Renditions should match the filters
        for filter, rendition in result.items():
            self.assertEqual(filter.spec, rendition.filter_spec)

        # Filter specs should match the filters that were provided as arguments
        self.assertEqual(
            {filter.spec for filter in result.keys()},
            {filter.spec for filter in filter_list},
        )

        # When the renditions already exist, there should be one query
        # for 'clash identification', but that is all
        with self.assertNumQueries(1):
            result = self.image.create_renditions(*filter_list)

        # Renditions should match the filters
        for filter, rendition in result.items():
            self.assertEqual(filter.spec, rendition.filter_spec)

        # Filter specs should match the filters that were provided as arguments
        self.assertEqual(
            {filter.spec for filter in result.keys()},
            {filter.spec for filter in filter_list},
        )

        # Another request should give us an equal result
        with self.assertNumQueries(1):
            second_result = self.image.create_renditions(*filter_list)
        self.assertEqual(result, second_result)

        # When only some renditions exist, we should see the create query
        # once again
        self.image.renditions.filter(filter_spec=self.SPECS[0]).delete()
        with self.assertNumQueries(2):
            third_result = self.image.create_renditions(*filter_list)

        # Equality check should fail, as newly created rendition has a different pk
        self.assertNotEqual(third_result, result)

        # But, we should see equality on the keys
        self.assertEqual(third_result.keys(), result.keys())

    def test_alt_attribute(self):
        rendition = self.image.get_rendition("width-400")
        self.assertEqual(rendition.alt, "Test image")

    def test_full_url(self):
        ren_img = self.image.get_rendition("original")
        full_url = ren_img.full_url
        img_name = ren_img.file.name.split("/")[1]
        self.assertEqual(full_url, f"http://testserver/media/images/{img_name}")

    @override_settings(
        CACHES={
            "renditions": {
                "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
            },
        },
    )
    def test_renditions_cache(self):
        cache = Rendition.cache_backend
        rendition = self.image.get_rendition("width-500")
        rendition_cache_key = rendition.get_cache_key()

        # Check rendition is saved to cache
        self.assertEqual(cache.get(rendition_cache_key), rendition)

        # Mark a rendition to check it comes from cache
        rendition._mark = "original"
        cache.set(rendition_cache_key, rendition)

        # Check if get_rendition returns the rendition from cache
        with self.assertNumQueries(0):
            new_rendition = self.image.get_rendition("width-500")
        self.assertEqual(new_rendition._mark, "original")

        # But, not if the rendition has been prefetched
        fresh_image = Image.objects.prefetch_related("renditions").get(pk=self.image.pk)
        with self.assertNumQueries(0):
            prefetched_rendition = fresh_image.get_rendition("width-500")
        self.assertFalse(hasattr(prefetched_rendition, "_mark"))

        # Check that the image instance is the same as the retrieved rendition
        self.assertIs(new_rendition.image, self.image)

        # changing the image file should invalidate the cache
        self.image.file = get_test_image_file(colour="green")
        self.image.save()
        # deleting renditions would normally happen within the 'edit' view on file change -
        # we're bypassing that here, so have to do it manually
        self.image.renditions.all().delete()
        new_rendition = self.image.get_rendition("width-500")
        self.assertFalse(hasattr(new_rendition, "_mark"))

        # changing it back should also generate a new rendition and not re-use
        # the original one (because that file has now been deleted in the change)
        self.image.file = get_test_image_file(colour="white")
        self.image.save()
        self.image.renditions.all().delete()
        new_rendition = self.image.get_rendition("width-500")
        self.assertFalse(hasattr(new_rendition, "_mark"))

    def test_prefers_rendition_cache_backend(self):
        with override_settings(
            CACHES={
                "default": {
                    "BACKEND": "django.core.cache.backends.dummy.DummyCache",
                },
                "renditions": {
                    "BACKEND": "django.core.cache.backends.dummy.DummyCache",
                },
            }
        ):
            self.assertEqual(Rendition.cache_backend, caches["renditions"])

    def test_uses_default_cache_when_no_renditions_cache(self):
        with override_settings(
            CACHES={
                "default": {
                    "BACKEND": "django.core.cache.backends.dummy.DummyCache",
                }
            }
        ):
            self.assertEqual(Rendition.cache_backend, caches["default"])

    def test_focal_point(self):
        self.image.focal_point_x = 100
        self.image.focal_point_y = 200
        self.image.focal_point_width = 50
        self.image.focal_point_height = 20
        self.image.save()

        # Generate a rendition that's half the size of the original
        rendition = self.image.get_rendition("width-320")

        self.assertEqual(rendition.focal_point.round(), Rect(37, 95, 63, 105))
        self.assertEqual(rendition.focal_point.centroid.x, 50)
        self.assertEqual(rendition.focal_point.centroid.y, 100)
        self.assertEqual(rendition.focal_point.width, 25)
        self.assertEqual(rendition.focal_point.height, 10)

        self.assertEqual(
            rendition.background_position_style, "background-position: 15% 41%;"
        )

    def test_background_position_style_default(self):
        # Generate a rendition that's half the size of the original
        rendition = self.image.get_rendition("width-320")

        self.assertEqual(
            rendition.background_position_style, "background-position: 50% 50%;"
        )

    @override_settings()
    def test_rendition_storage_setting_absent(self):
        del settings.WAGTAILIMAGES_RENDITION_STORAGE
        self.assertFalse(hasattr(settings, "WAGTAILIMAGES_RENDITION_STORAGE"))
        self.assertEqual(get_rendition_storage(), default_storage)

    @override_settings(
        WAGTAILIMAGES_RENDITION_STORAGE="wagtail.images.tests.test_models.CustomStorage"
    )
    def test_rendition_storage_setting_given_dotted_path(self):
        self.assertIsInstance(get_rendition_storage(), CustomStorage)

    @override_settings(WAGTAILIMAGES_RENDITION_STORAGE=CustomStorage())
    def test_rendition_storage_setting_given_storage_instance(self):
        self.assertEqual(
            get_rendition_storage(), settings.WAGTAILIMAGES_RENDITION_STORAGE
        )

    @override_settings(
        STORAGES={
            "default": {
                "BACKEND": "django.core.files.storage.FileSystemStorage",
            },
            "staticfiles": {
                "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
            },
            "custom_storage": {
                "BACKEND": "wagtail.images.tests.test_models.CustomStorage",
            },
        },
        WAGTAILIMAGES_RENDITION_STORAGE="custom_storage",
    )
    def test_rendition_storage_setting_given_storage_alias(self):
        self.assertEqual(
            get_rendition_storage(), storages[settings.WAGTAILIMAGES_RENDITION_STORAGE]
        )


@override_settings(
    CACHES={"default": {"BACKEND": "django.core.cache.backends.dummy.DummyCache"}}
)
class TestPrefetchRenditions(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.images = []
        self.event_pages_pks = []

        event_pages = EventPage.objects.all()[:3]
        for i, page in enumerate(event_pages):
            page.feed_image = image = Image.objects.create(
                title="Test image {i}",
                file=get_test_image_file(),
            )
            page.save(update_fields=["feed_image"])
            self.images.append(image)
            self.event_pages_pks.append(page.pk)

        # Generate renditions
        self.small_renditions = [
            image.get_rendition("max-100x100") for image in self.images
        ]
        self.large_renditions = [
            image.get_rendition("min-300x600") for image in self.images
        ]

    def test_prefetch_renditions_on_non_image_querysets(self):
        prefetch_images_and_small_renditions = Prefetch(
            "feed_image", queryset=Image.objects.prefetch_renditions("max-100x100")
        )
        with self.assertNumQueries(3):
            # One query to get the `EventPage`s, another one to fetch the feed images
            # and a last one to select matching renditions.
            pages = list(
                EventPage.objects.prefetch_related(
                    prefetch_images_and_small_renditions
                ).filter(pk__in=self.event_pages_pks)
            )

        with self.assertNumQueries(0):
            # No additional query since small renditions were prefetched.
            small_renditions = [
                page.feed_image.get_rendition("max-100x100") for page in pages
            ]

        self.assertListEqual(self.small_renditions, small_renditions)

        with self.assertNumQueries(3):
            # Additional queries since large renditions weren't prefetched.
            large_renditions = [
                page.feed_image.get_rendition("min-300x600") for page in pages
            ]

        self.assertListEqual(self.large_renditions, large_renditions)


class TestUsageCount(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

    def test_unused_image_usage_count(self):
        self.assertEqual(self.image.get_usage().count(), 0)

    def test_used_image_document_usage_count(self):
        with self.captureOnCommitCallbacks(execute=True):
            page = EventPage.objects.get(id=4)
            event_page_carousel_item = EventPageCarouselItem()
            event_page_carousel_item.page = page
            event_page_carousel_item.image = self.image
            event_page_carousel_item.save()
        self.assertEqual(self.image.get_usage().count(), 1)


class TestGetUsage(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

    def test_unused_image_get_usage(self):
        self.assertEqual(list(self.image.get_usage()), [])

    def test_used_image_document_get_usage(self):
        with self.captureOnCommitCallbacks(execute=True):
            page = EventPage.objects.get(id=4)
            event_page_carousel_item = EventPageCarouselItem()
            event_page_carousel_item.page = page
            event_page_carousel_item.image = self.image
            event_page_carousel_item.save()

        self.assertIsInstance(self.image.get_usage()[0], tuple)
        self.assertIsInstance(self.image.get_usage()[0][0], Page)
        self.assertIsInstance(self.image.get_usage()[0][1], list)
        self.assertIsInstance(self.image.get_usage()[0][1][0], ReferenceIndex)


class TestGetWillowImage(TestCase):
    fixtures = ["test.json"]

    def setUp(self):
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

    def test_willow_image_object_returned(self):
        with self.image.get_willow_image() as willow_image:
            self.assertIsInstance(willow_image, WillowImage)

    def test_with_missing_image(self):
        # Image id=1 in test fixtures has a missing image file
        bad_image = Image.objects.get(id=1)

        # Attempting to get the Willow image for images without files
        # should raise a SourceImageIOError
        with self.assertRaises(SourceImageIOError):
            with bad_image.get_willow_image():
                self.fail()  # Shouldn't get here

    def test_closes_image(self):
        # This tests that willow closes images after use
        with self.image.get_willow_image():
            self.assertFalse(self.image.file.closed)

        self.assertTrue(self.image.file.closed)

    def test_closes_image_on_exception(self):
        # This tests that willow closes images when the with is exited with an exception
        try:
            with self.image.get_willow_image():
                self.assertFalse(self.image.file.closed)
                raise ValueError("Something went wrong!")
        except ValueError:
            pass

        self.assertTrue(self.image.file.closed)

    def test_doesnt_close_open_image(self):
        # This tests that when the image file is already open, get_willow_image doesn't close it (#1256)
        self.image.file.open("rb")

        with self.image.get_willow_image():
            pass

        self.assertFalse(self.image.file.closed)

        self.image.file.close()


class TestIssue573(TestCase):
    """
    This tests for a bug which causes filename limit on Renditions to be reached
    when the Image has a long original filename and a big focal point key
    """

    def test_issue_573(self):
        # Create an image with a big filename and focal point
        image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(
                "thisisaverylongfilename-cdefghijklmnopqrstuvwxyzab-supercalifragilisticexpialidocious.png"
            ),
            focal_point_x=1000,
            focal_point_y=1000,
            focal_point_width=1000,
            focal_point_height=1000,
        )

        # Try creating a rendition from that image
        # This would crash if the bug is present
        image.get_rendition("fill-800x600")


@override_settings(_WAGTAILSEARCH_FORCE_AUTO_UPDATE=["elasticsearch"])
class TestIssue613(WagtailTestUtils, TestCase):
    def get_elasticsearch_backend(self):
        from django.conf import settings

        from wagtail.search.backends import get_search_backend

        if "elasticsearch" not in settings.WAGTAILSEARCH_BACKENDS:
            raise unittest.SkipTest("No elasticsearch backend active")

        return get_search_backend("elasticsearch")

    def setUp(self):
        self.search_backend = self.get_elasticsearch_backend()
        self.login()

    def add_image(self, **params):
        post_data = {
            "title": "Test image",
            "file": SimpleUploadedFile(
                "test.png", get_test_image_file().file.getvalue()
            ),
        }
        post_data.update(params)
        response = self.client.post(reverse("wagtailimages:add"), post_data)

        # Should redirect back to index
        self.assertRedirects(response, reverse("wagtailimages:index"))

        # Check that the image was created
        images = Image.objects.filter(title="Test image")
        self.assertEqual(images.count(), 1)

        # Test that size was populated correctly
        image = images.first()
        self.assertEqual(image.width, 640)
        self.assertEqual(image.height, 480)

        return image

    def edit_image(self, **params):
        # Create an image to edit
        self.image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

        # Edit it
        post_data = {
            "title": "Edited",
        }
        post_data.update(params)
        response = self.client.post(
            reverse("wagtailimages:edit", args=(self.image.id,)), post_data
        )

        # Should redirect back to index
        self.assertRedirects(response, reverse("wagtailimages:index"))

        # Check that the image was edited
        image = Image.objects.get(id=self.image.id)
        self.assertEqual(image.title, "Edited")
        return image

    def test_issue_613_on_add(self):
        # Reset the search index
        self.search_backend.reset_index()
        self.search_backend.add_type(Image)

        # Add an image with some tags
        image = self.add_image(tags="hello")
        self.search_backend.refresh_index()

        # Search for it by tag
        results = self.search_backend.search("hello", Image)

        # Check
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].id, image.id)

    def test_issue_613_on_edit(self):
        # Reset the search index
        self.search_backend.reset_index()
        self.search_backend.add_type(Image)

        # Add an image with some tags
        image = self.edit_image(tags="hello")
        self.search_backend.refresh_index()

        # Search for it by tag
        results = self.search_backend.search("hello", Image)

        # Check
        self.assertEqual(len(results), 1)
        self.assertEqual(results[0].id, image.id)


class TestIssue312(TestCase):
    def test_duplicate_renditions(self):
        # Create an image
        image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(),
        )

        # Get two renditions and check that they're the same
        rend1 = image.get_rendition("fill-100x100")
        rend2 = image.get_rendition("fill-100x100")
        self.assertEqual(rend1, rend2)

        # Now manually duplicate the renditon and check that the database blocks it
        self.assertRaises(
            IntegrityError,
            Rendition.objects.create,
            image=rend1.image,
            filter_spec=rend1.filter_spec,
            width=rend1.width,
            height=rend1.height,
            focal_point_key=rend1.focal_point_key,
        )


class TestFilenameReduction(TestCase):
    """
    This tests for a bug which results in filenames without extensions
    causing an infinite loop
    """

    def test_filename_reduction_no_ext(self):
        # Create an image with a big filename and no extension
        image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(
                "thisisaverylongfilename-defghijklmnopqrstuvwxyzabc-supercalifragilisticexpialidocioussuperlong"
            ),
        )

        # Saving file will result in infinite loop when bug is present
        image.save()
        self.assertEqual(
            "original_images/thisisaverylongfilename-defghijklmnopqrstuvwxyzabc-supercalifragilisticexpiali",
            image.file.name,
        )

    # Test for happy path. Long filename with extension
    def test_filename_reduction_ext(self):
        # Create an image with a big filename and extensions
        image = Image.objects.create(
            title="Test image",
            file=get_test_image_file(
                "thisisaverylongfilename-efghijklmnopqrstuvwxyzabcd-supercalifragilisticexpialidocioussuperlong.png"
            ),
        )

        image.save()
        self.assertEqual(
            "original_images/thisisaverylongfilename-efghijklmnopqrstuvwxyzabcd-supercalifragilisticexp.png",
            image.file.name,
        )


class TestRenditionOrientation(TestCase):
    """
    This tests for a bug where images with exif orientations which
    required rotation for display were cropped and sized based on the
    unrotated image dimensions.

    For example images with specified dimensions of 640x450 but an exif orientation of 6
    should appear as a 450x640 portrait, but instead were still cropped to 640x450.

    Actual image files are used so that exif orientation data will exist for the rotation to function correctly.
    """

    def assert_orientation_landscape_image_is_correct(self, rendition):
        """
        Check that the image has the correct colored pixels in the right places
        so that we know the image did not physically rotate.
        """

        from willow.plugins.pillow import PillowImage

        with rendition.get_willow_image() as willow_image:
            image = PillowImage.open(willow_image)
        # Check that the image is the correct size (and not rotated)
        self.assertEqual(image.get_size(), (600, 450))
        # Check that the red flower is in the bottom left
        # The JPEGs have compressed slightly differently so the colours won't be spot on
        colour = image.image.convert("RGB").getpixel((155, 282))
        self.assertAlmostEqual(colour[0], 217, delta=25)
        self.assertAlmostEqual(colour[1], 38, delta=25)
        self.assertAlmostEqual(colour[2], 46, delta=25)

        # Check that the water is at the bottom
        colour = image.image.convert("RGB").getpixel((377, 434))
        self.assertAlmostEqual(colour[0], 85, delta=25)
        self.assertAlmostEqual(colour[1], 93, delta=25)
        self.assertAlmostEqual(colour[2], 65, delta=25)

    def test_jpeg_with_orientation_1(self):
        with open("wagtail/images/tests/image_files/landscape_1.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 600)
        self.assertEqual(image.height, 450)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_2(self):
        with open("wagtail/images/tests/image_files/landscape_2.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 600)
        self.assertEqual(image.height, 450)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_3(self):
        with open("wagtail/images/tests/image_files/landscape_3.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 600)
        self.assertEqual(image.height, 450)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_4(self):
        with open("wagtail/images/tests/image_files/landscape_4.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 600)
        self.assertEqual(image.height, 450)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    # tests below here have a specified width x height in portrait but
    # an orientation specified of landscape, so the original shows a height > width
    # but the rendition is corrected to height < width.
    def test_jpeg_with_orientation_5(self):
        with open("wagtail/images/tests/image_files/landscape_6.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 450)
        self.assertEqual(image.height, 600)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_6(self):
        with open("wagtail/images/tests/image_files/landscape_6.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 450)
        self.assertEqual(image.height, 600)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_7(self):
        with open("wagtail/images/tests/image_files/landscape_7.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 450)
        self.assertEqual(image.height, 600)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)

    def test_jpeg_with_orientation_8(self):
        with open("wagtail/images/tests/image_files/landscape_8.jpg", "rb") as f:
            image = Image.objects.create(title="Test image", file=File(f))

        # check preconditions
        self.assertEqual(image.width, 450)
        self.assertEqual(image.height, 600)
        rendition = image.get_rendition("original")
        # Check dimensions stored on the model
        self.assertEqual(rendition.width, 600)
        self.assertEqual(rendition.height, 450)
        # Check actual image dimensions and orientation
        self.assert_orientation_landscape_image_is_correct(rendition)
