import { BadRequestException, ConflictException } from '@nestjs/common';
import { ActividadesService } from './actividades.service';

const dias = (n: number) => new Date(Date.now() - n * 86_400_000);

describe('ActividadesService', () => {
  const PENDIENTE = { id: 'act-1', empresa_id: 'emp-1', estado: 'pendiente', tipo: 'llamada', oportunidad_id: 'opo-1', checkin_at: null, checkout_at: null };

  const crear = (overrides: Record<string, any> = {}) => {
    const prisma: any = {
      crm_actividades: {
        findFirst: jest.fn().mockResolvedValue(PENDIENTE),
        findMany: jest.fn().mockResolvedValue([]),
        create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'act-n', ...data })),
        update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve({ ...PENDIENTE, id: where.id, ...data })),
      },
      crm_oportunidades: {
        findFirst: jest.fn().mockResolvedValue({ id: 'opo-1' }),
        findUnique: jest.fn().mockResolvedValue({ estado: 'abierta', proxima_actividad_at: null }),
        findMany: jest.fn().mockResolvedValue([]),
        update: jest.fn().mockResolvedValue({}),
        updateMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
      crm_parametros: { findMany: jest.fn().mockResolvedValue([]) },
      ...overrides,
    };
    const audit = { log: jest.fn().mockResolvedValue(undefined) };
    return { service: new ActividadesService(prisma, audit as any), prisma, audit };
  };

  describe('create', () => {
    it('exige que cuelgue de algo', async () => {
      const { service } = crear();
      await expect(
        service.create({ tipo: 'tarea', asunto: 'X', fecha_programada: new Date().toISOString() } as any, 'emp-1'),
      ).rejects.toThrow(BadRequestException);
    });

    it('al agendar deja la próxima actividad sellada en la oportunidad', async () => {
      const futura = new Date(Date.now() + 86_400_000);
      const { service, prisma } = crear({
        crm_actividades: {
          findFirst: jest.fn()
            .mockResolvedValueOnce(null) // no hay realizadas
            .mockResolvedValueOnce({ fecha_programada: futura }), // la próxima pendiente
          create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'act-n', ...data })),
        },
      });

      await service.create(
        { tipo: 'llamada', asunto: 'Llamar', oportunidad_id: 'opo-1', fecha_programada: futura.toISOString() } as any,
        'emp-1',
      );

      expect(prisma.crm_oportunidades.update.mock.calls[0][0].data.proxima_actividad_at).toEqual(futura);
    });
  });

  describe('completar', () => {
    it('sin resultado no se completa', async () => {
      const { service } = crear();
      await expect(service.completar('act-1', { resultado: '   ' } as any, 'emp-1')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('no se completa dos veces', async () => {
      const { service } = crear({
        crm_actividades: { findFirst: jest.fn().mockResolvedValue({ ...PENDIENTE, estado: 'realizada' }) },
      });
      await expect(service.completar('act-1', { resultado: 'ok' } as any, 'emp-1')).rejects.toThrow(
        ConflictException,
      );
    });

    it('sugiere agendar la próxima cuando la oportunidad queda sin nada pendiente', async () => {
      const { service } = crear();
      const r: any = await service.completar('act-1', { resultado: 'Atendió' } as any, 'emp-1');
      expect(r.sugerir_proxima).toBe(true);
    });

    it('no sugiere nada si ya hay otra agendada', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findUnique: jest.fn().mockResolvedValue({ estado: 'abierta', proxima_actividad_at: new Date() }),
          update: jest.fn().mockResolvedValue({}),
        },
      });
      const r: any = await service.completar('act-1', { resultado: 'Atendió' } as any, 'emp-1');
      expect(r.sugerir_proxima).toBe(false);
    });

    /** Dejarla roja tras registrar una llamada sería mentirle al vendedor. */
    it('completar saca a la oportunidad de estancada', async () => {
      const realizada = new Date();
      const { service, prisma } = crear({
        crm_actividades: {
          findFirst: jest.fn()
            .mockResolvedValueOnce(PENDIENTE)
            .mockResolvedValueOnce({ fecha_realizada: realizada })
            .mockResolvedValueOnce(null),
          update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve({ ...PENDIENTE, id: where.id, ...data })),
        },
      });

      await service.completar('act-1', { resultado: 'Atendió' } as any, 'emp-1');

      expect(prisma.crm_oportunidades.update.mock.calls[0][0].data).toMatchObject({
        estancada: false,
        estancada_desde: null,
      });
    });
  });

  describe('visitas', () => {
    it('el check-in es sólo para visitas', async () => {
      const { service } = crear();
      await expect(service.checkIn('act-1', { geo_lat: 0, geo_lng: 0 } as any, 'emp-1')).rejects.toThrow(
        BadRequestException,
      );
    });

    it('no se puede cerrar una visita sin haberla abierto', async () => {
      const { service } = crear({
        crm_actividades: { findFirst: jest.fn().mockResolvedValue({ ...PENDIENTE, tipo: 'visita' }) },
      });
      await expect(service.checkOut('act-1', 'emp-1')).rejects.toThrow(ConflictException);
    });

    it('guarda la posición de llegada', async () => {
      const { service, prisma } = crear({
        crm_actividades: {
          findFirst: jest.fn().mockResolvedValue({ ...PENDIENTE, tipo: 'visita' }),
          update: jest.fn().mockImplementation(({ data }) => Promise.resolve(data)),
        },
      });

      await service.checkIn('act-1', { geo_lat: -25.2637, geo_lng: -57.5759 } as any, 'emp-1');

      const data = prisma.crm_actividades.update.mock.calls[0][0].data;
      expect(Number(data.geo_lat)).toBeCloseTo(-25.2637);
      expect(data.checkin_at).toBeInstanceOf(Date);
    });
  });

  describe('estancamiento', () => {
    const opo = (over: Record<string, any> = {}) => ({
      id: 'opo-1', empresa_id: 'emp-1', created_at: dias(30),
      ultima_actividad_at: null, estancada: false, crm_etapas: { dias_estancamiento: 3 }, ...over,
    });

    it('marca la que supera los días de su etapa', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([opo({ ultima_actividad_at: dias(5) })]),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      });

      expect(await service.marcarEstancadas()).toEqual({ marcadas: 1, liberadas: 0 });
      expect(prisma.crm_oportunidades.updateMany.mock.calls[0][0].data.estancada).toBe(true);
    });

    it('no marca la que tuvo actividad reciente', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([opo({ ultima_actividad_at: dias(1) })]),
          updateMany: jest.fn(),
        },
      });
      expect(await service.marcarEstancadas()).toEqual({ marcadas: 0, liberadas: 0 });
    });

    /** El caso más grave: un negocio que nadie tocó nunca. */
    it('una oportunidad sin ninguna actividad se mide desde su creación', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([opo({ created_at: dias(10), ultima_actividad_at: null })]),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      });
      expect((await service.marcarEstancadas()).marcadas).toBe(1);
    });

    it('libera la que volvió a moverse: el job corrige en los dos sentidos', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([opo({ ultima_actividad_at: dias(1), estancada: true })]),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      });

      expect(await service.marcarEstancadas()).toEqual({ marcadas: 0, liberadas: 1 });
      expect(prisma.crm_oportunidades.updateMany.mock.calls[0][0].data.estancada).toBe(false);
    });

    it('sin días en la etapa usa el parámetro de la empresa', async () => {
      const { service } = crear({
        crm_oportunidades: {
          findMany: jest.fn().mockResolvedValue([
            opo({ crm_etapas: { dias_estancamiento: null }, ultima_actividad_at: dias(6) }),
          ]),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
        crm_parametros: { findMany: jest.fn().mockResolvedValue([{ empresa_id: 'emp-1', dias_estancamiento_default: 5 }]) },
      });
      expect((await service.marcarEstancadas()).marcadas).toBe(1);
    });

    it('sin oportunidades abiertas no toca la base', async () => {
      const { service, prisma } = crear();
      expect(await service.marcarEstancadas()).toEqual({ marcadas: 0, liberadas: 0 });
      expect(prisma.crm_oportunidades.updateMany).not.toHaveBeenCalled();
    });
  });
});
