import { BadRequestException } from '@nestjs/common';
import { ComprasService } from './compras.service';

describe('ComprasService - reglas F1/F2/F3', () => {
  const createService = () =>
    new ComprasService({} as any, {} as any, {} as any, {} as any, {} as any);

  describe('descuento por ítem', () => {
    it('calcula subtotal, IVA y costo unitario sobre el importe neto', () => {
      const service = createService();
      const items = (service as any).normalizeItems([
        {
          descripcion: 'Producto con descuento',
          cantidad: 2,
          precio_unitario: 110,
          descuento_porcentaje: 10,
          porcentaje_iva: 10,
        },
      ]);

      expect(items[0]).toMatchObject({
        descuento_pct: 10,
        descuento_monto: 22,
        subtotal: 198,
        monto_iva: 18,
        costo_unitario_neto: 99,
      });

      expect((service as any).calculateTotals(items)).toMatchObject({
        total_gravado_10: 198,
        total_iva_10: 18,
        subtotal: 198,
        total: 198,
      });
    });

    it('rechaza porcentajes fuera del rango de 0 a 100', async () => {
      const service = createService();
      const items = (service as any).normalizeItems([
        {
          descripcion: 'Producto',
          cantidad: 1,
          precio_unitario: 100,
          descuento_porcentaje: 101,
        },
      ]);

      await expect((service as any).validateItems({}, 'emp-1', items, false)).rejects.toThrow(
        'debe estar entre 0% y 100%',
      );
    });

    it('acepta un monto fijo y calcula su porcentaje equivalente', () => {
      const service = createService();
      const items = (service as any).normalizeItems([
        {
          descripcion: 'Producto con descuento fijo',
          cantidad: 2,
          precio_unitario: 125,
          descuento_tipo: 'monto',
          descuento_monto: 50,
          porcentaje_iva: 10,
        },
      ]);

      expect(items[0]).toMatchObject({
        descuento_tipo: 'monto',
        descuento_pct: 20,
        descuento_monto: 50,
        subtotal: 200,
        costo_unitario_neto: 100,
      });
    });

    it('respeta la precisión configurada por moneda para porcentaje y monto', () => {
      const service = createService();
      const config = {
        decimalesMonto: 2,
        decimalesPorcentaje: 3,
        modo: 'HALF_UP',
      };
      const items = (service as any).normalizeItems(
        [
          {
            descripcion: 'Producto con precisión del proveedor',
            cantidad: 10,
            precio_unitario: 1_881_000,
            descuento_tipo: 'porcentaje',
            descuento_porcentaje: 30.888,
            porcentaje_iva: 10,
          },
        ],
        config,
      );

      expect(items[0]).toMatchObject({
        descuento_pct: 30.888,
        descuento_monto: 5_810_032.8,
        subtotal: 12_999_967.2,
      });
      expect((service as any).calculateTotals(items, undefined, config)).toMatchObject({
        subtotal: 12_999_967.2,
        total: 12_999_967.2,
      });
    });

    it('rechaza un monto de descuento mayor al subtotal bruto del ítem', async () => {
      const service = createService();
      const items = (service as any).normalizeItems([
        {
          descripcion: 'Producto',
          cantidad: 1,
          precio_unitario: 100,
          descuento_tipo: 'monto',
          descuento_monto: 100.01,
        },
      ]);

      await expect((service as any).validateItems({}, 'emp-1', items, false)).rejects.toThrow(
        'el monto no puede superar 100',
      );
    });
  });

  describe('edición de compra con orden automática', () => {
    it('elimina el borrador automático vacío antes de validar órdenes activas', async () => {
      const service = createService();
      const tx = {
        orden_pago_proveedor_cab: {
          deleteMany: jest.fn().mockResolvedValue({ count: 1 }),
        },
      };

      await (service as any).cleanupEmptyAutoDraftOrderForCompra(tx, 'emp-1', 'compra-1');

      expect(tx.orden_pago_proveedor_cab.deleteMany).toHaveBeenCalledWith({
        where: {
          empresa_id: 'emp-1',
          compra_id: 'compra-1',
          origen: 'compra_auto',
          estado: { in: ['BORRADOR', 'borrador'] },
          activo: true,
          anulado: false,
          orden_pago_proveedor_det: { none: {} },
        },
      });
    });
  });

  describe('resolveFlujoCompra', () => {
    it('bloquea compra cuando modo es requisición obligatoria y no se informa requisicion_id', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);

      await expect(
        resolveFlujoCompra({
          tx: {},
          empresaId: 'emp-1',
          configCompras: {
            modo_flujo_compra: 'requisicion_obligatoria',
            afectar_stock_en_compra_directa: true,
          },
          requisicionId: null,
          ordenCompraId: null,
          items: [],
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it('bloquea compra cuando modo es requisición obligatoria y no se informa orden_compra_id', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);
      const assertRequisicionCompraValida = jest
        .spyOn(service as any, 'assertRequisicionCompraValida')
        .mockResolvedValue(undefined);

      await expect(
        resolveFlujoCompra({
          tx: {},
          empresaId: 'emp-1',
          configCompras: {
            modo_flujo_compra: 'requisicion_obligatoria',
            afectar_stock_en_compra_directa: true,
          },
          requisicionId: 'req-1',
          ordenCompraId: null,
          items: [],
        }),
      ).rejects.toThrow(BadRequestException);

      expect(assertRequisicionCompraValida).not.toHaveBeenCalled();
    });

    it('bloquea cuando se envía requisicion_det_id sin cabecera.requisicion_id', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);

      await expect(
        resolveFlujoCompra({
          tx: {},
          empresaId: 'emp-1',
          configCompras: {
            modo_flujo_compra: 'directo',
            afectar_stock_en_compra_directa: true,
          },
          requisicionId: null,
          ordenCompraId: null,
          items: [{ requisicion_det_id: 'req-det-1', producto_id: null, afecta_stock: false }],
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it('bloquea compra directa con stock cuando afectar_stock_en_compra_directa=false y no hay OC', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);

      await expect(
        resolveFlujoCompra({
          tx: {},
          empresaId: 'emp-1',
          configCompras: {
            modo_flujo_compra: 'directo',
            afectar_stock_en_compra_directa: false,
          },
          requisicionId: null,
          ordenCompraId: null,
          items: [{ requisicion_det_id: null, producto_id: 'prod-1', afecta_stock: true }],
        }),
      ).rejects.toThrow(BadRequestException);
    });

    it('permite registrar la factura sin volver a mover stock cuando se usa recepción libre', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);

      const output = await resolveFlujoCompra({
        tx: {},
        empresaId: 'emp-1',
        configCompras: {
          modo_flujo_compra: 'directo',
          afectar_stock_en_compra_directa: false,
          habilitar_orden_compra: false,
          habilitar_recepcion_compra: true,
        },
        requisicionId: null,
        ordenCompraId: null,
        items: [{ requisicion_det_id: null, producto_id: 'prod-1', afecta_stock: true }],
      });

      expect(output).toEqual({ afectarStockEnCompra: false });
    });

    it('permite continuar cuando hay requisición válida y retorna flag de stock según configuración', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);
      const assertRequisicionCompraValida = jest
        .spyOn(service as any, 'assertRequisicionCompraValida')
        .mockResolvedValue(undefined);

      const items = [{ requisicion_det_id: 'req-det-1', producto_id: 'prod-1', afecta_stock: true }];
      const output = await resolveFlujoCompra({
        tx: {},
        empresaId: 'emp-1',
        configCompras: {
          modo_flujo_compra: 'requisicion_opcional',
          afectar_stock_en_compra_directa: false,
        },
        requisicionId: 'req-1',
        ordenCompraId: 'oc-1',
        items,
      });

      expect(assertRequisicionCompraValida).toHaveBeenCalledWith({}, 'emp-1', 'req-1', items);
      expect(output).toEqual({ afectarStockEnCompra: false });
    });

    it('completa requisicion_det_id desde orden_compra_det_id cuando viene OC vinculada', async () => {
      const service = createService();
      const resolveFlujoCompra = (service as any).resolveFlujoCompra.bind(service);
      const assertRequisicionCompraValida = jest
        .spyOn(service as any, 'assertRequisicionCompraValida')
        .mockResolvedValue(undefined);

      const tx = {
        orden_compra_det: {
          findMany: jest.fn().mockResolvedValue([
            { id: 'oc-det-1', requisicion_det_id: 'req-det-1' },
            { id: 'oc-det-2', requisicion_det_id: 'req-det-2' },
          ]),
        },
      };
      const items = [
        { orden_compra_det_id: 'oc-det-1', requisicion_det_id: null, producto_id: 'prod-1', afecta_stock: true },
        { orden_compra_det_id: 'oc-det-2', requisicion_det_id: null, producto_id: null, afecta_stock: false },
      ];

      const output = await resolveFlujoCompra({
        tx,
        empresaId: 'emp-1',
        configCompras: {
          modo_flujo_compra: 'requisicion_opcional',
          afectar_stock_en_compra_directa: true,
        },
        requisicionId: 'req-1',
        ordenCompraId: 'oc-1',
        items,
      });

      expect(tx.orden_compra_det.findMany).toHaveBeenCalledWith({
        where: {
          id: { in: ['oc-det-1', 'oc-det-2'] },
          orden_compra_id: 'oc-1',
        },
        select: {
          id: true,
          requisicion_det_id: true,
        },
      });
      expect(items[0].requisicion_det_id).toBe('req-det-1');
      expect(items[1].requisicion_det_id).toBe('req-det-2');
      expect(assertRequisicionCompraValida).toHaveBeenCalledWith(tx, 'emp-1', 'req-1', items);
      expect(output).toEqual({ afectarStockEnCompra: true });
    });
  });

  describe('validateMatchingOrdenCompra', () => {
    it('rechaza vínculo de OC cuando la configuración deshabilita orden de compra', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);

      await expect(
        validateMatchingOrdenCompra(
          {},
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [{ orden_compra_det_id: null }],
          { ordenCompraHabilitada: false, matching3ViasHabilitado: true },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('rechaza cuando se envía orden_compra_det_id sin cabecera.orden_compra_id', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);

      await expect(
        validateMatchingOrdenCompra(
          {},
          'emp-1',
          null,
          'prov-1',
          null,
          [{ orden_compra_det_id: 'det-1' }],
          { ordenCompraHabilitada: true, matching3ViasHabilitado: true },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('con matching 3 vías activo exige orden_compra_det_id en todos los ítems', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);
      const tx = {
        orden_compra_cab: {
          findFirst: jest.fn().mockResolvedValue({
            id: 'oc-1',
            proveedor_id: 'prov-1',
            estado: 'pendiente',
            orden_compra_det: [
              {
                id: 'det-1',
                producto_id: 'prod-1',
                descripcion: 'Producto 1',
                cantidad: 10,
                cantidad_recibida: 10,
                precio_unitario: 100,
              },
            ],
          }),
        },
      };

      await expect(
        validateMatchingOrdenCompra(
          tx,
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [{ orden_compra_det_id: null, producto_id: 'prod-1', descripcion: 'Producto 1', cantidad: 1, precio_unitario: 100 }],
          { ordenCompraHabilitada: true, matching3ViasHabilitado: true },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('rechaza facturación cuando la OC está pendiente de aprobación', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);
      const tx = {
        orden_compra_cab: {
          findFirst: jest.fn().mockResolvedValue({
            id: 'oc-1',
            proveedor_id: 'prov-1',
            estado: 'pendiente_aprobacion',
            orden_compra_det: [
              {
                id: 'det-1',
                producto_id: 'prod-1',
                descripcion: 'Producto 1',
                cantidad: 10,
                cantidad_recibida: 10,
                precio_unitario: 100,
              },
            ],
          }),
        },
      };

      await expect(
        validateMatchingOrdenCompra(
          tx,
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [{ orden_compra_det_id: 'det-1', producto_id: 'prod-1', descripcion: 'Producto 1', cantidad: 1, precio_unitario: 100 }],
          { ordenCompraHabilitada: true, matching3ViasHabilitado: true },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('exige detalle de OC por línea cuando el flujo de recepción impacta en facturas aunque matching esté deshabilitado', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);
      const tx = {
        orden_compra_cab: {
          findFirst: jest.fn().mockResolvedValue({
            id: 'oc-1',
            proveedor_id: 'prov-1',
            estado: 'pendiente',
            orden_compra_det: [
              {
                id: 'det-1',
                producto_id: 'prod-1',
                descripcion: 'Producto 1',
                cantidad: 10,
                cantidad_recibida: 10,
                precio_unitario: 100,
              },
            ],
          }),
        },
      };

      await expect(
        validateMatchingOrdenCompra(
          tx,
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [{ orden_compra_det_id: null, producto_id: 'prod-1', descripcion: 'Producto 1', cantidad: 1, precio_unitario: 100 }],
          {
            ordenCompraHabilitada: true,
            matching3ViasHabilitado: false,
            recepcionRequeridaParaFacturar: true,
          },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('rechaza cuando facturación acumulada supera lo recibido por línea', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);
      const tx = {
        orden_compra_cab: {
          findFirst: jest.fn().mockResolvedValue({
            id: 'oc-1',
            proveedor_id: 'prov-1',
            estado: 'pendiente',
            orden_compra_det: [
              {
                id: 'det-1',
                producto_id: 'prod-1',
                descripcion: 'Producto 1',
                cantidad: 10,
                cantidad_recibida: 5,
                precio_unitario: 100,
              },
            ],
          }),
        },
        compra_det: {
          findMany: jest.fn().mockResolvedValue([
            { orden_compra_det_id: 'det-1', cantidad: 3 },
          ]),
        },
      };

      await expect(
        validateMatchingOrdenCompra(
          tx,
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [
            {
              orden_compra_det_id: 'det-1',
              producto_id: 'prod-1',
              descripcion: 'Producto 1',
              cantidad: 3,
              precio_unitario: 100,
            },
          ],
          { ordenCompraHabilitada: true, matching3ViasHabilitado: true },
        ),
      ).rejects.toThrow(BadRequestException);
    });

    it('con matching deshabilitado permite ítems sin referencia de detalle de OC', async () => {
      const service = createService();
      const validateMatchingOrdenCompra = (service as any).validateMatchingOrdenCompra.bind(service);
      const tx = {
        orden_compra_cab: {
          findFirst: jest.fn().mockResolvedValue({
            id: 'oc-1',
            proveedor_id: 'prov-1',
            estado: 'pendiente',
            orden_compra_det: [
              {
                id: 'det-1',
                producto_id: 'prod-1',
                descripcion: 'Producto 1',
                cantidad: 10,
                cantidad_recibida: 2,
                precio_unitario: 100,
              },
            ],
          }),
        },
      };

      await expect(
        validateMatchingOrdenCompra(
          tx,
          'emp-1',
          null,
          'prov-1',
          'oc-1',
          [
            {
              orden_compra_det_id: null,
              producto_id: 'prod-1',
              descripcion: 'Producto 1',
              cantidad: 1,
              precio_unitario: 110,
            },
          ],
          { ordenCompraHabilitada: true, matching3ViasHabilitado: false },
        ),
      ).resolves.toBeUndefined();
    });
  });
});

/**
 * Filtro por rubro en el buscador de proveedores de Compras.
 * Ver docs/plan-rubros-multi-sucursal.md.
 */
describe('ComprasService - searchProveedores por rubro', () => {
  const crearService = () => {
    const prisma: any = { proveedores: { findMany: jest.fn().mockResolvedValue([]) } };
    return { service: new ComprasService(prisma, {} as any, {} as any, {} as any, {} as any), prisma };
  };

  it('sin rubro no agrega filtro extra', async () => {
    const { service, prisma } = crearService();

    await service.searchProveedores('emp-1', 'nuar', 20);

    expect(prisma.proveedores.findMany.mock.calls[0][0].where.AND).toBeUndefined();
  });

  it('con rubro filtra sin pisar el OR de la búsqueda por texto', async () => {
    const { service, prisma } = crearService();

    await service.searchProveedores('emp-1', 'nuar', 20, 'rub-desp');

    const where = prisma.proveedores.findMany.mock.calls[0][0].where;
    expect(where.OR).toHaveLength(2);
    expect(where.AND).toEqual([
      { OR: [{ proveedor_rubros: { none: {} } }, { proveedor_rubros: { some: { rubro_id: 'rub-desp' } } }] },
    ]);
  });

  it('sigue excluyendo acreedores varios cuando filtra por rubro', async () => {
    const { service, prisma } = crearService();

    await service.searchProveedores('emp-1', 'nuar', 20, 'rub-desp');

    expect(prisma.proveedores.findMany.mock.calls[0][0].where.NOT).toEqual({ tipo_entidad: 'ACREEDOR_VARIO' });
  });
});
