// Tests unitarios del motor de finiquito (Código del Trabajo Paraguay).
// Cubren los casos obligatorios del plan-rrhh.md:
//   - escala de preaviso por antigüedad (Art. 87)
//   - indemnización 15 días por año cumplido (Art. 91-94)
//   - fracción >= 6 meses cuenta como año completo
//   - reglas distintas por tipo de desvinculación (renuncia vs despido injustificado vs fallecimiento)
//   - vacaciones proporcionales según escala anual (Art. 219)
//   - aguinaldo proporcional (Art. 244): salario × meses_trabajados / 12
//   - salario pendiente override y vacaciones no gozadas override
//   - validación: empleado inexistente o sin fecha_ingreso
//   - finiquito al pagar marca empleado como EGRESADO con fecha_egreso

import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { DesvinculacionesService } from './desvinculaciones.service';

const EMPRESA_ID = 'empresa-1';

const empleado = (overrides: any = {}) => ({
  id: 'emp-1',
  empresa_id: EMPRESA_ID,
  numero_empleado: 'EMP-001',
  cedula_identidad: '1234567',
  nombres: 'Juan',
  apellidos: 'Pérez',
  salario_base: 3_000_000,
  fecha_ingreso: new Date('2020-01-01'),
  fecha_egreso: null,
  estado: 'ACTIVO',
  centro_costo_id: 'cc-1',
  ...overrides,
});

const buildPrisma = (overrides: any = {}) => {
  const emp = overrides.empleado ?? empleado();
  return {
    rrhh_empleados: {
      findFirst: jest.fn().mockResolvedValue(emp),
      update: jest.fn().mockResolvedValue(emp),
    },
    rrhh_vacaciones_saldo: {
      findFirst: jest.fn().mockResolvedValue(overrides.saldoPrev ?? null),
    },
    rrhh_desvinculaciones: {
      findFirst: jest.fn().mockResolvedValue(overrides.prevDesv ?? null),
      create: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: 'desv-1', ...data })),
      update: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: 'desv-1', ...data })),
    },
    $transaction: jest.fn().mockImplementation((cb: any) => cb({
      rrhh_desvinculaciones: {
        update: jest.fn().mockImplementation(({ data }: any) =>
          Promise.resolve({
            id: 'desv-1',
            empleado_id: emp.id,
            fecha_ultimo_dia: overrides.fechaUltimo ?? new Date('2026-05-31'),
            ...data,
          }),
        ),
      },
      rrhh_empleados: {
        update: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: emp.id, ...data })),
      },
    })),
  };
};

const createService = (overrides: any = {}) => {
  const prisma = buildPrisma(overrides);
  const legajosCompletitud = overrides.legajosCompletitud ?? undefined;
  const parametros = overrides.parametros ?? undefined;
  const service = new DesvinculacionesService(
    prisma as any,
    legajosCompletitud as any,
    parametros as any,
    overrides.contabilidad,
    overrides.consumoFuncionario,
    overrides.recibos,
  );
  return { service, prisma, legajosCompletitud, parametros };
};

