import { Logger } from '@nestjs/common';
import { CrmPresupuestoSyncService } from './crm-presupuesto-sync.service';

const ETAPAS = [
  { id: 'e-calif', nombre: 'Calificación', orden: 0, probabilidad: 10, campos_obligatorios: ['origen_id'] },
  { id: 'e-demo', nombre: 'Necesidad / Demo', orden: 1, probabilidad: 30, campos_obligatorios: ['valor_unico'] },
  { id: 'e-prop', nombre: 'Propuesta enviada', orden: 2, probabilidad: 50, campos_obligatorios: ['presupuesto_id'] },
  { id: 'e-nego', nombre: 'Negociación', orden: 3, probabilidad: 70, campos_obligatorios: ['fecha_cierre_estimada'] },
];

const OPO = (over: Record<string, any> = {}) => ({
  id: 'opo-1', empresa_id: 'emp-1', sucursal_id: 'suc-1', cliente_id: 'cli-1',
  pipeline_id: 'pip-1', etapa_id: 'e-demo', vendedor_id: 'v-1', presupuesto_id: null,
  estado: 'abierta', valor_unico: 1_000_000, origen_id: 'o-1',
  fecha_cierre_estimada: new Date('2026-12-01'),
  crm_etapas: ETAPAS[1], ...over,
});

