/**
 * E2E — Importación de mercadería del exterior.
 *
 * Complementa a `importacion-mercaderia-exterior.integration.spec.ts`, que prueba
 * las piezas por separado. Acá se recorre el circuito **por la puerta de entrada
 * real**: `ComprasService.create`, el mismo método que llama el controller cuando
 * el usuario guarda la factura en la pantalla de Compras.
 *
 * Existe para cerrar los tres huecos que el otro spec dejaba declarados:
 *
 *   1. `create` de punta a punta (el otro llamaba al helper privado).
 *   2. El guard de doble stock: una factura vinculada a un embarque NO puede
 *      descargar inventario al cargarse.
 *   3. El guard de doble contabilización en el cierre del despacho.
 *
 * Los dos guards son los invariantes más caros de equivocar del circuito, y
 * hasta ahora estaban escritos pero no ejercidos.
 *
 * Corre contra la base local. Crea lo suyo con prefijo `E2E`, lo borra en el
 * `afterAll` aunque falle, y purga restos de corridas muertas al arrancar.
 */
import { PrismaClient } from '@prisma/client';
import { ComprasService } from '../../compras/compras.service';
import { ImportacionesCosteoService } from '../services/importaciones-costeo.service';
import { ImportacionesService } from '../services/importaciones.service';

const prisma = new PrismaClient();
const TAG = `E2E${Date.now().toString(36).slice(-5)}`;

type Fixtures = {
  empresaId: string;
  depositoId: string;
  proveedorId: string;
  monedaId: string;
  condicionContadoId: string;
  medioPagoId: string;
  productoA: string;
  productoB: string;
  conceptoFobId: string;
};

let fx: Fixtures | null = null;
/** Config de compras original de la empresa, para restaurarla al terminar. */
let configOriginal: { afectar: boolean | null; oc: boolean | null } | null = null;
const creados = { embarques: [] as string[], compras: [] as string[] };

const noop = () => Promise.resolve(undefined);
const costeo = new ImportacionesCosteoService();

/** Lotes se stubea desactivado: su flujo tiene cobertura propia y acá sólo
 *  agregaría fixtures. Contabilidad es fire-and-forget en `create`. */
const lotesStub = {
  isEnabledForEmpresa: () => Promise.resolve(false),
  getEmpresaConfig: () => Promise.resolve({ compras_credito_requiere_cuotas: false }),
};
const compras = new ComprasService(
  prisma as any,
  lotesStub as any,
  { integrarFacturaCompra: noop, integrarCompra: noop } as any,
  { syncTesoreriaOrdenPago: noop } as any,
  costeo,
);
const importaciones = new ImportacionesService(
  prisma as any,
  costeo,
  { convertir: noop } as any,
  { integrarCierreDespacho: noop } as any,
  { presentar: noop } as any,
);

