import { RendicionViaticosContabilidadService } from '../../rendicion-viaticos/rendicion-viaticos-contabilidad.service';
import { FaltaMapeoError } from './falta-mapeo.error';
import { conResolvedor, crearIntegracionMock as crear } from './__helpers__/integracion-mock';
import { DIMENSIONES_POR_ORIGEN } from './reglas-mapeo';

// Reglas por dimensión en cobros, pagos, tesorería, RRHH, recibos y comisiones
// (spec 2026-10-13 "reglas de cuenta por dimensión", §5.1): cada integrar* resuelve
// sus cuentas con el resolvedor del documento y deja la traza en cada línea. Sin
// reglas, el asiento es el de siempre.

const lineas = (s: any) => s.asientos.crearConfirmado.mock.calls[0][3].lineas;
const dimensiones = (s: any) => s.mapeo.crearResolvedor.mock.calls[0][1].dimensiones;
const publicadas = (s: any) => Object.keys(dimensiones(s)).sort();
const tabla = (origen: string) => [...DIMENSIONES_POR_ORIGEN[origen]].sort();
const linea = (s: any, desc: string) => lineas(s).find((l: any) => l.descripcion === desc);
const traza = (s: any, desc: string) => {
  const l = linea(s, desc);
  return [l.cuenta_id, l.mapeo_concepto, l.mapeo_nivel, l.mapeo_regla_id];
};
const regla = (concepto: string, cuenta_id: string, dimension: string, valores: string[], id = 'r1') => ({
  id, concepto, orden: 1, modo: 'CUENTA', cuenta_id, estado: 'ACTIVA',
  vigente_desde: new Date('2026-01-01'), vigente_hasta: null, condiciones: [{ dimension, operador: 'EN', valores }],
}) as any;

// ── Recibo multi (armado de asientos-cobros-pagos.spec.ts) ─────────────────
const recibo = (over: any = {}) => ({
  id: 'rec-1', estado: 'emitido', modo: 'MULTI', numero_recibo: '001-001-0000001', fecha_emision: new Date('2026-10-08T12:00:00Z'),
  usuario_id: null, sucursal_id: 'suc-1', cliente_id: 'cli-1', moneda: { codigo: 'PYG' }, cotizacion: null, monto_total: 0,
  total_facturas: 0, total_intereses: 0, total_facturas_compra: 0, diferencia: 0, diferencia_tipo: 'EXACTO',
  facturas_multi: [], nc_aplicadas_multi: [], retenciones_multi: [], medios_pago_multi: [], facturas_compra_aplicadas: [], gastos_asociados: [],
  ...over,
});
const baseRecibo = (row: any, extra: any = {}) => ({
  recibos_cobro: { findUnique: jest.fn().mockResolvedValue(row) },
  tes_cuentas: { findMany: jest.fn().mockResolvedValue(extra.tesCuentas ?? []) },
  factura_cab: { findMany: jest.fn().mockResolvedValue(extra.facturas ?? []) },
  clientes: { findUnique: jest.fn().mockResolvedValue({ tipo_cliente: 'mayorista' }) },
});

