"""Pruebas de la entrega de un archivo al expediente de EFC.

Cubren las tres cosas que hacen que este carril no pierda ni duplique archivos: el
**ensure-then-upload** cuando el provisional todavía no existe allá, el **corte directo**
(``delete_local``) que borra la copia local solo al confirmar, y la idempotencia por
``crm_document_ref`` cuando un timeout ambiguo hace reintentar.
"""

import pytest

from api.v1.modules.crm.expediente_gateway import service as gateway
from api.v1.modules.crm.expediente_gateway.models import (
    FILE_KIND_DOCUMENTO,
    MAX_ATTEMPTS,
    SOURCE_CRM_DOCUMENTS,
    STATUS_FAILED,
    STATUS_PENDING,
    STATUS_SENT,
    EfcFileOutbox,
)
from api.v1.modules.crm.expedientes import service as expedientes_service
from api.v1.modules.crm.documents.models import Document
from api.v1.modules.crm.service_requests import service as sr_service
from api.v1.modules.crm.service_requests.dto import ServiceRequestCreate
from core.efc_client import EfcClientError
from tests.conftest import COMPANY_ID, TENANT_ID

CONTENIDO = b"%PDF-1.4 guia madre"
S3_KEY = "tenants/1/companies/1/expedientes/1/guia.pdf"


class _ClienteFalso:
    """Doble del cliente de EFC. Registra qué se le pidió, para poder afirmarlo."""

    is_configured = True

    def __init__(self, *, upload_falla_con=None, falla_solo_la_primera=True):
        self.uploads = []
        self.ingests = []
        self._upload_falla_con = upload_falla_con
        self._falla_solo_la_primera = falla_solo_la_primera

    def ingest_expediente(self, payload):
        self.ingests.append(payload)
        return {"status": "created", "efc": {"pedimento_id": "ped-1"}}

    def upload_documento(self, org_id, company_id, expediente_id, tipo, filename, content,
                         content_type="application/octet-stream", crm_document_ref=None):
        primera = not self.uploads
        self.uploads.append({
            "org_id": org_id, "company_id": company_id, "expediente_id": expediente_id,
            "tipo": tipo, "filename": filename, "content": content,
            "content_type": content_type, "crm_document_ref": crm_document_ref,
        })
        if self._upload_falla_con is not None and (primera or not self._falla_solo_la_primera):
            raise self._upload_falla_con
        return {"id": f"doc-{len(self.uploads)}"}


@pytest.fixture()
def entorno(db, monkeypatch):
    """EFC encendido, storage simulado y organización ya resuelta."""
    from core.config import settings

    monkeypatch.setattr(settings, "EFC_API_URL", "https://efc.example.test/", raising=False)
    monkeypatch.setattr(gateway, "_dispatch_delivery", lambda *a, **k: None)
    monkeypatch.setattr(gateway, "_dispatch_file_delivery", lambda *a, **k: None)
    monkeypatch.setattr(gateway, "_tenant_slug", lambda tid: ("temex", "TEMEX"))
    monkeypatch.setattr(gateway, "_resolve_org_id", lambda c, t: "org-1")

    borrados = []
    import core.storage_s3 as storage

    monkeypatch.setattr(storage, "get_object_bytes", lambda key: CONTENIDO)
    monkeypatch.setattr(storage, "delete_object_if_exists", lambda key: borrados.append(key))

    solicitud = sr_service.create_service_request(
        db, ServiceRequestCreate(operation_type="importacion"), TENANT_ID, COMPANY_ID, "user-1"
    )
    expediente = expedientes_service.find_by_service_request(db, solicitud.id, TENANT_ID, COMPANY_ID)
    return {"db": db, "expediente": expediente, "borrados": borrados}


def _documento_local(db, expediente) -> Document:
    doc = Document(
        doc_type="MBL",
        name="guia.pdf",
        file_key=S3_KEY,
        content_type="application/pdf",
        size_bytes=len(CONTENIDO),
        expediente_id=expediente.id,
        efc_sync_state="PENDING",
        tenant_id=TENANT_ID,
        company_id=COMPANY_ID,
    )
    db.add(doc)
    db.flush()
    doc.efc_document_ref = f"CRMDOC-{COMPANY_ID}-{doc.id}"
    db.commit()
    return doc


