import { MarcacionesDedupService } from './marcaciones-dedup.service';

const EMPRESA_ID = 'empresa-1';
const EMPLEADO_A = 'emp-a';
const EMPLEADO_B = 'emp-b';
const BATCH = 'batch-1';

type RawRow = {
  id: string;
  empleado_id: string | null;
  timestamp_marcacion: Date;
  estado_importacion: string;
  batch_importacion_id: string;
};

type DBState = {
  raws: RawRow[];
  limpias: Map<
    string,
    {
      id: string;
      empleado_id: string;
      fecha: Date;
      hora_entrada: Date | null;
      hora_salida: Date | null;
      marcacion_raw_entrada_id: string | null;
      marcacion_raw_salida_id: string | null;
      procesado_novedades: boolean;
    }
  >;
  logs: Array<{
    id: string;
    empresa_id: string;
    empleado_id: string;
    fecha: Date;
    batch_importacion_id: string;
    cantidad_marcaciones_raw: number;
    cantidad_duplicados_descartados: number;
    timestamps_descartados: unknown;
    motivo_correccion: string;
  }>;
};

const utcDate = (iso: string) => {
  const d = new Date(iso);
  return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
};

const fechaKey = (empleadoId: string, fecha: Date) => `${empleadoId}|${fecha.toISOString()}`;

function buildPrisma(state: DBState) {
  // findMany evalúa el where para devolver el subset correcto.
  const findRawMany = jest.fn().mockImplementation((args: any) => {
    const where = args?.where ?? {};
    let result = [...state.raws];
    if (where.empresa_id) result = result.filter((r) => true);
    if (where.batch_importacion_id) {
      result = result.filter((r) => r.batch_importacion_id === where.batch_importacion_id);
    }
    if (where.empleado_id?.not !== undefined) {
      result = result.filter((r) => r.empleado_id !== null);
    }
    if (where.estado_importacion?.not) {
      const bad = where.estado_importacion.not;
      result = result.filter((r) => r.estado_importacion !== bad);
    }
    if (where.empleado_id?.in) {
      const set = new Set(where.empleado_id.in);
      result = result.filter((r) => r.empleado_id && set.has(r.empleado_id));
    }
    if (where.timestamp_marcacion) {
      const gte: Date | undefined = where.timestamp_marcacion.gte;
      const lt: Date | undefined = where.timestamp_marcacion.lt;
      if (gte) result = result.filter((r) => r.timestamp_marcacion >= gte);
      if (lt) result = result.filter((r) => r.timestamp_marcacion < lt);
    }
    result.sort((a, b) => a.timestamp_marcacion.getTime() - b.timestamp_marcacion.getTime());
    return Promise.resolve(
      result.map((r) => ({
        id: r.id,
        empleado_id: r.empleado_id,
        timestamp_marcacion: r.timestamp_marcacion,
        estado_importacion: r.estado_importacion,
        batch_importacion_id: r.batch_importacion_id,
      })),
    );
  });

  const updateMany = jest.fn().mockImplementation((args: any) => {
    const ids: string[] = args.where?.id?.in ?? [];
    const estado = args.data?.estado_importacion;
    if (estado) {
      for (const id of ids) {
        const row = state.raws.find((r) => r.id === id);
        if (row) row.estado_importacion = estado;
      }
    }
    return Promise.resolve({ count: ids.length });
  });

  const findUniqueLimpia = jest.fn().mockImplementation((args: any) => {
    const key = args.where?.empleado_id_fecha;
    if (!key) return Promise.resolve(null);
    const k = fechaKey(key.empleado_id, key.fecha);
    const found = state.limpias.get(k);
    return Promise.resolve(
      found
        ? { id: found.id, procesado_novedades: found.procesado_novedades }
        : null,
    );
  });

  const updateLimpia = jest.fn().mockImplementation((args: any) => {
    for (const v of state.limpias.values()) {
      if (v.id === args.where.id) {
        v.hora_entrada = args.data.hora_entrada;
        v.hora_salida = args.data.hora_salida;
        v.marcacion_raw_entrada_id = args.data.marcacion_raw_entrada_id;
        v.marcacion_raw_salida_id = args.data.marcacion_raw_salida_id;
        v.procesado_novedades = args.data.procesado_novedades;
        return Promise.resolve(v);
      }
    }
    return Promise.resolve(null);
  });

  const createLimpia = jest.fn().mockImplementation((args: any) => {
    const id = `lim-${state.limpias.size + 1}`;
    const fila = {
      id,
      empleado_id: args.data.empleado_id,
      fecha: args.data.fecha,
      hora_entrada: args.data.hora_entrada,
      hora_salida: args.data.hora_salida,
      marcacion_raw_entrada_id: args.data.marcacion_raw_entrada_id,
      marcacion_raw_salida_id: args.data.marcacion_raw_salida_id,
      procesado_novedades: false,
    };
    state.limpias.set(fechaKey(fila.empleado_id, fila.fecha), fila);
    return Promise.resolve(fila);
  });

  const findFirstLog = jest.fn().mockImplementation((args: any) => {
    const found = state.logs.find(
      (l) =>
        l.empleado_id === args.where.empleado_id &&
        l.fecha.getTime() === args.where.fecha.getTime() &&
        l.batch_importacion_id === args.where.batch_importacion_id,
    );
    return Promise.resolve(found ?? null);
  });

  const createLog = jest.fn().mockImplementation((args: any) => {
    const row = { id: `log-${state.logs.length + 1}`, ...args.data };
    state.logs.push(row);
    return Promise.resolve(row);
  });

  const tx = {
    rrhh_marcaciones_raw: { updateMany },
    rrhh_marcaciones_limpias: {
      findUnique: findUniqueLimpia,
      update: updateLimpia,
      create: createLimpia,
    },
  };

  const $transaction = jest.fn().mockImplementation((cb: (t: typeof tx) => any) => cb(tx));

  return {
    prisma: {
      rrhh_marcaciones_raw: { findMany: findRawMany, updateMany },
      rrhh_marcaciones_limpias: {
        findUnique: findUniqueLimpia,
        update: updateLimpia,
        create: createLimpia,
      },
      rrhh_marcaciones_log_correcciones: { findFirst: findFirstLog, create: createLog },
      $transaction,
    } as any,
    spies: { createLimpia, updateLimpia, createLog, updateMany },
  };
}

