// 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 { 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,
  );
  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();
  });
});