def _fila(db, expediente, documento, **kwargs) -> EfcFileOutbox:
    row = EfcFileOutbox(
        kind=FILE_KIND_DOCUMENTO,
        s3_key=S3_KEY,
        file_name="guia.pdf",
        content_type="application/pdf",
        efc_tipo="MBL",
        source_table=SOURCE_CRM_DOCUMENTS,
        source_id=documento.id,
        crm_document_ref=documento.efc_document_ref,
        expediente_ref=expediente.id,
        delete_local=kwargs.pop("delete_local", True),
        status=kwargs.pop("status", STATUS_PENDING),
        tenant_id=TENANT_ID,
        company_id=COMPANY_ID,
        **kwargs,
    )
    db.add(row)
    db.commit()
    return row


# ── Camino feliz ─────────────────────────────────────────────────────────────

def test_entrega_feliz_marca_la_fila_y_el_documento(entorno):
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    cliente = _ClienteFalso()

    gateway.deliver_file_row(db, row, cliente)

    assert row.status == STATUS_SENT
    assert row.efc_document_id == "doc-1"
    assert row.sent_at is not None
    assert documento.efc_sync_state == "SYNCED"
    assert documento.efc_document_id == "doc-1"
    assert documento.efc_synced_at is not None


def test_la_subida_lleva_el_crm_document_ref_y_el_contenido_leido_de_minio(entorno):
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    cliente = _ClienteFalso()

    gateway.deliver_file_row(db, row, cliente)

    assert len(cliente.uploads) == 1
    subida = cliente.uploads[0]
    assert subida["crm_document_ref"] == documento.efc_document_ref
    assert subida["content"] == CONTENIDO
    assert subida["tipo"] == "MBL"
    assert subida["expediente_id"] == expediente.id


# ── Ensure-then-upload ───────────────────────────────────────────────────────

def test_un_404_de_expediente_crea_el_provisional_y_reintenta_una_vez(entorno):
    """La creación del provisional y la subida son colas distintas: la subida puede llegar antes.

    Sin esto, el primer documento de cada expediente fallaría y esperaría al barrido.
    """
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("no está", status_code=404, code="expediente_no_encontrado")
    )

    gateway.deliver_file_row(db, row, cliente)

    assert len(cliente.ingests) == 1
    assert cliente.ingests[0]["folio"] == expediente.folio
    assert cliente.ingests[0]["storage_token"] == expediente.efc_storage_token
    assert len(cliente.uploads) == 2  # el que falló + UNO de reintento
    assert row.status == STATUS_SENT


def test_un_404_con_OTRO_code_no_dispara_el_ensure(entorno):
    """El ensure se dispara por el ``code``, no por el 404 a secas.

    Si se disparara por cualquier 404, un documento no encontrado crearía provisionales espurios.
    """
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("otro", status_code=404, code="documento_no_encontrado"),
        falla_solo_la_primera=False,
    )

    gateway.deliver_file_row(db, row, cliente)

    assert cliente.ingests == []
    assert len(cliente.uploads) == 1
    assert row.attempts == 1


def test_el_ensure_reintenta_UNA_vez_y_no_entra_en_bucle(entorno):
    """Si el reintento vuelve a dar 404, se registra el fallo. No se reintenta indefinidamente."""
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("no está", status_code=404, code="expediente_no_encontrado"),
        falla_solo_la_primera=False,
    )

    gateway.deliver_file_row(db, row, cliente)

    assert len(cliente.ingests) == 1
    assert len(cliente.uploads) == 2
    assert row.attempts == 1
    assert row.status == STATUS_FAILED  # un 404 no es retryable


# ── Corte directo (delete_local) ─────────────────────────────────────────────

def test_al_confirmar_se_borra_la_copia_local(entorno):
    """«EFC es la fuente única» se cumple así: el objeto local se borra AL CONFIRMAR, no antes."""
    db, expediente, borrados = entorno["db"], entorno["expediente"], entorno["borrados"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, delete_local=True)

    gateway.deliver_file_row(db, row, _ClienteFalso())

    assert borrados == [S3_KEY]
    # La key local se limpia: dejarla apuntaría a un objeto que ya no existe y la descarga se
    # ramificaría por el camino equivocado.
    assert documento.file_key is None