describe('recibo multi', () => {
  it('en USD: regla MONEDA en ANTICIPOS_CLIENTES cambia solo el anticipo; el resto DEFECTO, MEDIO_PAGO o FICHA', async () => {
    // 100 USD facturados a 7.400 y cobrados a 7.400 (sin diferencia de cambio).
    const s = crear(
      baseRecibo(
        recibo({
          moneda: { codigo: 'USD' }, cotizacion: 7_400, total_facturas: 100, monto_total: 100,
          facturas_multi: [{ factura_cab_id: 'fac-1', monto_pagado: 100, monto_nc: 0 }],
          medios_pago_multi: [
            { medio: 'EFECTIVO', monto: 20 },
            { medio: 'TRANSFERENCIA', monto: 20 },
            { medio: 'TRANSFERENCIA', monto: 20, cuenta_tesoreria_id: 'tes-usd' },
            { medio: 'SALDO_FAVOR', monto: 40 },
          ],
        }),
        { facturas: [{ id: 'fac-1', dticam: 7_400 }], tesCuentas: [{ id: 'tes-usd', cuenta_contable_id: 'cta-banco-usd' }] },
      ),
      [], {}, [regla('ANTICIPOS_CLIENTES', 'cta-ANT_USD', 'MONEDA', ['USD'])],
    );
    s.mediosPago.resolverPorCodigoMedio.mockResolvedValue(new Map([[5, 'cta-recaudacion']]));

    await s.integrarReciboMulti('rec-1', 'emp');

    expect(traza(s, 'Anticipo cliente consumido')).toEqual(['cta-ANT_USD', 'ANTICIPOS_CLIENTES', 'REGLA', 'r1']);
    expect(traza(s, 'Cancelación CxC')).toEqual(['cta-CLIENTES', 'CLIENTES', 'DEFECTO', undefined]);
    const cobro = lineas(s).filter((l: any) => l.descripcion === 'Cobro recibo');
    expect(cobro.map((l: any) => [l.cuenta_id, l.mapeo_concepto, l.mapeo_nivel])).toEqual([
      ['cta-CAJA_GENERAL', 'CAJA_GENERAL', 'DEFECTO'],
      ['cta-recaudacion', undefined, 'MEDIO_PAGO'],
      ['cta-banco-usd', 'FICHA_CUENTA_TESORERIA', 'FICHA'],
    ]);
    expect(lineas(s).some((l: any) => 'concepto' in l)).toBe(false);
  });

  it('con retención y cheque sin mapear, y una regla activa en otro concepto, avisa los dos faltantes juntos', async () => {
    const s = crear(
      baseRecibo(
        recibo({
          total_facturas: 1_100_000, monto_total: 1_000_000,
          retenciones_multi: [{ tipo: 'IVA', monto: 100_000 }],
          medios_pago_multi: [{ medio: 'CHEQUE', monto: 1_000_000, cheque_fecha_vencimiento: new Date('2026-10-08') }],
        }),
      ),
      ['CHEQUES_EN_CARTERA', 'RETENCION_IVA_FAVOR'], {}, [regla('CLIENTES', 'cta-CLI_PYG', 'MONEDA', ['PYG'])],
    );

    await s.integrarReciboMulti('rec-1', 'emp');

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    const conceptos = s.avisosMapeo.registrarFaltaMapeo.mock.calls[0][0].conceptos;
    expect(conceptos).toEqual(expect.arrayContaining(['CHEQUES_EN_CARTERA', 'RETENCION_IVA_FAVOR']));
    expect(conceptos).toHaveLength(2);
  });

  it('el gasto descontado sin cuenta congelada resuelve el pasivo con el contexto del gasto, no del recibo', async () => {
    const conGasto = (reglas: any[]) => crear({
      ...baseRecibo(
        recibo({
          total_facturas: 1_000, monto_total: 900,
          medios_pago_multi: [{ medio: 'EFECTIVO', monto: 900 }],
          gastos_asociados: [{
            monto_aplicado: 100, cuenta_pagar: null,
            gasto: { proveedor_id: 'p1', sucursal_id: 'suc-gasto', fecha_emision: new Date('2026-09-01'), moneda: { codigo: 'USD' } },
          }],
        }),
      ),
      proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'ACREEDOR_VARIO', cuenta_contable_id: null }) },
    }, [], {}, reglas);

    // TIPO_PROVEEDOR es del gasto: aplica.
    const s1 = conGasto([regla('ACREEDORES_VARIOS', 'cta-ACR_REGLA', 'TIPO_PROVEEDOR', ['ACREEDOR_VARIO'])]);
    await s1.integrarReciboMulti('rec-1', 'emp');
    expect(traza(s1, 'Gasto descontado del cobro')).toEqual(['cta-ACR_REGLA', 'ACREEDORES_VARIOS', 'REGLA', 'r1']);
    const ctxGasto = s1.mapeo.crearResolvedor.mock.calls.find((c: any) => c[1].dimensiones.SUCURSAL === 'suc-gasto')[1];
    expect(ctxGasto).toMatchObject({ fecha: new Date('2026-09-01'), dimensiones: { SUCURSAL: 'suc-gasto', MONEDA: 'USD', TIPO_PROVEEDOR: 'ACREEDOR_VARIO' } });

    // TIPO_CLIENTE es del recibo: no aplica al pasivo del gasto.
    const s2 = conGasto([regla('ACREEDORES_VARIOS', 'cta-ACR_CLI', 'TIPO_CLIENTE', ['mayorista'])]);
    await s2.integrarReciboMulti('rec-1', 'emp');
    expect(traza(s2, 'Gasto descontado del cobro')).toEqual(['cta-ACREEDORES_VARIOS', 'ACREEDORES_VARIOS', 'DEFECTO', undefined]);
  });

  it('el gasto descontado con cuenta congelada en la CxP es FICHA', async () => {
    const s = crear(
      baseRecibo(
        recibo({
          total_facturas: 1_000, monto_total: 900,
          medios_pago_multi: [{ medio: 'EFECTIVO', monto: 900 }],
          gastos_asociados: [{ monto_aplicado: 100, gasto: { proveedor_id: 'p1' }, cuenta_pagar: { cuenta_pasivo_id: 'cta-congelada' } }],
        }),
      ),
    );

    await s.integrarReciboMulti('rec-1', 'emp');

    expect(traza(s, 'Gasto descontado del cobro')).toEqual(['cta-congelada', 'FICHA_CXP_CONGELADA', 'FICHA', undefined]);
  });

  it('en USD con diferencia de cambio: la línea de diferencia lleva su concepto', async () => {
    const s = crear(
      baseRecibo(
        recibo({
          moneda: { codigo: 'USD' }, cotizacion: 7_400, total_facturas: 220, monto_total: 220,
          facturas_multi: [{ factura_cab_id: 'fac-1', monto_pagado: 220, monto_nc: 0 }],
          medios_pago_multi: [{ medio: 'TRANSFERENCIA', monto: 220, cuenta_tesoreria_id: 'tes-usd' }],
        }),
        { facturas: [{ id: 'fac-1', dticam: 7_300 }], tesCuentas: [{ id: 'tes-usd', cuenta_contable_id: 'cta-banco-usd' }] },
      ),
      [], {}, [regla('DIFERENCIA_CAMBIO_GANADA', 'cta-DIF_USD', 'MONEDA', ['USD'])],
    );

    await s.integrarReciboMulti('rec-1', 'emp');

    expect(traza(s, 'Diferencia de cambio ganada (USD)')).toEqual(['cta-DIF_USD', 'DIFERENCIA_CAMBIO_GANADA', 'REGLA', 'r1']);
  });
});

