"""
Configuración común de la suite de pruebas.

Las variables obligatorias se completan antes de que pytest-django cargue la
configuración, para que la suite corra sin depender de un archivo .env.
"""

import ipaddress
import os
import socket

import pytest
from cryptography.fernet import Fernet

os.environ.setdefault('SECRET_KEY', 'clave-solo-para-pruebas')
os.environ.setdefault('DEBUG', 'false')
os.environ.setdefault('SETTINGS_ENCRYPTION_KEY', Fernet.generate_key().decode())

# Se pide ejecutar las tareas en el momento en vez de encolarlas, para no
# necesitar un Redis y un trabajador vivos.
#
# **En la práctica no alcanza**: pytest-django carga la configuración antes que
# este archivo, así que `CELERY_TASK_ALWAYS_EAGER` queda en falso y `delay()`
# intenta hablar con la cola de verdad. Por eso los tests que ejercitan trabajo
# encolado sustituyen `delay` —o llaman a la tarea a mano— en vez de confiar en
# esta línea. Se conserva porque sí sirve cuando la variable ya viene del
# entorno, que es como corre en integración continua.
os.environ.setdefault('CELERY_TASK_ALWAYS_EAGER', 'true')


def pytest_configure():
    """
    Baja el costo de hashear contraseñas, que es la mitad del reloj de la suite.

    Casi todos los tests crean una cuenta, y `create_user` hashea con el
    algoritmo de producción: cientos de miles de iteraciones pensadas para que a
    quien roba la base le cueste años. Acá esa lentitud no protege nada —las
    contraseñas son literales que están en el mismo archivo— y se paga una vez
    por test.

    No toca la configuración de la aplicación: se cambia después de que
    pytest-django cargó los ajustes y antes de que se hashee la primera
    contraseña, que es cuando Django memoriza la lista.
    """
    from django.conf import settings

    settings.PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']


# Hosts que la suite sí puede alcanzar: la base de datos y la cola locales.
_ALLOWED_HOSTNAMES = {'localhost', 'db', 'redis'}


def _is_local(host: str) -> bool:
    if host in _ALLOWED_HOSTNAMES:
        return True
    try:
        address = ipaddress.ip_address(host)
    except ValueError:
        return False
    return address.is_loopback or address.is_private


@pytest.fixture(autouse=True)
def block_external_network(monkeypatch):
    """
    Corta cualquier salida a la red que no sea la base de datos o la cola.

    Un test que llama a Google de verdad no es una prueba más completa: gasta
    cuota real de una propiedad real y ata la suite a un servicio ajeno. El
    flujo de trabajo de la constitución lo prohíbe, así que el corte es
    automático y no queda librado a la disciplina de cada quien.
    """
    real_getaddrinfo = socket.getaddrinfo
    real_connect = socket.socket.connect

    def guarded_getaddrinfo(host, *args, **kwargs):
        if not _is_local(str(host)):
            raise RuntimeError(
                f'La suite intentó resolver "{host}". Las pruebas no salen a la red: '
                f'usá las respuestas grabadas de tests/fixtures/.'
            )
        return real_getaddrinfo(host, *args, **kwargs)

    def guarded_connect(self, address):
        host = address[0] if isinstance(address, tuple) else str(address)
        if not _is_local(str(host)):
            raise RuntimeError(
                f'La suite intentó conectarse a "{host}". Las pruebas no salen a la red: '
                f'usá las respuestas grabadas de tests/fixtures/.'
            )
        return real_connect(self, address)

    monkeypatch.setattr(socket, 'getaddrinfo', guarded_getaddrinfo)
    monkeypatch.setattr(socket.socket, 'connect', guarded_connect)
