import { BadRequestException, ConflictException } from '@nestjs/common';
import { PresentismoEngineService } from './presentismo-engine.service';

// ── Helpers reutilizables para reducir boilerplate en los tests ──────
const empleado = (over: any = {}) => ({
  id: 'emp-1',
  sucursal_id: 'suc-1',
  departamento_id: 'dep-1',
  fecha_ingreso: new Date('2026-01-01'),
  fecha_egreso: null,
  ...over,
});

const turnoSimple = {
  turno_id: 'turno-1',
  turno: {
    bloques: [
      {
        orden: 1,
        hora_entrada: new Date('1970-01-01T08:00:00Z'),
        hora_salida: new Date('1970-01-01T17:00:00Z'),
      },
    ],
  },
};

const turnoCortado = {
  turno_id: 'turno-2',
  turno: {
    bloques: [
      {
        orden: 1,
        hora_entrada: new Date('1970-01-01T08:00:00Z'),
        hora_salida: new Date('1970-01-01T12:00:00Z'),
      },
      {
        orden: 2,
        hora_entrada: new Date('1970-01-01T13:30:00Z'),
        hora_salida: new Date('1970-01-01T17:30:00Z'),
      },
    ],
  },
};

describe('PresentismoEngineService', () => {
  let service: PresentismoEngineService;
  let prisma: any;
  let parametros: any;
  let resolver: any;

  beforeEach(() => {
    prisma = {
      rrhh_liquidaciones_cabecera: { findFirst: jest.fn() },
      rrhh_asistencia_novedades: {
        findFirst: jest.fn(),
        updateMany: jest.fn(),
        createMany: jest.fn(),
      },
      rrhh_empleados: { findMany: jest.fn() },
      rrhh_permisos_presentismo: { findMany: jest.fn() },
      rrhh_marcaciones_limpias: { findMany: jest.fn(), updateMany: jest.fn() },
      rrhh_presentismo_tolerancias_especiales: { findMany: jest.fn() },
    };
    parametros = { obtenerVigente: jest.fn().mockResolvedValue({ valor: '5' }) };
    resolver = { resolverTurnoVigente: jest.fn() };

    service = new PresentismoEngineService(prisma as any, parametros as any, resolver as any);
  });

  it('genera AUSENCIA_JUSTIFICADA cuando hay permiso JORNADA_COMPLETA', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([
      {
        id: 'emp-1',
        sucursal_id: 'suc-1',
        departamento_id: 'dep-1',
        fecha_ingreso: new Date('2026-01-01'),
        fecha_egreso: null,
      },
    ]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([
      {
        id: 'perm-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        tipo_permiso: 'JORNADA_COMPLETA',
        turno_bloque_orden: null,
        hora_desde: null,
        hora_hasta: null,
        minutos_tolerados: null,
      },
    ]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue({
      turno_id: 'turno-1',
      turno: {
        bloques: [
          {
            orden: 1,
            hora_entrada: new Date('1970-01-01T08:00:00Z'),
            hora_salida: new Date('1970-01-01T17:00:00Z'),
          },
        ],
      },
    });

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    expect(prisma.rrhh_asistencia_novedades.createMany).toHaveBeenCalledTimes(1);
    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          empleado_id: 'emp-1',
          tipo_novedad: 'AUSENCIA_JUSTIFICADA',
          permiso_id: 'perm-1',
          justificado: true,
          impacta_liquidacion: false,
        }),
      ]),
    );
  });

  it('genera AUSENCIA_BLOQUE cuando falta retorno en segundo bloque de turno cortado', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([
      {
        id: 'emp-1',
        sucursal_id: 'suc-1',
        departamento_id: 'dep-1',
        fecha_ingreso: new Date('2026-01-01'),
        fecha_egreso: null,
      },
    ]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:05:00Z'),
        hora_salida: new Date('1970-01-01T12:00:00Z'),
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue({
      turno_id: 'turno-2',
      turno: {
        bloques: [
          {
            orden: 1,
            hora_entrada: new Date('1970-01-01T08:00:00Z'),
            hora_salida: new Date('1970-01-01T12:00:00Z'),
          },
          {
            orden: 2,
            hora_entrada: new Date('1970-01-01T13:30:00Z'),
            hora_salida: new Date('1970-01-01T17:30:00Z'),
          },
        ],
      },
    });

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'AUSENCIA_BLOQUE',
          turno_bloque_orden: 2,
        }),
      ]),
    );
  });

  it('bloquea procesamiento si existe liquidación mensual cerrada del período', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue({
      id: 'liq-1',
      periodo_anio: 2026,
      periodo_mes: 5,
    });

    await expect(
      service.procesarPeriodo('empresa-1', {
        fechaDesde: '2026-05-01',
        fechaHasta: '2026-05-31',
      }),
    ).rejects.toBeInstanceOf(ConflictException);
  });

  it('genera TARDANZA_JUSTIFICADA cuando la tardanza está cubierta por permiso LLEGADA_TARDIA', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([
      {
        id: 'emp-1',
        sucursal_id: 'suc-1',
        departamento_id: 'dep-1',
        fecha_ingreso: new Date('2026-01-01'),
        fecha_egreso: null,
      },
    ]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([
      {
        id: 'perm-lt',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        tipo_permiso: 'LLEGADA_TARDIA',
        turno_bloque_orden: null,
        hora_desde: new Date('1970-01-01T08:00:00Z'),
        hora_hasta: new Date('1970-01-01T09:00:00Z'),
        minutos_tolerados: 30,
      },
    ]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:25:00Z'),
        hora_salida: new Date('1970-01-01T17:05:00Z'),
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue({
      turno_id: 'turno-1',
      turno: {
        bloques: [
          {
            orden: 1,
            hora_entrada: new Date('1970-01-01T08:00:00Z'),
            hora_salida: new Date('1970-01-01T17:00:00Z'),
          },
        ],
      },
    });

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'TARDANZA_JUSTIFICADA',
          permiso_id: 'perm-lt',
          justificado: true,
          impacta_liquidacion: false,
        }),
      ]),
    );
  });

  it('aplica tolerancia especial por sucursal antes que tolerancia global', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([
      {
        id: 'emp-1',
        sucursal_id: 'suc-1',
        departamento_id: 'dep-1',
        fecha_ingreso: new Date('2026-01-01'),
        fecha_egreso: null,
      },
    ]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:20:00Z'),
        hora_salida: new Date('1970-01-01T17:00:00Z'),
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([
      {
        fecha: new Date('2026-05-18'),
        tolerancia_entrada_min: 30,
        tolerancia_salida_min: 30,
        aplica_a: 'SUCURSAL',
        aplica_a_id: 'suc-1',
      },
    ]);
    resolver.resolverTurnoVigente.mockResolvedValue({
      turno_id: 'turno-1',
      turno: {
        bloques: [
          {
            orden: 1,
            hora_entrada: new Date('1970-01-01T08:00:00Z'),
            hora_salida: new Date('1970-01-01T17:00:00Z'),
          },
        ],
      },
    });

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    expect(prisma.rrhh_asistencia_novedades.createMany).not.toHaveBeenCalled();
  });

  it('no genera novedades cuando no hay turno vigente para el empleado', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([
      {
        id: 'emp-1',
        sucursal_id: 'suc-1',
        departamento_id: 'dep-1',
        fecha_ingreso: new Date('2026-01-01'),
        fecha_egreso: null,
      },
    ]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue(null);

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    expect(prisma.rrhh_asistencia_novedades.createMany).not.toHaveBeenCalled();
  });

  // ───────────────────────────────────────────────────────────────────────────
  // Cobertura ampliada — casos borde del motor M19
  // ───────────────────────────────────────────────────────────────────────────

  it('genera AUSENCIA_JORNADA cuando el empleado no marcó entrada y no hay permisos', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([empleado()]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue(turnoSimple);

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'AUSENCIA_JORNADA',
          impacta_liquidacion: true,
          justificado: false,
        }),
      ]),
    );
  });

  it('genera TARDANZA cuando el empleado entra fuera de tolerancia y no hay permiso LLEGADA_TARDIA', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([empleado()]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:20:00Z'),
        hora_salida: new Date('1970-01-01T17:00:00Z'),
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue(turnoSimple);

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'TARDANZA',
          impacta_liquidacion: true,
          justificado: false,
          minutos_tardanza: 20,
        }),
      ]),
    );
  });

  it('genera SALIDA_ANTICIPADA cuando el empleado se va antes de la tolerancia de salida', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([empleado()]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:00:00Z'),
        hora_salida: new Date('1970-01-01T16:30:00Z'), // 30 min antes
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue(turnoSimple);

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'SALIDA_ANTICIPADA',
          impacta_liquidacion: true,
          justificado: false,
        }),
      ]),
    );
  });

  it('genera AUSENCIA_JUSTIFICADA para el bloque 2 cuando hay permiso AUSENCIA_TURNO', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue(null);
    prisma.rrhh_asistencia_novedades.updateMany.mockResolvedValue({ count: 0 });
    prisma.rrhh_empleados.findMany.mockResolvedValue([empleado()]);
    prisma.rrhh_permisos_presentismo.findMany.mockResolvedValue([
      {
        id: 'perm-at',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        tipo_permiso: 'AUSENCIA_TURNO',
        turno_bloque_orden: 2,
        hora_desde: null,
        hora_hasta: null,
        minutos_tolerados: null,
      },
    ]);
    prisma.rrhh_marcaciones_limpias.findMany.mockResolvedValue([
      {
        id: 'ml-1',
        empleado_id: 'emp-1',
        fecha: new Date('2026-05-18'),
        hora_entrada: new Date('1970-01-01T08:00:00Z'),
        hora_salida: new Date('1970-01-01T12:00:00Z'),
      },
    ]);
    prisma.rrhh_presentismo_tolerancias_especiales.findMany.mockResolvedValue([]);
    resolver.resolverTurnoVigente.mockResolvedValue(turnoCortado);

    await service.procesarPeriodo('empresa-1', {
      fechaDesde: '2026-05-18',
      fechaHasta: '2026-05-18',
      userId: 'u-1',
    });

    const payload = prisma.rrhh_asistencia_novedades.createMany.mock.calls[0][0].data;
    expect(payload).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          tipo_novedad: 'AUSENCIA_JUSTIFICADA',
          turno_bloque_orden: 2,
          permiso_id: 'perm-at',
          justificado: true,
        }),
      ]),
    );
    // No debe generar AUSENCIA_BLOQUE para el bloque 2 al haber permiso.
    expect(payload.some((p: any) => p.tipo_novedad === 'AUSENCIA_BLOQUE')).toBe(false);
  });

  it('rechaza el procesamiento si existen novedades del período ya aplicadas en liquidación', async () => {
    prisma.rrhh_liquidaciones_cabecera.findFirst.mockResolvedValue(null);
    // anularNovedadesPrevias detecta una novedad con liquidacion_id no nulo.
    prisma.rrhh_asistencia_novedades.findFirst.mockResolvedValue({ id: 'nov-aplicada' });

    await expect(
      service.procesarPeriodo('empresa-1', {
        fechaDesde: '2026-05-01',
        fechaHasta: '2026-05-31',
      }),
    ).rejects.toBeInstanceOf(ConflictException);
  });

  it('rechaza fechaDesde posterior a fechaHasta con BadRequestException', async () => {
    await expect(
      service.procesarPeriodo('empresa-1', {
        fechaDesde: '2026-05-31',
        fechaHasta: '2026-05-01',
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });
});