describe('DesvinculacionesService — calcularPreview', () => {
  // ── Antigüedad y preaviso ──────────────────────────────
  it.each([
    [new Date('2025-09-01'), 30, 'menos de 1 año'],
    [new Date('2023-01-01'), 45, '1-5 años'],
    [new Date('2018-01-01'), 60, '5-10 años'],
    [new Date('2010-01-01'), 90, 'más de 10 años'],
  ])(
    'preaviso según antigüedad: ingreso %s → %d días (%s)',
    async (fechaIngreso, diasEsperados) => {
      const { service } = createService({ empleado: empleado({ fecha_ingreso: fechaIngreso }) });
      const r = await service.calcularPreview(EMPRESA_ID, {
        empleado_id: 'emp-1',
        tipo_desvinculacion: 'DESPIDO_INJUSTIFICADO',
        fecha_ultimo_dia: '2026-05-31',
      });
      expect(r.dias_preaviso).toBe(diasEsperados);
      expect(r.preaviso_omitido).toBe(true);
      // monto = jornal_diario × días
      expect(r.monto_preaviso).toBeCloseTo(r.jornal_diario_calculo * diasEsperados, 0);
    },
  );

  // ── Tipos de desvinculación: preaviso e indemnización ───
  it('RENUNCIA: sin preaviso y sin indemnización', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    expect(r.preaviso_omitido).toBe(false);
    expect(r.dias_preaviso).toBe(0);
    expect(r.monto_preaviso).toBe(0);
    expect(r.anios_indemnizacion).toBe(0);
    expect(r.monto_indemnizacion).toBe(0);
  });

  it('DESPIDO_JUSTIFICADO: sin preaviso y sin indemnización (causal grave)', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'DESPIDO_JUSTIFICADO',
      fecha_ultimo_dia: '2026-05-31',
    });
    expect(r.monto_preaviso).toBe(0);
    expect(r.monto_indemnizacion).toBe(0);
  });

  it('DESPIDO_INJUSTIFICADO: paga preaviso (si omitido) + indemnización 15 días/año', async () => {
    // Ingreso 2020-01-01 → al 2026-05-31 = 6 años 5 meses (no llega a 6 + redondeo de 6m)
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2020-01-01') }),
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'DESPIDO_INJUSTIFICADO',
      fecha_ultimo_dia: '2026-05-31',
    });
    // 6 años cumplidos, 5 meses (< 6) → 6 años de indemnización (no redondea arriba)
    expect(r.anios_indemnizacion).toBe(6);
    // monto = jornal_diario × 15 × 6
    expect(r.monto_indemnizacion).toBeCloseTo(r.jornal_diario_calculo * 15 * 6, 0);
  });

  it('fracción ≥ 6 meses se redondea como año completo de indemnización', async () => {
    // Ingreso 2020-01-01 → al 2026-08-15 = 6 años 7 meses (≥ 6 meses)
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2020-01-01') }),
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'DESPIDO_INJUSTIFICADO',
      fecha_ultimo_dia: '2026-08-15',
    });
    expect(r.anios_indemnizacion).toBe(7); // 6 + 1 por redondeo
  });

  it('FALLECIMIENTO: paga preaviso + indemnización (heredero)', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'FALLECIMIENTO',
      fecha_ultimo_dia: '2026-05-31',
    });
    expect(r.preaviso_omitido).toBe(true);
    expect(r.monto_indemnizacion).toBeGreaterThan(0);
  });

  it('MUTUO_ACUERDO: aplica indemnización (lo pactado) sin preaviso por defecto', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'MUTUO_ACUERDO',
      fecha_ultimo_dia: '2026-05-31',
    });
    expect(r.preaviso_omitido).toBe(false);
    expect(r.monto_preaviso).toBe(0);
    expect(r.monto_indemnizacion).toBeGreaterThan(0);
  });

  it('override de preaviso_omitido=true en RENUNCIA: el usuario fuerza el pago', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
      preaviso_omitido: 'true',
    });
    expect(r.preaviso_omitido).toBe(true);
    expect(r.monto_preaviso).toBeGreaterThan(0);
  });

  // ── Vacaciones proporcionales (Art. 219) ────────────────
  it('vacaciones proporcionales: <5 años → 12 días anuales', async () => {
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2024-01-01') }), // ~2.4 años al 2026-05-31
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    // del 1-ene al 31-may = 151 días trabajados en el año
    // vacaciones proporcionales = 12 × 151 / 365 ≈ 4.96 días
    expect(r.dias_vacaciones_proporcional).toBeGreaterThan(4);
    expect(r.dias_vacaciones_proporcional).toBeLessThan(6);
    expect(r.monto_vacaciones_proporcional).toBeCloseTo(
      r.jornal_diario_calculo * r.dias_vacaciones_proporcional,
      0,
    );
  });

  it('vacaciones proporcionales: 5-10 años → 18 días anuales', async () => {
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2019-01-01') }), // ~7.4 años
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    // 18 × 151 / 365 ≈ 7.44 días
    expect(r.dias_vacaciones_proporcional).toBeGreaterThan(7);
    expect(r.dias_vacaciones_proporcional).toBeLessThan(8);
  });

  it('vacaciones proporcionales: >10 años → 30 días anuales', async () => {
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2010-01-01') }), // 16+ años
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    // 30 × 151 / 365 ≈ 12.41 días
    expect(r.dias_vacaciones_proporcional).toBeGreaterThan(12);
    expect(r.dias_vacaciones_proporcional).toBeLessThan(13);
  });

  it('vacaciones no gozadas: se leen del saldo del año previo si no se especifican', async () => {
    const { service } = createService({
      saldoPrev: { dias_pendientes: 5, cerrado: false },
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    expect(r.dias_vacaciones_no_gozadas).toBe(5);
    expect(r.monto_vacaciones_no_gozadas).toBeCloseTo(r.jornal_diario_calculo * 5, 0);
  });

  it('vacaciones no gozadas: override manual tiene prioridad sobre el saldo', async () => {
    const { service } = createService({
      saldoPrev: { dias_pendientes: 5, cerrado: false },
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
      dias_vacaciones_no_gozadas: 12,
    });
    expect(r.dias_vacaciones_no_gozadas).toBe(12);
  });

  // ── Aguinaldo proporcional (Art. 244) ────────────────────
  it('aguinaldo proporcional: salario × meses_trabajados_en_anio / 12', async () => {
    const { service } = createService({
      empleado: empleado({ salario_base: 3_000_000, fecha_ingreso: new Date('2020-01-01') }),
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });
    // del 1-ene al 31-may → 5 meses completos
    expect(r.aguinaldo_proporcional).toBeCloseTo((3_000_000 * 5) / 12, 0);
  });

  it('aguinaldo proporcional: override manual', async () => {
    const { service } = createService();
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
      aguinaldo_proporcional_override: 1_500_000,
    });
    expect(r.aguinaldo_proporcional).toBe(1_500_000);
  });

  // ── Total y validaciones ────────────────────────────────
  it('total = salario_pendiente + vac proporcional + vac no gozadas + aguinaldo + preaviso + indemnización', async () => {
    const { service } = createService({
      empleado: empleado({ salario_base: 3_000_000, fecha_ingreso: new Date('2020-01-01') }),
    });
    const r = await service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'DESPIDO_INJUSTIFICADO',
      fecha_ultimo_dia: '2026-05-31',
      salario_pendiente_override: 200_000,
      dias_vacaciones_no_gozadas: 5,
    });
    const sumaEsperada =
      r.salario_pendiente +
      r.monto_vacaciones_proporcional +
      r.monto_vacaciones_no_gozadas +
      r.aguinaldo_proporcional +
      r.monto_preaviso +
      r.monto_indemnizacion;
    expect(r.total_liquidacion_final).toBeCloseTo(sumaEsperada, 0);
  });

  it('empleado inexistente → NotFoundException', async () => {
    const prisma = buildPrisma();
    prisma.rrhh_empleados.findFirst = jest.fn().mockResolvedValue(null);
    const service = new DesvinculacionesService(prisma as any);
    await expect(
      service.calcularPreview(EMPRESA_ID, {
        empleado_id: 'emp-no-existe',
        tipo_desvinculacion: 'RENUNCIA',
        fecha_ultimo_dia: '2026-05-31',
      }),
    ).rejects.toThrow(NotFoundException);
  });

  it('fecha_ingreso ausente → BadRequestException', async () => {
    const { service } = createService({ empleado: empleado({ fecha_ingreso: null }) });
    await expect(
      service.calcularPreview(EMPRESA_ID, {
        empleado_id: 'emp-1',
        tipo_desvinculacion: 'RENUNCIA',
        fecha_ultimo_dia: '2026-05-31',
      }),
    ).rejects.toThrow(BadRequestException);
  });

  it('fecha_ultimo_dia anterior al ingreso → BadRequestException', async () => {
    const { service } = createService({
      empleado: empleado({ fecha_ingreso: new Date('2025-01-01') }),
    });
    await expect(
      service.calcularPreview(EMPRESA_ID, {
        empleado_id: 'emp-1',
        tipo_desvinculacion: 'RENUNCIA',
        fecha_ultimo_dia: '2020-05-31',
      }),
    ).rejects.toThrow(BadRequestException);
  });
});

