"""Afirmación del lado CRM del contrato con EFC.

``tests/contracts/efc_crm_contract.json`` es el contrato; esto comprueba que **este** repo lo
cumple. EFC debe afirmar su mitad contra una copia idéntica del mismo archivo.

Por qué existe, y por qué no basta con las otras pruebas: CRM y EFC se despliegan por separado. Las
pruebas de `test_efc_client.py` verifican que el cliente se comporta bien contra el EFC que el
cliente **cree** que existe; si EFC renombra una ruta o una clave del payload, esas pruebas siguen
verdes y el carril se rompe en producción. Lo único que atrapa esa deriva es un contrato escrito
aparte y afirmado desde los dos lados.

Nada aquí toca la red: las peticiones se capturan con ``httpx.MockTransport``.
"""

import json
from pathlib import Path

import httpx
import pytest

from core.efc_client import EfcClient

CONTRATO = json.loads(
    (Path(__file__).parent / "contracts" / "efc_crm_contract.json").read_text(encoding="utf-8")
)
CARRIL = CONTRATO["carril_efc"]
API_CRM = CONTRATO["api_crm"]


@pytest.fixture()
def capturadas():
    """Cliente contra EFC simulado que va guardando las peticiones que salen."""
    peticiones: list[httpx.Request] = []

    def handler(request):
        peticiones.append(request)
        if request.method == "GET" and request.url.path.endswith("/list/"):
            return httpx.Response(200, json=[])
        return httpx.Response(200, json={"id": "org-1"})

    cliente = EfcClient(
        base_url="https://efc.example.test",
        api_key="llave-de-prueba",
        timeout_ms=500,
        upload_timeout_ms=500,
        verify_ssl=False,
        transport=httpx.MockTransport(handler),
    )
    return cliente, peticiones


# ── Las rutas del carril ─────────────────────────────────────────────────────

@pytest.mark.parametrize(
    "nombre,llamada",
    [
        ("organizaciones_buscar", lambda c: c.buscar_organizaciones("temex")),
        ("organizaciones_resolver", lambda c: c.resolve_organizacion("temex", "TEMEX")),
        ("expediente_crear", lambda c: c.ingest_expediente({"folio": "EXP2026-08-001"})),
        ("expediente_completar", lambda c: c.completar_expediente("EXP2026-08-001", {})),
        ("expediente_detalle", lambda c: c.get_expediente("EXP2026-08-001", "org-1")),
        ("documentos_listar", lambda c: c.list_documentos("org-1", 42)),
    ],
)
def test_cada_llamada_del_cliente_pega_en_la_ruta_del_contrato(capturadas, nombre, llamada):
    cliente, peticiones = capturadas
    esperado = CARRIL["endpoints"][nombre]

    llamada(cliente)

    peticion = peticiones[-1]
    assert peticion.method == esperado["metodo"]
    assert peticion.url.path == _resolver(esperado["path"])


def test_la_descarga_apunta_a_la_ruta_del_contrato(capturadas):
    """``download_url`` la arma a mano para el proxy async, así que se comprueba aparte."""
    cliente, _ = capturadas
    esperado = CARRIL["endpoints"]["documento_descargar"]

    url = httpx.URL(cliente.download_url("doc-1"))
    assert url.path == _resolver(esperado["path"], doc_id="doc-1")


def test_la_subida_de_documento_pega_en_su_ruta_y_va_en_multipart(capturadas):
    cliente, peticiones = capturadas
    esperado = CARRIL["endpoints"]["documento_subir"]

    cliente.upload_documento("org-1", 1, 42, "MBL", "guia.pdf", b"%PDF-1.4", "application/pdf")

    peticion = peticiones[-1]
    assert peticion.method == esperado["metodo"]
    assert peticion.url.path == esperado["path"]
    assert esperado["content_type"] in peticion.headers["content-type"]


def test_el_reemplazo_de_documento_pega_en_su_ruta(capturadas):
    cliente, peticiones = capturadas
    esperado = CARRIL["endpoints"]["documento_reemplazar"]

    cliente.replace_documento("org-1", "doc-1", "guia.pdf", b"%PDF-1.4")

    peticion = peticiones[-1]
    assert peticion.method == esperado["metodo"]
    assert peticion.url.path == _resolver(esperado["path"], doc_id="doc-1")


