import { ConflictException, NotFoundException } from '@nestjs/common';
import { CanalesService } from './canales.service';
import { envs } from 'src/config';

const CRED_META = { access_token: 'EAAGtokenlargodeprueba', app_secret: 'secreto-de-app' };

const FILA = (over: Record<string, any> = {}) => ({
  id: 'can-1', empresa_id: 'emp-1', tipo: 'whatsapp', proveedor: 'meta_cloud',
  nombre: 'WhatsApp Ventas', identificador: '+595981123456', identificador_externo: '109876543210',
  webhook_verify_token: 'tok', credenciales_cifradas: null, equipo_id: null, activo: true,
  verificado_at: null, ultimo_error: null, ...over,
});

describe('CanalesService', () => {
  // La clave es de entorno; en test se fija para poder ejercitar el cifrado real.
  const claveOriginal = envs.crmEncryptionKey;
  beforeAll(() => { (envs as { crmEncryptionKey?: string }).crmEncryptionKey = 'k'.repeat(32); });
  afterAll(() => { (envs as { crmEncryptionKey?: string }).crmEncryptionKey = claveOriginal; });

  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      crm_canales: {
        findMany: jest.fn().mockResolvedValue([]),
        findFirst: jest.fn().mockResolvedValue(null),
        findUnique: jest.fn().mockResolvedValue(null),
        create: jest.fn().mockImplementation(({ data }) => Promise.resolve(FILA(data))),
        update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve(FILA({ id: where.id, ...data }))),
      },
      ...over,
    };
    const audit = { log: jest.fn().mockResolvedValue(undefined) };
    return { service: new CanalesService(prisma, audit as any), prisma, audit };
  };

  describe('validaciones de alta', () => {
    it('rechaza un proveedor que no sirve para el tipo', async () => {
      const { service } = crear();
      await expect(
        service.create({ tipo: 'whatsapp', proveedor: 'imap_smtp', nombre: 'X' }, 'emp-1'),
      ).rejects.toThrow(/no sirve para un canal de whatsapp/);
    });

    /** Descubrir que falta el token en el primer envío es peor que rechazar acá. */
    it('exige las credenciales que el proveedor necesita', async () => {
      const { service } = crear();
      await expect(
        service.create({ tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: {} }, 'emp-1'),
      ).rejects.toMatchObject({ response: { campos_faltantes: ['access_token', 'app_secret'] } });
    });

    it('Evolution pide base_url e instancia, no token', async () => {
      const { service } = crear();
      await expect(
        service.create({ tipo: 'whatsapp', proveedor: 'evolution_baileys', nombre: 'X', credenciales: {} }, 'emp-1'),
      ).rejects.toMatchObject({ response: { campos_faltantes: ['base_url', 'instancia'] } });
    });

    it('no permite dos canales del mismo tipo con el mismo número', async () => {
      const { service } = crear({ crm_canales: { findFirst: jest.fn().mockResolvedValue(FILA()) } });
      await expect(
        service.create(
          { tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', identificador: '+595981123456', credenciales: CRED_META },
          'emp-1',
        ),
      ).rejects.toThrow(ConflictException);
    });
  });

  describe('credenciales', () => {
    it('se guardan cifradas, nunca en claro', async () => {
      const { service, prisma } = crear();
      await service.create(
        { tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: CRED_META },
        'emp-1',
      );
      const guardado = prisma.crm_canales.create.mock.calls[0][0].data.credenciales_cifradas as string;
      expect(guardado).not.toContain('EAAGtokenlargodeprueba');
      expect(guardado).toMatch(/^[0-9a-f]{32}:[0-9a-f]+$/);
    });

    /** Si salieran en claro por API, cifrarlas en base no serviría de nada. */
    it('la vista pública las devuelve enmascaradas', async () => {
      const { service } = crear();
      const r = await service.create(
        { tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: CRED_META },
        'emp-1',
      );
      expect(r.credenciales.access_token).toBe('EAAGto…ueba');
      expect(r.credenciales.access_token).not.toContain('tokenlargo');
      expect(r.credenciales_configuradas).toBe(true);
    });

    it('base_url e instancia no se enmascaran: no son secretos', async () => {
      const { service } = crear();
      const r = await service.create(
        {
          tipo: 'whatsapp', proveedor: 'evolution_cloud', nombre: 'X',
          credenciales: { base_url: 'https://evo.midominio.com', instancia: 'ventas', api_key: 'k'.repeat(20) },
        },
        'emp-1',
      );
      expect(r.credenciales.base_url).toBe('https://evo.midominio.com');
      expect(r.credenciales.instancia).toBe('ventas');
      expect(r.credenciales.api_key).toMatch(/…/);
    });

    it('al editar se mezclan: lo omitido conserva su valor', async () => {
      const { service, prisma } = crear();
      const creado = await service.create(
        { tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: CRED_META },
        'emp-1',
      );
      const cifradas = prisma.crm_canales.create.mock.calls[0][0].data.credenciales_cifradas;
      prisma.crm_canales.findFirst.mockResolvedValue(FILA({ credenciales_cifradas: cifradas }));

      await service.update('can-1', { credenciales: { app_secret: 'nuevo-secreto' } }, 'emp-1');

      const data = prisma.crm_canales.update.mock.calls[0][0].data;
      const nuevas = service['descifrarCredenciales'](data.credenciales_cifradas) as Record<string, string>;
      expect(nuevas.app_secret).toBe('nuevo-secreto');
      expect(nuevas.access_token).toBe('EAAGtokenlargodeprueba');
      expect(creado.id).toBeDefined();
    });

    it('las credenciales vacías no se guardan', async () => {
      const { service, prisma } = crear();
      await service.create(
        { tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: { ...CRED_META, api_key: '   ' } },
        'emp-1',
      );
      const cifradas = prisma.crm_canales.create.mock.calls[0][0].data.credenciales_cifradas;
      expect(Object.keys(service['descifrarCredenciales'](cifradas))).toEqual(['access_token', 'app_secret']);
    });

    /** Si la clave cambió, una credencial ilegible no debe tumbar la pantalla. */
    it('una credencial que no se puede descifrar no rompe el listado', async () => {
      const { service } = crear({
        crm_canales: { findMany: jest.fn().mockResolvedValue([FILA({ credenciales_cifradas: 'basura:nohex' })]) },
      });
      const [canal] = await service.findAll('emp-1');
      expect(canal.credenciales_configuradas).toBe(true);
      expect(canal.credenciales.access_token).toBeNull();
    });
  });

  describe('vista pública', () => {
    it('avisa que Baileys no permite envíos automáticos', async () => {
      const { service } = crear({
        crm_canales: { findMany: jest.fn().mockResolvedValue([FILA({ proveedor: 'evolution_baileys' })]) },
      });
      expect((await service.findAll('emp-1'))[0].permite_envios_automaticos).toBe(false);
    });

    it('Meta Cloud sí los permite', async () => {
      const { service } = crear({ crm_canales: { findMany: jest.fn().mockResolvedValue([FILA()]) } });
      expect((await service.findAll('emp-1'))[0].permite_envios_automaticos).toBe(true);
    });

    it('no expone el cifrado ni el campo crudo', async () => {
      const { service } = crear({
        crm_canales: { findMany: jest.fn().mockResolvedValue([FILA({ credenciales_cifradas: 'abc:def' })]) },
      });
      const [canal] = await service.findAll('emp-1');
      expect(canal).not.toHaveProperty('credenciales_cifradas');
    });
  });

  describe('alcance por empresa', () => {
    it('un canal de otra empresa es 404', async () => {
      const { service } = crear();
      await expect(service.findOne('can-x', 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('la baja es lógica: el historial de conversaciones lo referencia', async () => {
      const { service, prisma } = crear({ crm_canales: {
        findFirst: jest.fn().mockResolvedValue(FILA()),
        update: jest.fn().mockImplementation(({ data }) => Promise.resolve(FILA(data))),
      } });
      await service.remove('can-1', 'emp-1');
      expect(prisma.crm_canales.update.mock.calls[0][0].data.activo).toBe(false);
      expect(prisma.crm_canales.delete).toBeUndefined();
    });
  });

  describe('resolución para webhooks', () => {
    it('busca sólo canales activos por identificador externo', async () => {
      const { service, prisma } = crear();
      await service.porIdentificadorExterno('109876543210');
      expect(prisma.crm_canales.findFirst.mock.calls[0][0].where).toEqual({
        identificador_externo: '109876543210', activo: true,
      });
    });

    it('sin canal devuelve null en vez de lanzar', async () => {
      const { service } = crear();
      expect(await service.porIdentificadorExterno('inexistente')).toBeNull();
    });
  });

  it('sin CRM_ENCRYPTION_KEY el guardado falla nombrando la variable', async () => {
    (envs as { crmEncryptionKey?: string }).crmEncryptionKey = '';
    const { service } = crear();
    await expect(
      service.create({ tipo: 'whatsapp', proveedor: 'meta_cloud', nombre: 'X', credenciales: CRED_META }, 'emp-1'),
    ).rejects.toThrow(/CRM_ENCRYPTION_KEY/);
    (envs as { crmEncryptionKey?: string }).crmEncryptionKey = 'k'.repeat(32);
  });
});