// ── Recibo simple ──────────────────────────────────────────────────────────
describe('cobro (recibo simple)', () => {
  const cobro = { id: 'c1', empresa_id: 'emp', sucursal_id: 'suc-1', cliente_id: 'cli-1', moneda: { codigo: 'PYG' }, numero_recibo: '1',
    fecha_emision: new Date('2026-10-13'), monto_total: 1_000, usuario_id: null };
  const base = () => ({
    recibos_cobro: { findUnique: jest.fn().mockResolvedValue(cobro) },
    recibo_cobro_detalle: { findMany: jest.fn().mockResolvedValue([]) },
    clientes: { findUnique: jest.fn().mockResolvedValue({ tipo_cliente: 'mayorista' }) },
  });

  it('regla TIPO_CLIENTE en CLIENTES; Caja por defecto', async () => {
    const s = crear(base(), [], {}, [regla('CLIENTES', 'cta-CLI_MAY', 'TIPO_CLIENTE', ['mayorista'])]);
    await s.integrarCobro('c1');
    expect(traza(s, 'Clientes')).toEqual(['cta-CLI_MAY', 'CLIENTES', 'REGLA', 'r1']);
    expect(traza(s, 'Cobranza')).toEqual(['cta-CAJA_GENERAL', 'CAJA_GENERAL', 'DEFECTO', undefined]);
  });

  it('publica sucursal, moneda y tipo de cliente', async () => {
    const s = crear(base());
    await s.integrarCobro('c1');
    expect(publicadas(s)).toEqual(tabla('recibos_cobro'));
    expect(dimensiones(s)).toEqual({ SUCURSAL: 'suc-1', MONEDA: 'PYG', TIPO_CLIENTE: 'mayorista' });
  });
});

// ── Pago a proveedor ───────────────────────────────────────────────────────
describe('pago a proveedor', () => {
  const pago = (over: any = {}) => ({ id: 'pg1', empresa_id: 'emp', proveedor_id: 'p1', moneda: { codigo: 'USD' }, cotizacion: 7_000, monto: 10,
    fecha_pago: new Date('2026-10-13'), numero_recibo: 'R1', referencia: null, usuario_id: null, medio_pago_id: null, cuentas_pagar: null, ...over });
  const base = (p: any) => ({
    pagos_proveedor: { findUnique: jest.fn().mockResolvedValue(p) },
    proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'PROVEEDOR_EXTERIOR', cuenta_contable_id: null }) },
  });

  it('regla MONEDA en CAJA_GENERAL; el pasivo sale del resolvedor con su concepto', async () => {
    const s = crear(base(pago()), [], {}, [regla('CAJA_GENERAL', 'cta-CAJA_USD', 'MONEDA', ['USD'])]);
    await s.integrarPago('pg1');
    expect(traza(s, 'Salida de fondos')).toEqual(['cta-CAJA_USD', 'CAJA_GENERAL', 'REGLA', 'r1']);
    expect(traza(s, 'Proveedor')).toEqual(['cta-PROVEEDORES_EXTERIOR', 'PROVEEDORES_EXTERIOR', 'DEFECTO', undefined]);
  });

  it('la cuenta del medio configurado va con MEDIO_PAGO; la congelada en la CxP es FICHA', async () => {
    const s = crear(base(pago({ medio_pago_id: 'mp1', cuentas_pagar: { cuenta_pasivo_id: 'cta-congelada' } })));
    s.mediosPago.resolverPorMedio.mockResolvedValue(new Map([['mp1', 'cta-banco']]));
    await s.integrarPago('pg1');
    expect(traza(s, 'Salida de fondos')).toEqual(['cta-banco', undefined, 'MEDIO_PAGO', undefined]);
    expect(traza(s, 'Proveedor')).toEqual(['cta-congelada', 'FICHA_CXP_CONGELADA', 'FICHA', undefined]);
  });

  it('publica moneda y tipo de proveedor (el pago no tiene sucursal)', async () => {
    const s = crear(base(pago()));
    await s.integrarPago('pg1');
    expect(publicadas(s)).toEqual(tabla('pagos_proveedor'));
    expect(dimensiones(s)).toEqual({ MONEDA: 'USD', TIPO_PROVEEDOR: 'PROVEEDOR_EXTERIOR' });
  });
});

