/**
 * QA de integración — Importación de mercadería del exterior.
 *
 * Corre contra la base LOCAL de desarrollo, no contra mocks: lo que se quiere
 * verificar son invariantes que dependen de consultas reales (que el stock se
 * mueva una sola vez, que la factura del exterior no entre al Libro IVA, que el
 * costo no se contabilice dos veces). Con Prisma mockeado ninguna de esas
 * afirmaciones probaría nada.
 *
 * Crea sus propios datos, valida y los borra en el `afterAll`, incluso si un
 * test falla. No toca nada preexistente de la empresa que usa como anfitriona.
 *
 * Si la base local no tiene los prerrequisitos (una empresa con productos,
 * depósito y proveedor), la suite se saltea con un mensaje en vez de fallar:
 * un entorno sin datos no es un defecto del código.
 *
 * Ver docs/qa-importacion-mercaderia-exterior.md para el detalle de cada caso.
 */
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();

/** Marca de los registros creados acá, para poder limpiarlos sin ambigüedad.
 *  Corta a proposito: imp_embarques.numero es VarChar(20). */
const TAG = `QA${Date.now().toString(36).slice(-6)}`;

type Fixtures = {
  empresaId: string;
  depositoId: string;
  proveedorId: string;
  monedaUsdId: string | null;
  productoA: string;
  productoB: string;
  conceptoFobId: string;
};

let fx: Fixtures | null = null;
const creados = {
  embarques: [] as string[],
  compras: [] as string[],
  movimientos: [] as string[],
};

/** Los servicios reciben la transacción por parámetro, así que alcanza con
 *  instanciarlos a mano. Lo que no se ejerce acá (contabilidad, lotes, pagos) se
 *  stubea: tiene su propia cobertura y arrastraría medio AppModule. */
const noop = () => Promise.resolve(undefined);
const costeo = new ImportacionesCosteoService();
const importaciones = new ImportacionesService(
  prisma as any,
  costeo,
  { convertir: noop } as any,
  { integrarCierreDespacho: noop } as any,
  { presentar: noop } as any,
);
const compras = new ComprasService(
  prisma as any,
  {} as any,
  { integrarCompra: noop } as any,
  {} as any,
  costeo,
);

/**
 * Barre restos de corridas anteriores que hayan muerto antes del `afterAll`.
 *
 * Sin esto la suite es frágil de una forma engañosa: los asserts que cuentan
 * filas del período (p.ej. "un solo despacho") empiezan a fallar por basura
 * acumulada y no por el código, que fue exactamente lo que pasó la primera vez
 * que se corrió. Se limita a lo que crea esta suite: numero LIKE 'QA%'.
 */
async function purgarRestos() {
  const viejos = await prisma.imp_embarques.findMany({
    where: { numero: { startsWith: 'QA' } },
    select: { id: true },
  });
  if (!viejos.length) return;
  const ids = viejos.map((e) => e.id);
  const comprasViejas = await prisma.compra_cab.findMany({
    where: { embarque_id: { in: ids } },
    select: { id: true },
  });
  const compraIds = comprasViejas.map((c) => c.id);

  await prisma.movimientos_inventario.deleteMany({
    where: { documento_origen: 'IMP', documento_id: { in: ids } },
  });
  if (compraIds.length) {
    await prisma.imp_componentes_costo.deleteMany({ where: { compra_id: { in: compraIds } } });
    await prisma.compra_subtotales.deleteMany({ where: { compra_id: { in: compraIds } } });
    await prisma.cuentas_pagar.deleteMany({ where: { compra_id: { in: compraIds } } });
    await prisma.compra_cab.deleteMany({ where: { id: { in: compraIds } } });
  }
  await prisma.imp_despachos.deleteMany({ where: { embarque_id: { in: ids } } });
  await prisma.imp_embarques.deleteMany({ where: { id: { in: ids } } });
}

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

  // Empresa anfitriona: la que más productos activos tenga con depósito y
  // proveedor. No se fija por UUID para que la suite sobreviva a un reseed.
  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, usd] = await Promise.all([
    prisma.depositos.findFirst({ where: { empresa_id: empresaId, active: true }, select: { id: true } }),
    prisma.proveedores.findFirst({ where: { empresa_id: empresaId, activo: true }, select: { id: true } }),
    prisma.productos.findMany({
      where: { empresa_id: empresaId, deleted: false, maneja_inventario: true },
      select: { id: true },
      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: 'USD' }, select: { id: true } }),
  ]);

  if (!deposito || !proveedor || productos.length < 2 || !conceptoFob) return;

  fx = {
    empresaId,
    depositoId: deposito.id,
    proveedorId: proveedor.id,
    monedaUsdId: usd?.id ?? null,
    productoA: productos[0].id,
    productoB: productos[1].id,
    conceptoFobId: conceptoFob.id,
  };
});