def test_el_cliente_NO_sabe_borrar_documentos_en_efc():
    """El endpoint de borrado existe en EFC y el CRM **deliberadamente no lo llama**.

    ``record.Document`` no tiene vigencia ni purga, así que la política implícita del sistema es
    conservar, y un documento que mañana puede ser parte del expediente de un pedimento real es
    riesgo de retención fiscal. La baja en el CRM es lógica. Que el método no exista es lo que
    impide que alguien lo llame "porque estaba ahí": esta prueba se pone roja si aparece.
    """
    metodos = {m for m in dir(EfcClient) if "elimin" in m or "delete" in m or "borrar" in m}
    assert metodos == set(), f"apareció una operación de borrado hacia EFC: {metodos}"
    assert "documento_eliminar" in CARRIL["endpoints"], "el endpoint existe del lado de EFC"


def _resolver(plantilla: str, **valores) -> str:
    """Rellena los marcadores de la plantilla con los valores que usan las pruebas."""
    defaults = {"folio": "EXP2026-08-001", "doc_id": "doc-1"}
    defaults.update(valores)
    return plantilla.format(**defaults)


# ── Las formas de los payloads ───────────────────────────────────────────────

def test_el_alta_de_expediente_manda_exactamente_las_claves_del_contrato(capturadas):
    """Ni una de más ni una de menos.

    Una clave de menos y EFC responde ``payload_invalido``; una de más y el serializer de EFC la
    ignora en silencio, que es peor: el dato se cree enviado y no lo está.
    """
    cliente, peticiones = capturadas
    esperadas = set(CARRIL["endpoints"]["expediente_crear"]["request_claves"])

    cliente.ingest_expediente({clave: "x" for clave in esperadas})

    assert set(json.loads(peticiones[-1].content)) == esperadas


def test_la_subida_manda_los_campos_de_formulario_del_contrato(capturadas):
    cliente, peticiones = capturadas
    esperados = set(CARRIL["endpoints"]["documento_subir"]["form_claves"])
    archivo = CARRIL["endpoints"]["documento_subir"]["archivo_campo"]

    cliente.upload_documento(
        "org-1", 1, 42, "MBL", "guia.pdf", b"%PDF-1.4", "application/pdf",
        crm_document_ref="CRMDOC-1-7",
    )

    cuerpo = peticiones[-1].content.decode("latin-1")
    faltantes = [c for c in esperados if f'name="{c}"' not in cuerpo]
    assert faltantes == [], f"el multipart no lleva {faltantes}"
    assert f'name="{archivo}"' in cuerpo


def test_toda_peticion_del_carril_lleva_el_header_de_autenticacion(capturadas):
    cliente, peticiones = capturadas
    header = CARRIL["header_autenticacion"]

    cliente.resolve_organizacion("temex")
    cliente.ingest_expediente({"folio": "EXP2026-08-001"})
    cliente.upload_documento("org-1", 1, 42, "MBL", "g.pdf", b"x")

    assert peticiones, "no salió ninguna petición"
    for peticion in peticiones:
        assert peticion.headers.get(header) == "llave-de-prueba"


# ── El catálogo de errores ───────────────────────────────────────────────────

def test_el_code_que_dispara_el_ensure_then_upload_esta_en_el_catalogo():
    """Si EFC renombra este code, el CRM deja de recuperarse solo y los documentos se quedan
    pendientes para siempre **sin que nada falle a gritos**. Es el code más frágil del carril."""
    critico = CARRIL["codigos_con_significado_para_el_crm"]["expediente_no_encontrado"]
    assert critico["http"] == 404
    assert "expediente_no_encontrado" in CARRIL["codigos_error"]["404"]


def test_el_gateway_ramifica_por_el_code_exacto_del_contrato():
    """El código del CRM tiene ese ``code`` escrito literal. Que coincida con el contrato es lo que
    esta prueba fija; que el contrato coincida con EFC lo fija la suite del otro repo."""
    from pathlib import Path as _Path

    fuente = (
        _Path(__file__).parent.parent
        / "api" / "v1" / "modules" / "crm" / "expediente_gateway" / "service.py"
    ).read_text(encoding="utf-8")

    assert '"expediente_no_encontrado"' in fuente