// ── Orden de pago (armado de integracion-sin-respaldos.spec.ts) ────────────
describe('orden de pago', () => {
  const orden = (det: any = {}) => ({
    id: 'op1', empresa_id: 'emp', sucursal_id: 'suc-1', estado: 'PAGADO', anulado: false, monto_total: 100, moneda: { codigo: 'PYG' }, cotizacion: null,
    fecha_pago: new Date('2026-10-13'), numero_orden_pago: 'OP-1', proveedor_id: 'p1', usuario_id: 'u1',
    orden_pago_proveedor_det: [{ monto: 100, medio_pago_id: 'mp-cheque', medio_pago: { codigo: 2 }, cuenta_bancaria_id: 'tes1', cuentas_pagar: { cuenta_pasivo_id: 'pas', compra_id: null }, ...det }],
  });
  const base = (o: any) => ({
    orden_pago_proveedor_cab: { findUnique: jest.fn().mockResolvedValue(o) },
    tes_reglas: { findMany: jest.fn().mockResolvedValue([]) },
    tes_cuentas: { findMany: jest.fn().mockResolvedValue([{ id: 'tes1', cuenta_contable_id: 'cta-banco' }]) },
    proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'PROVEEDOR_EXTERIOR', cuenta_contable_id: null }) },
  });

  it('con cheque y proveedor del exterior: regla TIPO_PROVEEDOR en CHEQUES_A_PAGAR', async () => {
    const s = crear(base(orden()), [], {}, [regla('CHEQUES_A_PAGAR', 'cta-CHQ_EXT', 'TIPO_PROVEEDOR', ['PROVEEDOR_EXTERIOR'])]);
    await s.integrarOrdenPago('op1');
    expect(traza(s, 'Cheque emitido a pagar')).toEqual(['cta-CHQ_EXT', 'CHEQUES_A_PAGAR', 'REGLA', 'r1']);
    // La cuenta congelada en la CxP es un dato guardado, no sale del mapeo: FICHA.
    expect(traza(s, 'Cancelación deuda proveedor')).toEqual(['pas', 'FICHA_CXP_CONGELADA', 'FICHA', undefined]);
  });

  it('CxP sin cuenta congelada: el pasivo sale del resolvedor; la cuenta de tesorería elegida es FICHA', async () => {
    const s = crear(base(orden({ medio_pago: { codigo: 5 }, cuentas_pagar: { cuenta_pasivo_id: null, compra_id: null } })), [], {}, [
      regla('PROVEEDORES_EXTERIOR', 'cta-PROV_EXT_SUC', 'SUCURSAL', ['suc-1']),
    ]);
    await s.integrarOrdenPago('op1');
    expect(traza(s, 'Cancelación deuda proveedor')).toEqual(['cta-PROV_EXT_SUC', 'PROVEEDORES_EXTERIOR', 'REGLA', 'r1']);
    expect(traza(s, 'Pago (banco/caja)')).toEqual(['cta-banco', 'FICHA_CUENTA_TESORERIA', 'FICHA', undefined]);
  });

  it('sin cuenta de tesorería ni medio: Caja por defecto', async () => {
    const s = crear(base(orden({ medio_pago: { codigo: 1 }, cuenta_bancaria_id: null })));
    await s.integrarOrdenPago('op1');
    expect(traza(s, 'Pago (banco/caja)')).toEqual(['cta-CAJA_GENERAL', 'CAJA_GENERAL', 'DEFECTO', undefined]);
  });

  it('publica sucursal, moneda y tipo de proveedor', async () => {
    const s = crear(base(orden()));
    await s.integrarOrdenPago('op1');
    expect(publicadas(s)).toEqual(tabla('orden_pago_proveedor_cab'));
    expect(dimensiones(s)).toEqual({ SUCURSAL: 'suc-1', MONEDA: 'PYG', TIPO_PROVEEDOR: 'PROVEEDOR_EXTERIOR' });
  });
});

