"""
Lo que se hace con cada imagen antes de guardarla.

Lo que se defiende: que una foto de teléfono quede acotada y derecha, que lo
que no es imagen se rechace al abrirlo y no por lo que dijo el navegador, y
que el resultado pese lo que tiene que pesar.
"""

from io import BytesIO

import pytest
from PIL import Image, ImageChops

from apps.ai import images, watermark
from tests.images import jpeg_bytes, png_bytes


def opened(raw: bytes) -> Image.Image:
    return Image.open(BytesIO(raw))


class TestSource:
    def test_a_phone_photo_is_scaled_down_to_the_model_size(self):
        raw = jpeg_bytes(4000, 3000)

        normalized = images.normalize_source(raw)

        result = opened(normalized)
        assert result.format == 'JPEG'
        assert (result.width, result.height) == (1536, 1152)
        assert len(normalized) < len(raw)

    def test_a_small_photo_is_not_scaled_up(self):
        result = opened(images.normalize_source(png_bytes(640, 480)))

        assert (result.width, result.height) == (640, 480)
        assert result.format == 'JPEG'

    def test_the_exif_orientation_is_applied(self):
        """
        Los teléfonos guardan la foto acostada y anotan «girala» en el EXIF.
        El modelo no lee EXIF, así que sin esto recibiría el patio de costado.
        """
        raw = jpeg_bytes(400, 300, exif_orientation=6)

        result = opened(images.normalize_source(raw))

        assert (result.width, result.height) == (300, 400)

    def test_what_is_not_an_image_is_rejected_when_opened(self):
        with pytest.raises(images.NotAnImage):
            images.normalize_source(b'{"not": "an image"}')

    def test_a_photo_that_already_fits_is_kept_byte_for_byte(self):
        """La landing la comprime con más calidad que la nuestra: recomprimirla la perdería."""
        raw = jpeg_bytes(1536, 1024)

        assert images.normalize_source(raw) == raw

    @pytest.mark.parametrize(
        'raw',
        [
            pytest.param(jpeg_bytes(1600, 1000), id='larger'),
            pytest.param(jpeg_bytes(400, 300, exif_orientation=6), id='rotated'),
            pytest.param(png_bytes(640, 480), id='png'),
            pytest.param(jpeg_bytes(400, 300, mode='L'), id='grayscale'),
        ],
    )
    def test_a_photo_that_does_not_fit_is_normalized(self, raw):
        normalized = images.normalize_source(raw)

        assert normalized != raw
        result = opened(normalized)
        assert result.format == 'JPEG'
        assert result.mode == 'RGB'
        assert images.source_is_normalized(result)

    def test_a_cut_jpeg_that_otherwise_fits_is_rejected(self):
        raw = jpeg_bytes(800, 600)

        with pytest.raises(images.NotAnImage):
            images.normalize_source(raw[: len(raw) // 2])


class TestResult:
    def test_the_png_from_the_provider_becomes_a_jpeg(self):
        result = opened(images.encode_result(png_bytes(1024, 1024)))

        assert result.format == 'JPEG'
        assert (result.width, result.height) == (1024, 1024)

    def test_garbage_from_the_provider_is_rejected(self):
        with pytest.raises(images.NotAnImage):
            images.encode_result(b'not a png')

    def test_with_a_badge_it_is_burned_and_the_size_does_not_change(self):
        plain = images.encode_result(png_bytes(1024, 1024))
        burned = images.encode_result(png_bytes(1024, 1024), watermark.badge_path('blue'))

        result = opened(burned)
        assert result.format == 'JPEG'
        assert (result.width, result.height) == (1024, 1024)
        assert burned != plain


class TestBadge:
    """
    La marca de agua: dónde cae y que lo de afuera quede intacto.

    Sobre un color liso el desenfoque no cambia nada, así que lo único que
    difiere del original es el badge, y su caja dice dónde quedó.
    """

    @pytest.mark.parametrize(
        ('size', 'box'),
        [
            # La medida aprobada: 180 de diámetro a 100 de cada borde.
            ((1024, 1024), (744, 744, 924, 924)),
            # Otro tamaño, en proporción a su lado menor (1536 / 1024 = 1,5).
            ((2048, 1536), (1628, 1116, 1898, 1386)),
        ],
    )
    def test_it_lands_in_the_bottom_right_corner_in_proportion(self, size, box):
        photo = Image.new('RGB', size, (30, 120, 200))

        burned = images.burn_badge(photo, watermark.badge_path('blue'))

        assert burned.size == size
        changed = ImageChops.difference(photo, burned).getbbox()
        assert changed is not None
        # El círculo del badge no llena su cuadrado hasta el último píxel.
        left, top, right, bottom = box
        inset = (box[2] - box[0]) // 20
        assert left <= changed[0] <= left + inset
        assert top <= changed[1] <= top + inset
        assert right - inset <= changed[2] <= right
        assert bottom - inset <= changed[3] <= bottom

    def test_what_is_outside_the_circle_is_not_blurred(self):
        """El desenfoque es del vidrio: fuera del círculo la foto queda como estaba."""
        # Rayas de dos píxeles: cualquier punto desenfocado cambiaría de color.
        photo = Image.new('RGB', (1024, 1024), (30, 120, 200))
        for x in range(0, 1024, 4):
            photo.paste((240, 240, 240), (x, 0, x + 2, 1024))

        burned = images.burn_badge(photo, watermark.badge_path('blue'))

        # Las esquinas del cuadrado del badge (744 a 924) caen fuera del círculo.
        for point in [(746, 746), (921, 746), (746, 921), (921, 921)]:
            assert burned.getpixel(point) == photo.getpixel(point)
        # Y el centro sí cambió.
        assert burned.getpixel((834, 834)) != photo.getpixel((834, 834))