describe('DesvinculacionesService — crear', () => {
  it('bloquea creación si ya existe un finiquito no anulado para el empleado', async () => {
    const { service } = createService({
      prevDesv: { id: 'desv-prev', estado: 'CALCULADO' },
    });
    await expect(
      service.crear(EMPRESA_ID, {
        empleado_id: 'emp-1',
        tipo_desvinculacion: 'RENUNCIA',
        fecha_ultimo_dia: '2026-05-31',
      }),
    ).rejects.toThrow(ConflictException);
  });

  it('crea en estado CALCULADO con los montos del preview', async () => {
    const { service, prisma } = createService();
    const desv = await service.crear(
      EMPRESA_ID,
      {
        empleado_id: 'emp-1',
        tipo_desvinculacion: 'DESPIDO_INJUSTIFICADO',
        fecha_ultimo_dia: '2026-05-31',
      },
      'user-1',
    );
    expect(desv.estado).toBe('CALCULADO');
    expect(prisma.rrhh_desvinculaciones.create).toHaveBeenCalled();
    const data = (prisma.rrhh_desvinculaciones.create as jest.Mock).mock.calls[0][0].data;
    expect(data.empleado_id).toBe('emp-1');
    expect(data.tipo_desvinculacion).toBe('DESPIDO_INJUSTIFICADO');
    expect(data.creado_por).toBe('user-1');
    expect(Number(data.total_liquidacion_final)).toBeGreaterThan(0);
  });
});