describe('CrmPresupuestoSyncService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      presupuesto_cab: { findUnique: jest.fn().mockResolvedValue({ id: 'pre-1', numero: 'PRE-0001', oportunidad_id: 'opo-1' }) },
      crm_oportunidades: {
        findUnique: jest.fn().mockResolvedValue(OPO()),
        update: jest.fn().mockResolvedValue({}),
      },
      crm_etapas: { findMany: jest.fn().mockResolvedValue(ETAPAS) },
      crm_actividades: { create: jest.fn().mockResolvedValue({ id: 'act-1' }) },
      vendedores_cobradores: { findUnique: jest.fn().mockResolvedValue({ usuario_id: 'u-9' }) },
      ...over,
    };
    return { service: new CrmPresupuestoSyncService(prisma), prisma };
  };

  const etapasEscritas = (prisma: any) =>
    prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data.etapa_id).filter(Boolean);

  describe('no hay nada que sincronizar', () => {
    it('un presupuesto que no nació del CRM se ignora', async () => {
      const { service, prisma } = crear({
        presupuesto_cab: { findUnique: jest.fn().mockResolvedValue({ id: 'pre-1', numero: 'PRE-1', oportunidad_id: null }) },
      });
      await service.presupuestoAceptado('pre-1');
      expect(prisma.crm_oportunidades.findUnique).not.toHaveBeenCalled();
      expect(prisma.crm_actividades.create).not.toHaveBeenCalled();
    });

    it('una oportunidad ya ganada no se toca', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: { findUnique: jest.fn().mockResolvedValue(OPO({ estado: 'ganada' })), update: jest.fn() },
      });
      await service.presupuestoAceptado('pre-1');
      expect(prisma.crm_oportunidades.update).not.toHaveBeenCalled();
      expect(prisma.crm_actividades.create).not.toHaveBeenCalled();
    });

    /**
     * El cliente aceptando desde el portal no tiene por qué ver un 500 porque la
     * etapa no se pudo mover: el presupuesto ya quedó aceptado.
     */
    it('un fallo de base no se propaga al que llama', async () => {
      const logError = jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined);
      const { service } = crear({
        crm_oportunidades: { findUnique: jest.fn().mockRejectedValue(new Error('conexión caída')), update: jest.fn() },
      });
      await expect(service.presupuestoAceptado('pre-1')).resolves.toBeUndefined();
      expect(logError).toHaveBeenCalled();
      logError.mockRestore();
    });
  });

  describe('enviado', () => {
    it('vincula el presupuesto y avanza a la etapa que lo exige', async () => {
      const { service, prisma } = crear();
      await service.presupuestoEnviado('pre-1');

      const datos = prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data);
      expect(datos[0]).toMatchObject({ presupuesto_id: 'pre-1' });
      expect(etapasEscritas(prisma)).toEqual(['e-prop']);
    });

    /** La etapa se elige por los campos que exige, no por su nombre: el cliente puede renombrarla. */
    it('si ninguna etapa exige presupuesto_id, no inventa un destino', async () => {
      const sinPropuesta = ETAPAS.filter((e) => e.id !== 'e-prop');
      const { service, prisma } = crear({ crm_etapas: { findMany: jest.fn().mockResolvedValue(sinPropuesta) } });
      await service.presupuestoEnviado('pre-1');
      expect(etapasEscritas(prisma)).toEqual([]);
    });

    it('no retrocede una oportunidad que ya está en negociación', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: { findUnique: jest.fn().mockResolvedValue(OPO({ etapa_id: 'e-nego', crm_etapas: ETAPAS[3] })), update: jest.fn().mockResolvedValue({}) },
      });
      await service.presupuestoEnviado('pre-1');
      expect(etapasEscritas(prisma)).toEqual([]);
    });

    it('si el presupuesto ya era el vigente no reescribe el vínculo', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: { findUnique: jest.fn().mockResolvedValue(OPO({ presupuesto_id: 'pre-1' })), update: jest.fn().mockResolvedValue({}) },
      });
      await service.presupuestoEnviado('pre-1');
      const datos = prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data);
      expect(datos.some((d: any) => 'presupuesto_id' in d)).toBe(false);
    });
  });

  describe('aceptado', () => {
    it('avanza a la última etapa abierta y deja la tarea de cerrar', async () => {
      const { service, prisma } = crear();
      await service.presupuestoAceptado('pre-1');

      expect(etapasEscritas(prisma)).toEqual(['e-nego']);
      const act = prisma.crm_actividades.create.mock.calls[0][0].data;
      expect(act).toMatchObject({ tipo: 'tarea', oportunidad_id: 'opo-1', vendedor_id: 'v-1', usuario_id: 'u-9' });
      expect(act.asunto).toContain('PRE-0001');
    });

    /** Ganar emite documentos: eso es del asistente de cierre y de nadie más. */
    it('nunca marca la oportunidad ganada', async () => {
      const { service, prisma } = crear();
      await service.presupuestoAceptado('pre-1');
      const datos = prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data);
      expect(datos.some((d: any) => d.estado === 'ganada')).toBe(false);
    });

    /**
     * Forzar la etapa saltándose sus campos obligatorios vaciaría de sentido la
     * regla que sostiene el embudo. Se avisa y decide el vendedor.
     */
    it('si al destino le faltan campos obligatorios no mueve: avisa', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: {
          findUnique: jest.fn().mockResolvedValue(OPO({ fecha_cierre_estimada: null })),
          update: jest.fn().mockResolvedValue({}),
        },
      });
      await service.presupuestoAceptado('pre-1');

      expect(etapasEscritas(prisma)).toEqual([]);
      const asuntos = prisma.crm_actividades.create.mock.calls.map((c: any[]) => c[0].data.asunto);
      expect(asuntos[0]).toContain('Negociación');
      expect(prisma.crm_actividades.create.mock.calls[0][0].data.detalle).toContain('fecha_cierre_estimada');
    });

    it('sin vendedor asignado la tarea queda sin usuario, no falla', async () => {
      const { service, prisma } = crear({
        crm_oportunidades: { findUnique: jest.fn().mockResolvedValue(OPO({ vendedor_id: null })), update: jest.fn().mockResolvedValue({}) },
      });
      await service.presupuestoAceptado('pre-1');
      expect(prisma.crm_actividades.create.mock.calls[0][0].data.usuario_id).toBeNull();
      expect(prisma.vendedores_cobradores.findUnique).not.toHaveBeenCalled();
    });
  });

  describe('rechazado', () => {
    /** Perder exige un motivo del catálogo; el texto libre del portal no lo es. */
    it('no marca perdida ni cambia la etapa: deja la tarea con el motivo', async () => {
      const { service, prisma } = crear();
      await service.presupuestoRechazado('pre-1', 'Consiguieron mejor precio');

      expect(etapasEscritas(prisma)).toEqual([]);
      const act = prisma.crm_actividades.create.mock.calls[0][0].data;
      expect(act.asunto).toContain('rechazado');
      expect(act.detalle).toContain('Consiguieron mejor precio');
      const datos = prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data);
      expect(datos.some((d: any) => d.estado === 'perdida')).toBe(false);
    });

    it('sin motivo pide averiguarlo antes de dar por perdida', async () => {
      const { service, prisma } = crear();
      await service.presupuestoRechazado('pre-1');
      expect(prisma.crm_actividades.create.mock.calls[0][0].data.detalle).toMatch(/sin dejar motivo/i);
    });
  });
});
