import { ConflictException, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
import { PresupuestoOportunidadService } from './presupuesto-oportunidad.service';

const ITEM = (over: Record<string, any> = {}) => ({
  producto_id: 'prod-1', descripcion: 'Taladro X200', cantidad: 10,
  precio_unitario: 110_000, descuento_pct: 0, iva_tasa: 10, tipo_cobro: 'unico', orden: 0, ...over,
});

const OPO = (over: Record<string, any> = {}) => ({
  id: 'opo-1', empresa_id: 'emp-1', numero: 'OPP-0000124', titulo: 'Equipamiento planta 2',
  estado: 'abierta', cliente_id: 'cli-1', sucursal_id: 'suc-1', moneda_id: 'mon-1',
  vendedor_id: 'v-1', items: [ITEM()], ...over,
});

describe('PresupuestoOportunidadService', () => {
  const crear = (over: Record<string, any> = {}) => {
    const prisma: any = {
      crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO()), update: jest.fn().mockResolvedValue({}) },
      presupuesto_cab: { findFirst: jest.fn().mockResolvedValue(null), update: jest.fn().mockResolvedValue({}) },
      productos: { findMany: jest.fn().mockResolvedValue([{ id: 'prod-1', maneja_inventario: true }]) },
      $transaction: jest.fn().mockResolvedValue([]),
      ...over,
    };
    const presupuestos = { create: jest.fn().mockResolvedValue({ id: 'pre-1', numero: 'PRE-0001' }) };
    return { service: new PresupuestoOportunidadService(prisma, presupuestos as any), prisma, presupuestos };
  };

  const dtoDe = (presupuestos: any) => presupuestos.create.mock.calls[0][0];

  describe('precondiciones', () => {
    it('la oportunidad tiene que existir en la empresa', async () => {
      const { service } = crear({ crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(null) } });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('una oportunidad cerrada no se cotiza', async () => {
      const { service } = crear({ crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ estado: 'ganada' })) } });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toThrow(ConflictException);
    });

    it('sin ítems no hay nada que cotizar', async () => {
      const { service } = crear({ crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ items: [] })) } });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toThrow(/ítems/i);
    });

    /** El presupuesto va dirigido a un cliente del padrón; un lead no lo es todavía. */
    it('una oportunidad sin cliente pide convertir el lead primero', async () => {
      const { service } = crear({ crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ cliente_id: null })) } });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toThrow(/lead/i);
    });

    /** Cotizar sólo la parte única sería cotizar de menos. Mismo criterio que el cierre. */
    it('con ítems recurrentes devuelve 422', async () => {
      const { service } = crear({
        crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ items: [ITEM({ tipo_cobro: 'recurrente', frecuencia: 'mensual' })] })) },
      });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toMatchObject({
        response: { requiere: 'CRM_RECURRENTE' },
      });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toThrow(UnprocessableEntityException);
    });

    it('si ya hay un presupuesto vigente devuelve 409 con su id', async () => {
      const { service, presupuestos } = crear({
        presupuesto_cab: { findFirst: jest.fn().mockResolvedValue({ id: 'pre-9', numero: 'PRE-0009', estado: 'SENT' }) },
      });
      await expect(service.generar('opo-1', {}, 'emp-1')).rejects.toMatchObject({
        response: { presupuesto_id: 'pre-9' },
      });
      expect(presupuestos.create).not.toHaveBeenCalled();
    });

    it('un presupuesto rechazado o vencido no bloquea volver a cotizar', async () => {
      const { service, prisma } = crear();
      await service.generar('opo-1', {}, 'emp-1');
      expect(prisma.presupuesto_cab.findFirst.mock.calls[0][0].where.estado.notIn)
        .toEqual(expect.arrayContaining(['REJECTED', 'EXPIRED', 'CANCELLED', 'SUPERSEDED']));
    });
  });

  describe('mapeo de ítems', () => {
    it('un producto de catálogo con stock va como PRODUCTO', async () => {
      const { service, presupuestos } = crear();
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos).items[0]).toMatchObject({
        tipo: 'PRODUCTO', producto_id: 'prod-1', cantidad: 10, precio_unitario: 110_000, iva_categoria: 'IVA_10',
      });
    });

    it('un producto sin inventario va como SERVICIO', async () => {
      const { service, presupuestos } = crear({
        productos: { findMany: jest.fn().mockResolvedValue([{ id: 'prod-1', maneja_inventario: false }]) },
      });
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos).items[0].tipo).toBe('SERVICIO');
    });

    /**
     * M36 sólo suma al total los ítems PRODUCTO y SERVICIO: mandar un ítem libre
     * como TEXTO_LIBRE lo dejaría cotizado en cero sin que nadie lo note.
     */
    it('un ítem sin producto de catálogo va como SERVICIO, no como texto libre', async () => {
      const { service, presupuestos } = crear({
        crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ items: [ITEM({ producto_id: null, descripcion: 'Flete a Encarnación' })] })), update: jest.fn() },
        productos: { findMany: jest.fn().mockResolvedValue([]) },
      });
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos).items[0]).toMatchObject({ tipo: 'SERVICIO', descripcion: 'Flete a Encarnación' });
      expect(dtoDe(presupuestos).items[0].producto_id).toBeUndefined();
    });

    it('traduce la tasa de IVA a la categoría de M36', async () => {
      const { service, presupuestos } = crear({
        crm_oportunidades: {
          findFirst: jest.fn().mockResolvedValue(OPO({ items: [ITEM({ iva_tasa: 10 }), ITEM({ iva_tasa: 5 }), ITEM({ iva_tasa: 0 })] })),
          update: jest.fn(),
        },
      });
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos).items.map((i: any) => i.iva_categoria)).toEqual(['IVA_10', 'IVA_5', 'EXENTO']);
    });

    it('numera las líneas desde 1 respetando el orden de la oportunidad', async () => {
      const { service, presupuestos } = crear({
        crm_oportunidades: { findFirst: jest.fn().mockResolvedValue(OPO({ items: [ITEM(), ITEM({ orden: 1 })] })), update: jest.fn() },
      });
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos).items.map((i: any) => i.orden)).toEqual([1, 2]);
    });
  });

  describe('cabecera y vínculo', () => {
    it('hereda cliente, vendedor, sucursal y moneda de la oportunidad', async () => {
      const { service, presupuestos } = crear();
      await service.generar('opo-1', {}, 'emp-1');
      expect(dtoDe(presupuestos)).toMatchObject({
        cliente_id: 'cli-1', vendedor_id: 'v-1', sucursal_id: 'suc-1', moneda_id: 'mon-1',
        titulo: 'Equipamiento planta 2',
      });
      expect(dtoDe(presupuestos).observaciones_internas).toContain('OPP-0000124');
    });

    it('el título del DTO gana sobre el de la oportunidad', async () => {
      const { service, presupuestos } = crear();
      await service.generar('opo-1', { titulo: '  Propuesta revisada  ' }, 'emp-1');
      expect(dtoDe(presupuestos).titulo).toBe('Propuesta revisada');
    });

    it('vincula en las dos direcciones en una sola transacción', async () => {
      const { service, prisma } = crear();
      await service.generar('opo-1', {}, 'emp-1');

      expect(prisma.$transaction).toHaveBeenCalledTimes(1);
      expect(prisma.presupuesto_cab.update).toHaveBeenCalledWith({ where: { id: 'pre-1' }, data: { oportunidad_id: 'opo-1' } });
      expect(prisma.crm_oportunidades.update.mock.calls[0][0].data).toMatchObject({ presupuesto_id: 'pre-1' });
    });

    /** El presupuesto nace en DRAFT: la etapa avanza cuando se envía al cliente. */
    it('no mueve la etapa al generar', async () => {
      const { service, prisma } = crear();
      await service.generar('opo-1', {}, 'emp-1');
      const datos = prisma.crm_oportunidades.update.mock.calls.map((c: any[]) => c[0].data);
      expect(datos.some((d: any) => 'etapa_id' in d)).toBe(false);
    });

    it('devuelve el presupuesto creado con los dos números en el mensaje', async () => {
      const { service } = crear();
      const r = await service.generar('opo-1', {}, 'emp-1');
      expect(r.message).toContain('PRE-0001');
      expect(r.message).toContain('OPP-0000124');
      expect(r.data).toMatchObject({ id: 'pre-1', oportunidad_id: 'opo-1' });
    });
  });
});
