import { EcommerceStockReservaService } from './ecommerce-stock-reserva.service';

const EMPRESA = 'emp-1';
const SESSION = 'sess-1';
const DEPOSITO = 'dep-1';

/**
 * Mock de Prisma con soporte para `$transaction(cb)`: la callback recibe el
 * mismo objeto, de modo que las llamadas dentro de la transacción se registran
 * en los mismos jest.fn().
 */
const buildPrisma = (overrides: any = {}) => {
  const prisma: any = {
    // Disponible por producto: se consulta con $queryRaw ... FOR UPDATE
    $queryRaw: jest.fn().mockResolvedValue([{ disponible: 10 }]),
    $executeRaw: jest.fn().mockResolvedValue(1),
    ecommerce_stock_reserva: {
      create: jest.fn().mockResolvedValue({}),
      findMany: jest.fn().mockResolvedValue([]),
      updateMany: jest.fn().mockResolvedValue({ count: 0 }),
    },
    ecommerce_checkout_session: {
      findUnique: jest.fn().mockResolvedValue(null),
      update: jest.fn().mockResolvedValue({}),
    },
    ...overrides,
  };
  prisma.$transaction = jest.fn().mockImplementation((cb: any) => cb(prisma));
  return prisma;
};

describe('EcommerceStockReservaService', () => {
  describe('reservar', () => {
    it('con stock suficiente: incrementa cantidad_reservada y registra la reserva', async () => {
      const prisma = buildPrisma();
      const service = new EcommerceStockReservaService(prisma);

      const res = await service.reservar({
        empresaId: EMPRESA,
        sessionId: SESSION,
        depositoId: DEPOSITO,
        lineas: [{ productoId: 'p-1', cantidad: 3 }],
      });

      expect(res).toEqual({ ok: true, sinStock: [] });
      // Se tomó el stock (UPDATE stock_deposito ... cantidad_reservada + 3)
      expect(prisma.$executeRaw).toHaveBeenCalledTimes(1);
      expect(prisma.ecommerce_stock_reserva.create).toHaveBeenCalledTimes(1);
      const creado = prisma.ecommerce_stock_reserva.create.mock.calls[0][0].data;
      expect(creado).toMatchObject({
        empresa_id: EMPRESA,
        session_id: SESSION,
        producto_id: 'p-1',
        deposito_id: DEPOSITO,
      });
      // El TTL debe quedar en el futuro.
      expect(creado.expira_at.getTime()).toBeGreaterThan(Date.now());
    });

    it('sin stock: no reserva, devuelve el producto en sinStock y libera lo ya tomado', async () => {
      const prisma = buildPrisma({
        // Sólo hay 1 disponible y se piden 5.
        $queryRaw: jest.fn().mockResolvedValue([{ disponible: 1 }]),
      });
      const service = new EcommerceStockReservaService(prisma);

      const res = await service.reservar({
        empresaId: EMPRESA,
        sessionId: SESSION,
        depositoId: DEPOSITO,
        lineas: [{ productoId: 'p-1', cantidad: 5 }],
      });

      expect(res.ok).toBe(false);
      expect(res.sinStock).toEqual(['p-1']);
      // No se tomó stock ni se creó la reserva de esa línea.
      expect(prisma.ecommerce_stock_reserva.create).not.toHaveBeenCalled();
      // Se llamó a liberar() para no dejar trabado lo de otras líneas.
      expect(prisma.ecommerce_stock_reserva.findMany).toHaveBeenCalled();
    });

    it('reserva parcial: si una línea no tiene stock, libera TODO (no deja stock trabado)', async () => {
      const prisma = buildPrisma({
        // p-1 tiene stock, p-2 no.
        $queryRaw: jest
          .fn()
          .mockResolvedValueOnce([{ disponible: 10 }])
          .mockResolvedValueOnce([{ disponible: 0 }]),
      });
      // Al liberar, hay 1 reserva activa (la de p-1).
      prisma.ecommerce_stock_reserva.findMany.mockResolvedValue([
        { id: 'r-1', producto_id: 'p-1', deposito_id: DEPOSITO, cantidad: 2 },
      ]);
      const service = new EcommerceStockReservaService(prisma);

      const res = await service.reservar({
        empresaId: EMPRESA,
        sessionId: SESSION,
        depositoId: DEPOSITO,
        lineas: [
          { productoId: 'p-1', cantidad: 2 },
          { productoId: 'p-2', cantidad: 1 },
        ],
      });

      expect(res.ok).toBe(false);
      expect(res.sinStock).toEqual(['p-2']);
      // La reserva de p-1 se devuelve: marcada como liberada.
      expect(prisma.ecommerce_stock_reserva.updateMany).toHaveBeenCalledWith(
        expect.objectContaining({ data: expect.objectContaining({ liberada: true }) }),
      );
    });
  });

  describe('liberar', () => {
    it('devuelve el stock y marca las reservas como liberadas', async () => {
      const prisma = buildPrisma();
      prisma.ecommerce_stock_reserva.findMany.mockResolvedValue([
        { id: 'r-1', producto_id: 'p-1', deposito_id: DEPOSITO, cantidad: 2 },
        { id: 'r-2', producto_id: 'p-2', deposito_id: DEPOSITO, cantidad: 1 },
      ]);
      const service = new EcommerceStockReservaService(prisma);

      const n = await service.liberar(SESSION);

      expect(n).toBe(2);
      // Un UPDATE de stock_deposito por reserva.
      expect(prisma.$executeRaw).toHaveBeenCalledTimes(2);
      expect(prisma.ecommerce_stock_reserva.updateMany).toHaveBeenCalledWith({
        where: { id: { in: ['r-1', 'r-2'] } },
        data: expect.objectContaining({ liberada: true }),
      });
    });

    it('es idempotente: sin reservas activas no toca el stock', async () => {
      const prisma = buildPrisma();
      prisma.ecommerce_stock_reserva.findMany.mockResolvedValue([]);
      const service = new EcommerceStockReservaService(prisma);

      const n = await service.liberar(SESSION);

      expect(n).toBe(0);
      expect(prisma.$executeRaw).not.toHaveBeenCalled();
      expect(prisma.ecommerce_stock_reserva.updateMany).not.toHaveBeenCalled();
    });
  });

  describe('expirarVencidas', () => {
    it('expira la sesión pendiente de pago y libera su stock', async () => {
      const prisma = buildPrisma();
      prisma.ecommerce_stock_reserva.findMany
        // 1ª llamada: reservas vencidas (distinct por sesión)
        .mockResolvedValueOnce([{ session_id: SESSION }])
        // 2ª llamada: dentro de liberar()
        .mockResolvedValueOnce([{ id: 'r-1', producto_id: 'p-1', deposito_id: DEPOSITO, cantidad: 2 }]);
      prisma.ecommerce_checkout_session.findUnique.mockResolvedValue({
        id: SESSION,
        estado: 'pendiente_pago',
        codigo: 'ECOM-2026-0001',
      });
      const service = new EcommerceStockReservaService(prisma);

      const res = await service.expirarVencidas();

      expect(res).toEqual({ sesiones: 1, reservas: 1 });
      expect(prisma.ecommerce_checkout_session.update).toHaveBeenCalledWith(
        expect.objectContaining({ data: expect.objectContaining({ estado: 'expirado' }) }),
      );
    });

    it('si la sesión ya se pagó, NO la expira: sólo limpia la reserva (el stock lo maneja el pedido ERP)', async () => {
      const prisma = buildPrisma();
      prisma.ecommerce_stock_reserva.findMany
        .mockResolvedValueOnce([{ session_id: SESSION }])
        .mockResolvedValueOnce([]);
      prisma.ecommerce_checkout_session.findUnique.mockResolvedValue({
        id: SESSION,
        estado: 'pago_confirmado',
        codigo: 'ECOM-2026-0002',
      });
      const service = new EcommerceStockReservaService(prisma);

      const res = await service.expirarVencidas();

      expect(res.sesiones).toBe(0);
      expect(prisma.ecommerce_checkout_session.update).not.toHaveBeenCalled();
    });
  });
});
