import { NotFoundException } from '@nestjs/common';
import { TimelineService } from './timeline.service';

const f = (iso: string) => new Date(`${iso}T12:00:00.000Z`);

describe('TimelineService', () => {
  const CLIENTE = {
    id: 'cli-1', saldo_pendiente: 3_000_000, limite_credito: 5_000_000,
    credito_habilitado: true, bloqueado_credito: false, etiquetas: ['mayorista'], segmento: 'A',
    telefono_e164: '+595981123456',
    personas: { razon_social: 'Ferretería Central', ruc: '80012345', email: 'f@x.com', celular: '0981123456' },
  };

  const crear = (over: Record<string, any> = {}) => {
    const vacio = { findMany: jest.fn().mockResolvedValue([]) };
    const prisma: any = {
      clientes: { findFirst: jest.fn().mockResolvedValue(CLIENTE) },
      factura_cab: { ...vacio },
      // Lo facturado se calcula en SQL: `total_factura` está siempre en NULL en
      // este sistema y la fuente real es factura_subtotales.
      $queryRaw: jest.fn().mockResolvedValue([{ total: 0, cantidad: 0 }]),
      crm_oportunidades: {
        ...vacio,
        aggregate: jest.fn().mockResolvedValue({ _sum: { valor_unico: 0, valor_mrr: 0 }, _count: { _all: 0 } }),
      },
      crm_actividades: { ...vacio, findFirst: jest.fn().mockResolvedValue(null) },
      presupuesto_cab: { ...vacio },
      nota_credito_cab: { ...vacio },
      recibos_cobro: { ...vacio },
      crm_leads: { ...vacio },
      ...over,
    };
    return { service: new TimelineService(prisma), prisma };
  };

  describe('resumen', () => {
    it('un cliente de otra empresa no existe', async () => {
      const { service } = crear({ clientes: { findFirst: jest.fn().mockResolvedValue(null) } });
      await expect(service.resumen('cli-ajeno', 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('calcula el disponible de crédito', async () => {
      const { service } = crear();
      const r: any = await service.resumen('cli-1', 'emp-1');
      expect(r.credito).toMatchObject({ limite: 5_000_000, saldo_pendiente: 3_000_000, disponible: 2_000_000 });
    });

    /** Un vendedor externo puede ver el historial sin enterarse de cuánto debe. */
    it('sin permiso de saldo, el bloque de crédito no viaja', async () => {
      const { service } = crear();
      const r: any = await service.resumen('cli-1', 'emp-1', false);
      expect(r.credito).toBeNull();
      expect(r.cliente.razon_social).toBe('Ferretería Central');
    });

    it('sin límite cargado no inventa un disponible', async () => {
      const { service } = crear({
        clientes: { findFirst: jest.fn().mockResolvedValue({ ...CLIENTE, limite_credito: 0 }) },
      });
      expect((await service.resumen('cli-1', 'emp-1')).credito?.disponible).toBeNull();
    });

    /**
     * `factura_cab.total_factura` está SIEMPRE en NULL en este sistema y el
     * propio repo lo advierte ("NO USAR"). La primera versión lo usaba igual, así
     * que el LTV daba 0 para todos los clientes. Se descubrió cargando datos de
     * prueba: la ficha mostraba "1 factura" y "facturado Gs 0".
     */
    it('el facturado sale de factura_subtotales, no de total_factura', async () => {
      const { service, prisma } = crear();
      prisma.$queryRaw.mockResolvedValue([{ total: 3_554_404, cantidad: 1 }]);

      const r = await service.resumen('cli-1', 'emp-1');

      expect(r.ltv).toBe(3_554_404);
      expect(r.facturas_emitidas).toBe(1);
      const sql = prisma.$queryRaw.mock.calls[0][0].join('?');
      expect(sql).toContain('factura_subtotales');
      expect(sql).not.toContain('total_factura');
    });

    it('excluye las anuladas y convierte la moneda extranjera', async () => {
      const { service, prisma } = crear();
      await service.resumen('cli-1', 'emp-1');
      const sql = prisma.$queryRaw.mock.calls[0][0].join('?');
      expect(sql).toContain("<> 'Anulado'");
      // Sin la cotización, una factura en dólares quedaría mal por un factor de 7.000.
      expect(sql).toContain('COALESCE(fc.dticam, 1)');
    });

    it('filtra por empresa dentro del SQL', async () => {
      const { service, prisma } = crear();
      await service.resumen('cli-1', 'emp-1');
      const sql = prisma.$queryRaw.mock.calls[0][0].join('?');
      expect(sql).toContain('fc.empresa_id =');
      expect(prisma.$queryRaw.mock.calls[0]).toContain('emp-1');
    });
  });

  describe('timeline', () => {
    const conFacturas = (fechas: string[]) => ({
      factura_cab: {
        findMany: jest.fn().mockResolvedValue(
          fechas.map((d, i) => ({
            id: `fac-${i}`, dest: '001', dpunexp: '001', dnumdoc: `000000${i}`,
            dfeemide: f(d), estado: 'Aprobado', estado_sifen: 'Aprobado',
            dticam: null, factura_subtotales: [{ dtotgralope: 1000 }],
          })),
        ),
        aggregate: jest.fn().mockResolvedValue({ _sum: {}, _count: { _all: 0 } }),
      },
    });

    it('mezcla fuentes distintas y ordena por fecha descendente', async () => {
      const { service } = crear({
        ...conFacturas(['2026-05-10']),
        recibos_cobro: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'rec-1', numero_recibo: 'R-1', fecha_emision: f('2026-06-01'), monto_total: 500, estado: 'Aplicado' },
          ]),
        },
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'opo-1', numero: 'OPP-0000001', titulo: 'Reposición', created_at: f('2026-04-01'), valor_unico: 100, valor_mrr: 0, estado: 'abierta', etapa_id: 'e1' },
          ]),
          aggregate: jest.fn().mockResolvedValue({ _sum: {}, _count: { _all: 0 } }),
        },
      });

      const r = await service.timeline('cli-1', 'emp-1');

      expect(r.eventos.map((e) => e.tipo)).toEqual(['cobro', 'factura', 'oportunidad']);
    });

    it('el filtro por tipo no consulta las fuentes descartadas', async () => {
      const { service, prisma } = crear(conFacturas(['2026-05-10']));

      const r = await service.timeline('cli-1', 'emp-1', { tipos: ['factura'] });

      expect(r.eventos.every((e) => e.tipo === 'factura')).toBe(true);
      expect(prisma.recibos_cobro.findMany).not.toHaveBeenCalled();
      expect(prisma.crm_leads.findMany).not.toHaveBeenCalled();
    });

    /**
     * Cada fuente trae limite+1: si aportara exactamente `limite`, al mezclarla
     * con otra quedarían eventos fuera de la página que nunca se verían.
     */
    it('pide un evento de más a cada fuente para no perder ninguno al mezclar', async () => {
      const { service, prisma } = crear(conFacturas(['2026-05-10']));
      await service.timeline('cli-1', 'emp-1', { limite: 10 });
      expect(prisma.factura_cab.findMany.mock.calls[0][0].take).toBe(11);
    });

    it('devuelve cursor cuando hay más páginas', async () => {
      const fechas = Array.from({ length: 5 }, (_, i) => `2026-0${i + 1}-01`);
      const { service } = crear(conFacturas(fechas));

      const r = await service.timeline('cli-1', 'emp-1', { limite: 3 });

      expect(r.eventos).toHaveLength(3);
      expect(r.hay_mas).toBe(true);
      expect(r.siguiente_cursor).toEqual(r.eventos[2].fecha);
    });

    it('en la última página no hay cursor', async () => {
      const { service } = crear(conFacturas(['2026-05-10']));
      const r = await service.timeline('cli-1', 'emp-1', { limite: 30 });
      expect(r.hay_mas).toBe(false);
      expect(r.siguiente_cursor).toBeNull();
    });

    it('el cursor se traduce a un corte por fecha en cada fuente', async () => {
      const { service, prisma } = crear(conFacturas([]));
      await service.timeline('cli-1', 'emp-1', { cursor: '2026-06-01T00:00:00.000Z' });
      expect(prisma.factura_cab.findMany.mock.calls[0][0].where.dfeemide).toEqual({
        lt: new Date('2026-06-01T00:00:00.000Z'),
      });
    });

    it('el límite se recorta: nadie pide 5.000 eventos de una', async () => {
      const { service, prisma } = crear(conFacturas([]));
      await service.timeline('cli-1', 'emp-1', { limite: 5000 });
      expect(prisma.factura_cab.findMany.mock.calls[0][0].take).toBe(101);
    });

    it('la actividad realizada se ubica por su fecha real, no por la programada', async () => {
      const { service } = crear({
        crm_actividades: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'act-1', tipo: 'llamada', asunto: 'Seguimiento', estado: 'realizada',
              fecha_programada: f('2026-05-01'), fecha_realizada: f('2026-05-20'), resultado: 'Atendió', detalle: null, oportunidad_id: null },
          ]),
          findFirst: jest.fn().mockResolvedValue(null),
        },
      });

      const r = await service.timeline('cli-1', 'emp-1');
      expect(r.eventos[0].fecha).toEqual(f('2026-05-20'));
    });

    it('toda consulta filtra por empresa_id', async () => {
      const { service, prisma } = crear(conFacturas([]));
      await service.timeline('cli-1', 'emp-1');
      expect(prisma.factura_cab.findMany.mock.calls[0][0].where).toMatchObject({ empresa_id: 'emp-1', cliente_id: 'cli-1' });
    });
  });
});