describe('DesvinculacionesService — workflow M14 (legajo completitud)', () => {
  it('aprobar: retorna advertencias si legajo incompleto y parámetro no bloquea', async () => {
    const legajosCompletitud = {
      verificarBloquea: jest.fn().mockResolvedValue({
        faltantes: [{ codigo: 'FOTO_PERFIL', nombre: 'Foto perfil' }],
        vencidos: [],
      }),
    };
    const parametros = {
      obtenerVigente: jest.fn().mockResolvedValue({ valor: 'false' }),
    };
    const { service } = createService({
      prevDesv: {
        id: 'desv-1',
        empresa_id: EMPRESA_ID,
        empleado_id: 'emp-1',
        estado: 'CALCULADO',
        observaciones: null,
      },
      legajosCompletitud,
      parametros,
    });

    const aprobado = await service.aprobar('desv-1', EMPRESA_ID, {});

    expect(aprobado.estado).toBe('APROBADO');
    expect(aprobado.advertencias).toBeDefined();
    expect(Array.isArray(aprobado.advertencias)).toBe(true);
    expect(aprobado.advertencias[0]).toContain('Legajo incompleto detectado');
  });

  it('pagar: bloquea con 422 si legajo incompleto y parámetro obliga bloqueo', async () => {
    const legajosCompletitud = {
      verificarBloquea: jest.fn().mockResolvedValue({
        faltantes: [{ codigo: 'CERTIFICADO_MED', nombre: 'Certificado médico' }],
        vencidos: [],
      }),
    };
    const parametros = {
      obtenerVigente: jest.fn().mockResolvedValue({ valor: 'true' }),
    };
    const { service, prisma } = createService({
      prevDesv: {
        id: 'desv-1',
        empresa_id: EMPRESA_ID,
        empleado_id: 'emp-1',
        estado: 'APROBADO',
        observaciones: null,
        fecha_ultimo_dia: new Date('2026-05-31'),
      },
      legajosCompletitud,
      parametros,
    });

    await expect(service.pagar('desv-1', EMPRESA_ID, {})).rejects.toMatchObject({
      status: 422,
    });
    expect(prisma.$transaction).not.toHaveBeenCalled();
  });
});


