import { aplicarNcAlEmitir, decidirAplicacionAlEmitir, revertirNcDeCuotas, saldoPendienteDeCuotas } from './aplicacion-nc';

describe('decidirAplicacionAlEmitir', () => {
  describe('ESTRICTO', () => {
    it('aplica contra la factura origen, como siempre', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'ESTRICTO', montoNc: 300, saldoFactura: 1000 });
      expect(r).toEqual({ aplicarAhora: true, motivo: 'ESTRICTO', sobranteCredito: 0 });
    });

    it('NC mayor al saldo (la factura ya tenía cobros): lo que no entra queda como crédito', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'ESTRICTO', montoNc: 1000, saldoFactura: 800 });
      expect(r).toEqual({ aplicarAhora: true, motivo: 'ESTRICTO', sobranteCredito: 200 });
    });

    it('sin saldo pendiente no hay nada que aplicar y toda la NC queda como crédito', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'ESTRICTO', montoNc: 300, saldoFactura: 0 });
      expect(r).toEqual({ aplicarAhora: false, motivo: 'ESTRICTO', sobranteCredito: 300 });
    });
  });

  describe('FLEXIBLE', () => {
    it('NC parcial: queda como crédito disponible (no toca la factura)', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'FLEXIBLE', montoNc: 300, saldoFactura: 1000 });
      expect(r).toEqual({ aplicarAhora: false, motivo: 'FLEXIBLE_CREDITO', sobranteCredito: 300 });
    });

    it('NC que cubre exactamente el saldo: se aplica al emitir (anulación total)', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'FLEXIBLE', montoNc: 960000, saldoFactura: 960000 });
      expect(r).toEqual({ aplicarAhora: true, motivo: 'FLEXIBLE_CUBRE_SALDO', sobranteCredito: 0 });
    });

    it('NC mayor al saldo: cancela la factura y el sobrante queda como crédito', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'FLEXIBLE', montoNc: 1000, saldoFactura: 800 });
      expect(r).toEqual({ aplicarAhora: true, motivo: 'FLEXIBLE_CUBRE_SALDO', sobranteCredito: 200 });
    });

    it('tolera diferencias de redondeo de un guaraní', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'FLEXIBLE', montoNc: 999.99, saldoFactura: 1000 });
      expect(r.aplicarAhora).toBe(true);
      expect(r.sobranteCredito).toBe(0);
    });

    it('factura ya cobrada (saldo 0): toda la NC queda como crédito', () => {
      const r = decidirAplicacionAlEmitir({ modo: 'FLEXIBLE', montoNc: 500, saldoFactura: 0 });
      expect(r).toEqual({ aplicarAhora: false, motivo: 'FLEXIBLE_CREDITO', sobranteCredito: 500 });
    });
  });
});

describe('saldoPendienteDeCuotas', () => {
  it('usa saldo_pendiente cuando existe', () => {
    expect(saldoPendienteDeCuotas([{ saldo_pendiente: 300, dmoncuota: 500 }])).toBe(300);
  });

  it('una cuota sin saldo_pendiente cargado vale su monto completo', () => {
    // Cuotas creadas antes de que existiera saldo_pendiente: nunca se cobraron.
    expect(saldoPendienteDeCuotas([{ saldo_pendiente: null, dmoncuota: 500 }])).toBe(500);
  });

  it('suma varias cuotas e ignora montos negativos', () => {
    expect(
      saldoPendienteDeCuotas([
        { saldo_pendiente: 100, dmoncuota: 400 },
        { saldo_pendiente: null, dmoncuota: 250 },
        { saldo_pendiente: -5, dmoncuota: -5 },
      ]),
    ).toBe(350);
  });

  it('acepta Decimal de Prisma (string/number)', () => {
    expect(saldoPendienteDeCuotas([{ saldo_pendiente: '960000.0000', dmoncuota: '960000' }])).toBe(960000);
  });
});

/**
 * Transacción en memoria con las tres tablas que toca la aplicación al emitir.
 * Registra lo que se escribe para poder afirmar sobre el resultado real.
 */