afterAll(async () => {
  // Orden inverso a las FK. Se borra siempre, incluso si un test fallo antes.
  if (creados.movimientos.length) {
    await prisma.movimientos_inventario.deleteMany({ where: { id: { in: creados.movimientos } } });
  }
  if (creados.compras.length) {
    await prisma.imp_componentes_costo.deleteMany({ where: { compra_id: { in: creados.compras } } });
    await prisma.compra_det.deleteMany({ where: { compra_id: { in: creados.compras } } });
    await prisma.compra_subtotales.deleteMany({ where: { compra_id: { in: creados.compras } } });
    await prisma.cuentas_pagar.deleteMany({ where: { compra_id: { in: creados.compras } } });
    await prisma.compra_cab.deleteMany({ where: { id: { in: creados.compras } } });
  }
  if (creados.embarques.length) {
    // imp_* cuelgan del embarque con onDelete: Cascade.
    await prisma.movimientos_inventario.deleteMany({
      where: { documento_origen: 'IMP', documento_id: { in: creados.embarques } },
    });
    await prisma.imp_despachos.deleteMany({ where: { embarque_id: { in: creados.embarques } } });
    await prisma.imp_embarques.deleteMany({ where: { id: { in: creados.embarques } } });
  }
  await prisma.$disconnect();
});

/** Embarque CONSUMO_MASIVO en DESPACHADO, con dos ítems ya costeados. */
async function crearEmbarqueDespachado(opts?: { itemsSinProducto?: boolean; cantidadCero?: boolean }) {
  const f = fx as Fixtures;
  const embarque = await prisma.imp_embarques.create({
    data: {
      empresa_id: f.empresaId,
      numero: `${TAG}-${Math.random().toString(36).slice(2, 7)}`,
      proveedor_id: f.proveedorId,
      perfil: 'CONSUMO_MASIVO',
      estado: 'DESPACHADO',
      moneda_id: f.monedaUsdId,
    },
  });
  creados.embarques.push(embarque.id);

  await prisma.imp_embarque_items.createMany({
    data: [
      {
        empresa_id: f.empresaId,
        embarque_id: embarque.id,
        perfil: 'CONSUMO_MASIVO',
        producto_id: opts?.itemsSinProducto ? null : f.productoA,
        cantidad: opts?.cantidadCero ? 0 : 10,
        // El FOB del ítem es la base del prorrateo por defecto.
        fob_unit_usd: 10,
        fob_total_usd: 100,
        costo_unitario_gs: 15000,
        descripcion_origen: 'Item A',
        sku_proveedor: 'SKU-A',
      },
      {
        empresa_id: f.empresaId,
        embarque_id: embarque.id,
        perfil: 'CONSUMO_MASIVO',
        producto_id: f.productoB,
        cantidad: 4,
        fob_unit_usd: 20,
        fob_total_usd: 80,
        costo_unitario_gs: 25000,
        descripcion_origen: 'Item B',
        sku_proveedor: 'SKU-B',
      },
    ],
  });
  return embarque.id;
}

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