def test_con_delete_local_en_false_no_se_borra_nada(entorno):
    db, expediente, borrados = entorno["db"], entorno["expediente"], entorno["borrados"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, delete_local=False)

    gateway.deliver_file_row(db, row, _ClienteFalso())

    assert borrados == []
    assert documento.file_key == S3_KEY
    assert row.status == STATUS_SENT


def test_si_el_borrado_local_falla_la_entrega_sigue_siendo_valida(entorno, monkeypatch):
    """El archivo YA está en EFC. No poder borrar la copia local no invalida la entrega, y volver a
    intentarla subiría el mismo documento otra vez."""
    db, expediente = entorno["db"], entorno["expediente"]
    import core.storage_s3 as storage

    def _revienta(key):
        raise RuntimeError("MinIO no responde")

    monkeypatch.setattr(storage, "delete_object_if_exists", _revienta)

    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, delete_local=True)

    gateway.deliver_file_row(db, row, _ClienteFalso())

    assert row.status == STATUS_SENT
    assert documento.efc_sync_state == "SYNCED"


def test_el_borrado_local_ocurre_ANTES_de_marcar_enviada_pero_no_antes_de_subir(entorno):
    """Nunca se borra el original antes de confirmar la subida: si se borrara primero y la subida
    fallara, el archivo se habría perdido."""
    db, expediente, borrados = entorno["db"], entorno["expediente"], entorno["borrados"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, delete_local=True)
    cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("500", status_code=500, retryable=True),
        falla_solo_la_primera=False,
    )

    gateway.deliver_file_row(db, row, cliente)

    assert borrados == []  # la subida falló: el original sigue ahí
    assert row.status == STATUS_PENDING
    assert documento.file_key == S3_KEY


# ── Fallos ───────────────────────────────────────────────────────────────────

def test_un_fallo_de_efc_no_propaga_y_se_refleja_en_el_documento(entorno):
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, attempts=MAX_ATTEMPTS - 1)
    cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("EFC caído", status_code=503, retryable=True),
        falla_solo_la_primera=False,
    )

    gateway.deliver_file_row(db, row, cliente)  # no lanza

    assert row.status == STATUS_FAILED
    assert documento.efc_sync_state == "FAILED"
    assert "EFC caído" in documento.efc_error_detail
    assert documento.efc_attempts == MAX_ATTEMPTS


def test_una_fila_ya_enviada_no_vuelve_a_subir_el_archivo(entorno):
    """Segunda guarda de idempotencia. Un re-despacho tras un timeout ambiguo no duplica."""
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento, status=STATUS_SENT)
    cliente = _ClienteFalso()

    gateway.deliver_file_row(db, row, cliente)

    assert cliente.uploads == []


def test_un_expediente_borrado_deja_la_fila_reintentable(entorno):
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)
    row.expediente_ref = 999999
    db.commit()

    gateway.deliver_file_row(db, row, _ClienteFalso())

    assert row.status == STATUS_PENDING  # retryable: el expediente puede reaparecer
    assert row.attempts == 1


def test_un_reintento_tras_timeout_manda_el_mismo_ref_y_no_duplica(entorno):
    """El ``crm_document_ref`` es estable entre reintentos: EFC devuelve 200 con el que ya existía.

    Es la tercera capa de idempotencia y la que cubre el timeout ambiguo —EFC commiteó y contestó
    tarde—, donde el CRM no puede saber si el documento entró.
    """
    db, expediente = entorno["db"], entorno["expediente"]
    documento = _documento_local(db, expediente)
    row = _fila(db, expediente, documento)

    primer_cliente = _ClienteFalso(
        upload_falla_con=EfcClientError("timeout", retryable=True), falla_solo_la_primera=False
    )
    gateway.deliver_file_row(db, row, primer_cliente)
    assert row.status == STATUS_PENDING

    segundo_cliente = _ClienteFalso()
    gateway.deliver_file_row(db, row, segundo_cliente)

    assert primer_cliente.uploads[0]["crm_document_ref"] == segundo_cliente.uploads[0]["crm_document_ref"]
    assert row.status == STATUS_SENT
