import * as XLSX from 'xlsx';
import { BadRequestException } from '@nestjs/common';
import { CuentasPagarReportesService } from './cuentas-pagar-reportes.service';
import { armarPeriodos, COL_DESPUES, COL_SIN_PROGRAMAR, ConfigPlanPagos, lunesDe, normalizarConfigPlan, PlanPagosService, ubicarCuota } from './plan-pagos.service';

jest.mock('src/utils/alcance-sucursal', () => ({ alcanceSucursalUsuario: jest.fn().mockResolvedValue(null) }));

const DIA = 86_400_000;
const utc = (s: string) => new Date(`${s}T00:00:00Z`);
const SEMANAL: ConfigPlanPagos = { periodicidad: 'SEMANAL', diaSemana: 5, diaMes: 31 };
const tramos = (p: { desde: string; hasta: string; fecha_pago: string; titulo: string }[]) => p.map((x) => [x.desde, x.hasta, x.fecha_pago, x.titulo]);

describe('plan de pagos: períodos según la periodicidad de la empresa', () => {
  it('SEMANAL: lunes a domingo, titulada con el día de pago', () => {
    expect(tramos(armarPeriodos(utc('2026-10-14'), 2, SEMANAL))).toEqual([
      ['2026-10-12', '2026-10-18', '2026-10-16', 'Vie 16/10'],
      ['2026-10-19', '2026-10-25', '2026-10-23', 'Vie 23/10'],
    ]);
    expect(lunesDe(utc('2026-10-18')).toISOString().slice(0, 10)).toBe('2026-10-12');
  });

  it('QUINCENAL: del 1 al 15 y del 16 a fin de mes, cruzando de año', () => {
    expect(tramos(armarPeriodos(utc('2026-12-20'), 3, { ...SEMANAL, periodicidad: 'QUINCENAL' }))).toEqual([
      ['2026-12-16', '2026-12-31', '2026-12-31', '2ª Dic 26'],
      ['2027-01-01', '2027-01-15', '2027-01-15', '1ª Ene 27'],
      ['2027-01-16', '2027-01-31', '2027-01-31', '2ª Ene 27'],
    ]);
  });

  it('MENSUAL: el día configurado, o el último si el mes es más corto', () => {
    expect(tramos(armarPeriodos(utc('2027-01-10'), 2, { ...SEMANAL, periodicidad: 'MENSUAL', diaMes: 30 }))).toEqual([
      ['2027-01-01', '2027-01-31', '2027-01-30', 'Ene 2027'],
      ['2027-02-01', '2027-02-28', '2027-02-28', 'Feb 2027'],
    ]);
  });

  it('si el día de pago del período en curso ya pasó, se paga hoy', () => {
    const [actual] = armarPeriodos(utc('2026-10-12'), 1, { ...SEMANAL, periodicidad: 'MENSUAL', diaMes: 10 });
    expect(actual.fecha_pago).toBe('2026-10-12');
    const [sabado] = armarPeriodos(utc('2026-10-17'), 1, SEMANAL);
    expect(sabado.fecha_pago).toBe('2026-10-17');
  });

  it('una configuración inválida o vacía cae en los viernes', () => {
    expect(normalizarConfigPlan(null)).toEqual(SEMANAL);
    expect(normalizarConfigPlan({ plan_pagos_periodicidad: 'ANUAL', plan_pagos_dia_semana: 9, plan_pagos_dia_mes: 0 })).toEqual(SEMANAL);
  });
});

describe('plan de pagos: dónde cae cada cuota', () => {
  const hoy = utc('2026-10-14');
  const periodos = armarPeriodos(hoy, 2, SEMANAL);

  it('programada en un período visible → ese período, aunque haya vencido hace meses', () => {
    expect(ubicarCuota(periodos, hoy, utc('2026-03-01'), utc('2026-10-23'))).toEqual({ columna: '2026-10-19', origen: 'programada' });
  });

  it('programada en un período ya pasado y sin pagar → «Sin programar» como atrasada', () => {
    expect(ubicarCuota(periodos, hoy, utc('2026-10-20'), utc('2026-10-09'))).toEqual({ columna: COL_SIN_PROGRAMAR, origen: 'atrasada' });
  });

  it('programada más allá de lo visible → «Después»', () => {
    expect(ubicarCuota(periodos, hoy, null, utc('2026-12-04'))).toEqual({ columna: COL_DESPUES, origen: 'programada' });
  });

  it('sin programar y por vencer → sugerida en el período del vencimiento', () => {
    expect(ubicarCuota(periodos, hoy, utc('2026-10-14'), null)).toEqual({ columna: '2026-10-12', origen: 'sugerida' });
    expect(ubicarCuota(periodos, hoy, utc('2027-01-10'), null)).toEqual({ columna: COL_DESPUES, origen: 'sugerida' });
  });

  it('sin programar y vencida, o sin vencimiento → «Sin programar»', () => {
    expect(ubicarCuota(periodos, hoy, utc('2026-10-13'), null)).toEqual({ columna: COL_SIN_PROGRAMAR, origen: 'sin_programar' });
    expect(ubicarCuota(periodos, hoy, null, null)).toEqual({ columna: COL_SIN_PROGRAMAR, origen: 'sin_programar' });
  });

  it('lo programado es una fecha: cambiar a mensual lo ubica en su mes sin perderlo', () => {
    const mensual = armarPeriodos(hoy, 2, { ...SEMANAL, periodicidad: 'MENSUAL' });
    expect(ubicarCuota(mensual, hoy, null, utc('2026-11-06'))).toEqual({ columna: '2026-11-01', origen: 'programada' });
  });
});

