import { ConflictException } from '@nestjs/common';
import { ProductosService } from './productos.service';

const crearService = (overrides: any = {}) => {
  const prisma = {
    afectacion_iva: { findFirst: jest.fn().mockResolvedValue({ id: 'af-1', codigo: 1 }) },
    productos: {
      findMany: jest.fn().mockResolvedValue([{ id: 'p-1' }, { id: 'p-2' }]),
      updateMany: jest.fn().mockResolvedValue({ count: 2 }),
    },
    stock_deposito: { findMany: jest.fn().mockResolvedValue([]) },
    lotes_producto: { findMany: jest.fn().mockResolvedValue([]) },
    ...overrides,
  };
  const auditService = { log: jest.fn().mockResolvedValue(undefined) };
  const service = new ProductosService(
    prisma as any,
    {} as any,
    auditService as any,
    {} as any,
    {} as any,
  );
  return { service, prisma, auditService };
};

describe('ProductosService.bulkUpdate — IVA como grupo', () => {
  it('acepta el grupo completo y coherente', async () => {
    const { service, prisma } = crearService();

    await service.bulkUpdate(
      {
        ids: ['p-1', 'p-2'],
        cambios: { afectacion_id: 'af-1', porcentaje_iva: 10, proporcion_iva: 100 },
      } as any,
      'emp-1',
      'user-1',
    );

    expect(prisma.productos.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          afectacion_id: 'af-1',
          porcentaje_iva: 10,
          proporcion_iva: 100,
        }),
      }),
    );
  });

  it('rechaza el grupo incoherente sin escribir nada', async () => {
    const { service, prisma } = crearService();

    await expect(
      service.bulkUpdate(
        {
          ids: ['p-1'],
          cambios: { afectacion_id: 'af-1', porcentaje_iva: 0, proporcion_iva: 100 },
        } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(prisma.productos.updateMany).not.toHaveBeenCalled();
  });

  it('rechaza porcentaje_iva suelto, sin afectacion_id', async () => {
    const { service, prisma } = crearService();

    await expect(
      service.bulkUpdate(
        { ids: ['p-1'], cambios: { porcentaje_iva: 10 } } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(prisma.productos.updateMany).not.toHaveBeenCalled();
  });

  it('rechaza una afectación que no es de la empresa', async () => {
    const { service } = crearService({
      afectacion_iva: { findFirst: jest.fn().mockResolvedValue(null) },
    });

    await expect(
      service.bulkUpdate(
        {
          ids: ['p-1'],
          cambios: { afectacion_id: 'af-ajena', porcentaje_iva: 10, proporcion_iva: 100 },
        } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toThrow();
  });
});

describe('ProductosService.bulkUpdate — guarda de maneja_lote', () => {
  it('bloquea activar lotes si algún producto tiene stock', async () => {
    const { service, prisma } = crearService({
      stock_deposito: {
        findMany: jest.fn().mockResolvedValue([{ producto_id: 'p-1', cantidad_disponible: 43 }]),
      },
    });

    await expect(
      service.bulkUpdate(
        { ids: ['p-1', 'p-2'], cambios: { maneja_lote: true } } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(prisma.productos.updateMany).not.toHaveBeenCalled();
    expect(prisma.stock_deposito.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          producto_id: { in: ['p-1', 'p-2'] },
          OR: [{ cantidad_disponible: { gt: 0 } }, { cantidad_reservada: { gt: 0 } }],
        },
        distinct: ['producto_id'],
      }),
    );
  });

  it('bloquea desactivar lotes si algún producto tiene lotes vivos', async () => {
    const { service, prisma } = crearService({
      lotes_producto: {
        findMany: jest.fn().mockResolvedValue([{ producto_id: 'p-2' }]),
      },
    });

    await expect(
      service.bulkUpdate(
        { ids: ['p-1', 'p-2'], cambios: { maneja_lote: false } } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toBeInstanceOf(ConflictException);

    expect(prisma.productos.updateMany).not.toHaveBeenCalled();
    expect(prisma.lotes_producto.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: {
          producto_id: { in: ['p-1', 'p-2'] },
          activo: true,
          cantidad_disponible: { gt: 0 },
        },
        distinct: ['producto_id'],
      }),
    );
  });

  it('permite activar lotes cuando no hay stock', async () => {
    const { service, prisma } = crearService();

    await service.bulkUpdate(
      { ids: ['p-1', 'p-2'], cambios: { maneja_lote: true } } as any,
      'emp-1',
      'user-1',
    );

    expect(prisma.productos.updateMany).toHaveBeenCalled();
  });

  it('no consulta stock ni lotes si el cambio no toca maneja_lote', async () => {
    const { service, prisma } = crearService();

    await service.bulkUpdate(
      { ids: ['p-1'], cambios: { active: false } } as any,
      'emp-1',
      'user-1',
    );

    expect(prisma.stock_deposito.findMany).not.toHaveBeenCalled();
    expect(prisma.lotes_producto.findMany).not.toHaveBeenCalled();
  });

  it('cuenta productos distintos, no filas: un producto con stock en dos depósitos reporta 1', async () => {
    // Simula lo que hace Prisma con `distinct: ['producto_id']`: si la query lo
    // incluye, dos filas del mismo producto (una por depósito) colapsan a una sola.
    const filasCrudas = [
      { producto_id: 'p-1', deposito_id: 'd-1' },
      { producto_id: 'p-1', deposito_id: 'd-2' },
    ];
    const findMany = jest.fn((args: any) => {
      if (args?.distinct?.includes('producto_id')) {
        const vistos = new Set<string>();
        return Promise.resolve(
          filasCrudas.filter((f) => (vistos.has(f.producto_id) ? false : (vistos.add(f.producto_id), true))),
        );
      }
      return Promise.resolve(filasCrudas);
    });
    const { service, prisma } = crearService({ stock_deposito: { findMany } });

    await expect(
      service.bulkUpdate(
        { ids: ['p-1', 'p-2'], cambios: { maneja_lote: true } } as any,
        'emp-1',
        'user-1',
      ),
    ).rejects.toThrow(/1 de los productos/);

    expect(prisma.productos.updateMany).not.toHaveBeenCalled();
  });

  it('usa idsValidos (scopeados por empresa), no los ids crudos del payload', async () => {
    // Solo p-1 pertenece a la empresa; p-ajeno es de otra empresa o no existe.
    const { service, prisma } = crearService({
      productos: {
        findMany: jest.fn().mockResolvedValue([{ id: 'p-1' }]),
        updateMany: jest.fn().mockResolvedValue({ count: 1 }),
      },
      stock_deposito: {
        // Si la guarda usara `ids` crudo en vez de `idsValidos`, esta query
        // devolvería una fila para 'p-ajeno' y bloquearía la operación entera.
        findMany: jest.fn((args: any) =>
          Promise.resolve(
            args.where.producto_id.in.includes('p-ajeno') ? [{ producto_id: 'p-ajeno' }] : [],
          ),
        ),
      },
    });

    await service.bulkUpdate(
      { ids: ['p-1', 'p-ajeno'], cambios: { maneja_lote: true } } as any,
      'emp-1',
      'user-1',
    );

    expect(prisma.stock_deposito.findMany).toHaveBeenCalledWith(
      expect.objectContaining({ where: expect.objectContaining({ producto_id: { in: ['p-1'] } }) }),
    );
    expect(prisma.productos.updateMany).toHaveBeenCalled();
  });
});