describe('QA — Fase 1: la mercadería general entra al stock al liberar', () => {
  it('crea stock y movimiento de inventario por cada ítem, con el costo prorrateado', async () => {
    if (!hayEntorno()) return saltear('stock al liberar');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

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

    const res = await importaciones.liberarEmbarque(
      embarqueId,
      { deposito_destino_id: f.depositoId } as any,
      f.empresaId,
      undefined,
    );

    expect(res.items_actualizados).toBe(2);

    const movs = await prisma.movimientos_inventario.findMany({
      where: { documento_origen: 'IMP', documento_id: embarqueId },
      select: { id: true, producto_id: true, cantidad: true, costo_unitario: true, deposito_origen_id: true },
    });
    creados.movimientos.push(...movs.map((m) => m.id));

    // Invariante central: un movimiento por ítem, ni más ni menos.
    expect(movs).toHaveLength(2);
    expect(movs.every((m) => m.deposito_origen_id === f.depositoId)).toBe(true);

    const movA = movs.find((m) => m.producto_id === f.productoA);
    expect(Number(movA?.cantidad)).toBe(10);
    // El costo que viaja al kardex es el prorrateado, no el FOB pelado.
    expect(Number(movA?.costo_unitario)).toBe(15000);

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

    const emb = await prisma.imp_embarques.findUnique({ where: { id: embarqueId }, select: { estado: true } });
    expect(emb?.estado).toBe('LIBERADO');

    const items = await prisma.imp_embarque_items.findMany({
      where: { embarque_id: embarqueId },
      select: { estado: true, producto_id: true },
    });
    expect(items.every((i) => i.estado === 'DISPONIBLE' && i.producto_id)).toBe(true);
  });

  it('rechaza liberar si algún ítem no tiene producto del catálogo, sin dejar stock a medias', async () => {
    if (!hayEntorno()) return saltear('validación sin producto');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado({ itemsSinProducto: true });

    await expect(
      importaciones.liberarEmbarque(embarqueId, { deposito_destino_id: f.depositoId } as any, f.empresaId, undefined),
    ).rejects.toThrow(/no tienen un producto del catálogo/i);

    // Lo importante no es el error sino que no haya escrito NADA: el ítem B era
    // válido y no debe haber entrado solo.
    const movs = await prisma.movimientos_inventario.count({
      where: { documento_origen: 'IMP', documento_id: embarqueId },
    });
    expect(movs).toBe(0);
    const emb = await prisma.imp_embarques.findUnique({ where: { id: embarqueId }, select: { estado: true } });
    expect(emb?.estado).toBe('DESPACHADO');
  });

  it('rechaza liberar si algún ítem tiene cantidad en cero', async () => {
    if (!hayEntorno()) return saltear('validación cantidad cero');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado({ cantidadCero: true });

    await expect(
      importaciones.liberarEmbarque(embarqueId, { deposito_destino_id: f.depositoId } as any, f.empresaId, undefined),
    ).rejects.toThrow(/cantidad en cero/i);

    const movs = await prisma.movimientos_inventario.count({
      where: { documento_origen: 'IMP', documento_id: embarqueId },
    });
    expect(movs).toBe(0);
  });
});

describe('QA — Fase 3: la cotización con la que se valoriza es configurable', () => {
  const resolver = (empresaId: string, embarqueId: string, cotizacionFactura: number) =>
    (compras as any).resolverCotizacionImportacion(prisma, empresaId, embarqueId, cotizacionFactura);

  it('con criterio FACTURA usa la cotización de la factura', async () => {
    if (!hayEntorno()) return saltear('cotización FACTURA');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();
    await prisma.imp_config_empresa.updateMany({
      where: { empresa_id: f.empresaId },
      data: { criterio_cotizacion: 'FACTURA' },
    });
    await expect(resolver(f.empresaId, embarqueId, 7800)).resolves.toBe(7800);
  });

  it('con criterio CIERRE_EMBARQUE usa la del embarque y cae a la factura si no está cargada', async () => {
    if (!hayEntorno()) return saltear('cotización CIERRE_EMBARQUE');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();
    await prisma.imp_config_empresa.updateMany({
      where: { empresa_id: f.empresaId },
      data: { criterio_cotizacion: 'CIERRE_EMBARQUE' },
    });

    // Sin cotizacion_cierre todavía: no puede quedar en 1, tiene que caer a la factura.
    await expect(resolver(f.empresaId, embarqueId, 7800)).resolves.toBe(7800);

    await prisma.imp_embarques.update({ where: { id: embarqueId }, data: { cotizacion_cierre: 7350 } });
    await expect(resolver(f.empresaId, embarqueId, 7800)).resolves.toBe(7350);

    await prisma.imp_config_empresa.updateMany({
      where: { empresa_id: f.empresaId },
      data: { criterio_cotizacion: 'FACTURA' },
    });
  });
});