function txFalsa(cuotas: Array<{ id: string; nro_cuota: number; saldo_pendiente: number | null; dmoncuota: number; estado: string }>) {
  const estado = {
    cuotas: cuotas.map((c) => ({ ...c })),
    aplicaciones: [] as Array<{ factura_cuota_id: string; monto_aplicado: number }>,
    saldoDisponibleNc: undefined as number | undefined,
  };
  const tx = {
    factura_cuotas: {
      findMany: async () =>
        estado.cuotas.filter((c) => !['pagado', 'Pagado'].includes(c.estado)).sort((a, b) => a.nro_cuota - b.nro_cuota),
      update: async ({ where, data }: any) => {
        const c = estado.cuotas.find((x) => x.id === where.id)!;
        c.saldo_pendiente = Number(data.saldo_pendiente);
        c.estado = data.estado;
        return c;
      },
    },
    nc_aplicaciones_cuota: {
      create: async ({ data }: any) => {
        estado.aplicaciones.push({ factura_cuota_id: data.factura_cuota_id, monto_aplicado: Number(data.monto_aplicado) });
        return data;
      },
    },
    nota_credito_cab: {
      update: async ({ data }: any) => {
        estado.saldoDisponibleNc = Number(data.rec_saldo_disponible);
        return data;
      },
    },
  };
  return { tx, estado };
}

describe('aplicarNcAlEmitir', () => {
  const base = { empresaId: 'e1', notaCreditoId: 'nc1', facturaId: 'f1' };

  it('FLEXIBLE que cubre el saldo: cancela la cuota y la NC deja de estar disponible (no se puede usar dos veces)', async () => {
    const { tx, estado } = txFalsa([{ id: 'c1', nro_cuota: 1, saldo_pendiente: null, dmoncuota: 960000, estado: 'pendiente' }]);
    const r = await aplicarNcAlEmitir(tx as any, { ...base, modo: 'FLEXIBLE', montoNc: 960000 });
    expect(r.motivo).toBe('FLEXIBLE_CUBRE_SALDO');
    expect(estado.cuotas[0]).toMatchObject({ saldo_pendiente: 0, estado: 'pagado' });
    expect(estado.aplicaciones).toEqual([{ factura_cuota_id: 'c1', monto_aplicado: 960000 }]);
    expect(estado.saldoDisponibleNc).toBe(0);
  });

  it('FLEXIBLE con sobrante: cancela las cuotas en orden y deja disponible solo lo que sobra', async () => {
    const { tx, estado } = txFalsa([
      { id: 'c2', nro_cuota: 2, saldo_pendiente: 300, dmoncuota: 500, estado: 'pendiente' },
      { id: 'c1', nro_cuota: 1, saldo_pendiente: 500, dmoncuota: 500, estado: 'pendiente' },
    ]);
    await aplicarNcAlEmitir(tx as any, { ...base, modo: 'FLEXIBLE', montoNc: 1000 });
    expect(estado.aplicaciones.map((a) => a.factura_cuota_id)).toEqual(['c1', 'c2']); // FIFO por nro_cuota
    expect(estado.cuotas.every((c) => c.estado === 'pagado')).toBe(true);
    expect(estado.saldoDisponibleNc).toBe(200);
  });

  it('FLEXIBLE parcial: no toca la factura ni el saldo disponible (queda para el recibo)', async () => {
    const { tx, estado } = txFalsa([{ id: 'c1', nro_cuota: 1, saldo_pendiente: 1000, dmoncuota: 1000, estado: 'pendiente' }]);
    const r = await aplicarNcAlEmitir(tx as any, { ...base, modo: 'FLEXIBLE', montoNc: 300 });
    expect(r.aplicarAhora).toBe(false);
    expect(estado.cuotas[0]).toMatchObject({ saldo_pendiente: 1000, estado: 'pendiente' });
    expect(estado.aplicaciones).toEqual([]);
    expect(estado.saldoDisponibleNc).toBeUndefined();
  });

  it('ESTRICTO parcial: baja el saldo y la NC deja de estar disponible (ya se descontó de la factura)', async () => {
    // Antes quedaba en NULL (= su total) y el recibo la volvía a ofrecer: una NC
    // de 300 terminaba descontando 600.
    const { tx, estado } = txFalsa([{ id: 'c1', nro_cuota: 1, saldo_pendiente: 1000, dmoncuota: 1000, estado: 'pendiente' }]);
    await aplicarNcAlEmitir(tx as any, { ...base, modo: 'ESTRICTO', montoNc: 300 });
    expect(estado.cuotas[0]).toMatchObject({ saldo_pendiente: 700, estado: 'pendiente' });
    expect(estado.aplicaciones).toEqual([{ factura_cuota_id: 'c1', monto_aplicado: 300 }]);
    expect(estado.saldoDisponibleNc).toBe(0);
  });

  it('ESTRICTO mayor al saldo: cancela la factura y deja disponible solo lo que sobra', async () => {
    const { tx, estado } = txFalsa([{ id: 'c1', nro_cuota: 1, saldo_pendiente: 800, dmoncuota: 1000, estado: 'pendiente' }]);
    await aplicarNcAlEmitir(tx as any, { ...base, modo: 'ESTRICTO', montoNc: 1000 });
    expect(estado.cuotas[0]).toMatchObject({ saldo_pendiente: 0, estado: 'pagado' });
    expect(estado.saldoDisponibleNc).toBe(200);
  });

  it('ESTRICTO con factura ya cobrada: no toca cuotas y la NC queda entera como crédito', async () => {
    const { tx, estado } = txFalsa([{ id: 'c1', nro_cuota: 1, saldo_pendiente: 0, dmoncuota: 1000, estado: 'pagado' }]);
    const r = await aplicarNcAlEmitir(tx as any, { ...base, modo: 'ESTRICTO', montoNc: 300 });
    expect(r.aplicarAhora).toBe(false);
    expect(estado.aplicaciones).toEqual([]);
    expect(estado.saldoDisponibleNc).toBeUndefined();
  });
});