async function purgarRestos() {
  // Limpieza en SQL y en orden de dependencias. Una compra de contado arrastra
  // cuenta por pagar, orden de pago automatica y pago al proveedor; ir modelo por
  // modelo con Prisma es fragil (nombres que no coinciden, FK que aparecen
  // despues). El scope es siempre lo que crea esta suite: embarques 'E2E%' y
  // facturas '99%'.
  const sentencias = [
    `CREATE TEMP TABLE IF NOT EXISTS _e2e_emb AS
       SELECT id FROM imp_embarques WHERE numero LIKE 'E2E%'`,
    `CREATE TEMP TABLE IF NOT EXISTS _e2e_com AS
       SELECT id FROM compra_cab
        WHERE numero_factura LIKE '99%' OR embarque_id IN (SELECT id FROM _e2e_emb)`,
    `CREATE TEMP TABLE IF NOT EXISTS _e2e_cxp AS
       SELECT id FROM cuentas_pagar WHERE compra_id IN (SELECT id FROM _e2e_com)`,
    `CREATE TEMP TABLE IF NOT EXISTS _e2e_op AS
       SELECT id FROM orden_pago_proveedor_cab WHERE compra_id IN (SELECT id FROM _e2e_com)`,

    `DELETE FROM pagos_proveedor WHERE cuenta_pagar_id IN (SELECT id FROM _e2e_cxp)`,
    `DELETE FROM orden_pago_proveedor_det WHERE cuenta_pagar_id IN (SELECT id FROM _e2e_cxp)
        OR orden_pago_id IN (SELECT id FROM _e2e_op)`,
    `DELETE FROM orden_pago_proveedor_cab WHERE id IN (SELECT id FROM _e2e_op)`,
    `DELETE FROM rec_multi_gastos WHERE cuenta_pagar_id IN (SELECT id FROM _e2e_cxp)`,
    `UPDATE imp_componentes_costo SET cuenta_pagar_id = NULL
       WHERE cuenta_pagar_id IN (SELECT id FROM _e2e_cxp)`,
    `DELETE FROM cuentas_pagar WHERE id IN (SELECT id FROM _e2e_cxp)`,

    `DELETE FROM imp_componentes_costo WHERE compra_id IN (SELECT id FROM _e2e_com)
        OR embarque_id IN (SELECT id FROM _e2e_emb)`,
    `DELETE FROM lotes_producto WHERE compra_id IN (SELECT id FROM _e2e_com)`,
    `DELETE FROM compra_forma_pagos WHERE compra_id IN (SELECT id FROM _e2e_com)`,
    `DELETE FROM compra_subtotales WHERE compra_id IN (SELECT id FROM _e2e_com)`,
    `DELETE FROM compra_det WHERE compra_id IN (SELECT id FROM _e2e_com)`,
    `DELETE FROM movimientos_inventario WHERE documento_id IN (SELECT id FROM _e2e_com)
        OR (documento_origen = 'IMP' AND documento_id IN (SELECT id FROM _e2e_emb))`,
    `DELETE FROM compra_cab WHERE id IN (SELECT id FROM _e2e_com)`,

    `DELETE FROM imp_despachos WHERE embarque_id IN (SELECT id FROM _e2e_emb)`,
    `DELETE FROM imp_embarque_items WHERE embarque_id IN (SELECT id FROM _e2e_emb)`,
    `DELETE FROM imp_embarques_bitacora WHERE embarque_id IN (SELECT id FROM _e2e_emb)`,
    `DELETE FROM imp_embarques WHERE id IN (SELECT id FROM _e2e_emb)`,

    `DROP TABLE IF EXISTS _e2e_cxp`,
    `DROP TABLE IF EXISTS _e2e_op`,
    `DROP TABLE IF EXISTS _e2e_com`,
    `DROP TABLE IF EXISTS _e2e_emb`,
  ];
  for (const sql of sentencias) {
    await prisma.$executeRawUnsafe(sql);
  }
}

