"""
El motor del catálogo del lado de la pantalla, contra los mismos casos que el
del servidor.

`conditions.ts` es el espejo de `catalog.evaluate`, `is_shown`, `options_for`
y `services_for`, y de la limpieza de respuestas de `clean_request`. Lo que se
defiende es que decida lo mismo que `tests/unit/test_ai_catalog.py` prueba en
Python: qué servicio ofrece cada área, qué opciones quedan según lo contestado,
qué campo aparece y cuándo, qué se contesta solo, y qué se descarta al cambiar
una respuesta anterior.
"""

import pytest

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

CONDITIONS = '@/pages/Ai/ImageGenerator/conditions'
DRAFT = '@/pages/Ai/ImageGenerator/draft'

pytestmark = needs_frontend

CATALOG = catalog.catalog_props('/static/')
SCHEMAS = {schema['service']: schema for schema in CATALOG['services']}


def field_of(service: str, field_id: str) -> dict:
    return next(field for field in SCHEMAS[service]['fields'] if field['id'] == field_id)


def condition(field: str, operator: str, value) -> dict:
    return {'field': field, 'operator': operator, 'value': value}


def draft(selected=None, containers=None, grouped=True, notes='', guards=None, markup=None) -> dict:
    """Un borrador como lo lleva la pantalla, con lo que el caso no mira ya puesto."""
    return {
        'selected': selected or [],
        'containers': containers or {},
        'grouped': grouped,
        'notes': notes,
        'guards': guards or {},
        'markup': markup,
    }


#: Unas marcas guardadas en la pantalla, como las deja el editor.
SAVED_MARKUP = {'image': 'data:image/png;base64,AAAA', 'note': 'Red: pavers.'}


#: Las tres áreas que comparten diseño, con la primera de la taxonomía ya
#: contestada: es la que el grupo copia a las demás.
PAVING = {
    'selected': ['driveway', 'entryway', 'walkway'],
    'containers': {
        'driveway': {
            'service': 'paver',
            'answers': {'material': 'brick', 'color': 'red_terracotta'},
        }
    },
}

PAVER_COLOR = field_of('paver', 'color')
PAVER_SIZE = field_of('paver', 'size')
PAVER_PATTERN = field_of('paver', 'pattern')
PAVER_TURF = field_of('paver', 'turf_strips')
PAVER_COPING = field_of('paver', 'coping')
POUR_LAYOUT = field_of('concrete_pour', 'layout')

#: Todos los tamaños del esquema, para no escribir la lista dos veces: agregar
#: uno en `catalog.py` lo trae a los casos de patrón y de franjas de pasto.
PAVER_SIZES = [option['value'] for option in PAVER_SIZE['options']]

