import { crearIntegracionMock } from './__helpers__/integracion-mock';
import { MapeoMediosPagoService } from './mapeo-medios-pago.service';

/**
 * Mapeo de medio de pago con cuenta desactivada o de agrupación (2026-10-14).
 *
 * Antes `resolverPorMedio` lo ignoraba con un `logger.warn` y el asiento salía
 * con la cuenta del concepto (CAJA_GENERAL y compañía): el sistema elegía una
 * cuenta en silencio. Ahora, si el documento USA ese medio, no hay asiento: queda
 * constancia y aviso con un detalle que nombra el medio y la cuenta. Un medio con
 * mapeo roto que el documento no usa no cambia nada.
 *
 * El integrador corre con el `MapeoMediosPagoService` real sobre un Prisma de
 * mentira: la tabla `cont_mapeo_medio_pago` y los medios.
 */

const TRANSF = 'mp-transf';
const EFECTIVO = 'mp-efectivo';
const MEDIOS = [
  { id: EFECTIVO, codigo: 1, descripcion: 'Efectivo' },
  { id: TRANSF, codigo: 5, descripcion: 'Transferencia' },
];

type Mapeo = { medio: string; flujo: 'COBRO' | 'PAGO'; cuenta: string; codigo: string; active?: boolean; acepta_movimientos?: boolean };

/** Integrador con el servicio de medios real; `mapeos` es lo que la empresa configuró. */
const crear = (prisma: any, mapeos: Mapeo[], faltan: string[] = []) => {
  const s = crearIntegracionMock(prisma, faltan);
  const medios = Object.create(MapeoMediosPagoService.prototype) as any;
  medios.logger = { warn: jest.fn(), log: jest.fn() };
  medios.prisma = {
    cont_mapeo_medio_pago: {
      findMany: jest.fn().mockImplementation(async ({ where }: any) =>
        mapeos
          .filter((m) => m.flujo === where.flujo)
          .map((m) => ({
            medio_pago_id: m.medio,
            flujo: m.flujo,
            cuenta_contable_id: m.cuenta,
            medio_pago: { codigo: MEDIOS.find((x) => x.id === m.medio)?.codigo, descripcion: MEDIOS.find((x) => x.id === m.medio)?.descripcion },
            cuenta: { codigo: m.codigo, descripcion: 'Cuenta', active: m.active ?? true, acepta_movimientos: m.acepta_movimientos ?? true },
          })),
      ),
    },
    medio_pago: {
      findMany: jest.fn().mockImplementation(async ({ where }: any) => MEDIOS.filter((m) => where.id.in.includes(m.id))),
      findFirst: jest.fn().mockResolvedValue(null), // sin cheque en estos documentos
    },
  };
  s.mediosPago = medios;
  return s;
};

const lineas = (s: any) => s.asientos.crearConfirmado.mock.calls[0][3].lineas;
const aviso = (s: any) => s.avisosMapeo.registrarFaltaMapeo.mock.calls[0][0];

const TRANSF_DESACTIVADA: Mapeo = { medio: TRANSF, flujo: 'COBRO', cuenta: 'cta-vieja', codigo: '1.01.02', active: false };
const TRANSF_ACTIVA: Mapeo = { medio: TRANSF, flujo: 'COBRO', cuenta: 'cta-banco', codigo: '1.01.03' };

