import { ContabilidadIntegracionService } from './integracion.service';
import { FaltaMapeoError } from './falta-mapeo.error';
import { conResolvedor } from './__helpers__/integracion-mock';

/**
 * Asientos de recibos de cobranza y órdenes de pago, probados contra la base de
 * DOBA (2026-10-08). Cada caso es un error que el asiento real tenía:
 *
 * - la NC aplicada en modo FLEXIBLE se volvía a acreditar a Clientes (ya lo hizo
 *   su propio asiento al emitirse) contra una "cuenta puente" que nada cancelaba;
 * - el cheque diferido se registraba como al día, porque se buscaba por un
 *   movimiento de tesorería que el cheque recibido ya no genera;
 * - la cuenta configurada para el medio pisaba la cuenta de tesorería elegida:
 *   una transferencia en USD entraba al banco en guaraníes;
 * - los recibos de RRHH por consumo de funcionarios no tenían `total_facturas`
 *   y el asiento salía con haber 0;
 * - los cobros y pagos en moneda extranjera no registraban diferencia de cambio;
 * - el cheque emitido en una orden de pago acreditaba Caja.
 *
 * 2026-10-13 (spec "mapeo sin decisiones internas"): los conceptos de cheques,
 * retenciones, etc. ya no caen a Caja ni se omiten: sin mapear, el documento queda
 * sin asiento y se avisa. `faltan` simula los conceptos no mapeados.
 */

const cuentaDe = (concepto: string) => `cta-${concepto}`;

type Linea = { cuenta_id: string; debe_moneda: number; haber_moneda: number; descripcion: string };

const crearServicio = (
  prisma: any,
  opciones: { cobro?: Map<number, string>; pago?: Map<string, string>; faltan?: string[]; idCheque?: string | null } = {},
) => {
  const service = Object.create(ContabilidadIntegracionService.prototype) as any;
  service.logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };
  service.prisma = {
    cont_documentos: {
      findFirst: jest.fn().mockResolvedValue(null),
      upsert: jest.fn().mockResolvedValue({ id: 'doc-1' }),
      create: jest.fn().mockResolvedValue({ id: 'doc-1' }),
    },
    $transaction: jest.fn().mockImplementation((fn: any) => fn(service.prisma)),
    // Dimensiones de cabecera para las reglas de cuenta (sin datos: no publica nada).
    empresas_sucursales: { findFirst: jest.fn().mockResolvedValue(null) },
    clientes: { findUnique: jest.fn().mockResolvedValue(null) },
    proveedores: { findFirst: jest.fn().mockResolvedValue(null) },
    ...prisma,
  };
  service.tieneModuloContabilidad = jest.fn().mockResolvedValue(true);
  service.periodos = { resolverPeriodoPorFecha: jest.fn().mockResolvedValue('per-1') };
  const faltan = opciones.faltan ?? [];
  service.mapeo = conResolvedor({
    getCuentaPorConcepto: jest.fn().mockImplementation((e: string, c: string) =>
      faltan.includes(c) ? Promise.reject(new FaltaMapeoError(e, [c])) : Promise.resolve(cuentaDe(c)),
    ),
  });
  service.mediosPago = {
    resolverPorCodigoMedio: jest.fn().mockResolvedValue(opciones.cobro ?? new Map()),
    resolverPorMedio: jest.fn().mockResolvedValue(opciones.pago ?? new Map()),
    idMedioPorCodigo: jest.fn().mockResolvedValue(opciones.idCheque ?? null),
  };
  service.avisosMapeo = { registrarFaltaMapeo: jest.fn().mockResolvedValue(undefined) };
  service.tipoCambio = { getTasa: jest.fn().mockRejectedValue(new Error('No hay tipo de cambio')) };
  service.resolverCuentaPasivo = jest.fn().mockResolvedValue(cuentaDe('PROVEEDORES'));
  service.asientos = { crearConfirmado: jest.fn().mockResolvedValue({}) };
  return service;
};

const lineasGrabadas = (service: any): Linea[] => service.asientos.crearConfirmado.mock.calls[0][3].lineas;
const totales = (lineas: Linea[]) => ({
  debe: lineas.reduce((s, l) => s + l.debe_moneda, 0),
  haber: lineas.reduce((s, l) => s + l.haber_moneda, 0),
});
const enCuenta = (lineas: Linea[], cuenta: string, lado: 'debe_moneda' | 'haber_moneda') =>
  lineas.filter((l) => l.cuenta_id === cuenta).reduce((s, l) => s + l[lado], 0);