REQUESTS = {
    # --- evaluate ---------------------------------------------------------
    'eq_true': call(
        CONDITIONS, 'evaluate', condition('turf_strips', 'eq', True), {'turf_strips': True}
    ),
    'eq_false': call(
        CONDITIONS, 'evaluate', condition('turf_strips', 'eq', True), {'turf_strips': False}
    ),
    'eq_absent': call(CONDITIONS, 'evaluate', condition('turf_strips', 'eq', True), {}),
    'eq_yes_is_not_true': call(
        CONDITIONS, 'evaluate', condition('turf_strips', 'eq', True), {'turf_strips': 'yes'}
    ),
    'in_hit': call(
        CONDITIONS, 'evaluate', condition('size', 'in', ['24x24', '24x36']), {'size': '24x36'}
    ),
    'in_miss': call(
        CONDITIONS, 'evaluate', condition('size', 'in', ['24x24', '24x36']), {'size': '16x24'}
    ),
    'in_absent': call(CONDITIONS, 'evaluate', condition('size', 'in', ['24x24', '24x36']), {}),
    'includes_hit': call(
        CONDITIONS,
        'evaluate',
        condition('areas', 'includes', 'pool'),
        {'areas': ['driveway', 'pool']},
    ),
    'includes_miss': call(
        CONDITIONS, 'evaluate', condition('areas', 'includes', 'pool'), {'areas': ['driveway']}
    ),
    'includes_not_a_list': call(
        CONDITIONS, 'evaluate', condition('areas', 'includes', 'pool'), {'areas': 'pool'}
    ),
    'includes_absent': call(CONDITIONS, 'evaluate', condition('areas', 'includes', 'pool'), {}),
    'all_hit': call(
        CONDITIONS,
        'evaluate',
        {'all': [condition('area', 'eq', 'pool'), condition('pool_present', 'eq', True)]},
        {'area': 'pool', 'pool_present': True},
    ),
    'all_miss_pool': call(
        CONDITIONS,
        'evaluate',
        {'all': [condition('area', 'eq', 'pool'), condition('pool_present', 'eq', True)]},
        {'area': 'pool', 'pool_present': False},
    ),
    'all_miss_area': call(
        CONDITIONS,
        'evaluate',
        {'all': [condition('area', 'eq', 'pool'), condition('pool_present', 'eq', True)]},
        {'area': 'driveway', 'pool_present': True},
    ),
    'none_is_true': call(CONDITIONS, 'evaluate', None, {}),
    # --- servicesFor ------------------------------------------------------
    **{
        f'services_{area}_{pool}': call(CONDITIONS, 'servicesFor', CATALOG, area, pool)
        for area in catalog.Area.values
        for pool in (False, True)
    },
    # --- optionsFor -------------------------------------------------------
    **{
        f'colors_{material}': call(CONDITIONS, 'optionsFor', PAVER_COLOR, {'material': material})
        for material in ('travertine', 'brick', 'porcelain', 'marble')
    },
    'colors_without_material': call(CONDITIONS, 'optionsFor', PAVER_COLOR, {}),
    **{
        f'sizes_{material}': call(CONDITIONS, 'optionsFor', PAVER_SIZE, {'material': material})
        for material in ('travertine', 'marble', 'porcelain', 'brick')
    },
    'pattern_french_mix': call(
        CONDITIONS, 'optionsFor', PAVER_PATTERN, {'size': 'french_pattern_mix'}
    ),
    **{
        f'pattern_{size}': call(CONDITIONS, 'optionsFor', PAVER_PATTERN, {'size': size})
        for size in PAVER_SIZES
    },
    'pattern_brick_4x8': call(
        CONDITIONS, 'optionsFor', PAVER_PATTERN, {'material': 'brick', 'size': '4x8'}
    ),
    # --- isShown ----------------------------------------------------------
    **{
        f'turf_shown_{size}': call(CONDITIONS, 'isShown', PAVER_TURF, {'size': size})
        for size in PAVER_SIZES
    },
    'turf_shown_without_size': call(CONDITIONS, 'isShown', PAVER_TURF, {}),
    'coping_with_pool_deck': call(
        CONDITIONS, 'isShown', PAVER_COPING, {'area_selected': ['driveway', 'pool_deck']}
    ),
    'coping_without_pool_deck': call(
        CONDITIONS, 'isShown', PAVER_COPING, {'area_selected': ['driveway']}
    ),
    'layout_with_strips': call(CONDITIONS, 'isShown', POUR_LAYOUT, {'turf_strips': True}),
    'layout_without_strips': call(CONDITIONS, 'isShown', POUR_LAYOUT, {'turf_strips': False}),
    'layout_unanswered': call(CONDITIONS, 'isShown', POUR_LAYOUT, {}),
    'layout_with_yes': call(CONDITIONS, 'isShown', POUR_LAYOUT, {'turf_strips': 'yes'}),
    # --- cleanAnswers / visibleFields / missingFields ------------------------
    'clean_color_follows_material': call(
        CONDITIONS, 'cleanAnswers', SCHEMAS['paver'], {'material': 'brick', 'color': 'ivory'}
    ),
    'clean_keeps_a_valid_color': call(
        CONDITIONS, 'cleanAnswers', SCHEMAS['paver'], {'material': 'travertine', 'color': 'ivory'}
    ),
    'clean_hidden_field_is_dropped': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['concrete_pour'],
        {'turf_strips': False, 'layout': 'standard_grid'},
    ),
    'clean_toggle_needs_a_boolean': call(
        CONDITIONS, 'cleanAnswers', SCHEMAS['concrete_pour'], {'turf_strips': 'yes'}
    ),
    'clean_overlay_shape_presets': call(
        CONDITIONS, 'cleanAnswers', SCHEMAS['concrete_overlay'], {}
    ),
    'stone_image_regular': call(
        CONDITIONS,
        'imageOf',
        field_of('concrete_overlay', 'stone_color'),
        field_of('concrete_overlay', 'stone_color')['options'][0],
        {'shape': 'regular'},
    ),
    'stone_image_irregular': call(
        CONDITIONS,
        'imageOf',
        field_of('concrete_overlay', 'stone_color'),
        field_of('concrete_overlay', 'stone_color')['options'][0],
        {'shape': 'irregular'},
    ),
    'stone_image_without_shape': call(
        CONDITIONS,
        'imageOf',
        field_of('concrete_overlay', 'stone_color'),
        field_of('concrete_overlay', 'stone_color')['options'][0],
        {},
    ),
    'paver_image': call(
        CONDITIONS,
        'imageOf',
        field_of('paver', 'material'),
        field_of('paver', 'material')['options'][0],
        {},
    ),
    'clean_french_pattern_presets': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['paver'],
        {'material': 'travertine', 'color': 'ivory', 'size': 'french_pattern_mix'},
    ),
    # El mismo contenedor con y sin la vereda de la pileta en el pedido: el
    # coping es la única respuesta que depende de eso.
    'clean_coping_with_pool_deck': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['paver'],
        {
            'material': 'travertine',
            'color': 'ivory',
            'size': '24x24',
            'pattern': 'large_grid',
            'coping': 'bullnose',
        },
        {'area_selected': ['driveway', 'pool_deck']},
    ),
    # Sin contestar queda sin respuesta: no tiene `default` desde que no se
    # ofrece dejar el borde como está.
    'clean_coping_unanswered': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['paver'],
        {'material': 'travertine', 'color': 'ivory', 'size': '24x24', 'pattern': 'large_grid'},
        {'area_selected': ['driveway', 'pool_deck']},
    ),
    'visible_paver_with_pool_deck': call(
        CONDITIONS,
        'visibleFields',
        SCHEMAS['paver'],
        {'material': 'travertine', 'color': 'ivory', 'size': '24x24'},
        {'area_selected': ['driveway', 'pool_deck']},
    ),
    'clean_coping_without_pool_deck': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['paver'],
        {
            'material': 'travertine',
            'color': 'ivory',
            'size': '24x24',
            'pattern': 'large_grid',
            'coping': 'bullnose',
        },
        {'area_selected': ['driveway']},
    ),
    'clean_optional_is_not_preset': call(
        CONDITIONS, 'cleanAnswers', SCHEMAS['pool_renovation'], {}
    ),
    'clean_unknown_key_is_dropped': call(
        CONDITIONS,
        'cleanAnswers',
        SCHEMAS['artificial_turf'],
        {'use_type': 'general_use', 'pergola': 'x'},
    ),
    'visible_overlay': call(CONDITIONS, 'visibleFields', SCHEMAS['concrete_overlay'], {}),
    'visible_paver_french': call(
        CONDITIONS,
        'visibleFields',
        SCHEMAS['paver'],
        {'material': 'travertine', 'color': 'ivory', 'size': 'french_pattern_mix'},
    ),
    'visible_paver_large': call(
        CONDITIONS,
        'visibleFields',
        SCHEMAS['paver'],
        {'material': 'travertine', 'color': 'ivory', 'size': '24x24'},
    ),
    'visible_pour_without_strips': call(
        CONDITIONS, 'visibleFields', SCHEMAS['concrete_pour'], {'turf_strips': False}
    ),
    'visible_pour_with_strips': call(
        CONDITIONS, 'visibleFields', SCHEMAS['concrete_pour'], {'turf_strips': True}
    ),
    'missing_pour_empty': call(CONDITIONS, 'missingFields', SCHEMAS['concrete_pour'], {}),
    'missing_pour_with_strips': call(
        CONDITIONS, 'missingFields', SCHEMAS['concrete_pour'], {'turf_strips': True}
    ),
    'missing_pour_done': call(
        CONDITIONS, 'missingFields', SCHEMAS['concrete_pour'], {'turf_strips': False}
    ),
    'missing_overlay_empty': call(CONDITIONS, 'missingFields', SCHEMAS['concrete_overlay'], {}),
    'missing_pool_optionals_can_be_absent': call(
        CONDITIONS,
        'missingFields',
        SCHEMAS['pool_renovation'],
        {'plaster_color': 'sky_blue', 'waterline_color': 'blues', 'waterline_size': '6x6'},
    ),
    # --- draft --------------------------------------------------------------
    'draft_from_analysis': call(
        DRAFT,
        'draftFromAnalysis',
        {'state': 'COMPLETED', 'areas': ['driveway', 'pool'], 'pool_present': True},
    ),
    'draft_from_malformed': call(
        DRAFT,
        'draftFromAnalysis',
        {'state': 'FAILED', 'error_code': 'MALFORMED_RESPONSE', 'areas': [], 'pool_present': False},
    ),
    'draft_from_generation': call(
        DRAFT,
        'draftFromGeneration',
        CATALOG,
        {
            'areas': [
                {
                    'area': 'driveway',
                    'service': 'concrete_pour',
                    'answers': {'turf_strips': True, 'layout': 'diamond_grid'},
                },
                {
                    'area': 'pool',
                    'service': 'pool_renovation',
                    'answers': {'plaster_color': 'capri'},
                },
            ],
            'notes': 'Leave the mailbox alone.',
            'guards': {'coverage': False, 'objects': True},
        },
    ),
    'draft_from_generation_with_marks': call(
        DRAFT,
        'draftFromGeneration',
        CATALOG,
        {
            'areas': [
                {'area': 'driveway', 'service': 'concrete_pour', 'answers': {'turf_strips': False}}
            ],
            'notes': '',
            'guards': {},
            'markup_url': '/media/ai/markups/0195c0de-0000-7000-8000-000000000001.png',
            'markup_note': 'Red: pavers.',
        },
    ),
    'draft_marks_saved': call(DRAFT, 'withMarkup', draft(notes='Keep it.'), SAVED_MARKUP),
    'draft_marks_removed': call(DRAFT, 'withMarkup', draft(markup=SAVED_MARKUP), None),
    'draft_guard_lowered': call(DRAFT, 'withGuard', draft(), 'coverage', False),
    'draft_guard_raised_again': call(
        DRAFT, 'withGuard', draft(guards={'coverage': False}), 'coverage', True
    ),
    'draft_untoggle_drops_the_container': call(
        DRAFT,
        'toggleArea',
        draft(
            selected=['driveway', 'pool'],
            containers={'driveway': {'service': 'paver', 'answers': {'material': 'brick'}}},
        ),
        'driveway',
        False,
    ),
    'draft_service_change_resets_answers': call(
        DRAFT,
        'withService',
        draft(
            selected=['driveway'],
            containers={'driveway': {'service': 'paver', 'answers': {'material': 'brick'}}},
        ),
        ['driveway'],
        'artificial_turf',
    ),
    'draft_left_to_the_notes': call(
        DRAFT,
        'withWholeArea',
        draft(
            selected=['patio_deck'],
            containers={'patio_deck': {'service': 'paver', 'answers': {'material': 'brick'}}},
        ),
        ['patio_deck'],
        False,
    ),
    'draft_whole_again': call(
        DRAFT,
        'withWholeArea',
        draft(
            selected=['patio_deck'],
            containers={
                'patio_deck': {'service': 'paver', 'answers': {}, 'wholeArea': False},
            },
        ),
        ['patio_deck'],
        True,
    ),
    'draft_service_change_keeps_the_switch': call(
        DRAFT,
        'withService',
        draft(
            selected=['patio_deck'],
            containers={
                'patio_deck': {
                    'service': 'paver',
                    'answers': {'material': 'brick'},
                    'wholeArea': False,
                },
            },
        ),
        ['patio_deck'],
        'artificial_turf',
    ),
    'draft_ordered_left_to_the_notes': call(
        DRAFT,
        'orderedContainers',
        CATALOG,
        draft(
            selected=['patio_deck', 'pool'],
            containers={'patio_deck': {'service': 'paver', 'answers': {}, 'wholeArea': False}},
        ),
    ),
    'draft_from_a_request_left_to_the_notes': call(
        DRAFT,
        'draftFromGeneration',
        CATALOG,
        {
            'areas': [
                {'area': 'patio_deck', 'service': 'paver', 'answers': {}, 'whole_area': False},
                {'area': 'driveway', 'service': 'paver', 'answers': {}},
            ],
            'notes': '',
            'guards': {},
            'markup_url': None,
            'markup_note': '',
        },
    ),
    'draft_answer_cleans': call(
        DRAFT,
        'withAnswer',
        CATALOG,
        draft(
            selected=['driveway'],
            containers={
                'driveway': {
                    'service': 'paver',
                    'answers': {'material': 'travertine', 'color': 'ivory'},
                }
            },
        ),
        ['driveway'],
        'material',
        'brick',
    ),
    'draft_ordered': call(
        DRAFT,
        'orderedContainers',
        CATALOG,
        draft(
            selected=['pool', 'driveway', 'patio_deck'],
            containers={'driveway': {'service': 'paver', 'answers': {}}},
        ),
    ),
    # --- áreas agrupadas ------------------------------------------------------
    'units_grouped': call(DRAFT, 'unitsOf', CATALOG, draft(**PAVING)),
    'units_apart': call(DRAFT, 'unitsOf', CATALOG, draft(**PAVING, grouped=False)),
    'units_alone': call(
        DRAFT,
        'unitsOf',
        CATALOG,
        draft(
            selected=['driveway', 'pool'],
            containers={'driveway': {'service': 'paver', 'answers': {'material': 'brick'}}},
        ),
    ),
    'grouped_containers': call(DRAFT, 'orderedContainers', CATALOG, draft(**PAVING)),
    'grouping_copies_the_first': call(
        DRAFT,
        'toggleGrouped',
        CATALOG,
        draft(**PAVING, grouped=False),
        True,
        False,
    ),
    'grouping_drops_a_service_not_shared': call(
        DRAFT,
        'toggleGrouped',
        CATALOG,
        draft(
            selected=['driveway', 'entryway'],
            containers={'driveway': {'service': 'artificial_turf', 'answers': {'shade': 'dark'}}},
            grouped=False,
        ),
        True,
        False,
    ),
    'regrouped_from_a_request': call(
        DRAFT,
        'draftFromGeneration',
        CATALOG,
        {
            'areas': [
                {'area': area, 'service': 'paver', 'answers': {'material': 'brick'}}
                for area in ('driveway', 'entryway', 'walkway')
            ],
            'notes': '',
        },
    ),
    'not_regrouped_from_a_request': call(
        DRAFT,
        'draftFromGeneration',
        CATALOG,
        {
            'areas': [
                {'area': 'driveway', 'service': 'paver', 'answers': {'material': 'brick'}},
                {'area': 'entryway', 'service': 'paver', 'answers': {'material': 'travertine'}},
                {'area': 'walkway', 'service': 'paver', 'answers': {'material': 'brick'}},
            ],
            'notes': '',
        },
    ),
    'ungrouping_leaves_a_copy_in_each': call(
        DRAFT,
        'toggleGrouped',
        CATALOG,
        draft(**PAVING),
        False,
        False,
    ),
    'draft_pending': call(
        DRAFT,
        'pendingOf',
        CATALOG,
        [
            {'area': 'patio_deck', 'service': '', 'answers': {}},
            {'area': 'driveway', 'service': 'concrete_pour', 'answers': {'turf_strips': True}},
            {
                'area': 'entryway',
                'service': 'concrete_overlay',
                'answers': {'grout_color': 'sand', 'stone_color': 'shadow_stone'},
            },
        ],
    ),
    'draft_errors': call(
        DRAFT,
        'containerErrors',
        {
            'areas[0].area': {'code': 'AREA_NOT_ALLOWED'},
            'areas[1].color': {'code': 'ANSWER_INVALID'},
            'areas[7].color': {'code': 'ANSWER_INVALID'},
            'areas': {'code': 'AREAS_EMPTY'},
        },
        [
            {'area': 'patio_deck', 'service': 'paver', 'answers': {}},
            {'area': 'driveway', 'service': 'paver', 'answers': {}},
        ],
    ),
}


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