beforeAll(async () => {
  await purgarRestos();

  const candidata = await prisma.$queryRaw<Array<{ empresa_id: string }>>`
    SELECT e.id AS empresa_id
      FROM empresas e
     WHERE EXISTS (SELECT 1 FROM productos p WHERE p.empresa_id = e.id AND p.deleted = false AND p.maneja_inventario = true)
       AND EXISTS (SELECT 1 FROM depositos d WHERE d.empresa_id = e.id AND d.active = true)
       AND EXISTS (SELECT 1 FROM proveedores pr WHERE pr.empresa_id = e.id AND pr.activo = true)
     ORDER BY (SELECT count(*) FROM productos p WHERE p.empresa_id = e.id AND p.deleted = false) DESC
     LIMIT 1`;
  if (!candidata.length) return;

  const empresaId = candidata[0].empresa_id;
  const [deposito, proveedor, productos, conceptoFob, moneda, contado, efectivo] = await Promise.all([
    prisma.depositos.findFirst({
      // depositos.sucursal_id es NOT NULL en el modelo: todo deposito tiene sucursal.
      where: { empresa_id: empresaId, active: true },
      select: { id: true },
    }),
    prisma.proveedores.findFirst({ where: { empresa_id: empresaId, activo: true }, select: { id: true } }),
    // Se saltean los dos primeros a proposito: son los que usa la suite de
    // integracion. Jest corre los archivos en paralelo y las dos afirman deltas
    // exactos de stock sobre el mismo producto, asi que compartirlo las hace
    // fallar una de cada tantas corridas sin que nada este roto.
    prisma.productos.findMany({
      where: { empresa_id: empresaId, deleted: false, maneja_inventario: true },
      select: { id: true },
      skip: 2,
      take: 2,
    }),
    prisma.imp_conceptos_costo.findFirst({
      where: { codigo: 'FOB', OR: [{ empresa_id: empresaId }, { empresa_id: null }] },
      select: { id: true },
    }),
    prisma.moneda.findFirst({ where: { codigo: 'PYG' }, select: { id: true } }),
    prisma.condicion_operacion.findFirst({ where: { codigo: 1 }, select: { id: true } }),
    prisma.medio_pago.findFirst({ where: { codigo: 1 }, select: { id: true } }),
  ]);
  if (!deposito || !proveedor || productos.length < 2 || !conceptoFob || !moneda || !contado || !efectivo) return;

  // El guard de doble stock solo se puede medir si la empresa descarga stock al
  // facturar. La anfitriona ingresa por recepcion contra OC, asi que con su
  // config el control negativo daria igual que el caso positivo y no probaria
  // nada. Se fuerza aca y se restaura en el afterAll.
  const cfg = await prisma.config_compras.findUnique({
    where: { empresa_id: empresaId },
    select: { afectar_stock_en_compra_directa: true, habilitar_orden_compra: true },
  });
  configOriginal = cfg
    ? { afectar: cfg.afectar_stock_en_compra_directa, oc: cfg.habilitar_orden_compra }
    : null;
  await prisma.config_compras.upsert({
    where: { empresa_id: empresaId },
    update: { afectar_stock_en_compra_directa: true },
    create: {
      empresa_id: empresaId,
      habilitar_requisicion_compra: false,
      modo_flujo_compra: 'directo',
      afectar_stock_en_compra_directa: true,
      habilitar_orden_compra: true,
    },
  });

  fx = {
    empresaId,
    depositoId: deposito.id,
    proveedorId: proveedor.id,
    monedaId: moneda.id,
    condicionContadoId: contado.id,
    medioPagoId: efectivo.id,
    productoA: productos[0].id,
    productoB: productos[1].id,
    conceptoFobId: conceptoFob.id,
  };
});

afterAll(async () => {
  await purgarRestos();
  if (fx && configOriginal) {
    await prisma.config_compras.update({
      where: { empresa_id: fx.empresaId },
      data: {
        afectar_stock_en_compra_directa: configOriginal.afectar,
        habilitar_orden_compra: configOriginal.oc,
      },
    });
  }
  await prisma.$disconnect();
});

async function crearEmbarqueDespachado() {
  const f = fx as Fixtures;
  const emb = await prisma.imp_embarques.create({
    data: {
      empresa_id: f.empresaId,
      numero: `${TAG}-${Math.random().toString(36).slice(2, 6)}`,
      proveedor_id: f.proveedorId,
      perfil: 'CONSUMO_MASIVO',
      estado: 'DESPACHADO',
      moneda_id: f.monedaId,
    },
  });
  creados.embarques.push(emb.id);
  await prisma.imp_embarque_items.create({
    data: {
      empresa_id: f.empresaId,
      embarque_id: emb.id,
      perfil: 'CONSUMO_MASIVO',
      producto_id: f.productoA,
      cantidad: 20,
      // El FOB del ítem es la base del prorrateo de la factura del exterior.
      fob_unit_usd: 100,
      fob_total_usd: 2000,
      costo_unitario_gs: 0,
      descripcion_origen: 'Mercadería importada',
    },
  });
  return emb.id;
}