describe('asiento del recibo de cobranza', () => {
  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,
    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 crear = (row: any, opciones: any = {}, tesCuentas: any[] = []) =>
    crearServicio(
      {
        recibos_cobro: { findUnique: jest.fn().mockResolvedValue(row) },
        tes_cuentas: { findMany: jest.fn().mockResolvedValue(tesCuentas) },
        factura_cab: { findMany: jest.fn().mockResolvedValue(opciones.facturas ?? []) },
      },
      opciones,
    );

  it('la NC flexible no vuelve a acreditar Clientes ni usa cuenta puente', async () => {
    // Factura de 2.200.000: se aplica una NC de 330.000 y se cobra el resto en efectivo.
    const service = crear(
      recibo({
        total_facturas: 2_200_000,
        monto_total: 1_870_000,
        nc_aplicadas_multi: [{ modo: 'FLEXIBLE', monto: 330_000 }],
        medios_pago_multi: [{ medio: 'EFECTIVO', monto: 1_870_000 }],
      }),
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, cuentaDe('CLIENTES'), 'haber_moneda')).toBe(1_870_000);
    expect(lineas.some((l) => l.cuenta_id === cuentaDe('NC_APLICADA_PUENTE'))).toBe(false);
    expect(totales(lineas).debe).toBe(totales(lineas).haber);
  });

  it('el cheque diferido sale de su propio vencimiento, sin movimiento de tesorería', async () => {
    const service = crear(
      recibo({
        total_facturas: 270_000,
        monto_total: 270_000,
        medios_pago_multi: [
          { medio: 'CHEQUE', monto: 170_000, cheque_fecha_vencimiento: new Date('2026-11-07') },
          { medio: 'CHEQUE', monto: 100_000, cheque_fecha_vencimiento: new Date('2026-10-08') },
        ],
      }),
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, cuentaDe('CHEQUES_EN_TRANSITO'), 'debe_moneda')).toBe(170_000);
    expect(enCuenta(lineas, cuentaDe('CHEQUES_EN_CARTERA'), 'debe_moneda')).toBe(100_000);
  });

  it('la cuenta de tesorería elegida manda sobre la configurada para el medio', async () => {
    const service = crear(
      recibo({
        total_facturas: 900_000,
        monto_total: 900_000,
        medios_pago_multi: [
          { medio: 'TRANSFERENCIA', monto: 400_000, cuenta_tesoreria_id: 'tes-banco' },
          { medio: 'EFECTIVO', monto: 500_000 },
        ],
      }),
      // El contador configuró ambos medios contra cuentas de recaudación.
      { cobro: new Map([[5, 'cta-recaudacion-transf'], [1, 'cta-recaudacion-efectivo']]) },
      [{ id: 'tes-banco', cuenta_contable_id: 'cta-banco' }],
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, 'cta-banco', 'debe_moneda')).toBe(400_000);
    // Sin cuenta de tesorería, el medio sigue usando su cuenta configurada.
    expect(enCuenta(lineas, 'cta-recaudacion-efectivo', 'debe_moneda')).toBe(500_000);
    expect(lineas.some((l) => l.cuenta_id === 'cta-recaudacion-transf')).toBe(false);
  });

  it('un recibo de consumo sin total_facturas igual cuadra contra Clientes', async () => {
    const service = crear(
      recibo({
        total_facturas: 0,
        monto_total: 6_200,
        medios_pago_multi: [{ medio: 'DESCUENTO_PLANILLA', monto: 6_200 }],
      }),
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, cuentaDe('CONSUMO_FUNCIONARIO_PUENTE'), 'debe_moneda')).toBe(6_200);
    expect(enCuenta(lineas, cuentaDe('CLIENTES'), 'haber_moneda')).toBe(6_200);
    // Estos recibos no tienen usuario: el autor va null, no '' (Prisma rechaza un UUID vacío).
    expect(service.asientos.crearConfirmado.mock.calls[0][1]).toBeNull();
  });

  it('los intereses no se acreditan dos veces', async () => {
    const service = crear(
      recibo({
        total_facturas: 1_000_000,
        total_intereses: 50_000,
        monto_total: 1_050_000,
        medios_pago_multi: [{ medio: 'EFECTIVO', monto: 1_050_000 }],
      }),
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, cuentaDe('CLIENTES'), 'haber_moneda')).toBe(1_000_000);
    expect(enCuenta(lineas, cuentaDe('INTERESES_MORATORIOS_COBRADOS'), 'haber_moneda')).toBe(50_000);
    expect(totales(lineas).debe).toBe(totales(lineas).haber);
  });

  it('en USD usa la cotización del recibo, cancela la deuda a la de la factura y registra la diferencia', async () => {
    // Factura de 220 USD emitida a 7.300 (1.606.000 en Deudores), cobrada a 7.400.
    const service = crear(
      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 }] },
      [{ id: 'tes-usd', cuenta_contable_id: 'cta-banco-usd' }],
    );

    await service.integrarReciboMulti('rec-1', 'emp-1');
    const lineas = lineasGrabadas(service);
    const asiento = service.asientos.crearConfirmado.mock.calls[0][3];

    expect(asiento.moneda_origen).toBe('PYG');
    expect(service.tipoCambio.getTasa).not.toHaveBeenCalled();
    expect(enCuenta(lineas, 'cta-banco-usd', 'debe_moneda')).toBe(1_628_000);
    expect(enCuenta(lineas, cuentaDe('CLIENTES'), 'haber_moneda')).toBe(1_606_000);
    expect(enCuenta(lineas, cuentaDe('DIFERENCIA_CAMBIO_GANADA'), 'haber_moneda')).toBe(22_000);
    expect(totales(lineas).debe).toBe(totales(lineas).haber);
  });

  it('el cheque diferido sin CHEQUES_EN_TRANSITO no cae a cartera ni a Caja: no asienta y avisa', async () => {
    const service = crear(
      recibo({
        total_facturas: 170_000,
        monto_total: 170_000,
        medios_pago_multi: [{ medio: 'CHEQUE', monto: 170_000, cheque_fecha_vencimiento: new Date('2026-11-07') }],
      }),
      { faltan: ['CHEQUES_EN_TRANSITO'] },
    );

    await expect(service.integrarReciboMulti('rec-1', 'emp-1')).resolves.toBeUndefined();

    expect(service.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(service.avisosMapeo.registrarFaltaMapeo).toHaveBeenCalledWith({
      empresaId: 'emp-1', origenTipo: 'recibos_multi', origenId: 'rec-1', conceptos: ['CHEQUES_EN_TRANSITO'],
    });
  });

  it('con retención de IVA y cheque, sin ninguno de los dos mapeados, avisa los dos juntos', async () => {
    const service = crear(
      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') }],
      }),
      { faltan: ['CHEQUES_EN_CARTERA', 'RETENCION_IVA_FAVOR'] },
    );

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

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

describe('asiento de la venta contado', () => {
  const CHEQUE = 'mp-cheque';
  const factura = {
    id: 'fac-1',
    empresa_id: 'emp-1',
    estado: 'Aprobado',
    cobro_diferido: false,
    condicion_operacion: { codigo: 1 },
    moneda: { codigo: 'PYG' },
    dticam: null,
    dfeemide: new Date('2026-10-08T12:00:00Z'),
    dest: '001',
    dpunexp: '001',
    dnumdoc: '0000001',
    factura_subtotales: [{ dtotgralope: 1_100_000, diva10: 100_000, diva5: 0, dsub10: 1_100_000, dsub5: 0, dsubexe: 0, dsubexo: 0 }],
    factura_forma_pagos: [{ medio_pago_id: CHEQUE, dmontipag: 1_100_000, dticamtipag: null }],
  };
  const crear = (faltan: string[] = []) =>
    crearServicio(
      {
        factura_cab: { findUnique: jest.fn().mockResolvedValue(factura) },
        inm_cobros: { findFirst: jest.fn().mockResolvedValue(null) },
      },
      { faltan, idCheque: CHEQUE },
    );

  it('el cheque cobrado entra por CHEQUES_EN_CARTERA', async () => {
    const service = crear();

    await service.integrarFacturaVenta('fac-1');

    expect(enCuenta(lineasGrabadas(service), cuentaDe('CHEQUES_EN_CARTERA'), 'debe_moneda')).toBe(1_100_000);
  });

  it('sin CHEQUES_EN_CARTERA no cae a Caja: no asienta, no falla y avisa', async () => {
    const service = crear(['CHEQUES_EN_CARTERA']);

    await expect(service.integrarFacturaVenta('fac-1')).resolves.toBeUndefined();

    expect(service.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(service.avisosMapeo.registrarFaltaMapeo).toHaveBeenCalledWith({
      empresaId: 'emp-1', origenTipo: 'factura_cab', origenId: 'fac-1', conceptos: ['CHEQUES_EN_CARTERA'],
    });
  });
});

describe('asiento de la orden de pago', () => {
  const CHEQUE = 2;
  const orden = (over: any = {}) => ({
    id: 'op-1',
    empresa_id: 'emp-1',
    estado: 'PAGADO',
    anulado: false,
    numero_orden_pago: '001-001-0000001',
    proveedor_id: 'prov-1',
    usuario_id: null,
    fecha_pago: new Date('2026-10-08T12:00:00Z'),
    moneda: { codigo: 'PYG' },
    cotizacion: 1,
    monto_total: 0,
    orden_pago_proveedor_det: [],
    ...over,
  });
  const det = (over: any) => ({
    monto: 0,
    medio_pago_id: 'mp-x',
    medio_pago: { codigo: 1 },
    cuenta_bancaria_id: null,
    cuentas_pagar: { id: 'cxp-1', cuenta_pasivo_id: 'cta-proveedores', compra_id: 'compra-1', origen_tipo: 'compra', origen_id: null },
    ...over,
  });
  const crear = (row: any, extra: any = {}) =>
    crearServicio(
      {
        orden_pago_proveedor_cab: { findUnique: jest.fn().mockResolvedValue(row) },
        tes_reglas: { findMany: jest.fn().mockResolvedValue(extra.reglas ?? []) },
        tes_cuentas: { findMany: jest.fn().mockResolvedValue(extra.tesCuentas ?? []) },
        compra_cab: { findMany: jest.fn().mockResolvedValue(extra.compras ?? []) },
        gasto_cab: { findMany: jest.fn().mockResolvedValue([]) },
      },
      { pago: extra.pago },
    );

  it('el cheque emitido acredita Cheques a pagar, no Caja', async () => {
    const service = crear(
      orden({
        monto_total: 1_000_000,
        orden_pago_proveedor_det: [det({ monto: 1_000_000, medio_pago: { codigo: CHEQUE }, cuenta_bancaria_id: 'tes-banco' })],
      }),
      { tesCuentas: [{ id: 'tes-banco', cuenta_contable_id: 'cta-banco' }] },
    );

    await service.integrarOrdenPago('op-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, cuentaDe('CHEQUES_A_PAGAR'), 'haber_moneda')).toBe(1_000_000);
    expect(enCuenta(lineas, cuentaDe('CAJA_GENERAL'), 'haber_moneda')).toBe(0);
  });

  it('sin cuenta de tesorería ni regla, usa la cuenta configurada para el medio al pagar', async () => {
    const service = crear(
      orden({ monto_total: 500_000, orden_pago_proveedor_det: [det({ monto: 500_000, medio_pago_id: 'mp-efectivo' })] }),
      { pago: new Map([['mp-efectivo', 'cta-fondo-fijo']]) },
    );

    await service.integrarOrdenPago('op-1');

    expect(enCuenta(lineasGrabadas(service), 'cta-fondo-fijo', 'haber_moneda')).toBe(500_000);
  });

  it('la cuenta de tesorería elegida manda sobre la configurada para el medio', async () => {
    const service = crear(
      orden({
        monto_total: 700_000,
        orden_pago_proveedor_det: [det({ monto: 700_000, medio_pago_id: 'mp-transf', cuenta_bancaria_id: 'tes-banco' })],
      }),
      { pago: new Map([['mp-transf', 'cta-otra']]), tesCuentas: [{ id: 'tes-banco', cuenta_contable_id: 'cta-banco' }] },
    );

    await service.integrarOrdenPago('op-1');

    expect(enCuenta(lineasGrabadas(service), 'cta-banco', 'haber_moneda')).toBe(700_000);
  });

  it('en USD cancela la deuda a la cotización de la compra y registra la pérdida', async () => {
    // Compra de 1.100 USD a 7.300, pagada a 7.400.
    const service = crear(
      orden({
        moneda: { codigo: 'USD' },
        cotizacion: 7_400,
        monto_total: 1_100,
        orden_pago_proveedor_det: [det({ monto: 1_100, cuenta_bancaria_id: 'tes-usd' })],
      }),
      { tesCuentas: [{ id: 'tes-usd', cuenta_contable_id: 'cta-banco-usd' }], compras: [{ id: 'compra-1', cotizacion: 7_300 }] },
    );

    await service.integrarOrdenPago('op-1');
    const lineas = lineasGrabadas(service);

    expect(enCuenta(lineas, 'cta-proveedores', 'debe_moneda')).toBe(8_030_000);
    expect(enCuenta(lineas, 'cta-banco-usd', 'haber_moneda')).toBe(8_140_000);
    expect(enCuenta(lineas, cuentaDe('DIFERENCIA_CAMBIO_PERDIDA'), 'debe_moneda')).toBe(110_000);
    expect(totales(lineas).debe).toBe(totales(lineas).haber);
  });
});