describe('DesvinculacionesService — consumo de funcionarios', () => {
  const preview = (service: DesvinculacionesService) =>
    service.calcularPreview(EMPRESA_ID, {
      empleado_id: 'emp-1',
      tipo_desvinculacion: 'RENUNCIA',
      fecha_ultimo_dia: '2026-05-31',
    });

  it('con el consumo apagado no descuenta la deuda del funcionario aunque tenga facturas', async () => {
    const consumoFuncionario = {
      estaHabilitado: jest.fn().mockResolvedValue(false),
      saldoTotal: jest.fn().mockResolvedValue(500_000),
    };
    const { service } = createService({ consumoFuncionario });
    const r = await preview(service);

    expect(r.consumo_descontado).toBe(0);
    expect(consumoFuncionario.saldoTotal).not.toHaveBeenCalled();
  });

  it('con el consumo activado lo descuenta del total y lo deja aparte de otros descuentos', async () => {
    const off = await preview(
      createService({ consumoFuncionario: { estaHabilitado: jest.fn().mockResolvedValue(false) } }).service,
    );
    const consumoFuncionario = {
      estaHabilitado: jest.fn().mockResolvedValue(true),
      saldoTotal: jest.fn().mockResolvedValue(150_000),
    };
    const r = await preview(createService({ consumoFuncionario }).service);

    expect(r.consumo_descontado).toBe(150_000);
    expect(r.otros_descuentos).toBe(0);
    expect(r.total_liquidacion_final).toBeCloseTo(off.total_liquidacion_final - 150_000, 2);
  });

  const desvAPagar = (extra: any = {}) => ({
    id: 'desv-1',
    empresa_id: EMPRESA_ID,
    empleado_id: 'emp-1',
    estado: 'APROBADO',
    observaciones: null,
    fecha_ultimo_dia: new Date('2026-05-31'),
    consumo_descontado: 30_000,
    consumo_recibo_id: null,
    ...extra,
  });

  const conCobro = (facturas: Array<{ cuenta_cobrar_id: string; saldo_pendiente: number }>, extra: any = {}) => {
    const tx = {
      rrhh_desvinculaciones: { update: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: 'desv-1', ...data })) },
      rrhh_empleados: { update: jest.fn().mockResolvedValue({}) },
      cuentas_cobrar: {
        findMany: jest.fn().mockResolvedValue(
          facturas.map((f) => ({ id: f.cuenta_cobrar_id, factura_venta_id: `fac-${f.cuenta_cobrar_id}`, saldo_pendiente: f.saldo_pendiente })),
        ),
        update: jest.fn().mockResolvedValue({}),
      },
      recibos_cobro: { create: jest.fn().mockResolvedValue({ id: 'rec-1' }) },
      recibo_cobro_facturas: { create: jest.fn().mockResolvedValue({}) },
    };
    const consumoFuncionario = {
      estaHabilitado: jest.fn().mockResolvedValue(true),
      facturasPendientes: jest.fn().mockResolvedValue(facturas),
    };
    const recibos = { _generarNumeroTx: jest.fn().mockResolvedValue('001-001-0000001') };
    const contabilidad = { integrarReciboMulti: jest.fn().mockResolvedValue(undefined) };
    const created = createService({
      prevDesv: desvAPagar(extra),
      empleado: empleado({ cliente_id: 'cli-1', sucursal_id: 'suc-1' }),
      consumoFuncionario,
      recibos,
      contabilidad,
    });
    (created.prisma as any).suscripcion_modulos = { findFirst: jest.fn().mockResolvedValue(null) };
    (created.prisma as any).$transaction = jest.fn((cb: any) => cb(tx));
    return { ...created, tx, consumoFuncionario, contabilidad };
  };

  it('al pagar cancela las facturas más viejas hasta cubrir lo descontado y emite el recibo', async () => {
    const { service, tx, contabilidad } = conCobro([
      { cuenta_cobrar_id: 'cc-vieja', saldo_pendiente: 20_000 },
      { cuenta_cobrar_id: 'cc-nueva', saldo_pendiente: 25_000 },
    ]);
    await service.pagar('desv-1', EMPRESA_ID, {}, 'u-1');

    expect(tx.cuentas_cobrar.update).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'cc-vieja' }, data: expect.objectContaining({ saldo_pendiente: 0, estado: 'cobrada' }) }),
    );
    expect(tx.cuentas_cobrar.update).toHaveBeenCalledWith(
      expect.objectContaining({ where: { id: 'cc-nueva' }, data: expect.objectContaining({ saldo_pendiente: 15_000, estado: 'pendiente' }) }),
    );
    expect(tx.recibos_cobro.create).toHaveBeenCalledWith({
      data: expect.objectContaining({
        monto_total: 30_000,
        cliente_id: 'cli-1',
        medios_pago_multi: { create: [{ medio: 'DESCUENTO_FINIQUITO', monto: 30_000 }] },
      }),
      select: { id: true },
    });
    expect(tx.rrhh_desvinculaciones.update).toHaveBeenCalledWith({ where: { id: 'desv-1' }, data: { consumo_recibo_id: 'rec-1' } });
    expect(contabilidad.integrarReciboMulti).toHaveBeenCalledWith('rec-1', EMPRESA_ID);
  });

  it('si hoy debe menos de lo descontado, frena el pago sin tocar nada', async () => {
    const { service, prisma, tx } = conCobro([{ cuenta_cobrar_id: 'cc-1', saldo_pendiente: 10_000 }]);

    await expect(service.pagar('desv-1', EMPRESA_ID, {})).rejects.toBeInstanceOf(ConflictException);
    expect((prisma as any).$transaction).not.toHaveBeenCalled();
    expect(tx.recibos_cobro.create).not.toHaveBeenCalled();
  });

  it('si el recibo ya se emitió no vuelve a cobrar', async () => {
    const { service, tx, consumoFuncionario } = conCobro(
      [{ cuenta_cobrar_id: 'cc-1', saldo_pendiente: 50_000 }],
      { consumo_recibo_id: 'rec-previo' },
    );
    await service.pagar('desv-1', EMPRESA_ID, {});

    expect(consumoFuncionario.facturasPendientes).not.toHaveBeenCalled();
    expect(tx.recibos_cobro.create).not.toHaveBeenCalled();
  });
});

