import { BadRequestException } from '@nestjs/common';
import * as XLSX from 'xlsx';
import { LeadsImportService, MAX_FILAS } from './leads-import.service';

/** Arma un .xlsx de verdad en memoria: así se prueba el parseo, no un mock del parseo. */
function planilla(filas: Record<string, unknown>[]): Express.Multer.File {
  const hoja = XLSX.utils.json_to_sheet(filas);
  const libro = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(libro, hoja, 'Hoja1');
  const buffer = XLSX.write(libro, { type: 'buffer', bookType: 'xlsx' }) as Buffer;
  return { originalname: 'contactos.xlsx', buffer } as Express.Multer.File;
}

const FILAS = [
  { 'Nombre y Apellido': 'Ana Giménez', Celular: '0981123456', Correo: 'ana@x.com', Empresa: 'Kiosco Ana' },
  { 'Nombre y Apellido': 'Luis Pérez', Celular: '0971000111', Correo: '', Empresa: '' },
];

describe('LeadsImportService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      crm_leads: { findFirst: jest.fn().mockResolvedValue(null) },
      clientes: { findFirst: jest.fn().mockResolvedValue(null) },
      crm_origenes: { findMany: jest.fn().mockResolvedValue([{ id: 'ori-wa', codigo: 'WHATSAPP', descripcion: 'WhatsApp' }]) },
      ...over,
    };
    const leads = { create: jest.fn().mockImplementation((dto: any) => Promise.resolve({ id: `lead-${dto.nombre}` })) };
    return { service: new LeadsImportService(prisma, leads as any), prisma, leads };
  };

  describe('lectura del archivo', () => {
    it('un archivo vacío se rechaza con un mensaje que dice qué mandar', async () => {
      const { service } = crear();
      await expect(service.preview({ originalname: 'x.xlsx', buffer: Buffer.alloc(0) } as Express.Multer.File, 'emp-1'))
        .rejects.toThrow(BadRequestException);
    });

    /**
     * xlsx acepta texto plano como si fuera un CSV, así que no llega a lanzar un
     * error de formato: lo que corta es que no haya filas de datos. El mensaje
     * sigue siendo útil, pero conviene saber que el rechazo viene por ahí.
     */
    it('algo que no es una planilla se rechaza por no tener filas', async () => {
      const { service } = crear();
      const file = { originalname: 'foto.png', buffer: Buffer.from('no soy un xlsx') } as Express.Multer.File;
      await expect(service.preview(file, 'emp-1')).rejects.toThrow(/no tiene filas/i);
    });

    /** Excel arrastra filas vacías: si se contaran, toda planilla parecería tener miles. */
    it('descarta las filas vacías del final', async () => {
      const { service } = crear();
      const r = await service.preview(planilla([...FILAS, { 'Nombre y Apellido': '', Celular: '', Correo: '', Empresa: '' }]), 'emp-1');
      expect(r.data.total_filas).toBe(2);
    });

    it('por encima del tope pide partir el archivo', async () => {
      const { service } = crear();
      const muchas = Array.from({ length: MAX_FILAS + 1 }, (_, i) => ({ Nombre: `Lead ${i}`, Celular: `098100${i}` }));
      await expect(service.preview(planilla(muchas), 'emp-1')).rejects.toThrow(new RegExp(`${MAX_FILAS}`));
    });
  });

  describe('preview', () => {
    it('sugiere el mapeo y no escribe nada', async () => {
      const { service, leads } = crear();
      const r = await service.preview(planilla(FILAS), 'emp-1');

      expect(r.data.mapeo_sugerido).toMatchObject({ nombre: 'Nombre y Apellido', telefono: 'Celular', email: 'Correo' });
      expect(r.data.resumen).toMatchObject({ total: 2, nuevos: 2, duplicados: 0, errores: 0 });
      expect(leads.create).not.toHaveBeenCalled();
    });

    it('avisa qué columnas quedaron sin mapear', async () => {
      const { service } = crear();
      const r = await service.preview(planilla([{ Nombre: 'Ana', Celular: '0981123456', Zona: 'Centro' }]), 'emp-1');
      expect(r.data.columnas_sin_mapear).toEqual(['Zona']);
    });

    it('marca como duplicado al que ya es cliente', async () => {
      const { service } = crear({ clientes: { findFirst: jest.fn().mockResolvedValue({ id: 'cli-7' }) } });
      const r = await service.preview(planilla(FILAS), 'emp-1');
      expect(r.data.resumen).toMatchObject({ nuevos: 0, duplicados: 2 });
      expect(r.data.filas[0]).toMatchObject({ clasificacion: 'duplicado', cliente_id: 'cli-7' });
    });

    /** La base todavía no los tiene, así que el dedupe de LeadsService no los vería. */
    it('detecta repetidos dentro del mismo archivo', async () => {
      const { service } = crear();
      const r = await service.preview(planilla([FILAS[0], FILAS[0]]), 'emp-1');
      expect(r.data.resumen).toMatchObject({ nuevos: 1, duplicados: 1 });
      expect(r.data.filas[1].motivo).toMatch(/repetido en el archivo/i);
    });

    it('un contacto sin ninguna forma de contacto es un error, no un lead', async () => {
      const { service } = crear();
      const r = await service.preview(planilla([{ Nombre: 'Ana', Celular: '', Correo: '' }]), 'emp-1');
      expect(r.data.filas[0]).toMatchObject({ clasificacion: 'error' });
      expect(r.data.filas[0].motivo).toMatch(/tel[eé]fono/i);
    });

    it('un teléfono que no se puede interpretar se reporta con el valor original', async () => {
      const { service } = crear();
      const r = await service.preview(planilla([{ Nombre: 'Ana', Celular: 'llamar al local' }]), 'emp-1');
      expect(r.data.filas[0].motivo).toContain('llamar al local');
    });

    it('devuelve a lo sumo 50 filas para la tabla de revisión', async () => {
      const { service } = crear();
      const muchas = Array.from({ length: 80 }, (_, i) => ({ Nombre: `Lead ${i}`, Celular: `098100${String(i).padStart(4, '0')}` }));
      const r = await service.preview(planilla(muchas), 'emp-1');
      expect(r.data.total_filas).toBe(80);
      expect(r.data.filas).toHaveLength(50);
    });
  });

  describe('importar', () => {
    it('crea un lead por fila delegando el dedupe en LeadsService', async () => {
      const { service, leads } = crear();
      const r = await service.importar(planilla(FILAS), {}, 'emp-1', 'u-1');

      expect(leads.create).toHaveBeenCalledTimes(2);
      expect(r.data.resumen).toMatchObject({ total: 2, nuevos: 2 });
      expect(leads.create.mock.calls[0][0]).toMatchObject({ nombre: 'Ana Giménez', telefono: '0981123456', empresa_nombre: 'Kiosco Ana' });
    });

    it('clasifica lo que LeadsService devuelve como duplicado', async () => {
      const { service, leads } = crear();
      leads.create
        .mockResolvedValueOnce({ duplicado: 'cliente', cliente_id: 'cli-7', razon_social: 'Kiosco Ana' })
        .mockResolvedValueOnce({ duplicado: 'lead', lead: { id: 'lead-3' } });

      const r = await service.importar(planilla(FILAS), {}, 'emp-1');

      expect(r.data.resumen).toMatchObject({ nuevos: 0, duplicados: 2 });
      expect(r.data.duplicados[0]).toMatchObject({ cliente_id: 'cli-7', motivo: 'Ya es cliente' });
      expect(r.data.duplicados[1]).toMatchObject({ lead_id: 'lead-3', motivo: 'Ya existe como lead' });
    });

    /** Si una fila mala cancelara todo, importar una planilla de 500 contactos sería imposible. */
    it('una fila que falla no cancela las demás', async () => {
      const { service, leads } = crear();
      leads.create.mockRejectedValueOnce(new Error('teléfono duplicado en índice único'));

      const r = await service.importar(planilla(FILAS), {}, 'emp-1');

      expect(r.data.resumen).toMatchObject({ total: 2, nuevos: 1, errores: 1 });
      expect(r.data.errores[0].motivo).toContain('índice único');
      expect(r.data.errores[0].fila).toBe(2);
    });

    it('respeta el mapeo explícito del frontend', async () => {
      const { service, leads } = crear();
      await service.importar(planilla(FILAS), { mapeo: { nombre: 'Empresa', telefono: 'Celular' } }, 'emp-1');
      expect(leads.create.mock.calls[0][0].nombre).toBe('Kiosco Ana');
    });

    it('un mapeo que apunta a una columna inexistente se rechaza', async () => {
      const { service } = crear();
      await expect(service.importar(planilla(FILAS), { mapeo: { nombre: 'Apodo' } }, 'emp-1'))
        .rejects.toThrow(/Apodo/);
    });

    it('un campo que no es de lead se rechaza', async () => {
      const { service } = crear();
      await expect(service.importar(planilla(FILAS), { mapeo: { apodo: 'Empresa' } }, 'emp-1'))
        .rejects.toThrow(/no es un campo importable/);
    });

    it('sin columna de nombre no se puede importar', async () => {
      const { service } = crear();
      await expect(service.importar(planilla([{ Celular: '0981123456' }]), {}, 'emp-1'))
        .rejects.toThrow(/nombre/i);
    });

    describe('origen', () => {
      const conOrigen = [{ Nombre: 'Ana', Celular: '0981123456', Origen: 'whatsapp' }];

      it('resuelve el origen por código o descripción, sin importar mayúsculas', async () => {
        const { service, leads } = crear();
        await service.importar(planilla(conOrigen), {}, 'emp-1');
        expect(leads.create.mock.calls[0][0].origen_id).toBe('ori-wa');
      });

      /** Crear un origen por cada variante de tipeo ensuciaría los reportes por canal. */
      it('un origen desconocido cae al default en vez de inventar uno', async () => {
        const { service, leads } = crear();
        await service.importar(planilla([{ Nombre: 'Ana', Celular: '0981123456', Origen: 'boca a boca' }]), { origen_id: 'ori-def' }, 'emp-1');
        expect(leads.create.mock.calls[0][0].origen_id).toBe('ori-def');
      });

      it('sin columna de origen usa el default del DTO', async () => {
        const { service, leads } = crear();
        await service.importar(planilla(FILAS), { origen_id: 'ori-def' }, 'emp-1');
        expect(leads.create.mock.calls[0][0].origen_id).toBe('ori-def');
      });
    });
  });
});
