"""
Las marcas dibujadas sobre la foto: la capa, lo que se deriva de ella y el resolver.

Lo que se defiende:
- La capa se acepta sólo si puede ser la del editor: transparente, del tamaño
  exacto de la foto y con algo pintado.
- La copia anotada sale de esa capa y no de otra cosa.
- Cada modo de «Config» es sólo una forma de mandar el dibujo: ninguno manda el
  parámetro `mask`, que es una regla y contradecía la nota.
- El bloque del prompt nombra el dibujo por su número real, y los dos modos
  dicen lo mismo de las marcas: sólo cambia cómo describen la imagen.
"""

from io import BytesIO

import pytest
from PIL import Image

from apps.ai import images, markup, prompt
from apps.ai.markup import MarkupMode
from apps.ai.prompt import AnswerInvalid, Markup
from tests.images import jpeg_bytes

SIZE = (64, 48)
#: Lo que se pinta en las pruebas: un rectángulo rojo opaco.
BOX = (10, 10, 30, 20)


def layer_bytes(size=SIZE, box=BOX, color=(239, 68, 68, 255), mode='RGBA') -> bytes:
    layer = Image.new('RGBA', size, (0, 0, 0, 0))
    if box is not None:
        layer.paste(color, box)
    buffer = BytesIO()
    (layer if mode == 'RGBA' else layer.convert(mode)).save(buffer, format='PNG')
    return buffer.getvalue()


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


class TestNormalizeMarkup:
    def test_a_layer_of_the_editor_is_accepted_as_rgba_png(self):
        normalized = opened(images.normalize_markup(layer_bytes(), SIZE))

        assert normalized.format == 'PNG'
        assert normalized.mode == 'RGBA'
        assert normalized.size == SIZE

    def test_another_size_is_refused_and_never_rescaled(self):
        """Un trazo reescalado cae corrido respecto de lo que se marcó."""
        with pytest.raises(images.NotAnImage):
            images.normalize_markup(layer_bytes(size=(32, 24), box=(1, 1, 5, 5)), SIZE)

    def test_a_layer_without_transparency_is_refused(self):
        with pytest.raises(images.NotAnImage):
            images.normalize_markup(layer_bytes(mode='RGB'), SIZE)

    def test_an_empty_layer_is_refused(self):
        """Sin nada pintado no hay qué mandar, y el prompt diría que sí."""
        with pytest.raises(images.NotAnImage):
            images.normalize_markup(layer_bytes(box=None), SIZE)

    def test_something_that_is_not_an_image_is_refused(self):
        with pytest.raises(images.NotAnImage):
            images.normalize_markup(b'no soy una imagen', SIZE)


class TestAnnotatedCopy:
    def test_the_marks_are_blended_over_the_photo_and_the_rest_is_the_photo(self):
        source = jpeg_bytes(*SIZE)
        photo = opened(source).convert('RGB')

        copy = opened(images.annotated_copy(source, layer_bytes()))

        assert copy.format == 'PNG'
        assert copy.size == SIZE
        # Fuera de las marcas, la foto tal cual.
        assert copy.getpixel((60, 45)) == photo.getpixel((60, 45))
        # Sobre la marca, ni la foto ni el rojo puro: se ve lo que hay debajo.
        marked = copy.getpixel((20, 15))
        assert marked != photo.getpixel((20, 15))
        assert marked != (239, 68, 68)


class TestProviderInputs:
    SOURCE = jpeg_bytes(*SIZE)

    def test_copy_sends_the_photo_with_the_marks_burned_in(self):
        [(name, raw, content_type)] = markup.provider_inputs(
            MarkupMode.COPY, self.SOURCE, layer_bytes()
        ).extra_images

        assert (name, content_type) == ('markup.png', 'image/png')
        # Opaca en todo: es la foto, con las marcas encima.
        assert opened(raw).convert('RGBA').getpixel((60, 45))[3] == 255

    def test_layer_sends_the_marks_alone_as_they_were_drawn(self):
        layer = layer_bytes()

        [(name, raw, content_type)] = markup.provider_inputs(
            MarkupMode.LAYER, self.SOURCE, layer
        ).extra_images

        assert (name, content_type) == ('markup.png', 'image/png')
        assert raw == layer

    def test_a_request_made_with_a_removed_mask_mode_sends_the_copy(self):
        """Los modos de máscara ya no existen; un pedido viejo que se reintente viaja como copia."""
        for legacy in ('mask', 'mask_and_copy'):
            [(_name, raw, _type)] = markup.provider_inputs(
                legacy, self.SOURCE, layer_bytes()
            ).extra_images

            assert opened(raw).convert('RGBA').getpixel((60, 45))[3] == 255

    def test_the_default_mode_is_the_copy(self):
        """El que mejor ubicó las zonas en la prueba a mano."""
        assert markup.DEFAULT_MODE == MarkupMode.COPY