def values(options) -> list[str]:
    return [option['value'] for option in options]


def ids(fields) -> list[str]:
    return [field['id'] for field in fields]


class TestConditions:
    def test_eq_compares_with_the_value(self, results):
        assert results['eq_true'] is True
        assert results['eq_false'] is False
        assert results['eq_absent'] is False
        # La condición compara con booleano, no con 'yes' (pregunta 44).
        assert results['eq_yes_is_not_true'] is False

    def test_in_asks_if_the_answer_is_in_the_list(self, results):
        assert results['in_hit'] is True
        assert results['in_miss'] is False
        assert results['in_absent'] is False

    def test_includes_asks_if_the_list_contains_the_value(self, results):
        assert results['includes_hit'] is True
        assert results['includes_miss'] is False
        assert results['includes_not_a_list'] is False
        assert results['includes_absent'] is False

    def test_all_of_needs_every_condition(self, results):
        assert results['all_hit'] is True
        assert results['all_miss_pool'] is False
        assert results['all_miss_area'] is False

    def test_no_condition_is_always_true(self, results):
        assert results['none_is_true'] is True


class TestServicesForArea:
    """La misma tabla que calcula el servidor, área por área, con y sin pileta."""

    @pytest.mark.parametrize('area', catalog.Area.values)
    @pytest.mark.parametrize('pool', [False, True])
    def test_each_area_offers_what_the_server_offers(self, results, area, pool):
        offered = [schema['service'] for schema in results[f'services_{area}_{pool}']]

        assert offered == catalog.services_for(area, pool)


