import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { CatalogosService } from './catalogos.service';

describe('CatalogosService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const tabla = (fila: any = null) => ({
      findMany: jest.fn().mockResolvedValue([]),
      findFirst: jest.fn().mockResolvedValue(fila),
      count: jest.fn().mockResolvedValue(0),
      create: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: 'nuevo', ...data })),
      update: jest.fn().mockImplementation(({ where, data }: any) => Promise.resolve({ id: where.id, ...data })),
      updateMany: jest.fn().mockResolvedValue({ count: 0 }),
    });
    const prisma: any = {
      crm_origenes: tabla(),
      crm_motivos: tabla(),
      crm_calendarios: tabla(),
      crm_sla_politicas: tabla(),
      $transaction: jest.fn().mockImplementation((fn: any) => fn(prisma)),
      ...over,
    };
    const audit = { log: jest.fn().mockResolvedValue(undefined) };
    return { service: new CatalogosService(prisma, audit as any), prisma, audit };
  };

  describe('orígenes', () => {
    it('normaliza el código a mayúsculas', async () => {
      const { service, prisma } = crear();
      await service.createOrigen({ codigo: ' tiktok ', descripcion: 'TikTok' }, 'emp-1', 'u-1');
      expect(prisma.crm_origenes.create.mock.calls[0][0].data).toMatchObject({ codigo: 'TIKTOK', descripcion: 'TikTok' });
    });

    it('un código activo repetido es un conflicto', async () => {
      const { service } = crear({
        crm_origenes: { findFirst: jest.fn().mockResolvedValue({ id: 'o-1', activo: true }) },
      });
      await expect(service.createOrigen({ codigo: 'WHATSAPP', descripcion: 'WhatsApp' }, 'emp-1'))
        .rejects.toThrow(ConflictException);
    });

    /** El caso real: dar de baja "FERIA" y volver a necesitarlo el año siguiente. */
    it('un código dado de baja se reactiva en vez de fallar', async () => {
      const { service, prisma } = crear({
        crm_origenes: {
          findFirst: jest.fn().mockResolvedValue({ id: 'o-1', activo: false, codigo: 'FERIA' }),
          update: jest.fn().mockImplementation(({ data }: any) => Promise.resolve({ id: 'o-1', ...data })),
        },
      });
      const r: any = await service.createOrigen({ codigo: 'FERIA', descripcion: 'Feria Expo' }, 'emp-1');
      expect(r).toMatchObject({ id: 'o-1', activo: true, descripcion: 'Feria Expo' });
      expect(prisma.crm_origenes.update).toHaveBeenCalled();
    });

    /** Borrar rompería los reportes del año pasado: lo histórico apunta acá. */
    it('la baja es lógica', async () => {
      const { service, prisma } = crear({
        crm_origenes: {
          findFirst: jest.fn().mockResolvedValue({ id: 'o-1', activo: true }),
          update: jest.fn().mockResolvedValue({ id: 'o-1', activo: false }),
        },
      });
      await service.removeOrigen('o-1', 'emp-1');
      expect(prisma.crm_origenes.update.mock.calls[0][0].data.activo).toBe(false);
      expect(prisma.crm_origenes.delete).toBeUndefined();
    });

    it('un id de otra empresa es 404', async () => {
      const { service } = crear({ crm_origenes: { findFirst: jest.fn().mockResolvedValue(null) } });
      await expect(service.updateOrigen('o-x', { descripcion: 'x' }, 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('deja rastro en auditoría', async () => {
      const { service, audit } = crear();
      await service.createOrigen({ codigo: 'TIKTOK', descripcion: 'TikTok' }, 'emp-1', 'u-1');
      expect(audit.log).toHaveBeenCalledWith(expect.objectContaining({ action: 'CREAR', entity_type: 'crm_origenes' }));
    });
  });

  describe('motivos', () => {
    it('filtra por tipo cuando se pide', async () => {
      const { service, prisma } = crear();
      await service.findMotivos('emp-1', 'perdida', true);
      expect(prisma.crm_motivos.findMany.mock.calls[0][0].where).toMatchObject({ tipo: 'perdida', activo: true });
    });

    it('el mismo texto en otro tipo no es duplicado', async () => {
      const { service, prisma } = crear();
      await service.createMotivo({ tipo: 'churn', descripcion: 'Precio' }, 'emp-1');
      expect(prisma.crm_motivos.findFirst.mock.calls[0][0].where).toMatchObject({ tipo: 'churn', descripcion: 'Precio' });
    });
  });

  describe('calendarios', () => {
    /** Un tramo invertido deja el SLA sin horas hábiles y nada vence nunca. */
    it('rechaza un tramo que termina antes de empezar', async () => {
      const { service } = crear();
      await expect(service.createCalendario(
        { nombre: 'Raro', horario: [{ dia: 1, desde: '18:00', hasta: '08:00' }] }, 'emp-1',
      )).rejects.toThrow(/antes de empezar/);
    });

    it('rechaza un calendario sin tramos', async () => {
      const { service } = crear();
      await expect(service.createCalendario({ nombre: 'Vacío', horario: [] }, 'emp-1'))
        .rejects.toThrow(BadRequestException);
    });

    it('marcar uno como default quita el default de los demás', async () => {
      const { service, prisma } = crear();
      await service.createCalendario(
        { nombre: 'Verano', horario: [{ dia: 1, desde: '07:00', hasta: '13:00' }], es_default: true }, 'emp-1',
      );
      expect(prisma.crm_calendarios.updateMany.mock.calls[0][0].data).toEqual({ es_default: false });
    });

    it('zona horaria por defecto Asunción', async () => {
      const { service, prisma } = crear();
      await service.createCalendario({ nombre: 'X', horario: [{ dia: 1, desde: '08:00', hasta: '18:00' }] }, 'emp-1');
      expect(prisma.crm_calendarios.create.mock.calls[0][0].data.zona_horaria).toBe('America/Asuncion');
    });

    /** Sin default el reloj de SLA no sabe cuándo correr. */
    it('no se puede dar de baja el calendario por defecto', async () => {
      const { service } = crear({
        crm_calendarios: { findFirst: jest.fn().mockResolvedValue({ id: 'c-1', es_default: true }) },
      });
      await expect(service.removeCalendario('c-1', 'emp-1')).rejects.toThrow(/por defecto/);
    });

    it('no se puede dar de baja uno que usan políticas activas', async () => {
      const { service } = crear({
        crm_calendarios: { findFirst: jest.fn().mockResolvedValue({ id: 'c-1', es_default: false }) },
        crm_sla_politicas: { count: jest.fn().mockResolvedValue(2) },
      });
      await expect(service.removeCalendario('c-1', 'emp-1')).rejects.toThrow(/2 pol/);
    });
  });

  describe('políticas de SLA', () => {
    const VALIDA = { nombre: 'Alta', prioridad: 'alta', min_primera_respuesta: 60, min_resolucion: 480 };

    it('resolver no puede tardar menos que responder', async () => {
      const { service } = crear();
      await expect(service.createSla({ ...VALIDA, min_primera_respuesta: 480, min_resolucion: 60 }, 'emp-1'))
        .rejects.toThrow(/resolución/);
    });

    it('al editar valida contra los valores que quedan, no sólo los enviados', async () => {
      const { service } = crear({
        crm_sla_politicas: {
          findFirst: jest.fn().mockResolvedValue({ id: 's-1', min_primera_respuesta: 60, min_resolucion: 480 }),
          update: jest.fn(),
        },
      });
      // Baja sólo la resolución por debajo de la primera respuesta que ya estaba guardada.
      await expect(service.updateSla('s-1', { min_resolucion: 30 }, 'emp-1')).rejects.toThrow(/resolución/);
    });

    it('un calendario de otra empresa se rechaza', async () => {
      const { service } = crear({
        crm_sla_politicas: { findFirst: jest.fn().mockResolvedValue(null) },
        crm_calendarios: { findFirst: jest.fn().mockResolvedValue(null) },
      });
      await expect(service.createSla({ ...VALIDA, calendario_id: 'c-ajeno' }, 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('tipo_cliente vacío se guarda como null, no como cadena vacía', async () => {
      const { service, prisma } = crear();
      await service.createSla({ ...VALIDA, tipo_cliente: '   ' }, 'emp-1');
      expect(prisma.crm_sla_politicas.create.mock.calls[0][0].data.tipo_cliente).toBeNull();
    });
  });
});
