"""
El prompt que la pantalla muestra es, byte a byte, el que el servidor manda.

La pantalla arma el prompt con la receta y el catálogo que le manda el
servidor; el servidor lo arma con `clean_request` más `build_prompt`. Son dos
implementaciones de la misma regla, en dos lenguajes, y lo único que las
mantiene iguales es esta comparación: cada pedido se arma de los dos lados y
tiene que dar exactamente lo mismo, con y sin pileta en la foto.
"""

import pytest

from apps.ai import catalog, prompt
from tests.frontend.harness import call, needs_frontend, resolve

PROMPT = '@/pages/Ai/ImageGenerator/prompt'

pytestmark = needs_frontend

#: Pedidos representativos: (contenedores tal como los manda la pantalla, si el
#: análisis vio una pileta, las notas libres tal como se escribieron, los guards
#: bajados).
CASES = {
    # Un área de Paver con todo contestado, opcionales incluidos.
    'paver_everything': (
        [
            {
                'area': 'driveway',
                'service': 'paver',
                'answers': {
                    'material': 'travertine',
                    'color': 'ivory',
                    'size': '24x24',
                    'pattern': 'large_grid',
                    'border': True,
                    'seal': 'matte',
                    'turf_strips': True,
                },
            }
        ],
        False,
        '',
        {},
    ),
    # Un campo que depende de **qué áreas trae el pedido** y no de lo contestado
    # en su caja: el coping se pregunta en la entrada de autos porque el pedido
    # incluye la vereda de la pileta. Es lo único que obliga a que los dos lados
    # limpien con el mismo contexto.
    'coping_across_containers': (
        [
            {
                'area': 'driveway',
                'service': 'paver',
                'answers': {
                    'material': 'travertine',
                    'color': 'ivory',
                    'size': '24x24',
                    'pattern': 'large_grid',
                    'coping': 'bullnose',
                },
            },
            {
                'area': 'pool_deck',
                'service': 'concrete_pour',
                'answers': {'turf_strips': False},
            },
        ],
        False,
        '',
        {},
    ),
    # Dos áreas fuera del orden de la taxonomía, una pileta en la foto que
    # nadie renueva, y un resto de rama abandonada (`layout` sin turf strips)
    # que los dos lados ignoran.
    'two_areas_pool_untouched': (
        [
            {
                'area': 'driveway',
                'service': 'concrete_pour',
                'answers': {'turf_strips': False, 'layout': 'standard_grid'},
            },
            {
                'area': 'patio_deck',
                'service': 'paver',
                'answers': {
                    'material': 'travertine',
                    'color': 'ivory',
                    'size': 'french_pattern_mix',
                    # `pattern` no viene: con la mezcla francesa queda fijo (R40).
                },
            },
        ],
        True,
        '',
        {},
    ),
    # La pileta renovada con bubblers y los opcionales ausentes (R49), más un
    # overlay cuyo patrón se contesta solo.
    'pool_with_bubblers': (
        [
            {
                'area': 'pool',
                'service': 'pool_renovation',
                'answers': {
                    'plaster_color': 'sky_blue',
                    'waterline_color': 'blues',
                    'waterline_size': '6x6',
                    'add_spa': '6x6_round',
                    'spa_location': 'perimeter_spillover',
                    'add_sun_shelf': 'with_bubblers',
                },
            },
            {
                'area': 'entryway',
                'service': 'concrete_overlay',
                'answers': {'grout_color': 'beige', 'stone_color': 'shadow_stone'},
            },
        ],
        True,
        '',
        {},
    ),
    # Con notas libres: espacio de sobra, una línea que arranca con `#` y un
    # hueco de tres saltos, que los dos lados tienen que limpiar igual.
    'notes_written_by_hand': (
        [
            {
                'area': 'walkway',
                'service': 'artificial_turf',
                'answers': {'use_type': 'general_use', 'shade': 'medium'},
            }
        ],
        False,
        '  ## Where I marked a 1, use the darker paver.  \n\n\n  Leave the mailbox alone.\n',
        {},
    ),
    # Los dos guards bajados con una nota que los usa: el cierre cambia de
    # redacción y el bloque de notas crece con la línea del guard de alcance.
    'guards_lowered': (
        [
            {
                'area': 'patio_deck',
                'service': 'paver',
                'answers': {
                    'material': 'porcelain',
                    'color': 'light_gray',
                    'size': '16x32',
                    'pattern': 'running_bond',
                },
            }
        ],
        False,
        'Leave the existing patio as it is and pave the lawn instead.',
        {'coverage': False, 'objects': False},
    ),
    # Un área que se deja a las notas al lado de otra que va entera: la línea
    # cambia sólo en la sección que se apagó.
    'area_left_to_the_notes': (
        [
            {
                'area': 'patio_deck',
                'service': 'paver',
                'answers': {
                    'material': 'travertine',
                    'color': 'noce',
                    'size': '6x12',
                    'pattern': 'running_bond',
                },
                'whole_area': False,
            },
            {
                'area': 'driveway',
                'service': 'concrete_pour',
                'answers': {'turf_strips': False},
                'whole_area': True,
            },
        ],
        False,
        'Keep the existing patio and extend it toward the lawn.',
        {'coverage': False},
    ),
    # Un guard bajado sin notas: la lista de lo que no se puede hacer cambia
    # igual, aunque no haya bloque de notas que la aproveche.
    'guard_lowered_without_notes': (
        [
            {
                'area': 'pool_deck',
                'service': 'concrete_pour',
                'answers': {'turf_strips': False},
            }
        ],
        True,
        '',
        {'objects': False},
    ),
}


