import sharp from 'sharp';
import { PipelineProductoService } from './pipeline-producto.service';

const png = (bg: { r: number; g: number; b: number }, w = 600) =>
  sharp({ create: { width: w, height: w, channels: 3, background: bg } }).png().toBuffer();
/** Mitad izquierda negra sobre blanco: dHash distinto de una imagen lisa (dos lisas serían "duplicadas"). */
const pngConContenido = async (w = 600) =>
  sharp({ create: { width: w, height: w, channels: 3, background: { r: 255, g: 255, b: 255 } } })
    .composite([{ input: await png({ r: 0, g: 0, b: 0 }, w / 2), top: 0, left: 0 }])
    .png()
    .toBuffer();

const producto = { id: 'p1', cod_producto: 'RT32K5', descripcion: 'HELADERA SAMSUNG RT32K5', marca: 'SAMSUNG', categoria: 'Heladeras' };
const norm = { id: 'p1', marca: 'Samsung', modelo: 'RT32K5', tipo: 'heladera', queries: ['q1', 'q2'] };
const ctx = { empresaId: 'e1', busquedaId: 'b1', usuarioId: 'u1' };

const crear = (opts: { visionTexto?: string; normTexto?: string } = {}) => {
  const serper = {
    buscar: jest.fn(async (q: string) => ({
      desdeCache: q === 'q2',
      resultados: [
        { imageUrl: 'https://a/1.png', title: 'Samsung RT32K5', domain: 'samsung.com', imageWidth: 600, imageHeight: 600 },
        { imageUrl: 'https://a/2.png', title: 'Otra', domain: 'x.com', imageWidth: 600, imageHeight: 600 },
        { imageUrl: 'https://a/html', title: 'html', domain: 'x.com', imageWidth: 600, imageHeight: 600 },
      ],
    })),
  };
  const ai = {
    completar: jest.fn(async () => ({ content: opts.normTexto ?? '[]', tokensIn: 1, tokensOut: 1, tokensUsados: 2, modelo: 'm' })),
    completarConImagenes: jest.fn(async () => ({
      content: opts.visionTexto ?? '[{"i":1,"coincide":0.9,"fondo_blanco":true,"marca_agua":false,"calidad":0.9,"nota":"ok"},{"i":2,"coincide":0.2,"fondo_blanco":true,"marca_agua":false,"calidad":0.5}]',
      tokensIn: 1, tokensOut: 1, tokensUsados: 2, modelo: 'm',
    })),
  };
  const buffers: Record<string, Buffer | null> = {};
  const descargar = jest.fn(async (url: string) => buffers[url] ?? null);
  const svc = new PipelineProductoService(serper as any, ai as any);
  (svc as any).descargar = descargar;
  return { svc, serper, ai, buffers, descargar };
};

describe('PipelineProductoService.procesarProducto', () => {
  it('busca las 2 queries, descarga únicas, evalúa y devuelve solo las que superan el umbral', async () => {
    const { svc, serper, ai, buffers } = crear();
    buffers['https://a/1.png'] = await png({ r: 255, g: 255, b: 255 });
    buffers['https://a/2.png'] = await pngConContenido();

    const r = await svc.procesarProducto(ctx, producto, norm);

    expect(serper.buscar).toHaveBeenCalledTimes(2);
    expect(r.queriesNuevas).toBe(1); // q2 vino de cache
    expect(ai.completarConImagenes).toHaveBeenCalledTimes(1);
    const [, , partes, meta] = ai.completarConImagenes.mock.calls[0] as unknown as [string, string, any[], unknown];
    expect(partes.filter((p: any) => p.type === 'image')).toHaveLength(2);
    expect(meta).toEqual({ feature: 'IMAGENES', usuarioId: 'u1', referenciaId: 'b1' });
    expect(r.sugerencias).toHaveLength(1);
    expect(r.sugerencias[0]).toMatchObject({ fuente_url: 'https://a/1.png', fuente_dominio: 'samsung.com', ancho: 600, alto: 600, vision: { coincide: 0.9 } });
  });

  it('sin candidatas descargables no llama a visión', async () => {
    const { svc, ai } = crear();
    const r = await svc.procesarProducto(ctx, producto, norm);
    expect(ai.completarConImagenes).not.toHaveBeenCalled();
    expect(r.sugerencias).toEqual([]);
  });

  it('si la visión devuelve JSON inválido dos veces, no hay sugerencias pero no lanza', async () => {
    const { svc, ai, buffers } = crear({ visionTexto: 'no json' });
    buffers['https://a/1.png'] = await png({ r: 255, g: 255, b: 255 });
    const r = await svc.procesarProducto(ctx, producto, norm);
    expect(ai.completarConImagenes).toHaveBeenCalledTimes(2);
    expect(r.sugerencias).toEqual([]);
  });
});

describe('PipelineProductoService.normalizarLote', () => {
  it('usa la respuesta del LLM y completa con fallback los que falten', async () => {
    const { svc } = crear({ normTexto: '[{"id":"p1","marca":"Samsung","modelo":"RT32K5","tipo":"heladera","queries":["a","b"]}]' });
    const otro = { id: 'p2', cod_producto: null, descripcion: 'VALDE DE PLASTICO', marca: 'SIN MARCA', categoria: null };
    const m = await svc.normalizarLote(ctx, [producto, otro]);
    expect(m.get('p1')?.queries).toEqual(['a', 'b']);
    expect(m.get('p2')?.queries[0]).toBe('VALDE DE PLASTICO');
  });
  it('si el LLM falla, todos salen por fallback', async () => {
    const { svc, ai } = crear();
    ai.completar.mockRejectedValueOnce(new Error('caído'));
    const m = await svc.normalizarLote(ctx, [producto]);
    expect(m.get('p1')?.queries[0]).toBe('SAMSUNG RT32K5');
  });
});