const raw = (
  id: string,
  empleadoId: string,
  iso: string,
  batchId = BATCH,
): RawRow => ({
  id,
  empleado_id: empleadoId,
  timestamp_marcacion: new Date(iso),
  estado_importacion: 'PENDIENTE',
  batch_importacion_id: batchId,
});

describe('MarcacionesDedupService.deduplicarBatch', () => {
  it('genera una sola marcación_limpia por empleado y día con min/max correctos', async () => {
    const state: DBState = {
      raws: [
        raw('r1', EMPLEADO_A, '2026-05-04T08:00:00Z'),
        raw('r2', EMPLEADO_A, '2026-05-04T17:05:00Z'),
        raw('r3', EMPLEADO_B, '2026-05-04T08:10:00Z'),
        raw('r4', EMPLEADO_B, '2026-05-04T16:55:00Z'),
      ],
      limpias: new Map(),
      logs: [],
    };
    const { prisma, spies } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    const res = await service.deduplicarBatch(EMPRESA_ID, BATCH);

    expect(res.grupos_procesados).toBe(2);
    expect(res.marcaciones_limpias_creadas).toBe(2);
    expect(res.marcaciones_duplicadas).toBe(0);
    expect(res.log_correcciones_creados).toBe(0);
    expect(state.limpias.size).toBe(2);
    expect(spies.createLog).not.toHaveBeenCalled();
  });

  it('marca duplicados y crea log cuando hay más de dos marcaciones del mismo día', async () => {
    const state: DBState = {
      raws: [
        raw('r1', EMPLEADO_A, '2026-05-04T08:00:00Z'),
        raw('r2', EMPLEADO_A, '2026-05-04T08:01:30Z'), // duplicado de entrada
        raw('r3', EMPLEADO_A, '2026-05-04T17:00:00Z'),
        raw('r4', EMPLEADO_A, '2026-05-04T17:02:10Z'), // duplicado de salida
        raw('r5', EMPLEADO_A, '2026-05-04T17:04:55Z'), // otra de salida
      ],
      limpias: new Map(),
      logs: [],
    };
    const { prisma, spies } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    const res = await service.deduplicarBatch(EMPRESA_ID, BATCH);

    expect(res.grupos_procesados).toBe(1);
    expect(res.marcaciones_limpias_creadas).toBe(1);
    expect(res.marcaciones_duplicadas).toBe(3);
    expect(res.log_correcciones_creados).toBe(1);

    const limpia = Array.from(state.limpias.values())[0];
    expect(limpia.marcacion_raw_entrada_id).toBe('r1');
    expect(limpia.marcacion_raw_salida_id).toBe('r5');

    const log = state.logs[0];
    expect(log.cantidad_marcaciones_raw).toBe(5);
    expect(log.cantidad_duplicados_descartados).toBe(3);
    expect(log.motivo_correccion).toBe('DUPLICADO_AMBOS');

    // Las raws no seleccionadas pasan a DUPLICADO; las seleccionadas a PROCESADO
    const r2 = state.raws.find((r) => r.id === 'r2')!;
    const r3 = state.raws.find((r) => r.id === 'r3')!;
    const r4 = state.raws.find((r) => r.id === 'r4')!;
    const r1 = state.raws.find((r) => r.id === 'r1')!;
    const r5 = state.raws.find((r) => r.id === 'r5')!;
    expect(r2.estado_importacion).toBe('DUPLICADO');
    expect(r3.estado_importacion).toBe('DUPLICADO');
    expect(r4.estado_importacion).toBe('DUPLICADO');
    expect(r1.estado_importacion).toBe('PROCESADO');
    expect(r5.estado_importacion).toBe('PROCESADO');
  });

  it('es idempotente: ejecutar el mismo batch dos veces no crea nuevos limpios ni nuevos logs', async () => {
    const state: DBState = {
      raws: [
        raw('r1', EMPLEADO_A, '2026-05-04T08:00:00Z'),
        raw('r2', EMPLEADO_A, '2026-05-04T08:01:30Z'),
        raw('r3', EMPLEADO_A, '2026-05-04T17:00:00Z'),
      ],
      limpias: new Map(),
      logs: [],
    };
    const { prisma } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    const r1 = await service.deduplicarBatch(EMPRESA_ID, BATCH);
    const r2 = await service.deduplicarBatch(EMPRESA_ID, BATCH);

    expect(r1.marcaciones_limpias_creadas).toBe(1);
    expect(r2.marcaciones_limpias_creadas).toBe(0);
    expect(r2.marcaciones_limpias_actualizadas).toBe(1);
    expect(state.limpias.size).toBe(1);
    expect(state.logs).toHaveLength(1);
  });

  it('ignora raws con empleado_id null (CI sin matching) y las que están en ERROR', async () => {
    const state: DBState = {
      raws: [
        { ...raw('r1', EMPLEADO_A, '2026-05-04T08:00:00Z') },
        { ...raw('r2', EMPLEADO_A, '2026-05-04T17:00:00Z') },
        // Sin matching
        { id: 'r3', empleado_id: null, timestamp_marcacion: new Date('2026-05-04T09:00:00Z'), estado_importacion: 'ERROR', batch_importacion_id: BATCH },
        // Empleado distinto pero en ERROR
        { id: 'r4', empleado_id: EMPLEADO_B, timestamp_marcacion: new Date('2026-05-04T10:00:00Z'), estado_importacion: 'ERROR', batch_importacion_id: BATCH },
      ],
      limpias: new Map(),
      logs: [],
    };
    const { prisma } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    const res = await service.deduplicarBatch(EMPRESA_ID, BATCH);

    // Sólo el grupo de EMPLEADO_A se procesa, sin contar ERROR ni null
    expect(res.grupos_procesados).toBe(1);
    expect(res.marcaciones_limpias_creadas).toBe(1);
    expect(state.limpias.size).toBe(1);
  });

  it('si sólo hay UNA marcación válida en el día, deja hora_salida en null', async () => {
    const state: DBState = {
      raws: [raw('r1', EMPLEADO_A, '2026-05-04T08:00:00Z')],
      limpias: new Map(),
      logs: [],
    };
    const { prisma } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    await service.deduplicarBatch(EMPRESA_ID, BATCH);

    const limpia = Array.from(state.limpias.values())[0];
    expect(limpia.marcacion_raw_entrada_id).toBe('r1');
    expect(limpia.marcacion_raw_salida_id).toBeNull();
    expect(limpia.hora_salida).toBeNull();
  });

  it('un segundo batch con marcaciones del mismo día actualiza la limpia y combina min/max global', async () => {
    const state: DBState = {
      raws: [
        // Batch 1: sólo entrada (procesado previo)
        { id: 'r1', empleado_id: EMPLEADO_A, timestamp_marcacion: new Date('2026-05-04T08:00:00Z'), estado_importacion: 'PROCESADO', batch_importacion_id: 'batch-old' },
        // Batch 2: agrega salida tardía
        { id: 'r2', empleado_id: EMPLEADO_A, timestamp_marcacion: new Date('2026-05-04T17:30:00Z'), estado_importacion: 'PENDIENTE', batch_importacion_id: BATCH },
      ],
      limpias: new Map([
        [
          fechaKey(EMPLEADO_A, utcDate('2026-05-04T00:00:00Z')),
          {
            id: 'lim-prev',
            empleado_id: EMPLEADO_A,
            fecha: utcDate('2026-05-04T00:00:00Z'),
            hora_entrada: new Date('1970-01-01T08:00:00Z'),
            hora_salida: null,
            marcacion_raw_entrada_id: 'r1',
            marcacion_raw_salida_id: null,
            procesado_novedades: true, // ya fue procesada por motor de novedades
          },
        ],
      ]),
      logs: [],
    };
    const { prisma } = buildPrisma(state);
    const service = new MarcacionesDedupService(prisma);

    const res = await service.deduplicarBatch(EMPRESA_ID, BATCH);

    expect(res.grupos_procesados).toBe(1);
    expect(res.marcaciones_limpias_actualizadas).toBe(1);
    const limpia = Array.from(state.limpias.values())[0];
    expect(limpia.marcacion_raw_entrada_id).toBe('r1');
    expect(limpia.marcacion_raw_salida_id).toBe('r2');
    // Al re-procesar, procesado_novedades vuelve a false para que el motor recalcule
    expect(limpia.procesado_novedades).toBe(false);
  });
});