def test_el_cliente_extrae_el_code_del_formato_de_error_del_contrato():
    forma = CARRIL["formato_error"]["forma"]
    assert set(forma) == {"error"}
    assert set(forma["error"]) == {"code", "message"}

    def handler(request):
        return httpx.Response(400, json={"error": {"code": "espacio_insuficiente", "message": "m"}})

    from core.efc_client import EfcClientError

    cliente = EfcClient(
        base_url="https://efc.example.test", api_key="k", timeout_ms=200,
        verify_ssl=False, transport=httpx.MockTransport(handler),
    )
    with pytest.raises(EfcClientError) as exc:
        cliente.resolve_organizacion("temex")

    assert exc.value.code == "espacio_insuficiente"
    assert exc.value.code in CARRIL["codigos_error"]["400"]
    assert exc.value.retryable is False


# ── El API de usuario del CRM ────────────────────────────────────────────────

def test_las_rutas_registradas_del_crm_son_las_del_contrato():
    """Cubre las dos direcciones: ninguna del contrato sin registrar, y ninguna registrada de más
    en estos dos routers. Un endpoint que aparece sin estar en el contrato es un endpoint que
    nadie del otro lado sabe que existe."""
    from api.v1.modules.crm.expediente_gateway.routes import router as gateway_router
    from api.v1.modules.crm.expedientes.routes import router as expedientes_router

    registradas = set()
    for router in (expedientes_router, gateway_router):
        for ruta in router.routes:
            # ``ruta.path`` ya trae el prefijo del router aplicado: concatenarlo lo duplicaría.
            for metodo in ruta.methods:
                if metodo in ("HEAD", "OPTIONS"):
                    continue
                registradas.add((metodo, ruta.path))

    del_contrato = {(e["metodo"], e["path"]) for e in API_CRM["endpoints"]}

    assert del_contrato - registradas == set(), "el contrato declara rutas que no existen"
    assert registradas - del_contrato == set(), "hay rutas fuera del contrato"


def test_la_respuesta_de_un_documento_nunca_expone_la_copia_local():
    """La copia local se borra al confirmar la entrega a EFC: una referencia expuesta al frontend
    es una referencia que va a dejar de existir."""
    from api.v1.modules.crm.expedientes.dto import ExpedienteDocumentResponse

    campos = set(ExpedienteDocumentResponse.model_fields)

    for prohibido in API_CRM["documento_response_prohibido"]:
        assert prohibido not in campos, f"la respuesta expone '{prohibido}'"
    faltantes = [c for c in API_CRM["documento_response_claves_minimas"] if c not in campos]
    assert faltantes == [], f"la respuesta no lleva {faltantes}"


def test_los_estados_de_sincronizacion_son_los_del_contrato():
    from api.v1.modules.crm.expediente_gateway.models import (
        STATUS_FAILED,
        STATUS_PENDING,
        STATUS_SENT,
    )

    # Los del outbox son en minúsculas; los que ve el frontend en el documento, en mayúsculas.
    assert {STATUS_PENDING, STATUS_SENT, STATUS_FAILED} == {"pending", "sent", "failed"}
    assert set(API_CRM["estados_sincronizacion"]) == {"PENDING", "SYNCED", "FAILED"}


def test_las_metricas_del_outbox_tienen_las_claves_del_contrato(db):
    from api.v1.modules.crm.expediente_gateway import service as gateway
    from tests.conftest import COMPANY_ID, TENANT_ID

    metricas = gateway.outbox_metrics(db, TENANT_ID, COMPANY_ID)

    assert set(API_CRM["metricas_outbox_claves"]).issubset(set(metricas))


def test_el_formato_del_folio_es_el_del_contrato(db):
    """El folio es lo que el usuario ve al guardar y lo que enlaza al expediente con EFC: su forma
    es contrato, no detalle."""
    import re

    # El generador es el del CRM (crm/common/folios.py), no uno del carril: el expediente y su
    # consecutivo son del CRM y esta prueba solo afirma que la FORMA que produce es la que EFC
    # espera. Por eso se llama a la implementación real y no se reimplementa el formato aquí.
    from api.v1.modules.crm.common.folios import next_folio
    from tests.conftest import COMPANY_ID, TENANT_ID

    # with_direction=False: el expediente no lleva sufijo I/E, a diferencia de la solicitud.
    folio = next_folio(db, TENANT_ID, COMPANY_ID, "EXP", None, with_direction=False)

    patron = (
        API_CRM["folio"]["formato"]
        .replace("{YYYY}", r"\d{4}")
        .replace("{MM}", r"\d{2}")
        .replace("{NNN}", r"\d{3,}")
    )
    assert re.fullmatch(patron, folio), f"{folio} no cumple {API_CRM['folio']['formato']}"