// ── Venta contado ───────────────────────────────────────────────────────────
describe('venta contado', () => {
  const factura = (formas: any[]) => ({
    id: 'f1', empresa_id: 'emp', estado: 'Aprobado', cliente_id: 'cli1', dest: '001', dpunexp: '001', dnumdoc: '0000010',
    dfeemide: new Date('2026-10-13'), cobro_diferido: false, condicion_operacion: { codigo: 1 }, moneda: { codigo: 'PYG' }, dticam: 1,
    factura_subtotales: [{ dtotgralope: 1100, diva10: 100, diva5: 0, dsub10: 1100, dsub5: 0, dsubexe: 0, dsubexo: 0 }],
    factura_forma_pagos: formas,
  });
  const base = (formas: any[]) => ({
    factura_cab: { findUnique: jest.fn().mockResolvedValue(factura(formas)) },
    inm_cobros: { findFirst: jest.fn().mockResolvedValue(null) },
  });
  const conTransferencia = [{ medio_pago_id: TRANSF, dmontipag: 1100, dticamtipag: null }];

  it('cobrada por un medio con cuenta desactivada: sin asiento y aviso que nombra medio y cuenta', async () => {
    const s = crear(base(conTransferencia), [TRANSF_DESACTIVADA]);

    await expect(s.integrarFacturaVenta('f1')).resolves.toBeUndefined();

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({
      empresaId: 'emp', origenTipo: 'factura_cab', origenId: 'f1', conceptos: ['MEDIO_PAGO_COBRO'], motivo: 'CUENTA_INUTILIZABLE',
    });
    expect(aviso(s).detalles.MEDIO_PAGO_COBRO).toContain('la cuenta 1.01.02 del medio Transferencia');
    expect(aviso(s).detalles.MEDIO_PAGO_COBRO).toContain('está desactivada');
    expect(aviso(s).detalles.MEDIO_PAGO_COBRO).toContain('Cuentas por medio de pago');
  });

  it('una cuenta de agrupación tampoco sirve', async () => {
    const s = crear(base(conTransferencia), [{ ...TRANSF_DESACTIVADA, active: true, acepta_movimientos: false }]);

    await s.integrarFacturaVenta('f1');

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s).detalles.MEDIO_PAGO_COBRO).toContain('es de agrupación');
  });

  it('el mismo documento con el medio mapeado a una cuenta activa: igual que hoy', async () => {
    const s = crear(base(conTransferencia), [TRANSF_ACTIVA]);

    await s.integrarFacturaVenta('f1');

    expect(lineas(s).find((l: any) => l.debe_moneda > 0)).toMatchObject({ cuenta_id: 'cta-banco', debe_moneda: 1100, mapeo_nivel: 'MEDIO_PAGO' });
    expect(s.avisosMapeo.registrarFaltaMapeo).not.toHaveBeenCalled();
  });

  it('un medio con mapeo roto que la factura no usa no cambia nada', async () => {
    const s = crear(base([{ medio_pago_id: EFECTIVO, dmontipag: 1100, dticamtipag: null }]), [TRANSF_DESACTIVADA]);

    await s.integrarFacturaVenta('f1');

    // Efectivo sin mapeo: el concepto de siempre.
    expect(lineas(s).find((l: any) => l.debe_moneda > 0)).toMatchObject({ cuenta_id: 'cta-CAJA_GENERAL', debe_moneda: 1100 });
    expect(s.avisosMapeo.registrarFaltaMapeo).not.toHaveBeenCalled();
  });

  it('el medio roto con monto 0 tampoco frena', async () => {
    const s = crear(
      base([
        { medio_pago_id: EFECTIVO, dmontipag: 1100, dticamtipag: null },
        { medio_pago_id: TRANSF, dmontipag: 0, dticamtipag: null },
      ]),
      [TRANSF_DESACTIVADA],
    );

    await s.integrarFacturaVenta('f1');

    expect(s.asientos.crearConfirmado).toHaveBeenCalled();
  });
});

