import { BadRequestException, ConflictException, NotFoundException } from '@nestjs/common';
import { CrmConfigService } from './crm-config.service';

describe('CrmConfigService', () => {
  const PIPELINE = { id: 'pip-1', empresa_id: 'emp-1', nombre: 'Ventas', activo: true };

  const crearService = (overrides: Record<string, any> = {}) => {
    const prisma: any = {
      crm_pipelines: {
        findFirst: jest.fn().mockResolvedValue(PIPELINE),
        findMany: jest.fn().mockResolvedValue([]),
        create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'pip-nuevo', ...data })),
        update: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'pip-1', ...data })),
        updateMany: jest.fn().mockResolvedValue({ count: 0 }),
      },
      crm_etapas: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue([]),
        create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'eta-nueva', ...data })),
        update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve({ id: where.id, ...data })),
      },
      ...overrides,
    };
    // reordenarEtapas pasa un array de promesas; el resto pasa un callback.
    prisma.$transaction = jest
      .fn()
      .mockImplementation((arg: any) => (typeof arg === 'function' ? arg(prisma) : Promise.all(arg)));
    const audit = { log: jest.fn().mockResolvedValue(undefined) };
    const seed = {
      necesitaSeed: jest.fn().mockResolvedValue(false),
      asegurarConfiguracion: jest.fn().mockResolvedValue({}),
    };
    return { service: new CrmConfigService(prisma as any, audit as any, seed as any), prisma, audit, seed };
  };

  describe('pipelines', () => {
    it('al marcar uno como default le saca el default a los demás', async () => {
      const { service, prisma } = crearService({
        crm_pipelines: {
          findFirst: jest.fn().mockResolvedValue(null),
          create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'pip-nuevo', ...data })),
          updateMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      });

      await service.createPipeline({ nombre: ' Mayorista ', es_default: true } as any, 'emp-1', 'user-1');

      expect(prisma.crm_pipelines.updateMany).toHaveBeenCalledWith({
        where: { empresa_id: 'emp-1', es_default: true },
        data: { es_default: false },
      });
      const data = prisma.crm_pipelines.create.mock.calls[0][0].data;
      expect(data.nombre).toBe('Mayorista');
      expect(data.empresa_id).toBe('emp-1');
      expect(data.created_by).toBe('user-1');
    });

    it('no toca el default de nadie si el pipeline nuevo no lo pide', async () => {
      const { service, prisma } = crearService({
        crm_pipelines: {
          findFirst: jest.fn().mockResolvedValue(null),
          create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'pip-nuevo', ...data })),
          updateMany: jest.fn().mockResolvedValue({ count: 0 }),
        },
      });

      await service.createPipeline({ nombre: 'Servicios' } as any, 'emp-1');

      expect(prisma.crm_pipelines.updateMany).not.toHaveBeenCalled();
    });

    it('rechaza un nombre ya usado en la misma empresa', async () => {
      const { service } = crearService({
        crm_pipelines: {
          findFirst: jest.fn().mockResolvedValue({ id: 'pip-existente', nombre: 'Ventas' }),
          create: jest.fn(),
          updateMany: jest.fn(),
        },
      });

      await expect(service.createPipeline({ nombre: 'Ventas' } as any, 'emp-1')).rejects.toThrow(ConflictException);
    });

    it('no deja tocar el pipeline de otra empresa', async () => {
      const { service } = crearService({
        crm_pipelines: { findFirst: jest.fn().mockResolvedValue(null), update: jest.fn() },
      });

      await expect(service.updatePipeline('pip-ajeno', { nombre: 'X' } as any, 'emp-1')).rejects.toThrow(
        NotFoundException,
      );
    });

    it('da de baja en lógico, nunca borra', async () => {
      const { service, prisma } = crearService();

      await service.removePipeline('pip-1', 'emp-1', 'user-1');

      expect(prisma.crm_pipelines.update.mock.calls[0][0].data.activo).toBe(false);
      expect(prisma.crm_pipelines.delete).toBeUndefined();
    });
  });

  describe('etapas', () => {
    it('sin orden explícito la agrega al final', async () => {
      const { service, prisma } = crearService({
        crm_etapas: {
          findFirst: jest
            .fn()
            .mockResolvedValueOnce(null) // no hay choque de nombre
            .mockResolvedValueOnce({ orden: 4 }), // última etapa
          create: jest.fn().mockImplementation(({ data }) => Promise.resolve({ id: 'eta-nueva', ...data })),
        },
      });

      await service.createEtapa('pip-1', { nombre: 'Negociación' } as any, 'emp-1');

      expect(prisma.crm_etapas.create.mock.calls[0][0].data.orden).toBe(5);
    });

    it('rechaza una segunda etapa ganada en el mismo pipeline', async () => {
      const { service } = crearService({
        crm_etapas: {
          findFirst: jest
            .fn()
            .mockResolvedValueOnce(null) // nombre libre
            .mockResolvedValueOnce({ nombre: 'Ganada' }), // ya hay una ganada
          create: jest.fn(),
        },
      });

      await expect(
        service.createEtapa('pip-1', { nombre: 'Cerrada OK', es_ganada: true } as any, 'emp-1'),
      ).rejects.toThrow(ConflictException);
    });

    it('rechaza una etapa que sea ganada y perdida a la vez', async () => {
      const { service } = crearService();

      await expect(
        service.createEtapa('pip-1', { nombre: 'Rara', es_ganada: true, es_perdida: true } as any, 'emp-1'),
      ).rejects.toThrow(BadRequestException);
    });

    it('al editar, la validación de resultado único se excluye a sí misma', async () => {
      const { service, prisma } = crearService({
        crm_etapas: {
          findFirst: jest
            .fn()
            .mockResolvedValueOnce({ id: 'eta-1', empresa_id: 'emp-1', pipeline_id: 'pip-1', nombre: 'Ganada' })
            .mockResolvedValue(null),
          update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve({ id: where.id, ...data })),
        },
      });

      await service.updateEtapa('eta-1', { es_ganada: true } as any, 'emp-1');

      const where = prisma.crm_etapas.findFirst.mock.calls[1][0].where;
      expect(where.id).toEqual({ not: 'eta-1' });
    });
  });

  describe('reordenarEtapas', () => {
    const activas = [{ id: 'a' }, { id: 'b' }, { id: 'c' }];

    it('persiste el orden recibido como 0..n', async () => {
      const { service, prisma } = crearService({
        crm_etapas: {
          findMany: jest.fn().mockResolvedValue(activas),
          update: jest.fn().mockImplementation(({ where, data }) => Promise.resolve({ id: where.id, ...data })),
        },
      });

      await service.reordenarEtapas('pip-1', { etapa_ids: ['c', 'a', 'b'] } as any, 'emp-1', 'user-1');

      const ordenes = prisma.crm_etapas.update.mock.calls.map((c: any[]) => [c[0].where.id, c[0].data.orden]);
      expect(ordenes).toEqual([
        ['c', 0],
        ['a', 1],
        ['b', 2],
      ]);
    });

    it('rechaza ids que no son del pipeline', async () => {
      const { service } = crearService({
        crm_etapas: { findMany: jest.fn().mockResolvedValue(activas), update: jest.fn() },
      });

      await expect(
        service.reordenarEtapas('pip-1', { etapa_ids: ['a', 'b', 'c', 'intruso'] } as any, 'emp-1'),
      ).rejects.toThrow(BadRequestException);
    });

    it('rechaza una lista parcial: dejaría el tablero mezclado', async () => {
      const { service } = crearService({
        crm_etapas: { findMany: jest.fn().mockResolvedValue(activas), update: jest.fn() },
      });

      await expect(service.reordenarEtapas('pip-1', { etapa_ids: ['a', 'b'] } as any, 'emp-1')).rejects.toThrow(
        BadRequestException,
      );
    });
  });

  describe('siembra automática', () => {
    it('la primera vez que se abre el CRM siembra la configuración', async () => {
      const { service, seed } = crearService();
      seed.necesitaSeed.mockResolvedValue(true);

      await service.findAllPipelines('emp-1');

      expect(seed.asegurarConfiguracion).toHaveBeenCalledWith('emp-1');
    });

    it('si la empresa ya tiene pipelines no vuelve a sembrar', async () => {
      const { service, seed } = crearService();

      await service.findAllPipelines('emp-1');

      expect(seed.asegurarConfiguracion).not.toHaveBeenCalled();
    });
  });

  describe('multiempresa', () => {
    it('toda lectura de etapas filtra por empresa_id', async () => {
      const { service, prisma } = crearService();

      await service.findEtapas('pip-1', 'emp-1', true);

      expect(prisma.crm_etapas.findMany.mock.calls[0][0].where).toMatchObject({
        pipeline_id: 'pip-1',
        empresa_id: 'emp-1',
      });
    });
  });
});