def requests() -> dict[str, dict]:
    props = catalog.catalog_props('/static/')
    recipe = prompt.recipe()
    return {
        name: call(PROMPT, 'composePrompt', recipe, props, areas, pool_present, notes, guards)
        for name, (areas, pool_present, notes, guards) in CASES.items()
    }


def rendered(name: str) -> str:
    areas, pool_present, notes, guards = CASES[name]
    return prompt.build_prompt(
        prompt.clean_request(areas, pool_present),
        pool_present,
        prompt.clean_notes(notes),
        guards,
    )


@pytest.fixture(scope='module')
def results():
    return resolve(requests())


@pytest.mark.parametrize('name', list(CASES))
def test_the_screen_composes_exactly_what_the_server_sends(results, name):
    assert results[name] == rendered(name)


def test_the_cases_cover_the_pieces_that_can_diverge():
    """Que los pedidos ejerciten lo que tiene forma propia en el prompt."""
    texts = {name: rendered(name) for name in CASES}

    assert '- Bubblers:' in texts['pool_with_bubblers']
    # La ubicación del spa sólo existe habiendo spa, y va pegada a él.
    spa = texts['pool_with_bubblers'].index('- Add a spa?:')
    assert spa < texts['pool_with_bubblers'].index('- Spa location: the spa set on the pool bond')
    assert 'leave the existing pool' in texts['two_areas_pool_untouched']
    assert 'leave the existing pool' not in texts['pool_with_bubblers']
    assert texts['two_areas_pool_untouched'].index('## Patio Deck') < texts[
        'two_areas_pool_untouched'
    ].index('## Driveway')
    assert '- Pattern / Layout: laid in a French pattern' in texts['two_areas_pool_untouched']
    # La forma arranca en su `default` y el prompt la dice igual.
    assert '- Shape: an irregular natural stone cut' in texts['pool_with_bubblers']
    assert '## Special Notes' in texts['notes_written_by_hand']
    assert '## Special Notes' not in texts['paver_everything']
    assert 'or in the Special Notes' in texts['guards_lowered']
    assert 'You may add outdoor objects' in texts['guard_lowered_without_notes']
    left, whole = texts['area_left_to_the_notes'].split('## Driveway')
    assert prompt.recipe()['area_instruction_by_notes'] in left
    assert prompt.recipe()['area_instruction'] in whole
    # El coping sobrevive a la limpieza porque el pedido trae la vereda de la
    # pileta, y sale en la sección de la entrada de autos, que es su contenedor.
    driveway = texts['coping_across_containers'].split('## Pool Deck')[0]
    assert (
        '- Coping: bullnose coping with a rounded front edge capping the edge of the pool'
    ) in driveway
    assert '- Coping:' not in texts['paver_everything']


def test_the_screen_shows_the_prompt_without_the_samples_the_server_attaches(results):
    """
    Las muestras se adjuntan del lado del servidor y su bloque no tiene sentido
    sin ellas: el número que el texto nombra es la posición de un archivo que la
    pantalla no manda. Así que la paridad es contra el prompt **antes** de
    adjuntarlas, y lo que se guarda con el pedido es el de después.
    """
    areas, pool_present, notes, guards = CASES['pool_with_bubblers']
    request = prompt.clean_request(areas, pool_present)
    references = prompt.references_of(request)
    sent = prompt.build_prompt(request, pool_present, prompt.clean_notes(notes), guards, references)

    pieces = prompt.recipe()
    blocks = sent.split(pieces['block_separator'])
    without = [block for block in blocks if not block.startswith(pieces['references_title'])]

    assert references, 'el caso tiene que traer una muestra, si no no prueba nada'
    assert pieces['references_title'] in sent
    # Sacarle el bloque de muestras al prompt que se manda da exactamente el que
    # se ve: el servidor no agrega nada más.
    assert len(without) == len(blocks) - 1
    assert pieces['block_separator'].join(without) == results['pool_with_bubblers']