class TestPaverDependencies:
    def test_colors_follow_the_material(self, results):
        for material in ('travertine', 'brick', 'porcelain', 'marble'):
            expected = catalog.options_for(PAVER_COLOR_PY, {'material': material})
            assert values(results[f'colors_{material}']) == [o.value for o in expected], material
        assert results['colors_without_material'] == []

    def test_sizes_follow_the_material(self, results):
        for material in ('travertine', 'marble', 'porcelain', 'brick'):
            expected = catalog.options_for(PAVER_SIZE_PY, {'material': material})
            assert values(results[f'sizes_{material}']) == [o.value for o in expected], material

    def test_the_french_pattern_is_tied_to_the_french_mix(self, results):
        assert values(results['pattern_french_mix']) == ['french_pattern']
        assert 'french_pattern' not in values(results['pattern_6x12'])

    def test_every_size_offers_the_same_patterns_as_the_server(self, results):
        """Tamaño por tamaño, contra el esquema: no hay una lista escrita a mano."""
        for size in PAVER_SIZES:
            expected = catalog.options_for(PAVER_PATTERN_PY, {'size': size})
            assert values(results[f'pattern_{size}']) == [o.value for o in expected], size

    def test_the_border_courses_need_the_material_too(self, results):
        assert 'sailor_course' not in values(results['pattern_4x8'])
        assert 'sailor_course' in values(results['pattern_brick_4x8'])
        assert 'soldier_course' in values(results['pattern_brick_4x8'])

    def test_turf_strips_follow_the_same_sizes_as_the_server(self, results):
        for size in PAVER_SIZES:
            expected = catalog.is_shown(PAVER_TURF_PY, {'size': size})
            assert results[f'turf_shown_{size}'] is expected, size
        assert results['turf_shown_without_size'] is False

    def test_the_coping_reads_the_areas_of_the_whole_request(self, results):
        assert results['coping_with_pool_deck'] is True
        assert results['coping_without_pool_deck'] is False