describe('PlanPagosService', () => {
  const hoyUtc = () => {
    const n = new Date();
    return Date.UTC(n.getFullYear(), n.getMonth(), n.getDate());
  };
  const enDias = (d: number) => new Date(hoyUtc() + d * DIA);

  function crear(cuentas: unknown[], config: Record<string, unknown> | null = { plan_pagos_periodicidad: 'SEMANAL', plan_pagos_dia_semana: 5 }, ordenes: unknown[] = []) {
    const prisma = {
      orden_pago_proveedor_det: { findMany: jest.fn().mockResolvedValue(ordenes) },
      cuentas_pagar: {
        findMany: jest.fn().mockResolvedValue(cuentas),
        updateMany: jest.fn().mockResolvedValue({ count: 1 }),
      },
      gasto_cab: { findMany: jest.fn().mockResolvedValue([]) },
      pagos_proveedor: { findMany: jest.fn().mockResolvedValue([]) },
      config_compras: { findUnique: jest.fn().mockResolvedValue(config) },
      empresas: { findUnique: jest.fn().mockResolvedValue({ razon_social: 'DOBA S.A.' }) },
    };
    const s = new PlanPagosService(prisma as never, new CuentasPagarReportesService(prisma as never));
    return { s, prisma };
  }

  const cuenta = (o: Record<string, unknown>) => ({
    id: 'cp-x',
    proveedor_id: 'prov-1',
    compra_id: null,
    origen_tipo: 'otro',
    origen_id: null,
    numero_cuota: 1,
    total_cuotas: 1,
    fecha_emision: enDias(-30),
    compra_cab: null,
    moneda: { codigo: 'PYG' },
    proveedores: { personas: { razon_social: 'CENTRAL CARGO', ruc: '800', dv: '1' } },
    fecha_pago_programada: null,
    ...o,
  });

  it('separa el total de cada período de lo que ya está programado', async () => {
    const { s } = crear([
      cuenta({ id: 'prog', fecha_vencimiento: enDias(-60), monto_original: 100, saldo_pendiente: 100, fecha_pago_programada: enDias(0) }),
      cuenta({ id: 'sug', fecha_vencimiento: enDias(0), monto_original: 40, saldo_pendiente: 40 }),
      cuenta({ id: 'venc', fecha_vencimiento: enDias(-3), monto_original: 7, saldo_pendiente: 7 }),
    ]);
    const plan = await s.plan('emp', undefined, { periodos: 2 });

    const actual = plan.periodos[0].clave;
    expect(plan.periodicidad).toBe('SEMANAL');
    expect(plan.totales[0].por_columna[actual]).toBe(140);
    expect(plan.totales[0].programado_por_columna[actual]).toBe(100);
    expect(plan.totales[0].por_columna[COL_SIN_PROGRAMAR]).toBe(7);
    expect(plan.filas.find((f) => f.cuenta_pagar_id === 'prog')).toEqual(expect.objectContaining({ origen: 'programada', proveedor_id: 'prov-1', saldo: 100 }));
  });

  it('una cuota en una orden de pago abierta lo informa y, si no estaba programada, va a la fecha de la orden', async () => {
    const op = (cuenta: string, fecha: Date) => ({
      cuenta_pagar_id: cuenta,
      orden_pago_proveedor_cab: { id: 'op-1', numero_orden_pago: 'OP-000123', estado: 'PENDIENTE_APROBACION', fecha_pago: fecha },
    });
    const { s } = crear(
      [
        cuenta({ id: 'manual', fecha_vencimiento: enDias(-30), monto_original: 50, saldo_pendiente: 50 }),
        cuenta({ id: 'plan', fecha_vencimiento: enDias(-30), monto_original: 70, saldo_pendiente: 70, fecha_pago_programada: enDias(7) }),
      ],
      undefined,
      [op('manual', enDias(-2)), op('plan', enDias(7))],
    );
    const plan = await s.plan('emp', undefined, { periodos: 3 });
    const manual = plan.filas.find((f) => f.cuenta_pagar_id === 'manual');
    const programada = plan.filas.find((f) => f.cuenta_pagar_id === 'plan');

    // Vencida pero ya en una orden: no está «por decidir», va al período en curso (la orden quedó atrás).
    expect(manual).toEqual(expect.objectContaining({ origen: 'en_orden', columna: plan.periodos[0].clave }));
    expect(manual.orden_abierta).toEqual(expect.objectContaining({ numero: 'OP-000123', estado: 'PENDIENTE_APROBACION' }));
    expect(programada).toEqual(expect.objectContaining({ origen: 'programada' }));
    expect(programada.orden_abierta?.numero).toBe('OP-000123');
    expect(plan.totales[0].por_columna[COL_SIN_PROGRAMAR]).toBe(0);
    expect(plan.totales[0].programado_por_columna[plan.periodos[0].clave]).toBe(50);
  });

  it('programar no mueve cuotas que ya están en una orden de pago abierta', async () => {
    const { s, prisma } = crear([], undefined, [
      { cuenta_pagar_id: 'en-op', orden_pago_proveedor_cab: { id: 'op', numero_orden_pago: 'OP-1', estado: 'BORRADOR', fecha_pago: enDias(0) } },
    ]);
    await expect(s.programar('emp', 'usr', ['en-op', 'libre'], enDias(7).toISOString().slice(0, 10))).resolves.toEqual({ actualizadas: 1, omitidas: 1 });
    expect(prisma.cuentas_pagar.updateMany.mock.calls[0][0].where.id).toEqual({ in: ['libre'] });
  });

  it('usa la periodicidad configurada por la empresa', async () => {
    const { s } = crear([], { plan_pagos_periodicidad: 'MENSUAL', plan_pagos_dia_mes: 10 });
    const plan = await s.plan('emp', undefined, { periodos: 12 });
    expect(plan.periodicidad).toBe('MENSUAL');
    expect(plan.periodos).toHaveLength(12);
    expect(plan.periodos[1].fecha_pago.slice(8)).toBe('10');
  });

  it('programar: graba fecha y autor solo sobre cuotas pendientes de la empresa', async () => {
    const { s, prisma } = crear([]);
    const fecha = enDias(7).toISOString().slice(0, 10);
    await expect(s.programar('emp', 'usr', ['a', 'a', 'b'], fecha)).resolves.toEqual({ actualizadas: 1, omitidas: 1 });
    const arg = prisma.cuentas_pagar.updateMany.mock.calls[0][0];
    expect(arg.where).toEqual(expect.objectContaining({ id: { in: ['a', 'b'] }, empresa_id: 'emp', estado: { notIn: ['pagada', 'anulada'] } }));
    expect(arg.data.fecha_pago_programada.toISOString().slice(0, 10)).toBe(fecha);
    expect(arg.data.fecha_pago_programada_por).toBe('usr');
  });

  it('programar: quitar la programación limpia fecha y autor', async () => {
    const { s, prisma } = crear([]);
    await s.programar('emp', 'usr', ['a'], null);
    expect(prisma.cuentas_pagar.updateMany.mock.calls[0][0].data).toEqual(
      expect.objectContaining({ fecha_pago_programada: null, fecha_pago_programada_por: null, fecha_pago_programada_at: null }),
    );
  });

  it('programar: rechaza períodos pasados y fechas inválidas', async () => {
    const { s } = crear([]);
    await expect(s.programar('emp', 'usr', ['a'], enDias(-14).toISOString().slice(0, 10))).rejects.toBeInstanceOf(BadRequestException);
    await expect(s.programar('emp', 'usr', ['a'], '2026-02-30')).rejects.toBeInstanceOf(BadRequestException);
  });

  it('el Excel trae una columna por período, el estado de cada cuota y la fila de programado', async () => {
    const { s } = crear([cuenta({ id: 'prog', fecha_vencimiento: enDias(5), monto_original: 1_500_000, saldo_pendiente: 1_500_000, fecha_pago_programada: enDias(0) })]);
    const wb = XLSX.read(await s.planXlsx('emp', undefined, { periodos: 3 }));
    const filas = XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets['Plan de pagos'], { header: 1, defval: '' });

    expect(filas[3]).toEqual(['Proveedor', 'Comprobante', 'Concepto', 'Cuota', 'Vence', 'Estado', 'Sin programar', expect.stringMatching(/^Vie /), expect.any(String), expect.any(String), 'Después']);
    expect(filas[4][5]).toBe('Programada');
    expect(filas[4][7]).toBe(1_500_000);
    expect(filas.find((r) => String(r[0]).trim() === 'Programado')?.[7]).toBe(1_500_000);
  });
});