/** DTO equivalente a lo que manda la pantalla de Compras al guardar. */
function dtoFactura(numero: string, embarqueId?: string) {
  const f = fx as Fixtures;
  return {
    cabecera: {
      proveedor_id: f.proveedorId,
      deposito_id: f.depositoId,
      moneda_id: f.monedaId,
      cotizacion: 1,
      condicion_operacion_id: f.condicionContadoId,
      fecha_emision: '2032-03-10',
      timbrado_proveedor: '12345678',
      establecimiento: '001',
      punto_expedicion: '001',
      numero_factura: numero,
      es_preimpreso: true,
      ...(embarqueId ? { embarque_id: embarqueId } : {}),
    },
    items: [
      {
        producto_id: f.productoA,
        descripcion: 'Mercadería importada',
        cantidad: 20,
        precio_unitario: 100_000,
        porcentaje_iva: 0,
        afecta_stock: true,
      },
    ],
    pagos: [{ medio_pago_id: f.medioPagoId, monto: 2_000_000 }],
  } as any;
}

const hay = () => fx !== null;
const saltear = (n: string) => {
  // eslint-disable-next-line no-console
  console.warn(`[E2E] Salteado "${n}": la base local no tiene los prerrequisitos.`);
};

describe('E2E — la factura del exterior entra por ComprasService.create', () => {
  it('crea la compra, genera el costo FOB y NO descarga stock', async () => {
    if (!hay()) return saltear('factura del exterior');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    const stockAntes = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    const previo = Number(stockAntes?.cantidad_disponible ?? 0);

    const res: any = await compras.create(dtoFactura('9900001', embarqueId), f.empresaId, undefined);
    const compraId = res?.id ?? res?.compra?.id ?? res?.compraId;
    expect(compraId).toBeTruthy();
    creados.compras.push(compraId);

    // 1) La compra quedó vinculada al embarque.
    const compra = await prisma.compra_cab.findUnique({
      where: { id: compraId },
      select: { embarque_id: true, total: true },
    });
    expect(compra?.embarque_id).toBe(embarqueId);

    // 2) Nació el componente de costo FOB atado a esa compra.
    const comp = await prisma.imp_componentes_costo.findFirst({
      where: { embarque_id: embarqueId, compra_id: compraId },
      select: { concepto_id: true, importe_gs: true, es_directo: true, criterio_prorrateo: true },
    });
    expect(comp).toBeTruthy();
    expect(comp?.concepto_id).toBe(f.conceptoFobId);
    expect(Number(comp?.importe_gs)).toBe(2_000_000);
    // Compartido por FOB: un directo sin item_id no llegaria a ningun item.
    expect(comp?.es_directo).toBe(false);
    expect(comp?.criterio_prorrateo).toBe('FOB');

    // 3) GUARD DE DOBLE STOCK — lo que este test existe para probar.
    //    La mercadería entra al liberar, no al facturar.
    const movs = await prisma.movimientos_inventario.count({ where: { documento_id: compraId } });
    expect(movs).toBe(0);

    const stockDespues = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    expect(Number(stockDespues?.cantidad_disponible ?? 0)).toBe(previo);
  });

  it('CONTROL NEGATIVO: la misma compra sin embarque SÍ descarga stock', async () => {
    if (!hay()) return saltear('control negativo');
    const f = fx as Fixtures;

    const antes = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    const previo = Number(antes?.cantidad_disponible ?? 0);

    const res: any = await compras.create(dtoFactura('9900002'), f.empresaId, undefined);
    const compraId = res?.id ?? res?.compra?.id ?? res?.compraId;
    creados.compras.push(compraId);

    // Sin esto, el test anterior no probaría nada: el stock podría no moverse
    // por cualquier otro motivo (config, flujo, ítem sin afecta_stock).
    const despues = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    expect(Number(despues?.cantidad_disponible ?? 0)).toBe(previo + 20);
  });
});