class TestConcretePour:
    def test_the_layout_appears_only_with_turf_strips(self, results):
        assert results['layout_with_strips'] is True
        assert results['layout_without_strips'] is False
        assert results['layout_unanswered'] is False
        assert results['layout_with_yes'] is False


class TestCleaning:
    """La regla de `clean_request`, aplicada en pantalla con cada respuesta (R34, R38, R40)."""

    def test_a_color_of_another_material_is_dropped(self, results):
        # El borde viaja apagado en los dos: un interruptor opcional sin tocar
        # no está sin contestar.
        assert results['clean_color_follows_material'] == {'material': 'brick', 'border': False}
        assert results['clean_keeps_a_valid_color'] == {
            'material': 'travertine',
            'color': 'ivory',
            'border': False,
        }

    def test_a_hidden_field_is_dropped(self, results):
        assert results['clean_hidden_field_is_dropped'] == {'turf_strips': False}

    def test_a_toggle_only_takes_a_boolean(self, results):
        assert results['clean_toggle_needs_a_boolean'] == {}

    def test_a_required_field_starts_at_its_default(self, results):
        assert results['clean_overlay_shape_presets'] == {'shape': 'irregular'}

    def test_a_required_field_with_one_option_answers_itself(self, results):
        assert results['clean_french_pattern_presets'] == {
            'material': 'travertine',
            'color': 'ivory',
            'size': 'french_pattern_mix',
            'pattern': 'french_pattern',
            'border': False,
        }

    def test_a_field_that_depends_on_the_whole_request_is_cleaned_with_it(self, results):
        """El coping se queda con la vereda de la pileta en el pedido, y se va sin ella."""
        assert results['clean_coping_with_pool_deck']['coping'] == 'bullnose'
        assert 'coping' not in results['clean_coping_without_pool_deck']

    def test_the_coping_starts_unanswered(self, results):
        assert 'coping' not in results['clean_coping_unanswered']

    def test_an_optional_field_is_never_preset(self, results):
        assert results['clean_optional_is_not_preset'] == {}

    def test_an_unknown_key_is_dropped(self, results):
        assert results['clean_unknown_key_is_dropped'] == {'use_type': 'general_use'}

    def test_the_same_cleaning_as_the_server(self):
        """Lo que la pantalla limpia es lo que el servidor acepta, en los casos que sí acepta."""
        from apps.ai import prompt

        cleaned = prompt.clean_request(
            [
                {
                    'area': 'patio_deck',
                    'service': 'paver',
                    'answers': {
                        'material': 'travertine',
                        'color': 'ivory',
                        'size': 'french_pattern_mix',
                    },
                }
            ],
            False,
        )
        assert cleaned[0].answers == {
            'material': 'travertine',
            'color': 'ivory',
            'size': 'french_pattern_mix',
            'pattern': 'french_pattern',
            'border': False,
        }


