import { ReportesCrmService, mediana, porcentaje } from './reportes-crm.service';

const ETAPAS = [
  { id: 'e-calif', nombre: 'Calificación', orden: 0, probabilidad: 10, es_ganada: false, es_perdida: false },
  { id: 'e-nego', nombre: 'Negociación', orden: 3, probabilidad: 70, es_ganada: false, es_perdida: false },
  { id: 'e-gana', nombre: 'Ganada', orden: 4, probabilidad: 100, es_ganada: true, es_perdida: false },
  { id: 'e-pierde', nombre: 'Perdida', orden: 5, probabilidad: 0, es_ganada: false, es_perdida: true },
];

const TODAS = { verAjenas: true };

describe('porcentaje', () => {
  it('redondea a un decimal', () => {
    expect(porcentaje(1, 3)).toBe(33.3);
    expect(porcentaje(2, 4)).toBe(50);
  });

  /** Dividir por cero daría NaN y el frontend mostraría "NaN%". */
  it('sin total devuelve 0', () => {
    expect(porcentaje(5, 0)).toBe(0);
  });
});

describe('mediana', () => {
  it('impar toma el del medio', () => {
    expect(mediana([1, 5, 9])).toBe(5);
  });

  it('par promedia los dos del medio', () => {
    expect(mediana([2, 4, 6, 10])).toBe(5);
  });

  it('vacío es 0', () => {
    expect(mediana([])).toBe(0);
  });

  /** Es el caso que justifica tener mediana además de promedio. */
  it('un valor extremo no la mueve como movería al promedio', () => {
    const dias = [5, 6, 7, 8, 240];
    expect(mediana(dias)).toBe(7);
    expect(Math.round(dias.reduce((a, b) => a + b, 0) / dias.length)).toBe(53);
  });
});