// ── Recibo simple ───────────────────────────────────────────────────────────
describe('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 = (medio: string) => ({
    recibos_cobro: { findUnique: jest.fn().mockResolvedValue(cobro) },
    recibo_cobro_detalle: { findMany: jest.fn().mockResolvedValue([{ medio_pago_id: medio, monto_pagado: 1_000 }]) },
  });

  it('cobrado con el medio roto: sin asiento y aviso', async () => {
    const s = crear(base(TRANSF), [TRANSF_DESACTIVADA]);

    await expect(s.integrarCobro('c1')).resolves.toBeUndefined();

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({ origenTipo: 'recibos_cobro', conceptos: ['MEDIO_PAGO_COBRO'], motivo: 'CUENTA_INUTILIZABLE' });
  });

  it('con el medio activo o sin usarlo: igual que hoy', async () => {
    const activo = crear(base(TRANSF), [TRANSF_ACTIVA]);
    await activo.integrarCobro('c1');
    expect(lineas(activo).find((l: any) => l.descripcion === 'Cobranza').cuenta_id).toBe('cta-banco');

    const otro = crear(base(EFECTIVO), [TRANSF_DESACTIVADA]);
    await otro.integrarCobro('c1');
    expect(lineas(otro).find((l: any) => l.descripcion === 'Cobranza').cuenta_id).toBe('cta-CAJA_GENERAL');
  });
});

// ── Recibo con varios medios ────────────────────────────────────────────────
describe('recibo multi', () => {
  const recibo = (medios: any[]) => ({
    id: 'rec-1', estado: 'emitido', modo: 'MULTI', numero_recibo: '001-001-0000001', fecha_emision: new Date('2026-10-13'),
    usuario_id: null, empresa_id: 'emp', moneda: { codigo: 'PYG' }, cotizacion: null, monto_total: 900, total_facturas: 900,
    total_intereses: 0, total_facturas_compra: 0, diferencia: 0, diferencia_tipo: 'EXACTO', facturas_multi: [],
    nc_aplicadas_multi: [], retenciones_multi: [], medios_pago_multi: medios, facturas_compra_aplicadas: [], gastos_asociados: [],
  });
  const base = (medios: any[]) => ({
    recibos_cobro: { findUnique: jest.fn().mockResolvedValue(recibo(medios)) },
    tes_cuentas: { findMany: jest.fn().mockResolvedValue([{ id: 'tes-banco', cuenta_contable_id: 'cta-tes' }]) },
    factura_cab: { findMany: jest.fn().mockResolvedValue([]) },
  });

  it('transferencia sin cuenta de tesorería y con el medio roto: sin asiento y aviso', async () => {
    const s = crear(base([{ medio: 'TRANSFERENCIA', monto: 900 }]), [TRANSF_DESACTIVADA]);

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

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({ origenTipo: 'recibos_multi', conceptos: ['MEDIO_PAGO_COBRO'], motivo: 'CUENTA_INUTILIZABLE' });
    expect(aviso(s).detalles.MEDIO_PAGO_COBRO).toContain('Transferencia');
  });

  it('un medio roto no tapa las demás faltas: la constancia lista todo y el detalle nombra cada medio roto', async () => {
    const s = crear(
      base([
        { medio: 'TRANSFERENCIA', monto: 400 },
        { medio: 'EFECTIVO', monto: 300 },
        { medio: 'CHEQUE', monto: 200, cheque_fecha_vencimiento: null },
      ]),
      [TRANSF_DESACTIVADA, { medio: EFECTIVO, flujo: 'COBRO', cuenta: 'cta-grupo', codigo: '1.01.01', acepta_movimientos: false }],
      ['CHEQUES_EN_CARTERA'],
    );

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

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(s.avisosMapeo.registrarFaltaMapeo).toHaveBeenCalledTimes(1);
    expect(aviso(s).conceptos).toEqual(expect.arrayContaining(['MEDIO_PAGO_COBRO', 'CHEQUES_EN_CARTERA']));
    expect(aviso(s).conceptos).toHaveLength(2);
    // Varias faltas mezcladas: no es solo una cuenta que no sirve.
    expect(aviso(s).motivo).toBeUndefined();
    const detalle = aviso(s).detalles.MEDIO_PAGO_COBRO;
    expect(detalle).toContain('la cuenta 1.01.02 del medio Transferencia (al cobrar) está desactivada');
    expect(detalle).toContain('la cuenta 1.01.01 del medio Efectivo (al cobrar) es de agrupación');
    // La ruta se dice una sola vez.
    expect(detalle.match(/Cuentas por medio de pago/g)).toHaveLength(1);
  });

  it('con la cuenta de tesorería elegida el mapeo del medio no se consulta: igual que hoy', async () => {
    const s = crear(base([{ medio: 'TRANSFERENCIA', monto: 900, cuenta_tesoreria_id: 'tes-banco' }]), [TRANSF_DESACTIVADA]);

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

    expect(lineas(s).find((l: any) => l.debe_moneda > 0).cuenta_id).toBe('cta-tes');
  });

  it('con el medio activo o sin usarlo: igual que hoy', async () => {
    const activo = crear(base([{ medio: 'TRANSFERENCIA', monto: 900 }]), [TRANSF_ACTIVA]);
    await activo.integrarReciboMulti('rec-1', 'emp');
    expect(lineas(activo).find((l: any) => l.debe_moneda > 0).cuenta_id).toBe('cta-banco');

    const otro = crear(base([{ medio: 'EFECTIVO', monto: 900 }]), [TRANSF_DESACTIVADA]);
    await otro.integrarReciboMulti('rec-1', 'emp');
    expect(lineas(otro).find((l: any) => l.debe_moneda > 0).cuenta_id).toBe('cta-CAJA_GENERAL');
  });
});