class TestImages:
    """La foto de una opción, con la misma regla que `Field.image_of` del servidor."""

    def test_a_stone_shows_the_shape_that_was_chosen(self, results):
        stone_color = catalog.schema_for('concrete_overlay').field('stone_color')
        first = stone_color.options[0]

        assert results['stone_image_regular'] == '/static/' + stone_color.image_of(
            first, {'shape': 'regular'}
        )
        assert results['stone_image_irregular'] == '/static/' + stone_color.image_of(
            first, {'shape': 'irregular'}
        )
        assert '/regular/' in results['stone_image_regular']
        assert '/irregular/' in results['stone_image_irregular']

    def test_without_the_shape_there_is_no_stone_photo(self, results):
        assert results['stone_image_without_shape'] is None

    def test_an_option_with_one_photo_ignores_the_answers(self, results):
        assert results['paver_image'] == '/static/ai/paver/material/travertine.jpg'


class TestWhatIsDrawn:
    def test_a_field_that_answers_itself_draws_no_control(self, results):
        # La forma arranca contestada y se dibuja igual: tiene dos opciones.
        assert ids(results['visible_overlay']) == ['shape', 'stone_color', 'grout_color']
        assert 'pattern' not in ids(results['visible_paver_french'])
        assert 'pattern' in ids(results['visible_paver_large'])

    def test_a_default_with_several_options_is_drawn(self, results):
        """
        Lo que se esconde es lo que no deja nada para elegir. El coping es
        obligatorio, tiene tres opciones y se dibuja cuando el pedido trae la
        vereda de la pileta.
        """
        assert 'coping' in ids(results['visible_paver_with_pool_deck'])
        assert 'coping' not in ids(results['visible_paver_large'])

    def test_turf_strips_show_from_twenty_four_square(self, results):
        assert ids(results['visible_paver_large']) == [
            'material',
            'color',
            'size',
            'pattern',
            'border',
            'seal',
            'turf_strips',
        ]
        assert 'turf_strips' not in ids(results['visible_paver_french'])

    def test_the_layout_shows_only_with_turf_strips(self, results):
        assert ids(results['visible_pour_without_strips']) == ['turf_strips']
        assert ids(results['visible_pour_with_strips']) == ['turf_strips', 'layout']

    def test_what_is_missing_is_only_what_is_shown_and_required(self, results):
        assert results['missing_pour_empty'] == ['turf_strips']
        assert results['missing_pour_with_strips'] == ['layout']
        assert results['missing_pour_done'] == []
        # La forma arranca en su `default`: no falta.
        assert results['missing_overlay_empty'] == ['stone_color', 'grout_color']
        assert results['missing_pool_optionals_can_be_absent'] == []