describe('QA — Fase 2 y 3: qué entra y qué no entra al Libro IVA', () => {
  it('el despacho aporta su IVA importación y la factura del exterior queda afuera', async () => {
    if (!hayEntorno()) return saltear('libro IVA');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    const fecha = new Date(Date.UTC(2031, 4, 15)); // mayo 2031: período sin datos reales
    await prisma.imp_despachos.create({
      data: {
        embarque_id: embarqueId,
        empresa_id: f.empresaId,
        numero_despacho: `${TAG}-DESP`,
        fecha_despacho: fecha,
        iva_importacion_gs: 3_000_000,
        total_tributos_gs: 3_000_000,
        estado: 'CONFIRMADO',
      },
    });

    const libro: any = await compras.getLibroIvaCompras(f.empresaId, 5, 2031, undefined, 'preimpresos', undefined);

    expect(libro.resumen.despachos_importacion.cantidad).toBe(1);
    expect(libro.resumen.despachos_importacion.iva_10).toBe(3_000_000);
    // La base se deriva de la tasa: 10 veces el IVA.
    expect(libro.resumen.despachos_importacion.gravada_10).toBe(30_000_000);
    // Y suma al crédito fiscal del período, que es el objetivo de la fase 2.
    expect(libro.resumen.credito_fiscal.iva_10).toBe(3_000_000);

    const fila = libro.detalle.find((d: any) => d.tipo === 'DESPACHO_IMPORTACION');
    expect(fila).toBeDefined();
    expect(fila.numero).toBe(`${TAG}-DESP`);

    // En el listado de electrónicos no tiene sentido: no es un documento que la
    // DNIT ya tenga por SIFEN.
    const soloElectronicos: any = await compras.getLibroIvaCompras(
      f.empresaId, 5, 2031, undefined, 'electronicos', undefined,
    );
    expect(soloElectronicos.resumen.despachos_importacion.cantidad).toBe(0);
  });

  it('una compra vinculada a un embarque no figura en el libro', async () => {
    if (!hayEntorno()) return saltear('compra del exterior fuera del libro');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    const compra = await prisma.compra_cab.create({
      data: {
        empresa_id: f.empresaId,
        proveedor_id: f.proveedorId,
        fecha_emision: new Date(Date.UTC(2031, 4, 20)),
        numero_factura: '0000001',
        establecimiento: '001',
        punto_expedicion: '001',
        total: 50_000_000,
        total_gravado_10: 0,
        total_iva_10: 0,
        total_exento: 50_000_000,
        es_preimpreso: true,
        anulado: false,
        embarque_id: embarqueId,
      },
    });
    creados.compras.push(compra.id);

    const libro: any = await compras.getLibroIvaCompras(f.empresaId, 5, 2031, undefined, 'todos', undefined);
    const filas = libro.detalle.filter((d: any) => d.numero === '001-001-0000001');
    expect(filas).toHaveLength(0);

    // Control negativo: la misma compra SIN embarque sí entra. Sin esto, el test
    // anterior pasaría igual si el filtro estuviera roto y no trajera nada.
    await prisma.compra_cab.update({ where: { id: compra.id }, data: { embarque_id: null } });
    const libroSinVinculo: any = await compras.getLibroIvaCompras(f.empresaId, 5, 2031, undefined, 'todos', undefined);
    expect(libroSinVinculo.detalle.filter((d: any) => d.numero === '001-001-0000001')).toHaveLength(1);
  });
});