class TestCleanMarkup:
    def test_the_note_is_required(self):
        with pytest.raises(AnswerInvalid) as raised:
            prompt.clean_markup_note('   ')

        assert (raised.value.code, raised.value.key) == ('MARKUP_NOTE_MISSING', 'markup')

    def test_the_note_is_cleaned_like_the_free_notes(self):
        """Una nota no se hace pasar por una sección del prompt."""
        assert prompt.clean_markup_note('  ## Red: pavers  ') == 'Red: pavers'

    def test_the_note_has_the_same_limit_as_the_free_notes(self):
        with pytest.raises(AnswerInvalid) as raised:
            prompt.clean_markup_note('x' * (prompt.NOTES_MAX_LENGTH + 1))

        assert raised.value.code == 'MARKUP_NOTE_TOO_LONG'


DRIVEWAY_POUR = {'area': 'driveway', 'service': 'concrete_pour', 'answers': {'turf_strips': False}}
OVERLAY_WITH_SAMPLE = {
    'area': 'entryway',
    'service': 'concrete_overlay',
    'answers': {'grout_color': 'sand', 'stone_color': 'shadow_stone'},
}


def cleaned(*containers):
    return prompt.clean_request(list(containers), False)


class TestMarkupBlock:
    NOTE = 'Red: a paver walkway. Yellow: remove these pavers.'

    def block_of(self, text: str) -> str:
        start = text.index('## Markup')
        end = text.index('\n\n', start)
        return text[start:end]

    def test_without_marks_there_is_no_block(self):
        assert '## Markup' not in prompt.build_prompt(cleaned(DRIVEWAY_POUR), False)

    def test_with_the_copy_it_names_the_image_and_leaves_the_colors_to_the_note(self):
        """El código no nombra ni interpreta los colores: lo hace la nota."""
        request = Markup(MarkupMode.COPY, self.NOTE)

        block = self.block_of(
            prompt.build_prompt(cleaned(DRIVEWAY_POUR), False, markup_request=request)
        )

        # Sin muestras, la copia es la imagen 2: la foto es la 1.
        assert 'Image 2 is the same photo with colored marks' in block
        assert 'Colors used' not in block
        assert 'they add to the sections above' in block
        assert block.endswith(self.NOTE)

    @pytest.mark.parametrize('mode', list(MarkupMode))
    def test_the_block_does_not_forbid_the_note_from_overriding_a_section(self, mode):
        """
        Con «never replace them», una nota que pedía dejar intacto un patio
        existente perdía contra la sección, que lo repavimentaba entero. Hasta
        dónde llega la nota lo deciden los guards y el interruptor del área.
        """
        request = Markup(mode, self.NOTE)

        block = self.block_of(
            prompt.build_prompt(cleaned(DRIVEWAY_POUR), False, markup_request=request)
        )

        assert 'never replace' not in block

    @pytest.mark.parametrize('mode', list(MarkupMode))
    def test_a_mark_the_note_does_not_mention_is_ignored(self, mode):
        """Una marca sin mención no pide nada: esa parte queda como en la foto."""
        request = Markup(mode, self.NOTE)

        block = self.block_of(
            prompt.build_prompt(cleaned(DRIVEWAY_POUR), False, markup_request=request)
        )

        assert 'Ignore any mark the note does not mention' in block

    @pytest.mark.parametrize('mode', list(MarkupMode))
    def test_the_drawing_is_numbered_after_the_samples(self, mode):
        """El dibujo va al final de la lista, detrás de las muestras del catálogo."""
        request_areas = cleaned(OVERLAY_WITH_SAMPLE)
        references = prompt.references_of(request_areas)
        request = Markup(mode, self.NOTE)

        text = prompt.build_prompt(
            request_areas, False, references=references, markup_request=request
        )

        assert len(references) == 1
        assert f'Image {2 + len(references)} is ' in self.block_of(text)

    def test_with_the_layer_it_says_it_is_only_the_marks(self):
        request = Markup(MarkupMode.LAYER, self.NOTE)

        block = self.block_of(
            prompt.build_prompt(cleaned(DRIVEWAY_POUR), False, markup_request=request)
        )

        assert 'Image 2 is a transparent layer, the same size as image 1' in block
        assert block.endswith(self.NOTE)

    def test_the_modes_differ_only_in_how_they_describe_the_image(self):
        """El modo es la forma de mandar el dibujo: lo que significan las marcas no cambia."""
        blocks = [
            self.block_of(
                prompt.build_prompt(
                    cleaned(DRIVEWAY_POUR), False, markup_request=Markup(mode, self.NOTE)
                )
            ).split('\n')
            for mode in MarkupMode
        ]

        copy, layer = blocks
        assert copy[1] != layer[1]
        assert copy[:1] + copy[2:] == layer[:1] + layer[2:]
        assert 'mask' not in '\n'.join(copy + layer)

    def test_it_goes_before_the_notes_and_before_the_closing_rules(self):
        request = Markup(MarkupMode.COPY, self.NOTE)

        text = prompt.build_prompt(
            cleaned(DRIVEWAY_POUR), False, notes='Keep the hedge.', markup_request=request
        )

        assert text.index('## Markup') < text.index('## Special Notes')
        assert text.index('## Markup') < text.index('Do not add people')
