"""
Nadie lee el cuerpo de una petición por su cuenta.

`apps/core/requests.py` existe porque Inertia manda el cuerpo como **JSON**
salvo que haya un archivo, y Django rellena `request.POST` sólo en el segundo
caso. Una vista que lea `request.POST` recibe un diccionario vacío y concluye
que no se mandó nada.

Ese fallo es de los peores que puede tener este producto, y no por su tamaño:

- **No rompe.** La vista corre entera y contesta bien; simplemente cree que el
  formulario vino en blanco.
- **La pantalla se ve correcta.** Quien la usa eligió cuatro direcciones y las
  ve marcadas; lo que quedó vacío está del otro lado del cable.
- **Un test lo puede tapar.** El cliente de pruebas de Django manda formulario
  por omisión, así que una prueba que postee `{...}` a secas ejercita un camino
  que el navegador nunca toma y pasa mientras la pantalla real falla.

Los tres juntos son la razón de que esto se verifique en vez de recordarse. Ya
pasó una vez —el alta de un lote de indexación leía `request.POST.getlist` y
recibía cero direcciones— y el módulo que lo evitaba estaba escrito, probado y a
un `import` de distancia. Lo que faltaba era que alguien lo obligara.

**Se mira el árbol de sintaxis y no el texto.** Con una expresión regular, este
mismo archivo se delataría a sí mismo: la prosa de arriba nombra `request.POST`
cuatro veces para explicar por qué no se usa, y una regla que prohíba mencionar
el problema deja el código sin poder documentarse. El árbol distingue una
lectura de una mención, que es exactamente la distinción que hace falta.

`request.GET` **no entra acá**: la querystring la parsea Django siempre, sin
importar cómo venga el cuerpo, así que leerla directo es correcto.
"""

import ast
from pathlib import Path

import pytest
from django.conf import settings

APPS = Path(settings.BASE_DIR) / 'apps'

#: El lector oficial. Es el único que puede tocar `request.POST`, porque es el
#: que sabe cuándo mirarlo.
READER = APPS / 'core' / 'requests.py'


def body_reads(source: str) -> list[int]:
    """
    En qué líneas el código **lee** `request.POST`, mencionarlo no cuenta.

    Cubre `.get`, `.getlist`, `.dict` y el acceso por clave sin enumerarlos: las
    cuatro cuelgan del mismo acceso al atributo, así que alcanza con encontrar
    ése. Y cubre también `self.request.POST`, que es como lo escribiría una
    vista de DRF.
    """
    tree = ast.parse(source)

    return sorted(
        node.lineno
        for node in ast.walk(tree)
        if isinstance(node, ast.Attribute)
        and node.attr == 'POST'
        and ast.unparse(node.value).split('.')[-1] == 'request'
    )


def python_files() -> list[Path]:
    """Todo el código de aplicación, menos el lector y lo generado."""
    return sorted(
        path
        for path in APPS.rglob('*.py')
        if path != READER
        # Las migraciones las escribe Django y no leen peticiones.
        and 'migrations' not in path.relative_to(APPS).parts
        and '__pycache__' not in path.relative_to(APPS).parts
    )


@pytest.mark.parametrize('path', python_files(), ids=lambda path: str(path.name))
def test_no_view_reads_the_body_on_its_own(path):
    """
    Leer el cuerpo se hace con `payload`, `field` o `field_list`, y con nada más.

    El mensaje del fallo dice qué usar en vez de sólo que algo está mal: quien
    lo encuentre está escribiendo una vista, no auditando esta regla.
    """
    lines = body_reads(path.read_text(encoding='utf-8'))

    assert not lines, (
        f'{path.name}, línea(s) {", ".join(str(line) for line in lines)}: se lee el cuerpo '
        f'directo. Con un cuerpo JSON —el que manda Inertia— eso está vacío y la vista '
        f'concluye que no se envió nada. Usá «payload», «field» o «field_list» de '
        f'apps/core/requests.py, que leen las dos formas.'
    )


def test_the_reader_is_the_one_place_that_may_read_it():
    """
    La regla vale porque hay un lugar donde sí se lee.

    Sin esta comprobación, alguien podría satisfacer la de arriba borrando el
    último acceso del repo y dejando el lector sin leer nada: la regla seguiría
    en verde sobre un producto que ya no sabe recibir un formulario con archivo.
    """
    assert body_reads(READER.read_text(encoding='utf-8')), (
        'El lector oficial dejó de mirar «request.POST». Si el multipart ya no se lee, '
        'la regla de arriba no está protegiendo nada.'
    )


def test_mentioning_it_in_prose_is_not_reading_it():
    """
    La regla se puede explicar sin romperla.

    Este archivo y el docstring de la exportación de cobertura nombran
    `request.POST` para contar por qué no se usa. Con una comprobación de texto
    los dos fallarían, y la salida sería dejar de escribir la explicación —que
    es la parte que evita que el defecto vuelva—.
    """
    prose = '"""Esto leía request.POST y estaba mal."""\n# request.POST tampoco\n'

    assert body_reads(prose) == []


def test_it_catches_the_shapes_that_actually_happen():
    """
    Una regla que no atrapa nada pasa en verde sobre un repo roto.

    La primera es literalmente la línea que falló en producción; las otras son
    las formas en que el mismo error se puede escribir sin darse cuenta.
    """
    assert body_reads("selected = request.POST.getlist('urls')\n") == [1]
    assert body_reads("value = request.POST.get('action')\n") == [1]
    assert body_reads('data = request.POST.dict()\n') == [1]
    assert body_reads("name = request.POST['hostname']\n") == [1]
    # Como lo escribiría una vista de DRF, donde la petición cuelga de `self`.
    assert body_reads('data = self.request.POST\n') == [1]


def test_the_querystring_is_not_the_body():
    """
    `request.GET` se lee directo y está bien.

    Django parsea la querystring siempre, sin importar cómo venga el cuerpo.
    Prohibirla acá mandaría a envolver en el lector algo que nunca tuvo el
    problema, y de paso convertiría la regla en ruido que se aprende a ignorar.
    """
    assert body_reads("opened = request.GET.get('url')\n") == []


def test_the_rule_covers_every_app():
    """
    Que la lista de archivos no se quede vacía por un cambio de carpetas.

    Un recorrido que no encuentra nada pasa en verde y no comprueba nada, que es
    la forma más silenciosa que tiene una red de contención de dejar de existir.
    """
    scanned = python_files()

    assert len(scanned) > 50
    assert any(path.name == 'views.py' for path in scanned)
    assert any(path.name == 'api.py' for path in scanned)
