import { ConflictException, NotFoundException, ServiceUnavailableException } from '@nestjs/common';
import { BusquedasService } from './busquedas.service';

const crear = (over: any = {}) => {
  const prisma = {
    producto_imagen_busquedas: {
      findFirst: jest.fn().mockResolvedValue(null),
      create: jest.fn(async ({ data }: any) => ({ id: 'b1', ...data })),
      update: jest.fn(async ({ where, data }: any) => ({ id: where.id, ...data })),
      findMany: jest.fn().mockResolvedValue([]),
      count: jest.fn().mockResolvedValue(0),
    },
    // Existen p1..p3; p3 ya tiene imagen. Los mocks respetan el `in` del where.
    productos: {
      findMany: jest.fn(async ({ where }: any) => ['p1', 'p2', 'p3'].filter((id) => where.id.in.includes(id)).map((id) => ({ id }))),
    },
    producto_imagenes: {
      findMany: jest.fn(async ({ where }: any) => (where.producto_id.in.includes('p3') ? [{ producto_id: 'p3' }] : [])),
    },
    ai_tarifas: { findFirst: jest.fn().mockResolvedValue({ precio_in_por_1m: 0.3, precio_out_por_1m: 1.2 }) },
    // p1 con stock, p2 en cero (p3 ni figura en stock_deposito).
    stock_deposito: {
      groupBy: jest.fn(async () => [
        { producto_id: 'p1', _sum: { cantidad_disponible: 7 } },
        { producto_id: 'p2', _sum: { cantidad_disponible: 0 } },
      ]),
    },
    ...over.prisma,
  };
  const aiConfig = { getConfigEfectiva: jest.fn().mockResolvedValue({ proveedor: 'deepseek', modelo: 'deepseek-flash', api_key: 'k', credencial_propia: false, credencial_modo: 'PLATAFORMA' }), ...over.aiConfig };
  const serper = { disponible: jest.fn().mockReturnValue(true), ...over.serper };
  const queue = { add: jest.fn().mockResolvedValue({ id: 'job-1' }) };
  const svc = new BusquedasService(prisma as any, aiConfig as any, serper as any, queue as any);
  return { svc, prisma, aiConfig, serper, queue };
};
const dto = { producto_ids: ['p1', 'p2', 'p3', 'p-borrado'], solo_sin_imagen: true };

describe('BusquedasService.estimar', () => {
  it('cuenta a procesar, omitidos (con imagen o inexistentes) y estima queries/costo/minutos', async () => {
    const { svc } = crear();
    const e = await svc.estimar('e1', dto);
    expect(e.a_procesar).toBe(2);
    expect(e.omitidos).toBe(2);
    expect(e.queries_estimadas).toBe(4);
    expect(e.costo_usd_estimado).toBeGreaterThan(0);
    expect(e.minutos_estimados).toBeGreaterThan(0);
  });
  it('con solo_sin_imagen=false no omite los que tienen imagen', async () => {
    const { svc } = crear();
    expect((await svc.estimar('e1', { ...dto, solo_sin_imagen: false })).a_procesar).toBe(3);
  });
  it('con solo_con_stock deja solo los que tienen existencias', async () => {
    const { svc } = crear();
    const e = await svc.estimar('e1', { ...dto, solo_con_stock: true });
    expect(e.a_procesar).toBe(1); // p1: tiene stock y no tiene imagen
    expect(e.omitidos).toBe(3);
  });
});

describe('BusquedasService.crear', () => {
  it('crea la búsqueda y encola el job con su id', async () => {
    const { svc, prisma, queue } = crear();
    const b = await svc.crear('e1', 'u1', dto);
    expect(prisma.producto_imagen_busquedas.create).toHaveBeenCalledWith(
      expect.objectContaining({ data: expect.objectContaining({ empresa_id: 'e1', usuario_id: 'u1', producto_ids: ['p1', 'p2'], total_productos: 2, omitidos: 2, solo_sin_imagen: true }) }),
    );
    expect(queue.add).toHaveBeenCalledWith('procesar-busqueda', { busqueda_id: 'b1' }, expect.objectContaining({ attempts: 2 }));
    expect(prisma.producto_imagen_busquedas.update).toHaveBeenCalledWith(expect.objectContaining({ data: { job_id: 'job-1' } }));
    expect(b.id).toBe('b1');
  });
  it('409 si ya hay una búsqueda activa', async () => {
    const { svc, prisma } = crear();
    prisma.producto_imagen_busquedas.findFirst.mockResolvedValueOnce({ id: 'otra', estado: 'EN_PROCESO' });
    await expect(svc.crear('e1', 'u1', dto)).rejects.toThrow(ConflictException);
  });
  it('409 si la empresa no tiene IA con key', async () => {
    const { svc } = crear({ aiConfig: { getConfigEfectiva: jest.fn().mockResolvedValue({ proveedor: 'anthropic', modelo: '', api_key: null }) } });
    await expect(svc.crear('e1', 'u1', dto)).rejects.toThrow(ConflictException);
  });
  it('503 si Serper no está configurado en la instancia', async () => {
    const { svc } = crear({ serper: { disponible: jest.fn().mockReturnValue(false) } });
    await expect(svc.crear('e1', 'u1', dto)).rejects.toThrow(ServiceUnavailableException);
  });
  it('409 si no queda ningún producto por procesar', async () => {
    const { svc } = crear();
    await expect(svc.crear('e1', 'u1', { producto_ids: ['p3'], solo_sin_imagen: true })).rejects.toThrow(ConflictException);
  });
});

describe('BusquedasService.cancelar / obtener', () => {
  it('cancela solo si está PENDIENTE o EN_PROCESO', async () => {
    const { svc, prisma } = crear();
    prisma.producto_imagen_busquedas.findFirst.mockResolvedValueOnce({ id: 'b1', estado: 'EN_PROCESO' });
    const r = await svc.cancelar('e1', 'b1');
    expect(r.estado).toBe('CANCELADA');
    prisma.producto_imagen_busquedas.findFirst.mockResolvedValueOnce({ id: 'b1', estado: 'COMPLETADA' });
    await expect(svc.cancelar('e1', 'b1')).rejects.toThrow(ConflictException);
  });
  it('404 si la búsqueda no es de la empresa', async () => {
    const { svc } = crear();
    await expect(svc.obtener('e1', 'nope')).rejects.toThrow(NotFoundException);
  });
});