describe('E2E — el circuito completo: factura → despacho → liberación', () => {
  it('el stock entra UNA sola vez y con el costo prorrateado completo', async () => {
    if (!hay()) return saltear('circuito completo');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    // 1) Factura del exterior: Gs. 2.000.000 por 20 unidades.
    const res: any = await compras.create(dtoFactura('9900003', embarqueId), f.empresaId, undefined);
    const compraId = res?.id ?? res?.compra?.id ?? res?.compraId;
    creados.compras.push(compraId);

    // 2) Despacho con tributos, que también son costo del embarque.
    await prisma.imp_despachos.create({
      data: {
        embarque_id: embarqueId,
        empresa_id: f.empresaId,
        numero_despacho: `${TAG}-D`,
        fecha_despacho: new Date(Date.UTC(2032, 2, 20)),
        arancel_gs: 400_000,
        iva_importacion_gs: 240_000,
        total_tributos_gs: 640_000,
        estado: 'CONFIRMADO',
      },
    });
    await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId,
        embarque_id: embarqueId,
        concepto_id: f.conceptoFobId,
        descripcion: 'Arancel',
        importe: 400_000,
        cotizacion: 1,
        importe_gs: 400_000,
        es_directo: false,
      },
    });
    await costeo.recalcular(prisma as any, f.empresaId, embarqueId);

    // El costo del ítem ya es FOB + arancel = 2.400.000 / 20 = 120.000 c/u.
    const item = await prisma.imp_embarque_items.findFirst({
      where: { embarque_id: embarqueId },
      select: { costo_unitario_gs: true, costo_total_gs: true },
    });
    expect(Number(item?.costo_total_gs)).toBe(2_400_000);
    expect(Number(item?.costo_unitario_gs)).toBe(120_000);

    const antes = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    const previo = Number(antes?.cantidad_disponible ?? 0);

    // 3) Liberación.
    await importaciones.liberarEmbarque(
      embarqueId,
      { deposito_destino_id: f.depositoId } as any,
      f.empresaId,
      undefined,
    );

    const movs = await prisma.movimientos_inventario.findMany({
      where: { documento_origen: 'IMP', documento_id: embarqueId },
      select: { cantidad: true, costo_unitario: true },
    });
    // UNA sola entrada por las 20 unidades: ni la factura ni la liberación duplican.
    expect(movs).toHaveLength(1);
    expect(Number(movs[0].cantidad)).toBe(20);
    // Y valorizada con el costo COMPLETO, no con el FOB pelado (100.000).
    expect(Number(movs[0].costo_unitario)).toBe(120_000);

    const despues = await prisma.stock_deposito.findUnique({
      where: { deposito_id_producto_id: { deposito_id: f.depositoId, producto_id: f.productoA } },
      select: { cantidad_disponible: true },
    });
    expect(Number(despues?.cantidad_disponible ?? 0)).toBe(previo + 20);
  });
});

describe('E2E — guard de doble contabilización del cierre de despacho', () => {
  it('el cierre toma los tributos pero deja afuera el costo que ya facturó la compra', async () => {
    if (!hay()) return saltear('guard contable');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    const res: any = await compras.create(dtoFactura('9900004', embarqueId), f.empresaId, undefined);
    const compraId = res?.id ?? res?.compra?.id ?? res?.compraId;
    creados.compras.push(compraId);

    await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId,
        embarque_id: embarqueId,
        concepto_id: f.conceptoFobId,
        descripcion: 'Arancel del despacho',
        importe: 500_000,
        cotizacion: 1,
        importe_gs: 500_000,
      },
    });

    // Misma selección que hace integrarCierreDespacho para armar el asiento.
    // Se ejerce el filtro, no el asiento: generar el asiento pide el módulo de
    // contabilidad entero (mapeo, períodos, plan de cuentas), que tiene su
    // propia suite. Lo que acá puede romperse es el filtro, y es lo que se mide.
    const delCierre = await prisma.imp_componentes_costo.findMany({
      where: { embarque_id: embarqueId, empresa_id: f.empresaId, activo: true, gasto_id: null, compra_id: null },
      select: { importe_gs: true, descripcion: true },
    });
    const totalCierre = delCierre.reduce((a, c) => a + Number(c.importe_gs), 0);

    // El arancel entra (no tiene comprobante propio).
    expect(delCierre).toHaveLength(1);
    expect(totalCierre).toBe(500_000);

    // Y los 2.000.000 de la factura NO: ya los contabilizó `integrarCompra`.
    // Sin el filtro por compra_id, el total del asiento seria 2.500.000.
    const todos = await prisma.imp_componentes_costo.findMany({
      where: { embarque_id: embarqueId, activo: true },
      select: { importe_gs: true },
    });
    expect(todos.reduce((a, c) => a + Number(c.importe_gs), 0)).toBe(2_500_000);
  });
});