/** Tx en memoria para la reversión: cuotas + aplicaciones registradas. */
function txReversion(
  cuotas: Array<{ id: string; saldo_pendiente: number | null; dmoncuota: number; estado: string }>,
  aplicaciones: Array<{ factura_cuota_id: string; monto_aplicado: number }>,
) {
  const estado = { cuotas: cuotas.map((c) => ({ ...c })), aplicaciones: [...aplicaciones], borradas: 0 };
  const tx = {
    factura_cuotas: {
      findUnique: async ({ where }: any) => estado.cuotas.find((c) => c.id === where.id) ?? null,
      update: async ({ where, data }: any) => {
        const c = estado.cuotas.find((x) => x.id === where.id)!;
        c.saldo_pendiente = Number(data.saldo_pendiente);
        c.estado = data.estado;
        return c;
      },
    },
    nc_aplicaciones_cuota: {
      findMany: async () => estado.aplicaciones,
      deleteMany: async () => {
        estado.borradas = estado.aplicaciones.length;
        estado.aplicaciones = [];
        return { count: estado.borradas };
      },
    },
  };
  return { tx, estado };
}

describe('revertirNcDeCuotas', () => {
  it('devuelve a cada cuota lo que la NC le había descontado y borra el registro', async () => {
    const { tx, estado } = txReversion(
      [
        { id: 'c1', saldo_pendiente: 0, dmoncuota: 500, estado: 'pagado' },
        { id: 'c2', saldo_pendiente: 300, dmoncuota: 500, estado: 'pendiente' },
      ],
      [
        { factura_cuota_id: 'c1', monto_aplicado: 500 },
        { factura_cuota_id: 'c2', monto_aplicado: 200 },
      ],
    );
    const revertido = await revertirNcDeCuotas(tx as any, 'nc1');
    expect(revertido).toBe(700);
    expect(estado.cuotas[0]).toMatchObject({ saldo_pendiente: 500, estado: 'pendiente' });
    expect(estado.cuotas[1]).toMatchObject({ saldo_pendiente: 500, estado: 'pendiente' });
    expect(estado.aplicaciones).toEqual([]);
  });

  it('sin aplicaciones registradas no toca nada', async () => {
    const { tx, estado } = txReversion([{ id: 'c1', saldo_pendiente: 100, dmoncuota: 500, estado: 'pendiente' }], []);
    expect(await revertirNcDeCuotas(tx as any, 'nc1')).toBe(0);
    expect(estado.cuotas[0].saldo_pendiente).toBe(100);
    expect(estado.borradas).toBe(0);
  });
});