describe('DesvinculacionesService — movimiento de tesorería del finiquito', () => {
  it('cada componente guarda su concepto contable (no la cuenta resuelta) y el total cuadra', async () => {
    const movCreate = jest.fn().mockResolvedValue({ id: 'mov1' });
    const prisma: any = {
      tes_cuentas: {
        findFirst: jest.fn().mockResolvedValue({ id: 'tes1', moneda: 'PYG' }),
        update: jest.fn().mockResolvedValue({}),
      },
      tes_movimientos: { findFirst: jest.fn().mockResolvedValue(null), create: movCreate },
      tes_categorias: { findFirst: jest.fn().mockResolvedValue({ id: 'cat-fin', cuenta_contable_id: 'cta-categoria' }) },
      $transaction: jest.fn().mockImplementation((fn: any) => fn(prisma)),
    };
    const contabilidad = { integrarMovimientoTesoreria: jest.fn().mockResolvedValue(undefined) };
    const service = new DesvinculacionesService(prisma, undefined, undefined, contabilidad as any);
    const d = (n: number) => new Prisma.Decimal(n);

    await (service as any).registrarMovimientoTesoreria({
      id: 'desv1',
      empresa_id: 'emp',
      cuenta_tesoreria_id: 'tes1',
      salario_pendiente: d(500),
      otros_haberes: d(0),
      otros_descuentos: d(200),
      consumo_descontado: d(0),
      monto_vacaciones_proporcional: d(0),
      monto_vacaciones_no_gozadas: d(0),
      aguinaldo_proporcional: d(0),
      monto_preaviso: d(1000),
      monto_indemnizacion: d(0),
      total_liquidacion_final: d(1300),
    });

    const detalles = movCreate.mock.calls[0][0].data.detalles.create;
    expect(detalles).toEqual([
      expect.objectContaining({ concepto_contable: 'PREAVISO', monto: 1000, cuenta_contable_id: null }),
      expect.objectContaining({ concepto_contable: 'SUELDOS_JORNALES', monto: 300, cuenta_contable_id: null }),
    ]);
    expect(detalles.reduce((s: number, x: any) => s + x.monto, 0)).toBe(movCreate.mock.calls[0][0].data.monto);
    expect(contabilidad.integrarMovimientoTesoreria).toHaveBeenCalledWith('mov1');
  });
});