// ── Orden de pago ───────────────────────────────────────────────────────────
describe('orden de pago', () => {
  const orden = (medio: string, cuentaBancaria: string | null = null) => ({
    id: 'op-1', empresa_id: 'emp', estado: 'PAGADO', anulado: false, numero_orden_pago: '1', proveedor_id: 'prov-1', usuario_id: null,
    fecha_pago: new Date('2026-10-13'), moneda: { codigo: 'PYG' }, cotizacion: 1, monto_total: 500,
    orden_pago_proveedor_det: [{
      monto: 500, medio_pago_id: medio, medio_pago: { codigo: medio === TRANSF ? 5 : 1 }, cuenta_bancaria_id: cuentaBancaria,
      cuentas_pagar: { id: 'cxp-1', cuenta_pasivo_id: 'cta-proveedores', compra_id: null, origen_tipo: 'compra', origen_id: null },
    }],
  });
  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: 'tes-banco', cuenta_contable_id: 'cta-tes' }]) },
    compra_cab: { findMany: jest.fn().mockResolvedValue([]) },
    gasto_cab: { findMany: jest.fn().mockResolvedValue([]) },
  });
  const PAGO_ROTO: Mapeo = { ...TRANSF_DESACTIVADA, flujo: 'PAGO' };

  it('pagada con el medio roto: sin asiento y aviso (al pagar)', async () => {
    const s = crear(base(orden(TRANSF)), [PAGO_ROTO]);

    await expect(s.integrarOrdenPago('op-1')).resolves.toBeUndefined();

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({ origenTipo: 'orden_pago_proveedor_cab', conceptos: ['MEDIO_PAGO_PAGO'], motivo: 'CUENTA_INUTILIZABLE' });
    expect(aviso(s).detalles.MEDIO_PAGO_PAGO).toContain('al pagar');
  });

  it('el mismo medio roto solo al cobrar no afecta el pago', async () => {
    const s = crear(base(orden(TRANSF)), [TRANSF_DESACTIVADA]);

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

    expect(lineas(s).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe('cta-CAJA_GENERAL');
  });

  it('con la cuenta bancaria del detalle o sin usar el medio: igual que hoy', async () => {
    const conBanco = crear(base(orden(TRANSF, 'tes-banco')), [PAGO_ROTO]);
    await conBanco.integrarOrdenPago('op-1');
    expect(lineas(conBanco).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe('cta-tes');

    const otro = crear(base(orden(EFECTIVO)), [PAGO_ROTO]);
    await otro.integrarOrdenPago('op-1');
    expect(lineas(otro).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe('cta-CAJA_GENERAL');
  });
});

// ── Pago a proveedor y compra contado: mismo camino ─────────────────────────
describe('pago a proveedor', () => {
  const pago = { id: 'pg1', empresa_id: 'emp', proveedor_id: 'p1', moneda: { codigo: 'PYG' }, cotizacion: 1, monto: 500,
    fecha_pago: new Date('2026-10-13'), numero_recibo: 'R1', referencia: null, usuario_id: null, medio_pago_id: TRANSF,
    cuentas_pagar: { cuenta_pasivo_id: 'cta-proveedores' } };

  it('pagado con el medio roto: sin asiento y aviso', async () => {
    const s = crear({ pagos_proveedor: { findUnique: jest.fn().mockResolvedValue(pago) } }, [{ ...TRANSF_DESACTIVADA, flujo: 'PAGO' }]);

    await s.integrarPago('pg1');

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({ conceptos: ['MEDIO_PAGO_PAGO'], motivo: 'CUENTA_INUTILIZABLE' });
  });
});

describe('compra contado', () => {
  const compra = { id: 'c1', empresa_id: 'emp', sucursal_id: null, condicion_operacion: { codigo: 1 }, 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: '1', estado: 'confirmada' };
  const base = (medio: string) => ({
    compra_cab: { findUnique: jest.fn().mockResolvedValue(compra) },
    config_compras: { findUnique: jest.fn().mockResolvedValue(null) },
    proveedores: { findFirst: jest.fn().mockResolvedValue({ tipo_entidad: 'PROVEEDOR_LOCAL', cuenta_contable_id: null }) },
    compra_forma_pagos: { findMany: jest.fn().mockResolvedValue([{ medio_pago_id: medio, monto: 1100 }]) },
  });
  const PAGO_ROTO: Mapeo = { ...TRANSF_DESACTIVADA, flujo: 'PAGO' };

  it('pagada con el medio roto: sin asiento y aviso', async () => {
    const s = crear(base(TRANSF), [PAGO_ROTO]);

    await expect(s.integrarFacturaCompra('c1')).resolves.toBeUndefined();

    expect(s.asientos.crearConfirmado).not.toHaveBeenCalled();
    expect(aviso(s)).toMatchObject({ origenTipo: 'compra_cab', conceptos: ['MEDIO_PAGO_PAGO'], motivo: 'CUENTA_INUTILIZABLE' });
  });

  it('con el medio activo o sin usarlo: igual que hoy', async () => {
    const activo = crear(base(TRANSF), [{ ...TRANSF_ACTIVA, flujo: 'PAGO' }]);
    await activo.integrarFacturaCompra('c1');
    expect(lineas(activo).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe('cta-banco');

    const otro = crear(base(EFECTIVO), [PAGO_ROTO]);
    await otro.integrarFacturaCompra('c1');
    expect(lineas(otro).find((l: any) => l.haber_moneda > 0).cuenta_id).toBe('cta-CAJA_GENERAL');
  });
});

// ── El servicio ─────────────────────────────────────────────────────────────
describe('MapeoMediosPagoService.resolverPorMedio', () => {
  it('devuelve las cuentas usables y aparte los medios con cuenta que no sirve', async () => {
    const s = crear({}, [TRANSF_DESACTIVADA, { medio: EFECTIVO, flujo: 'COBRO', cuenta: 'cta-recaud', codigo: '1.01.01.02' }]);

    const mapa = await s.mediosPago.resolverPorMedio('emp', 'COBRO');

    expect([...mapa]).toEqual([[EFECTIVO, 'cta-recaud']]);
    expect(mapa.inutilizables.get(TRANSF)).toMatchObject({ medio: 'Transferencia', cuentaCodigo: '1.01.02', motivo: 'está desactivada' });
  });

  it('resolverPorCodigoMedio conserva los inutilizables por código SIFEN', async () => {
    const s = crear({}, [TRANSF_DESACTIVADA]);

    const mapa = await s.mediosPago.resolverPorCodigoMedio('emp', 'COBRO');

    expect(mapa.size).toBe(0);
    expect(mapa.inutilizables.get(5)).toMatchObject({ medio: 'Transferencia' });
  });
});