class TestDraft:
    def test_it_starts_with_what_was_detected_or_empty(self, results):
        assert results['draft_from_analysis'] == draft(selected=['driveway', 'pool'])
        assert results['draft_from_malformed'] == draft()

    def test_a_raised_guard_leaves_no_trace_and_a_lowered_one_does(self, results):
        """
        El estado normal es no tener nada anotado: guardar los levantados en
        `true` haría que un borrador recién abierto y uno que se tocó dos veces
        se vieran distintos siendo lo mismo.
        """
        assert results['draft_guard_lowered']['guards'] == {'coverage': False}
        assert results['draft_guard_raised_again']['guards'] == {}

    def test_adjusting_brings_back_the_guards_as_they_were(self, results):
        assert results['draft_from_generation']['guards'] == {'coverage': False, 'objects': True}

    def test_adjusting_preloads_the_request(self, results):
        loaded = results['draft_from_generation']

        assert loaded['selected'] == ['driveway', 'pool']
        assert loaded['containers']['driveway'] == {
            'service': 'concrete_pour',
            'answers': {'turf_strips': True, 'layout': 'diamond_grid'},
        }
        assert loaded['containers']['pool'] == {
            'service': 'pool_renovation',
            'answers': {'plaster_color': 'capri'},
        }
        # Las notas vuelven al formulario: se ajusta lo que se escribió, no se
        # vuelve a escribir.
        assert loaded['notes'] == 'Leave the mailbox alone.'
        # Un pedido sin marcas vuelve sin marcas.
        assert loaded['markup'] is None

    def test_adjusting_brings_back_the_marks_from_the_request(self, results):
        """Vuelven por la ruta del pedido: ajustar sin tocarlas las conserva."""
        assert results['draft_from_generation_with_marks']['markup'] == {
            'image': '/media/ai/markups/0195c0de-0000-7000-8000-000000000001.png',
            'note': 'Red: pavers.',
        }

    def test_saving_and_removing_marks_touches_nothing_else(self, results):
        assert results['draft_marks_saved'] == draft(notes='Keep it.', markup=SAVED_MARKUP)
        assert results['draft_marks_removed'] == draft()

    def test_unticking_an_area_drops_its_container(self, results):
        assert results['draft_untoggle_drops_the_container'] == draft(selected=['pool'])

    def test_changing_the_service_resets_the_answers(self, results):
        assert results['draft_service_change_resets_answers']['containers']['driveway'] == {
            'service': 'artificial_turf',
            'answers': {},
        }

    def test_the_whole_area_switch_is_noted_only_when_off(self, results):
        """Como los guards: un contenedor nuevo y uno que nunca se tocó son el mismo."""
        assert results['draft_left_to_the_notes']['containers']['patio_deck'] == {
            'service': 'paver',
            'answers': {'material': 'brick'},
            'wholeArea': False,
        }
        assert results['draft_whole_again']['containers']['patio_deck'] == {
            'service': 'paver',
            'answers': {},
        }

    def test_changing_the_service_keeps_the_whole_area_switch(self, results):
        """Dice qué pasa con lo que ya existe en la foto, no con el servicio."""
        container = results['draft_service_change_keeps_the_switch']['containers']['patio_deck']

        assert container == {'service': 'artificial_turf', 'answers': {}, 'wholeArea': False}

    def test_the_request_always_says_whether_each_area_goes_whole(self, results):
        containers = results['draft_ordered_left_to_the_notes']

        assert [(c['area'], c['whole_area']) for c in containers] == [
            ('patio_deck', False),
            ('pool', True),
        ]

    def test_an_area_left_to_the_notes_comes_back_that_way_to_adjust(self, results):
        """Un pedido anterior al interruptor no lo trae, y vuelve encendido."""
        containers = results['draft_from_a_request_left_to_the_notes']['containers']

        assert containers['patio_deck']['wholeArea'] is False
        assert 'wholeArea' not in containers['driveway']

    def test_answering_cleans_what_no_longer_applies(self, results):
        assert results['draft_answer_cleans']['containers']['driveway']['answers'] == {
            'material': 'brick',
            'border': False,
        }

    def test_containers_follow_the_taxonomy_not_the_clicks(self, results):
        assert [c['area'] for c in results['draft_ordered']] == ['patio_deck', 'driveway', 'pool']
        assert results['draft_ordered'][1]['service'] == 'paver'
        assert results['draft_ordered'][0]['service'] == ''

    def test_pending_names_the_service_or_the_field(self, results):
        assert results['draft_pending'] == ['patio_deck.service', 'driveway.layout']

    def test_server_errors_land_on_their_container(self, results):
        errors = results['draft_errors']

        assert set(errors) == {'patio_deck', 'driveway'}
        assert set(errors['patio_deck']) == {'service'}
        assert set(errors['driveway']) == {'color'}
        assert 'pool' in errors['patio_deck']['service'].lower()


