import { MensajeriaService } from './mensajeria.service';
import { BaileysAutomaticoBloqueadoError } from './whatsapp/whatsapp-provider.interface';

const CANAL = (proveedor: string) => ({
  id: 'can-1', empresa_id: 'emp-1', proveedor, identificador: '+595981123456',
  identificador_externo: '109876543210', credenciales: { access_token: 'x' },
});

describe('MensajeriaService', () => {
  const crear = (proveedor = 'meta_cloud', over: Record<string, any> = {}) => {
    const provider = {
      enviarTexto: jest.fn().mockResolvedValue({ id_externo: 'out-1', estado: 'encolado' }),
      enviarPlantilla: jest.fn().mockResolvedValue({ id_externo: 'out-2', estado: 'encolado' }),
      enviarAdjunto: jest.fn().mockResolvedValue({ id_externo: 'out-3', estado: 'encolado' }),
      estadoSesion: undefined as unknown,
      ...over,
    };
    const prisma: any = {
      crm_tarifas_whatsapp: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue([]),
        upsert: jest.fn().mockImplementation(({ create }) => Promise.resolve(create)),
      },
    };
    const canales = {
      paraProveedor: jest.fn().mockResolvedValue(CANAL(proveedor)),
      marcarEstado: jest.fn().mockResolvedValue(undefined),
    };
    const factory = { para: jest.fn().mockReturnValue(provider) };
    return { service: new MensajeriaService(prisma, canales as any, factory as any), prisma, canales, provider };
  };

  describe('regla D4 — Baileys no hace envíos automáticos', () => {
    /**
     * Es el punto de toda la regla: un envío masivo por protocolo no oficial es
     * la forma más rápida de que Meta bloquee el número del cliente.
     */
    it('bloquea el envío automático en Baileys', async () => {
      const { service, provider } = crear('evolution_baileys');
      await expect(service.enviarAutomatico('can-1', '1', 'hola')).rejects.toThrow(
        BaileysAutomaticoBloqueadoError,
      );
      expect(provider.enviarTexto).not.toHaveBeenCalled();
    });

    it('el mensaje del error dice qué hacer', async () => {
      const { service } = crear('evolution_baileys');
      await expect(service.enviarAutomatico('can-1', '1', 'hola')).rejects.toThrow(/Meta Cloud API/);
    });

    /** Una persona escribiendo en la bandeja sí puede, incluso en Baileys. */
    it('permite el envío manual en Baileys', async () => {
      const { service, provider } = crear('evolution_baileys');
      await service.enviarManual('can-1', '1', 'hola');
      expect(provider.enviarTexto).toHaveBeenCalled();
    });

    it('en Meta Cloud el automático pasa', async () => {
      const { service, provider } = crear('meta_cloud');
      await service.enviarAutomatico('can-1', '1', 'hola');
      expect(provider.enviarTexto).toHaveBeenCalled();
    });

    it('en Evolution modo cloud el automático pasa: va por la API oficial', async () => {
      const { service, provider } = crear('evolution_cloud');
      await service.enviarAutomatico('can-1', '1', 'hola');
      expect(provider.enviarTexto).toHaveBeenCalled();
    });

    it('las plantillas son automáticas por definición: Baileys las bloquea', async () => {
      const { service } = crear('evolution_baileys');
      await expect(service.enviarPlantilla('can-1', '1', 'bienvenida', 'es')).rejects.toThrow(
        BaileysAutomaticoBloqueadoError,
      );
    });

    it('un adjunto manual en Baileys pasa; el automático no', async () => {
      const { service } = crear('evolution_baileys');
      await expect(service.enviarAdjunto('can-1', '1', 'u', 'image/jpeg')).resolves.toMatchObject({
        estado: 'encolado',
      });
      await expect(
        service.enviarAdjunto('can-1', '1', 'u', 'image/jpeg', undefined, true),
      ).rejects.toThrow(BaileysAutomaticoBloqueadoError);
    });
  });

  describe('registro del resultado', () => {
    it('un envío aceptado marca el canal como sano', async () => {
      const { service, canales } = crear();
      await service.enviarManual('can-1', '1', 'hola');
      expect(canales.marcarEstado).toHaveBeenCalledWith('can-1', true);
    });

    /** Un rechazo no es excepción: el agente tiene que poder reintentar. */
    it('un rechazo queda registrado con el motivo y no lanza', async () => {
      const { service, canales } = crear('meta_cloud', {
        enviarTexto: jest.fn().mockResolvedValue({ id_externo: null, estado: 'rechazado', error: 'Fuera de ventana' }),
      });
      const r = await service.enviarManual('can-1', '1', 'hola');
      expect(r.estado).toBe('rechazado');
      expect(canales.marcarEstado).toHaveBeenCalledWith('can-1', false, 'Fuera de ventana');
    });
  });

  describe('estadoSesion', () => {
    it('para Meta informa que no aplica en vez de inventar un estado', async () => {
      const { service } = crear('meta_cloud');
      expect(await service.estadoSesion('can-1')).toEqual({ soportado: false, estado: null });
    });

    it('para Baileys devuelve el estado real de la sesión', async () => {
      const { service } = crear('evolution_baileys', {
        estadoSesion: jest.fn().mockResolvedValue('qr_pendiente'),
      });
      expect(await service.estadoSesion('can-1')).toEqual({ soportado: true, estado: 'qr_pendiente' });
    });
  });

  describe('estimarCosto', () => {
    /**
     * Un costo de 0 en pantalla es peor que "no sé": invita a mandar campañas
     * creyendo que son gratis.
     */
    it('sin tarifa cargada devuelve null, no cero', async () => {
      const { service } = crear();
      expect(await service.estimarCosto('PY', 'marketing')).toBeNull();
    });

    it('usa la tarifa vigente más reciente anterior a la fecha', async () => {
      const { service, prisma } = crear();
      prisma.crm_tarifas_whatsapp.findFirst.mockResolvedValue({
        precio_usd: '0.0432', vigente_desde: new Date('2026-10-01'),
      });
      const r = await service.estimarCosto('py', 'utility', new Date('2026-10-15'));
      expect(r).toEqual({ precio_usd: 0.0432, vigente_desde: new Date('2026-10-01') });
      const where = prisma.crm_tarifas_whatsapp.findFirst.mock.calls[0][0];
      expect(where.where.pais).toBe('PY');
      expect(where.orderBy).toEqual({ vigente_desde: 'desc' });
    });

    it('normaliza el país a mayúsculas al cargar la tarifa', async () => {
      const { service, prisma } = crear();
      await service.upsertTarifa('py', 'marketing', 0.0625, '2026-10-01');
      expect(prisma.crm_tarifas_whatsapp.upsert.mock.calls[0][0].create.pais).toBe('PY');
    });

    it('un precio negativo se rechaza', async () => {
      const { service } = crear();
      await expect(service.upsertTarifa('PY', 'marketing', -1, '2026-10-01')).rejects.toThrow(/negativo/);
    });
  });
});