describe('QA — Fase 3: el componente de costo que nace de la factura', () => {
  it('crea el FOB como costo directo y lo ata a la compra', async () => {
    if (!hayEntorno()) return saltear('componente FOB');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();
    const compra = await prisma.compra_cab.create({
      data: {
        empresa_id: f.empresaId,
        proveedor_id: f.proveedorId,
        fecha_emision: new Date(),
        numero_factura: '0000002',
        total: 0,
        es_preimpreso: true,
        anulado: false,
        embarque_id: embarqueId,
      },
    });
    creados.compras.push(compra.id);

    await (compras as any).crearComponenteImportacion(prisma, {
      compraId: compra.id,
      empresaId: f.empresaId,
      embarqueId,
      proveedorId: f.proveedorId,
      monedaId: f.monedaUsdId,
      numeroFactura: 'WIN-0001',
      importe: 10_000,
      cotizacionFactura: 7_500,
      fechaDevengo: new Date(),
    });

    const comp = await prisma.imp_componentes_costo.findFirst({
      where: { embarque_id: embarqueId, compra_id: compra.id },
    });
    expect(comp).toBeTruthy();
    expect(comp?.concepto_id).toBe(f.conceptoFobId);
    expect(Number(comp?.importe)).toBe(10_000);
    expect(Number(comp?.cotizacion)).toBe(7_500);
    expect(Number(comp?.importe_gs)).toBe(75_000_000);
    // Compartido y prorrateado por FOB: un componente directo sin `item_id` no
    // llega a ningún ítem y el costo se perdería en silencio.
    expect(comp?.es_directo).toBe(false);
    expect(comp?.criterio_prorrateo).toBe('FOB');
  });

  it('convertir un costo cargado a mano lo reusa en vez de duplicarlo', async () => {
    if (!hayEntorno()) return saltear('conversión de costo');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();

    const manual = await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId,
        embarque_id: embarqueId,
        concepto_id: f.conceptoFobId,
        importe: 9_000,
        cotizacion: 1,
        importe_gs: 9_000,
        es_directo: true,
      },
    });

    const compra = await prisma.compra_cab.create({
      data: {
        empresa_id: f.empresaId,
        proveedor_id: f.proveedorId,
        fecha_emision: new Date(),
        numero_factura: '0000003',
        total: 0,
        es_preimpreso: true,
        anulado: false,
        embarque_id: embarqueId,
      },
    });
    creados.compras.push(compra.id);

    await (compras as any).crearComponenteImportacion(prisma, {
      compraId: compra.id,
      empresaId: f.empresaId,
      embarqueId,
      convertirComponenteId: manual.id,
      proveedorId: f.proveedorId,
      monedaId: f.monedaUsdId,
      numeroFactura: 'WIN-0002',
      importe: 12_000,
      cotizacionFactura: 7_000,
      fechaDevengo: new Date(),
    });

    // Invariante: sigue habiendo UNO solo. Duplicarlo inflaría el costeo.
    const todos = await prisma.imp_componentes_costo.findMany({ where: { embarque_id: embarqueId } });
    expect(todos).toHaveLength(1);
    expect(todos[0].id).toBe(manual.id);
    expect(todos[0].compra_id).toBe(compra.id);
    // El importe pasa a ser el de la factura, que es el dato con respaldo.
    expect(Number(todos[0].importe)).toBe(12_000);
    expect(Number(todos[0].importe_gs)).toBe(84_000_000);
  });

  it('no deja pisar un costo que ya tiene comprobante', async () => {
    if (!hayEntorno()) return saltear('doble conversión');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();
    const compra = await prisma.compra_cab.create({
      data: {
        empresa_id: f.empresaId,
        proveedor_id: f.proveedorId,
        fecha_emision: new Date(),
        numero_factura: '0000004',
        total: 0,
        es_preimpreso: true,
        anulado: false,
        embarque_id: embarqueId,
      },
    });
    creados.compras.push(compra.id);

    const yaVinculado = await prisma.imp_componentes_costo.create({
      data: {
        empresa_id: f.empresaId,
        embarque_id: embarqueId,
        concepto_id: f.conceptoFobId,
        importe: 1,
        cotizacion: 1,
        importe_gs: 1,
        compra_id: compra.id,
      },
    });

    await expect(
      (compras as any).crearComponenteImportacion(prisma, {
        compraId: compra.id,
        empresaId: f.empresaId,
        embarqueId,
        convertirComponenteId: yaVinculado.id,
        proveedorId: f.proveedorId,
        monedaId: f.monedaUsdId,
        importe: 5,
        cotizacionFactura: 1,
        fechaDevengo: new Date(),
      }),
    ).rejects.toThrow(/ya tiene un comprobante/i);
  });

  it('no deja imputar una factura a un embarque cerrado', async () => {
    if (!hayEntorno()) return saltear('embarque cerrado');
    const f = fx as Fixtures;
    const embarqueId = await crearEmbarqueDespachado();
    await prisma.imp_embarques.update({ where: { id: embarqueId }, data: { estado: 'CERRADO' } });

    await expect(
      (compras as any).crearComponenteImportacion(prisma, {
        compraId: null,
        empresaId: f.empresaId,
        embarqueId,
        proveedorId: f.proveedorId,
        monedaId: f.monedaUsdId,
        importe: 100,
        cotizacionFactura: 1,
        fechaDevengo: new Date(),
      }),
    ).rejects.toThrow(/CERRADO/i);
  });
});