class TestGrouping:
    """
    Las áreas que en un trabajo real llevan el mismo diseño se contestan juntas.

    Lo que se defiende es que juntarlas sea sólo una forma de contestar: el
    pedido sigue llevando un contenedor por área, con lo que dice la caja del
    grupo y no con lo que cada área tenga guardado de antes.
    """

    def test_the_three_that_share_a_design_become_one_container(self, results):
        units = results['units_grouped']

        assert [unit['areas'] for unit in units] == [['driveway', 'entryway', 'walkway']]
        assert units[0]['service'] == 'paver'
        assert units[0]['answers'] == {'material': 'brick', 'color': 'red_terracotta'}

    def test_apart_each_one_keeps_its_own_container(self, results):
        assert [unit['areas'] for unit in results['units_apart']] == [
            ['driveway'],
            ['entryway'],
            ['walkway'],
        ]

    def test_a_single_one_of_them_is_not_a_group(self, results):
        """Con una sola no hay nada que juntar, y por eso la pantalla esconde el interruptor."""
        assert [unit['areas'] for unit in results['units_alone']] == [['driveway'], ['pool']]

    def test_the_request_still_travels_one_container_per_area(self, results):
        containers = results['grouped_containers']

        assert [container['area'] for container in containers] == [
            'driveway',
            'entryway',
            'walkway',
        ]
        assert {container['service'] for container in containers} == {'paver'}
        assert all(
            container['answers'] == {'material': 'brick', 'color': 'red_terracotta'}
            for container in containers
        )

    def test_grouping_copies_the_first_of_the_taxonomy(self, results):
        containers = results['grouping_copies_the_first']['containers']

        assert results['grouping_copies_the_first']['grouped'] is True
        assert containers['entryway'] == containers['driveway']
        assert containers['walkway'] == containers['driveway']

    def test_grouping_drops_a_service_that_not_every_area_offers(self, results):
        """La entrada de la casa no lleva césped: el grupo arranca sin servicio."""
        containers = results['grouping_drops_a_service_not_shared']['containers']

        assert containers['driveway'] == {'service': '', 'answers': {}}
        assert containers['entryway'] == {'service': '', 'answers': {}}

    def test_ungrouping_leaves_each_area_with_what_the_group_said(self, results):
        apart = results['ungrouping_leaves_a_copy_in_each']

        assert apart['grouped'] is False
        assert apart['containers']['walkway'] == {
            'service': 'paver',
            'answers': {'material': 'brick', 'color': 'red_terracotta'},
        }

    def test_a_request_asked_as_a_group_comes_back_as_a_group(self, results):
        """
        Ajustar un pedido agrupado no lo desarma: separarlo obligaría a repetir
        el mismo cambio tres veces.
        """
        assert results['regrouped_from_a_request']['grouped'] is True
        assert results['not_regrouped_from_a_request']['grouped'] is False


PAVER_COLOR_PY = catalog.schema_for('paver').field('color')
PAVER_SIZE_PY = catalog.schema_for('paver').field('size')
PAVER_PATTERN_PY = catalog.schema_for('paver').field('pattern')
PAVER_TURF_PY = catalog.schema_for('paver').field('turf_strips')