describe('ReportesCrmService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      crm_etapas: { findMany: jest.fn().mockResolvedValue(ETAPAS) },
      crm_oportunidades: {
        groupBy: jest.fn().mockResolvedValue([]),
        findMany: jest.fn().mockResolvedValue([]),
      },
      crm_origenes: { findMany: jest.fn().mockResolvedValue([{ id: 'ori-wa', codigo: 'WHATSAPP', descripcion: 'WhatsApp' }]) },
      crm_leads: { groupBy: jest.fn().mockResolvedValue([]) },
      crm_motivos: { findMany: jest.fn().mockResolvedValue([]) },
      vendedores_cobradores: { findMany: jest.fn().mockResolvedValue([]) },
      ...over,
    };
    return { service: new ReportesCrmService(prisma), prisma };
  };

  describe('pipeline', () => {
    it('pondera el valor por la probabilidad de la etapa', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([
            { etapa_id: 'e-nego', _count: { _all: 2 }, _sum: { valor_unico: 10_000_000, valor_mrr: 0 } },
          ]),
        },
      });
      const r = await service.pipeline('emp-1', TODAS, {});
      const nego = r.data.etapas.find((e) => e.etapa_id === 'e-nego');
      expect(nego).toMatchObject({ cantidad: 2, valor: 10_000_000, valor_ponderado: 7_000_000 });
      expect(r.data.totales.valor_ponderado).toBe(7_000_000);
    });

    /** Ganada y Perdida no son embudo: son el resultado. */
    it('excluye las etapas de resultado', async () => {
      const { service } = crear();
      const r = await service.pipeline('emp-1', TODAS, {});
      expect(r.data.etapas.map((e) => e.etapa_id)).toEqual(['e-calif', 'e-nego']);
    });

    it('una etapa sin oportunidades aparece en cero, no desaparece', async () => {
      const { service } = crear();
      const r = await service.pipeline('emp-1', TODAS, {});
      expect(r.data.etapas.every((e) => e.cantidad === 0)).toBe(true);
      expect(r.data.etapas).toHaveLength(2);
    });

    it('sólo cuenta oportunidades abiertas', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', TODAS, {});
      expect(prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.estado).toBe('abierta');
    });
  });

  describe('visibilidad', () => {
    it('con VER_AJENAS no filtra por vendedor', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', { verAjenas: true }, {});
      expect(prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.vendedor_id).toBeUndefined();
    });

    it('sin VER_AJENAS filtra por el vendedor del usuario', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', { verAjenas: false, vendedor_id: 'v-1' }, {});
      expect(prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.vendedor_id).toBe('v-1');
    });

    /** Mejor un reporte vacío que mostrarle el embudo entero a quien no corresponde. */
    it('sin vendedor asociado no ve nada en vez de verlo todo', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', { verAjenas: false, vendedor_id: null }, {});
      expect(prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.vendedor_id)
        .toBe('00000000-0000-0000-0000-000000000000');
    });
  });

  describe('rango de fechas', () => {
    it('sin fechas no filtra', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', TODAS, {});
      expect(prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.created_at).toBeUndefined();
    });

    /** Quien pide "hasta el 31" espera que el 31 entre completo. */
    it('hasta es inclusive: suma un día al límite', async () => {
      const { service, prisma } = crear();
      await service.pipeline('emp-1', TODAS, { desde: '2026-01-01', hasta: '2026-12-31' });
      const rango = prisma.crm_oportunidades.groupBy.mock.calls[0][0].where.created_at;
      expect(rango.gte).toEqual(new Date('2026-01-01'));
      expect(rango.lt).toEqual(new Date('2027-01-01'));
    });
  });

  describe('conversion', () => {
    it('calcula la tasa sobre cerradas, no sobre todas', async () => {
      const { service } = crear({
        crm_leads: { groupBy: jest.fn().mockResolvedValue([{ origen_id: 'ori-wa', _count: { _all: 20 } }]) },
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([
            { origen_id: 'ori-wa', estado: 'ganada', _count: { _all: 3 }, _sum: { valor_unico: 9_000_000 } },
            { origen_id: 'ori-wa', estado: 'perdida', _count: { _all: 1 }, _sum: { valor_unico: 1_000_000 } },
            { origen_id: 'ori-wa', estado: 'abierta', _count: { _all: 6 }, _sum: { valor_unico: 5_000_000 } },
          ]),
        },
      });
      const r = await service.conversion('emp-1', TODAS, {});
      // 3 ganadas sobre 4 cerradas = 75%, no 3 sobre 10.
      expect(r.data[0]).toMatchObject({ leads: 20, oportunidades: 10, ganadas: 3, tasa_conversion: 75, monto_ganado: 9_000_000 });
    });

    it('no muestra orígenes sin movimiento', async () => {
      const { service } = crear();
      expect((await service.conversion('emp-1', TODAS, {})).data).toHaveLength(0);
    });

    it('agrupa aparte lo que vino sin origen', async () => {
      const { service } = crear({
        crm_leads: { groupBy: jest.fn().mockResolvedValue([{ origen_id: null, _count: { _all: 5 } }]) },
      });
      const r = await service.conversion('emp-1', TODAS, {});
      expect(r.data[0]).toMatchObject({ origen: 'Sin origen', leads: 5 });
    });
  });

  describe('motivosPerdida', () => {
    it('ordena por frecuencia y reparte porcentajes', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([
            { motivo_perdida_id: 'm-precio', _count: { _all: 3 }, _sum: { valor_unico: 3_000_000 } },
            { motivo_perdida_id: 'm-comp', _count: { _all: 7 }, _sum: { valor_unico: 7_000_000 } },
          ]),
        },
        crm_motivos: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'm-precio', descripcion: 'Precio' },
            { id: 'm-comp', descripcion: 'Competencia' },
          ]),
        },
      });
      const r = await service.motivosPerdida('emp-1', TODAS, {});
      expect(r.data.map((m) => m.motivo)).toEqual(['Competencia', 'Precio']);
      expect(r.data[0].porcentaje).toBe(70);
    });

    /** Los motivos se dan de baja lógica, pero igual conviene no mostrar un uuid. */
    it('un motivo dado de baja se nombra, no se deja en blanco', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([{ motivo_perdida_id: 'm-viejo', _count: { _all: 1 }, _sum: { valor_unico: 0 } }]),
        },
      });
      expect((await service.motivosPerdida('emp-1', TODAS, {})).data[0].motivo).toBe('Motivo dado de baja');
    });

    it('las perdidas sin motivo se agrupan', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([{ motivo_perdida_id: null, _count: { _all: 2 }, _sum: { valor_unico: 0 } }]),
        },
      });
      expect((await service.motivosPerdida('emp-1', TODAS, {})).data[0].motivo).toBe('Sin motivo');
    });
  });

  describe('vendedores', () => {
    it('ordena por monto ganado y calcula la tasa de cierre', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([
            { vendedor_id: 'v-1', estado: 'ganada', _count: { _all: 3 }, _sum: { valor_unico: 9_000_000 } },
            { vendedor_id: 'v-1', estado: 'perdida', _count: { _all: 1 }, _sum: { valor_unico: 0 } },
            { vendedor_id: 'v-2', estado: 'ganada', _count: { _all: 1 }, _sum: { valor_unico: 20_000_000 } },
          ]),
        },
        vendedores_cobradores: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'v-1', nombre: 'Ana', apellido: 'Giménez' },
            { id: 'v-2', nombre: 'Luis', apellido: null },
          ]),
        },
      });
      const r = await service.vendedores('emp-1', TODAS, {});
      expect(r.data.map((v) => v.vendedor)).toEqual(['Luis', 'Ana Giménez']);
      expect(r.data[1]).toMatchObject({ ganadas: 3, perdidas: 1, tasa_cierre: 75 });
    });

    it('las oportunidades sin vendedor se muestran como "Sin asignar"', async () => {
      const { service } = crear({
        crm_oportunidades: {
          groupBy: jest.fn().mockResolvedValue([{ vendedor_id: null, estado: 'abierta', _count: { _all: 4 }, _sum: { valor_unico: 1_000 } }]),
        },
      });
      expect((await service.vendedores('emp-1', TODAS, {})).data[0]).toMatchObject({ vendedor: 'Sin asignar', abiertas: 4 });
    });
  });

  describe('cicloVenta', () => {
    it('cuenta días entre el alta y el cierre real', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([
            { created_at: new Date('2026-01-01'), fecha_cierre_real: new Date('2026-01-11'), valor_unico: 1_000_000 },
            { created_at: new Date('2026-02-01'), fecha_cierre_real: new Date('2026-02-21'), valor_unico: 2_000_000 },
          ]),
        },
      });
      const r = await service.cicloVenta('emp-1', TODAS, {});
      expect(r.data).toMatchObject({
        ganadas: 2, dias_promedio: 15, dias_mediana: 15, dias_minimo: 10, dias_maximo: 20,
        monto_ganado: 3_000_000,
      });
    });

    it('sin ventas ganadas devuelve ceros, no NaN', async () => {
      const { service } = crear();
      expect(await service.cicloVenta('emp-1', TODAS, {})).toMatchObject({
        data: { ganadas: 0, dias_promedio: 0, dias_mediana: 0 },
      });
    });

    /** Un cierre cargado con fecha anterior al alta daría días negativos. */
    it('nunca devuelve días negativos', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([
            { created_at: new Date('2026-03-10'), fecha_cierre_real: new Date('2026-03-01'), valor_unico: 0 },
          ]),
        },
      });
      expect((await service.cicloVenta('emp-1', TODAS, {})).data.dias_promedio).toBe(0);
    });
  });

  describe('resumen', () => {
    it('devuelve los cinco reportes juntos', async () => {
      const { service } = crear();
      const r = await service.resumen('emp-1', TODAS, {});
      expect(Object.keys(r.data)).toEqual([
        'pipeline', 'conversion_por_origen', 'motivos_perdida', 'vendedores', 'ciclo_venta',
      ]);
    });
  });
});