// ── Movimiento de tesorería (armado de movimiento-concepto-contable.spec.ts) ─
describe('movimiento de tesorería', () => {
  const mov = (det: any, moneda = 'USD') => ({
    id: 'm1', empresa_id: 'emp', tipo: 'EGRESO', fecha: new Date('2026-10-13'), estado: 'CONFIRMADO', descripcion: 'Anticipo', origen_tipo: null,
    referencia: null, created_by: 'u1', embarque_id: null, tipo_pago_importacion: null, cuenta: { cuenta_contable_id: 'cta-banco', moneda }, cuenta_id: 'tes1',
    detalles: [{ categoria_id: 'cat', monto: 100, cuenta_contable_id: null, concepto_contable: null, categoria: { nombre: 'Anticipo', cuenta_contable_id: 'cta-categoria' }, ...det }],
  });
  const base = (m: any) => ({
    tes_movimientos: { findUnique: jest.fn().mockResolvedValue(m) },
    tes_categoria_cuenta_contable: { findMany: jest.fn().mockResolvedValue([]) },
  });

  it('detalle con concepto en cuenta USD: regla MONEDA; la cuenta de tesorería es FICHA', async () => {
    const s = crear(base(mov({ concepto_contable: 'DESCUENTOS_VARIOS_RRHH' })), [], {}, [
      regla('DESCUENTOS_VARIOS_RRHH', 'cta-DESC_USD', 'MONEDA', ['USD']),
    ]);
    await s.integrarMovimientoTesoreria('m1');
    const [det, tes] = lineas(s);
    expect(det).toMatchObject({ cuenta_id: 'cta-DESC_USD', mapeo_concepto: 'DESCUENTOS_VARIOS_RRHH', mapeo_nivel: 'REGLA', mapeo_regla_id: 'r1' });
    expect(tes).toMatchObject({ cuenta_id: 'cta-banco', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
  });

  it('[I1] un solo concepto frenado por una regla rota: el aviso conserva la regla', async () => {
    const s = crear(base(mov({ concepto_contable: 'DESCUENTOS_VARIOS_RRHH' })), [], {}, [
      regla('DESCUENTOS_VARIOS_RRHH', 'grupo-2.1', 'MONEDA', ['USD']),
    ]);
    await s.integrarMovimientoTesoreria('m1');
    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(s.avisosMapeo.registrarFaltaMapeo.mock.calls[0][0]).toMatchObject({
      conceptos: ['DESCUENTOS_VARIOS_RRHH'], motivo: 'CUENTA_INUTILIZABLE', regla: { id: 'r1', orden: 1 },
    });
  });

  it('la categoría de tesorería y la cuenta escrita en el detalle son FICHA, con su origen', async () => {
    // Categoría del sistema sin cuenta para la empresa: la global.
    const s1 = crear(base(mov({})));
    await s1.integrarMovimientoTesoreria('m1');
    expect(lineas(s1)[0]).toMatchObject({ cuenta_id: 'cta-categoria', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CATEGORIA_TESORERIA_GLOBAL' });

    const s2 = crear(base(mov({ cuenta_contable_id: 'cta-vieja' })));
    await s2.integrarMovimientoTesoreria('m1');
    expect(lineas(s2)[0]).toMatchObject({ cuenta_id: 'cta-vieja', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_DETALLE_MOVIMIENTO' });
  });

  it('categoría con cuenta asignada para la empresa: FICHA_CATEGORIA_TESORERIA', async () => {
    const s = crear({
      ...base(mov({})),
      tes_categoria_cuenta_contable: { findMany: jest.fn().mockResolvedValue([{ categoria_id: 'cat', cuenta_contable_id: 'cta-cat-emp' }]) },
    });
    await s.integrarMovimientoTesoreria('m1');
    expect(lineas(s)[0]).toMatchObject({ cuenta_id: 'cta-cat-emp', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CATEGORIA_TESORERIA' });
  });

  it('categoría propia de la empresa (su cuenta la puso la empresa): FICHA_CATEGORIA_TESORERIA, no la global', async () => {
    const s = crear(base(mov({ categoria: { nombre: 'Propia', empresa_id: 'emp', cuenta_contable_id: 'cta-propia' } })));
    await s.integrarMovimientoTesoreria('m1');
    expect(lineas(s)[0]).toMatchObject({ cuenta_id: 'cta-propia', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CATEGORIA_TESORERIA' });
  });

  it('publica la moneda de la cuenta de tesorería', async () => {
    const s = crear(base(mov({})));
    await s.integrarMovimientoTesoreria('m1');
    expect(publicadas(s)).toEqual(tabla('tes_movimientos'));
    expect(dimensiones(s)).toEqual({ MONEDA: 'USD' });
  });
});

// ── Transferencia entre cuentas ────────────────────────────────────────────
describe('transferencia entre cuentas propias', () => {
  const fecha = new Date('2026-10-13');
  const movs: Record<string, any> = {
    mo: { id: 'mo', fecha, monto: 7_300_000, created_by: null, referencia: null, descripcion: null, cuenta: { cuenta_contable_id: 'cta-banco-gs', moneda: 'PYG', nombre: 'Banco Gs' } },
    md: { id: 'md', fecha, monto: 1_000, created_by: null, referencia: null, descripcion: null, cuenta: { cuenta_contable_id: 'cta-banco-usd', moneda: 'USD', nombre: 'Banco USD' } },
  };
  const base = () => ({
    tes_movimientos: { findUnique: jest.fn().mockImplementation(({ where }: any) => Promise.resolve(movs[where.id])) },
    cont_tipo_cambio: { findFirst: jest.fn().mockResolvedValue({ tasa: 7_400 }) },
  });

  it('regla MONEDA en la diferencia de cambio (moneda extranjera de la transferencia); las cuentas de tesorería FICHA', async () => {
    const s = crear(base(), [], {}, [regla('DIFERENCIA_CAMBIO_GANADA', 'cta-DIF_USD', 'MONEDA', ['USD'])]);
    await s.integrarTransferenciaTes('mo', 'md', 'tr1', 'emp');
    const [origen, destino, dif] = lineas(s);
    expect(dif).toMatchObject({ cuenta_id: 'cta-DIF_USD', mapeo_concepto: 'DIFERENCIA_CAMBIO_GANADA', mapeo_nivel: 'REGLA' });
    expect(origen).toMatchObject({ mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
    expect(destino).toMatchObject({ mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
    expect(publicadas(s)).toEqual(tabla('tes_transferencias'));
    expect(dimensiones(s)).toEqual({ MONEDA: 'USD' });
  });
});

// ── Bancard ────────────────────────────────────────────────────────────────
it('pago Bancard: CLIENTES por defecto, la cuenta por liquidar de la configuración es MEDIO_PAGO, publica la moneda', async () => {
  const s = crear({});
  s.prisma.pago_bancard = { findUnique: jest.fn().mockResolvedValue({
    id: 'b1', empresa_id: 'emp', estado: 'aprobado', monto: 100, moneda: 'PYG', shop_process_id: 1, origen_modulo: 'X',
    fecha_confirmacion: new Date('2026-10-13'), empresa: { bancard_config: { cuenta_contable_por_liquidar: 'cta-bancard' } },
  }) };
  await s.integrarPagoBancard('b1');
  expect(traza(s, 'Cliente')).toEqual(['cta-CLIENTES', 'CLIENTES', 'DEFECTO', undefined]);
  expect(traza(s, 'Bancard por liquidar')).toEqual(['cta-bancard', undefined, 'MEDIO_PAGO', undefined]);
  expect(publicadas(s)).toEqual(tabla('pago_bancard'));
});

// ── Liquidación de sueldos (armado de integracion-sin-respaldos.spec.ts) ───
describe('liquidación de sueldos', () => {
  const base = () => ({
    rrhh_liquidaciones_cabecera: {
      findUnique: jest.fn().mockResolvedValue({
        id: 'r1', empresa_id: 'emp', estado: 'CERRADA', periodo_anio: 2026, periodo_mes: 9, tipo: 'MENSUAL',
        resumenes: [{ total_ingresos: 1000, ips_obrero: 90, ips_patronal: 165, ips_admin: 0, neto_a_pagar: 910, total_egresos: 90, empleado: { id: 'x', centro_costo_id: null } }],
      }),
    },
    cont_centros_costo: { findMany: jest.fn().mockResolvedValue([]) },
  });

  it('sin reglas: las mismas líneas que hoy, todas DEFECTO', async () => {
    const s = crear(base());
    await s.integrarLiquidacionRrhh('r1');
    expect(lineas(s).map((l: any) => [l.cuenta_id, l.debe_moneda, l.haber_moneda])).toEqual([
      ['cta-SUELDOS_JORNALES', 1000, 0],
      ['cta-APORTE_PATRONAL_IPS', 165, 0],
      ['cta-SUELDOS_A_PAGAR', 0, 910],
      ['cta-IPS_A_DEPOSITAR', 0, 255],
    ]);
    expect(lineas(s).map((l: any) => l.mapeo_concepto)).toEqual(['SUELDOS_JORNALES', 'APORTE_PATRONAL_IPS', 'SUELDOS_A_PAGAR', 'IPS_A_DEPOSITAR']);
    expect(lineas(s).every((l: any) => l.mapeo_nivel === 'DEFECTO')).toBe(true);
    expect(publicadas(s)).toEqual(tabla('rrhh_liquidacion'));
    expect(dimensiones(s)).toEqual({ MONEDA: 'PYG' });
  });

  it('con IPS y sin sus conceptos, una regla en otro concepto no cambia el aviso conjunto', async () => {
    const s = crear(base(), ['APORTE_PATRONAL_IPS', 'IPS_A_DEPOSITAR'], {}, [regla('SUELDOS_JORNALES', 'cta-X', 'MONEDA', ['PYG'])]);
    await s.integrarLiquidacionRrhh('r1');
    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(s.avisosMapeo.registrarFaltaMapeo.mock.calls[0][0].conceptos).toEqual(['APORTE_PATRONAL_IPS', 'IPS_A_DEPOSITAR']);
  });
});

// ── Comisiones ─────────────────────────────────────────────────────────────
describe('comisiones', () => {
  const liq = { id: 'l1', empresa_id: 'emp', total_comisiones: 110, created_at: new Date('2026-10-13'), fecha_pago: new Date('2026-10-14'),
    vendedor_cobrador_id: 'v1', periodo_desde: new Date('2026-10-01'), periodo_hasta: new Date('2026-10-13') };
  const base = (regimen = 'EXTERNO_IVA_ADICIONAL') => ({
    liquidaciones_comisiones: { findUnique: jest.fn().mockResolvedValue(liq) },
    vendedores_cobradores: { findUnique: jest.fn().mockResolvedValue({ nombre: 'A', apellido: 'B', regimen_comision: regimen }) },
  });

  it('devengamiento: regla MONEDA=PYG en GASTO_COMISIONES_VENTAS; IVA y a pagar por defecto', async () => {
    const s = crear(base(), [], {}, [regla('GASTO_COMISIONES_VENTAS', 'cta-COM_PYG', 'MONEDA', ['PYG'])]);
    await s.integrarDevengamientoComisiones('l1', 'emp');
    expect(traza(s, 'Gasto comisiones')).toEqual(['cta-COM_PYG', 'GASTO_COMISIONES_VENTAS', 'REGLA', 'r1']);
    expect(traza(s, 'IVA crédito comisiones')).toEqual(['cta-IVA_CREDITO_10', 'IVA_CREDITO_10', 'DEFECTO', undefined]);
    expect(traza(s, 'Comisiones a pagar')).toEqual(['cta-COMISIONES_A_PAGAR', 'COMISIONES_A_PAGAR', 'DEFECTO', undefined]);
    expect(publicadas(s)).toEqual(tabla('liquidacion_comisiones_deveng'));
  });

  it('pago: ambas líneas por defecto con su concepto', async () => {
    const s = crear(base('EMPLEADO'));
    await s.integrarPagoComisiones('l1', 'emp');
    expect(traza(s, 'Cancelación comisiones a pagar')).toEqual(['cta-COMISIONES_A_PAGAR', 'COMISIONES_A_PAGAR', 'DEFECTO', undefined]);
    expect(traza(s, 'Salida de fondos')).toEqual(['cta-PAGO_COMISIONES_FONDOS', 'PAGO_COMISIONES_FONDOS', 'DEFECTO', undefined]);
    expect(publicadas(s)).toEqual(tabla('liquidacion_comisiones_pago'));
  });

  it('supervisor: regla MONEDA en PAGO_COMISIONES_FONDOS', async () => {
    const s = crear({
      comisiones_supervisores: { findUnique: jest.fn().mockResolvedValue({
        id: 'cs1', total_comision: 50, updated_at: new Date('2026-10-13'), created_at: null,
        periodo_desde: new Date('2026-10-01'), periodo_hasta: new Date('2026-10-13'), supervisor: { nombre: 'S', apellido: 'V' },
      }) },
    }, [], {}, [regla('PAGO_COMISIONES_FONDOS', 'cta-BANCO_COM', 'MONEDA', ['PYG'])]);
    await s.integrarComisionSupervisor('cs1', 'emp');
    expect(traza(s, 'Gasto comisión supervisor')).toEqual(['cta-GASTO_COMISIONES_VENTAS', 'GASTO_COMISIONES_VENTAS', 'DEFECTO', undefined]);
    expect(traza(s, 'Salida de fondos')).toEqual(['cta-BANCO_COM', 'PAGO_COMISIONES_FONDOS', 'REGLA', 'r1']);
    expect(publicadas(s)).toEqual(tabla('comision_supervisor'));
  });
});

// ── Rendición de viáticos (integrador propio) ──────────────────────────────
describe('rendición de viáticos', () => {
  const armar = (reglas: any[] = [], extra: { adelanto?: any; devolucion?: any } = {}) => {
    const prisma: any = {
      suscripcion_modulos: { findFirst: jest.fn().mockResolvedValue({ id: 'sm' }) },
      rendicion_adelanto: { findFirst: jest.fn().mockResolvedValue({
        id: 'ad1', monto: 500, fecha: new Date('2026-10-13'), cont_documento_id: null,
        rendicion: { numero_rendicion: 'RV-1', moneda: { codigo: 'USD' }, empleado: { nombres: 'Ana', apellidos: 'Pérez' } },
        tes_cuenta: { id: 'tes1', cuenta_contable_id: 'cta-tes', nombre: 'Banco' }, ...extra.adelanto,
      }) },
      rendicion_devolucion: { findFirst: jest.fn().mockResolvedValue({
        id: 'dev1', monto: 200, fecha: new Date('2026-10-13'), cont_documento_id: null, tipo: 'REINTEGRO', diferido: true,
        rendicion: { numero_rendicion: 'RV-1', moneda: null, empleado: { nombres: 'Ana', apellidos: 'Pérez' } }, tes_cuenta: null, ...extra.devolucion,
      }) },
      cont_plan_cuentas: { findUnique: jest.fn().mockResolvedValue({ codigo: '1.01', active: true }) },
      cont_documentos: { create: jest.fn().mockResolvedValue({ id: 'doc1' }) },
    };
    const asientos = { crearConfirmado: jest.fn().mockResolvedValue({}) };
    const mapeo = conResolvedor({
      getCuentaPorConcepto: jest.fn().mockImplementation((e: string, c: string) =>
        c === 'FALTA' ? Promise.reject(new FaltaMapeoError(e, [c])) : Promise.resolve(`cta-${c}`)),
    }, reglas);
    const s = new RendicionViaticosContabilidadService(prisma, asientos as any, mapeo, { resolverPeriodoPorFecha: jest.fn().mockResolvedValue('p') } as any,
      { log: jest.fn() } as any, { registrarFaltaMapeo: jest.fn() } as any);
    return { s, asientos, mapeo };
  };

  it('adelanto: regla MONEDA de la rendición en ANTICIPO_VIATICO; la cuenta de tesorería es FICHA', async () => {
    const { s, asientos, mapeo } = armar([regla('ANTICIPO_VIATICO', 'cta-ANT_USD', 'MONEDA', ['USD'])]);
    await s.registrarAdelanto('ad1', 'emp');
    const [debe, haber] = asientos.crearConfirmado.mock.calls[0][3].lineas;
    expect(debe).toMatchObject({ cuenta_id: 'cta-ANT_USD', mapeo_concepto: 'ANTICIPO_VIATICO', mapeo_nivel: 'REGLA', mapeo_regla_id: 'r1' });
    expect(haber).toMatchObject({ cuenta_id: 'cta-tes', mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
    expect(mapeo.crearResolvedor.mock.calls[0][1]).toMatchObject({ fecha: new Date('2026-10-13'), dimensiones: { MONEDA: 'USD' } });
  });

  it('reintegro diferido sin moneda en la rendición: PYG, ambas líneas por defecto', async () => {
    const { s, asientos, mapeo } = armar();
    await s.registrarDevolucion('dev1', 'emp');
    const ls = asientos.crearConfirmado.mock.calls[0][3].lineas;
    expect(ls.map((l: any) => [l.cuenta_id, l.mapeo_concepto, l.mapeo_nivel])).toEqual([
      ['cta-ANTICIPO_VIATICO', 'ANTICIPO_VIATICO', 'DEFECTO'],
      ['cta-REINTEGRO_EMPLEADO_PENDIENTE', 'REINTEGRO_EMPLEADO_PENDIENTE', 'DEFECTO'],
    ]);
    expect(mapeo.crearResolvedor.mock.calls[0][1].dimensiones).toEqual({ MONEDA: 'PYG' });
  });

  it('devolución a una cuenta de tesorería: su cuenta es FICHA_CUENTA_TESORERIA', async () => {
    const { s, asientos } = armar([], { devolucion: { tipo: 'DEVOLUCION', diferido: false, tes_cuenta: { id: 'tes1', cuenta_contable_id: 'cta-tes', nombre: 'Banco' } } });
    await s.registrarDevolucion('dev1', 'emp');
    const ls = asientos.crearConfirmado.mock.calls[0][3].lineas;
    expect(ls.find((l: any) => l.cuenta_id === 'cta-tes')).toMatchObject({ mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
  });

  it('pago de la CxP del empleado: la cuenta de tesorería es FICHA_CUENTA_TESORERIA', async () => {
    const { s, asientos } = armar();
    (s as any).prisma.tes_cuentas = { findUnique: jest.fn().mockResolvedValue({ cuenta_contable_id: 'cta-tes' }) };
    (s as any).prisma.cuentas_pagar_empleado = { findFirst: jest.fn().mockResolvedValue(null) };
    await s.registrarPagoCxpEmpleado('cxp1', 'emp', 100, new Date('2026-10-13'), 'tes1');
    const ls = asientos.crearConfirmado.mock.calls[0][3].lineas;
    expect(ls.find((l: any) => l.cuenta_id === 'cta-tes')).toMatchObject({ mapeo_nivel: 'FICHA', mapeo_concepto: 'FICHA_CUENTA_TESORERIA' });
  });
});

// ── Invariante: el pago debita la misma cuenta que acreditó el documento ──
describe('cuenta de pasivo congelada en la CxP', () => {
  const compra = { id: 'c1', empresa_id: 'emp', sucursal_id: 'suc-1', condicion_operacion: { codigo: 2 }, total: 1100, total_iva_10: 100, total_iva_5: 0,
    moneda: { codigo: 'PYG' }, cotizacion: null, fecha_emision: new Date('2026-10-13'), proveedor_id: 'p1', numero_factura: '1', usuario_id: null };
  const exterior = { tipo_entidad: 'PROVEEDOR_EXTERIOR', cuenta_contable_id: null };
  const baseCompra = () => ({
    compra_cab: { findUnique: jest.fn().mockResolvedValue(compra) },
    config_compras: { findUnique: jest.fn().mockResolvedValue(null) },
    proveedores: { findFirst: jest.fn().mockResolvedValue(exterior) },
  });
  const reglaExt = [regla('PROVEEDORES_EXTERIOR', 'cta-PROV_EXT_SUC', 'SUCURSAL', ['suc-1'])];
  const orden = (cuentaPasivo: string) => ({
    id: 'op1', empresa_id: 'emp', sucursal_id: 'suc-2', estado: 'PAGADO', anulado: false, monto_total: 1100, moneda: { codigo: 'PYG' }, cotizacion: null,
    fecha_pago: new Date('2026-10-20'), numero_orden_pago: 'OP-1', proveedor_id: 'p1', usuario_id: 'u1',
    orden_pago_proveedor_det: [{ monto: 1100, medio_pago_id: null, medio_pago: { codigo: 1 }, cuenta_bancaria_id: null, cuentas_pagar: { cuenta_pasivo_id: cuentaPasivo, compra_id: 'c1' } }],
  });
  const baseOP = (o: any) => ({
    orden_pago_proveedor_cab: { findUnique: jest.fn().mockResolvedValue(o) },
    tes_reglas: { findMany: jest.fn().mockResolvedValue([]) },
    tes_cuentas: { findMany: jest.fn().mockResolvedValue([]) },
    proveedores: { findFirst: jest.fn().mockResolvedValue(exterior) },
  });

  it('con una regla SUCURSAL en PROVEEDORES_EXTERIOR, la compra acredita y la OP debita la misma cuenta (la de la regla)', async () => {
    // Al confirmar la compra se congela la cuenta con el contexto de la compra.
    const sCongelar = crear(baseCompra(), [], {}, reglaExt);
    const congelada = await sCongelar.cuentaPasivoParaCongelar('emp', { proveedor_id: 'p1', sucursal_id: 'suc-1', moneda_id: null, fecha: compra.fecha_emision });
    expect(congelada).toBe('cta-PROV_EXT_SUC');

    // El asiento de la compra acredita con el mismo contexto: la misma cuenta.
    const sCompra = crear(baseCompra(), [], {}, reglaExt);
    await sCompra.integrarFacturaCompra('c1');
    expect(traza(sCompra, 'Proveedor')).toEqual(['cta-PROV_EXT_SUC', 'PROVEEDORES_EXTERIOR', 'REGLA', 'r1']);

    // La OP es de otra sucursal: igual debita la cuenta congelada.
    const sOP = crear(baseOP(orden(congelada)), [], {}, reglaExt);
    await sOP.integrarOrdenPago('op1');
    expect(linea(sOP, 'Cancelación deuda proveedor').cuenta_id).toBe('cta-PROV_EXT_SUC');
  });

  it('sin reglas: congela y acredita la cuenta por defecto, como hoy', async () => {
    const s = crear(baseCompra());
    expect(await s.cuentaPasivoParaCongelar('emp', { proveedor_id: 'p1', moneda_id: null, fecha: compra.fecha_emision })).toBe('cta-PROVEEDORES_EXTERIOR');
    await s.integrarFacturaCompra('c1');
    expect(traza(s, 'Proveedor')).toEqual(['cta-PROVEEDORES_EXTERIOR', 'PROVEEDORES_EXTERIOR', 'DEFECTO', undefined]);
  });

  it('cuentaPasivoParaCongelar usa la moneda del documento (moneda_id) y su fecha', async () => {
    const p: any = { ...baseCompra(), moneda: { findUnique: jest.fn().mockResolvedValue({ codigo: 'USD' }) } };
    const s = crear(p, [], {}, [regla('PROVEEDORES_EXTERIOR', 'cta-PROV_USD', 'MONEDA', ['USD'])]);
    expect(await s.cuentaPasivoParaCongelar('emp', { proveedor_id: 'p1', moneda_id: 'mon-usd', fecha: new Date('2026-08-01') })).toBe('cta-PROV_USD');
    expect(s.mapeo.crearResolvedor.mock.calls[0][1]).toMatchObject({ fecha: new Date('2026-08-01'), dimensiones: { MONEDA: 'USD', TIPO_PROVEEDOR: 'PROVEEDOR_EXTERIOR' } });
  });

  it('gasto a crédito: congela y acredita la misma cuenta con una regla SUCURSAL', async () => {
    const g = { id: 'g1', empresa_id: 'emp', sucursal_id: 'suc-1', anulado: false, deducible: true, forma_pago: 'CREDITO', proveedor_id: 'p1',
      moneda: { codigo: 'PYG' }, cotizacion: null, fecha_emision: new Date('2026-10-13'), total: 1000, total_iva_10: 0, total_iva_5: 0,
      gasto_det: [{ subtotal: 1000, monto_iva: 0, tipo_gasto: null }] };
    const s = crear({
      gasto_cab: { findUnique: jest.fn().mockResolvedValue(g) },
      config_compras: { findUnique: jest.fn().mockResolvedValue(null) },
      proveedores: { findFirst: jest.fn().mockResolvedValue(exterior) },
    }, [], {}, reglaExt);
    const congelada = await s.cuentaPasivoParaCongelar('emp', { proveedor_id: 'p1', sucursal_id: 'suc-1', moneda_id: null, fecha: g.fecha_emision });
    await s.integrarGasto('g1');
    expect(lineas(s).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe(congelada);
    expect(congelada).toBe('cta-PROV_EXT_SUC');
  });
});
