import { BadRequestException, NotFoundException } from '@nestjs/common';
import { OrdenesCompraService } from './ordenes-compra.service';

describe('OrdenesCompraService - reglas F2', () => {
  const createBaseDto = () => ({
    cabecera: {
      sucursal_id: 'suc-1',
      proveedor_id: 'prov-1',
      moneda_id: 'mon-1',
      requisicion_id: null,
      fecha_emision: '2026-04-27',
      observaciones: 'OC test',
    },
    items: [
      {
        producto_id: 'prod-1',
        descripcion: 'Item 1',
        cantidad: 2,
        precio_unitario: 1000,
        porcentaje_iva: 10,
      },
    ],
  });

  const createService = (txOverrides: Record<string, any> = {}) => {
    const tx = {
      config_compras: {
        findUnique: jest.fn().mockResolvedValue({
          empresa_id: 'emp-1',
          habilitar_orden_compra: true,
          modo_flujo_compra: 'directo',
          requiere_aprobacion: false,
          workflow_obligatorio: false,
        }),
        create: jest.fn(),
      },
      orden_compra_cab: {
        create: jest.fn().mockResolvedValue({ id: 'oc-1' }),
        findFirst: jest.fn().mockResolvedValue({
          id: 'oc-1',
          estado: 'pendiente_aprobacion',
          observaciones: null,
          requisicion_id: null,
        }),
        findMany: jest.fn().mockResolvedValue([]),
        update: jest.fn().mockResolvedValue(undefined),
        findUnique: jest.fn().mockResolvedValue({
          id: 'oc-1',
          estado: 'pendiente',
          orden_compra_det: [{ id: 'det-1', cantidad: 10, cantidad_recibida: 5 }],
        }),
      },
      orden_compra_det: {
        createMany: jest.fn().mockResolvedValue(undefined),
      },
      empresas_sucursales: {
        findFirst: jest.fn().mockResolvedValue({ id: 'suc-1' }),
      },
      proveedores: {
        findFirst: jest.fn().mockResolvedValue({ id: 'prov-1' }),
      },
      moneda: {
        findUnique: jest.fn().mockResolvedValue({ id: 'mon-1' }),
      },
      recepcion_compra_cab: {
        count: jest.fn().mockResolvedValue(0),
      },
      recepcion_compra_det: {
        findMany: jest.fn().mockResolvedValue([]),
      },
      compra_cab: {
        count: jest.fn().mockResolvedValue(0),
      },
      gasto_cab: {
        count: jest.fn().mockResolvedValue(0),
      },
      suscripciones: {
        findFirst: jest.fn().mockResolvedValue(null),
      },
      suscripcion_modulos: {
        findFirst: jest.fn().mockResolvedValue(null),
      },
      requisicion_cab: {
        findFirst: jest.fn().mockResolvedValue(null),
        update: jest.fn().mockResolvedValue(undefined),
      },
      requisicion_historial: {
        create: jest.fn().mockResolvedValue(undefined),
      },
      compra_det: {
        findMany: jest.fn().mockResolvedValue([]),
      },
      ...txOverrides,
    };

    const prisma = {
      $transaction: jest.fn(async (cb: any) => cb(tx)),
      compra_det: tx.compra_det,
      orden_compra_cab: tx.orden_compra_cab,
      audit_logs: {
        findMany: jest.fn().mockResolvedValue([]),
      },
      usuario: {
        findMany: jest.fn().mockResolvedValue([]),
      },
    };

    const auditService = {
      log: jest.fn().mockResolvedValue(undefined),
    };

    const service = new OrdenesCompraService(prisma as any, auditService as any);
    jest.spyOn(service, 'findOne').mockResolvedValue({ id: 'oc-1' } as any);

    return { service, tx, auditService };
  };

  describe('create', () => {
    it('bloquea creación si la orden de compra está deshabilitada por configuración', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: false,
        modo_flujo_compra: 'directo',
      });

      await expect(service.create(createBaseDto() as any, 'emp-1', 'u-1')).rejects.toThrow(BadRequestException);
    });

    it('bloquea creación si modo_flujo_compra es requisicion_obligatoria y no se informa requisicion_id', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: true,
        modo_flujo_compra: 'requisicion_obligatoria',
        requiere_aprobacion: false,
        workflow_obligatorio: false,
      });

      await expect(service.create(createBaseDto() as any, 'emp-1', 'u-1')).rejects.toThrow(BadRequestException);
    });

    it('crea en pendiente_aprobacion cuando configuración exige aprobación', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: true,
        modo_flujo_compra: 'directo',
        requiere_aprobacion: true,
        workflow_obligatorio: false,
      });

      jest.spyOn(service as any, 'validateCabeceraRefs').mockResolvedValue(undefined);
      jest.spyOn(service as any, 'normalizeItems').mockReturnValue([
        {
          requisicion_det_id: null,
          producto_id: 'prod-1',
          descripcion: 'Item 1',
          cantidad: 2,
          precio_unitario: 1000,
          porcentaje_iva: 10,
          subtotal: 2000,
        },
      ]);
      jest.spyOn(service as any, 'assertRequisicionCompraValida').mockResolvedValue(undefined);
      jest.spyOn(service as any, 'calculateTotals').mockReturnValue({
        subtotal: 2000,
        total_iva: 181.82,
        total: 2000,
      });
      jest.spyOn(service as any, 'getNextNumeroOrdenCompra').mockResolvedValue('001-001-0000001');
      jest.spyOn(service, 'recalculateOrdenCompraEstado').mockResolvedValue(undefined);

      await service.create(createBaseDto() as any, 'emp-1', 'u-1');

      expect(tx.orden_compra_cab.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            estado: 'pendiente_aprobacion',
          }),
        }),
      );
    });
  });

  describe('aprobar/cancelar/recalculate', () => {
    it('aprobar mueve estado de pendiente_aprobacion a pendiente', async () => {
      const { service, tx } = createService();

      await service.aprobar('oc-1', 'emp-1');

      expect(tx.orden_compra_cab.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'oc-1' },
          data: expect.objectContaining({ estado: 'pendiente' }),
        }),
      );
    });

    it('cancelar bloquea si existen recepciones activas', async () => {
      const { service, tx } = createService();
      tx.recepcion_compra_cab.count.mockResolvedValue(1);

      await expect(service.cancelar('oc-1', { motivo: 'x' } as any, 'emp-1')).rejects.toThrow(BadRequestException);
    });

    it('si hay otra OC vigente de la misma requisición, no rechaza la requisición', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'pendiente',
        observaciones: null,
        requisicion_id: 'req-1',
      });
      tx.requisicion_cab.findFirst.mockResolvedValue({
        id: 'req-1',
        estado: 'aprobada',
      });
      tx.orden_compra_cab.findMany.mockResolvedValue([
        { id: 'oc-1', estado: 'cancelada' },
        { id: 'oc-2', estado: 'pendiente' },
      ]);

      await service.cancelar('oc-1', { motivo: 'Proveedor no confirma' } as any, 'emp-1');

      expect(tx.requisicion_historial.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({ accion: 'oc_cancelada', requisicion_id: 'req-1' }),
        }),
      );
      expect(tx.requisicion_cab.update).not.toHaveBeenCalled();
    });

    it('si todas las OCs vinculadas están canceladas y no hay ejecución, rechaza la requisición', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'pendiente',
        observaciones: null,
        requisicion_id: 'req-1',
      });
      tx.requisicion_cab.findFirst.mockResolvedValue({
        id: 'req-1',
        estado: 'aprobada',
      });
      tx.orden_compra_cab.findMany.mockResolvedValue([
        { id: 'oc-1', estado: 'cancelada' },
        { id: 'oc-2', estado: 'cancelada' },
      ]);
      tx.recepcion_compra_cab.count.mockResolvedValue(0);
      tx.compra_cab.count.mockResolvedValue(0);
      tx.gasto_cab.count.mockResolvedValue(0);

      await service.cancelar('oc-1', { motivo: 'Proveedor rechazó condiciones' } as any, 'emp-1');

      expect(tx.requisicion_cab.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'req-1' },
          data: expect.objectContaining({ estado: 'rechazada' }),
        }),
      );
      expect(tx.requisicion_historial.create).toHaveBeenCalledTimes(2);
    });

    it('recalculateOrdenCompraEstado marca parcial cuando existe progreso pero no cierre total', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findUnique.mockResolvedValue({
        id: 'oc-1',
        estado: 'pendiente',
        orden_compra_det: [{ id: 'det-1', cantidad: 10, cantidad_recibida: 5 }],
      });
      tx.compra_det.findMany.mockResolvedValue([]);

      await service.recalculateOrdenCompraEstado(tx as any, 'oc-1');

      expect(tx.orden_compra_cab.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'oc-1' },
          data: expect.objectContaining({ estado: 'parcial' }),
        }),
      );
    });

    it('recalculateOrdenCompraEstado marca completada cuando recepción y facturación cubren 100%', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findUnique.mockResolvedValue({
        id: 'oc-1',
        estado: 'parcial',
        orden_compra_det: [{ id: 'det-1', cantidad: 10, cantidad_recibida: 10 }],
      });
      tx.compra_det.findMany.mockResolvedValue([{ orden_compra_det_id: 'det-1', cantidad: 10 }]);

      await service.recalculateOrdenCompraEstado(tx as any, 'oc-1');

      expect(tx.orden_compra_cab.update).toHaveBeenCalledWith(
        expect.objectContaining({
          where: { id: 'oc-1' },
          data: expect.objectContaining({ estado: 'completada' }),
        }),
      );
    });
  });

  describe('matching 3 vías formal', () => {
    it('retorna estado completo cuando recepción y facturación cubren la orden sin desvíos', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: true,
        habilitar_recepcion_compra: true,
        habilitar_matching_3_vias: true,
      });
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'parcial',
        orden_compra_det: [
          {
            id: 'det-1',
            producto_id: 'prod-1',
            descripcion: 'Item 1',
            cantidad: 10,
            cantidad_recibida: 10,
            precio_unitario: 100,
            productos: { id: 'prod-1', descripcion: 'Item 1', cod_producto: 'P-1' },
          },
        ],
      });
      tx.recepcion_compra_det.findMany.mockResolvedValue([
        {
          orden_compra_det_id: 'det-1',
          recepcion_id: 'rec-1',
          cantidad_recibida: 10,
          recepcion_compra_cab: { fecha_recepcion: new Date('2026-04-27'), estado: 'aplicada' },
        },
      ]);
      tx.compra_det.findMany.mockResolvedValue([
        {
          orden_compra_det_id: 'det-1',
          compra_id: 'comp-1',
          cantidad: 10,
          precio_unitario: 100,
          compra_cab: {
            fecha_emision: new Date('2026-04-27'),
            timbrado_proveedor: '12345678',
            establecimiento: '001',
            punto_expedicion: '001',
            numero_factura: '0000001',
          },
        },
      ]);

      const result = await service.getMatching3Vias('oc-1', 'emp-1');

      expect(result.estado_matching).toBe('completo');
      expect(result.resumen.cantidad_ordenada).toBe(10);
      expect(result.resumen.cantidad_recibida).toBe(10);
      expect(result.resumen.cantidad_facturada).toBe(10);
      expect(result.resumen.lineas_completas).toBe(1);
      expect(result.lineas[0].estado_matching).toBe('completo');
      expect(result.lineas[0].desvio_precio).toBe(false);
    });

    it('marca inconsistente cuando hay desvío de precio o facturación sobre recepción', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: true,
        habilitar_recepcion_compra: true,
        habilitar_matching_3_vias: true,
      });
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'parcial',
        orden_compra_det: [
          {
            id: 'det-1',
            producto_id: 'prod-1',
            descripcion: 'Item 1',
            cantidad: 10,
            cantidad_recibida: 6,
            precio_unitario: 100,
            productos: { id: 'prod-1', descripcion: 'Item 1', cod_producto: 'P-1' },
          },
        ],
      });
      tx.recepcion_compra_det.findMany.mockResolvedValue([
        {
          orden_compra_det_id: 'det-1',
          recepcion_id: 'rec-1',
          cantidad_recibida: 6,
          recepcion_compra_cab: { fecha_recepcion: new Date('2026-04-27'), estado: 'aplicada' },
        },
      ]);
      tx.compra_det.findMany.mockResolvedValue([
        {
          orden_compra_det_id: 'det-1',
          compra_id: 'comp-1',
          cantidad: 8,
          precio_unitario: 105,
          compra_cab: {
            fecha_emision: new Date('2026-04-27'),
            timbrado_proveedor: '12345678',
            establecimiento: '001',
            punto_expedicion: '001',
            numero_factura: '0000001',
          },
        },
      ]);

      const result = await service.getMatching3Vias('oc-1', 'emp-1');

      expect(result.estado_matching).toBe('inconsistente');
      expect(result.resumen.lineas_inconsistentes).toBe(1);
      expect(result.resumen.lineas_con_desvio_precio).toBe(1);
      expect(result.resumen.lineas_con_exceso_facturacion).toBe(1);
      expect(result.hallazgos.some((h: any) => h.codigo === 'PRICE_DEVIATION')).toBe(true);
      expect(result.hallazgos.some((h: any) => h.codigo === 'BILLING_OVERFLOW')).toBe(true);
    });

    it('marca no_aplica cuando matching está deshabilitado por config_compras', async () => {
      const { service, tx } = createService();
      tx.config_compras.findUnique.mockResolvedValue({
        empresa_id: 'emp-1',
        habilitar_orden_compra: true,
        habilitar_recepcion_compra: true,
        habilitar_matching_3_vias: false,
      });
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'pendiente',
        orden_compra_det: [
          {
            id: 'det-1',
            producto_id: 'prod-1',
            descripcion: 'Item 1',
            cantidad: 10,
            cantidad_recibida: 0,
            precio_unitario: 100,
            productos: { id: 'prod-1', descripcion: 'Item 1', cod_producto: 'P-1' },
          },
        ],
      });

      const result = await service.getMatching3Vias('oc-1', 'emp-1');

      expect(result.estado_matching).toBe('no_aplica');
      expect(result.lineas[0].estado_matching).toBe('no_aplica');
      expect(result.hallazgos.some((h: any) => h.codigo === 'MATCHING_DISABLED')).toBe(true);
    });
  });

  describe('generatePdf', () => {
    afterEach(() => {
      jest.restoreAllMocks();
    });

    it('lanza not found cuando la OC no existe', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findFirst.mockResolvedValue(null);

      await expect(service.generatePdf('oc-x', 'emp-1')).rejects.toThrow(NotFoundException);
    });

    it('bloquea PDF cuando la OC está pendiente de aprobación', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        estado: 'pendiente_aprobacion',
        orden_compra_det: [],
      });

      await expect(service.generatePdf('oc-1', 'emp-1')).rejects.toThrow(BadRequestException);
    });

    it('genera payload y retorna buffer/filename de PDF', async () => {
      const { service, tx } = createService();
      tx.orden_compra_cab.findFirst.mockResolvedValue({
        id: 'oc-1',
        numero_orden: '001-001-0000001',
        estado: 'pendiente',
        fecha_emision: new Date('2026-05-01T00:00:00.000Z'),
        fecha_entrega: new Date('2026-05-10T00:00:00.000Z'),
        observaciones: 'Test PDF',
        total: 100000,
        cotizacion: 1,
        empresas: {
          razon_social: 'Empresa Demo',
          ruc: '80012345',
          dv: '6',
          celular: '0991000000',
          email: 'demo@empresa.com',
          logo: null,
        },
        empresas_sucursales: {
          descripcion: 'Casa Central',
          punto_establecimiento: '001',
          direccion: 'Av. Principal',
          telefono: '021000000',
          email: 'sucursal@empresa.com',
        },
        proveedores: {
          personas: {
            razon_social: 'Proveedor SA',
            ruc: '90012345',
            dv: '2',
            direccion: 'Calle Proveedor',
            telefono: '0981000000',
            email: 'proveedor@test.com',
          },
        },
        moneda: { codigo: 'PYG', descripcion: 'Guaraní', simbolo: 'Gs.' },
        requisicion_compra_cab: { numero_requisicion: 'REQ-2026-000001' },
        orden_compra_det: [
          {
            descripcion: 'Item demo',
            cantidad: 2,
            precio_unitario: 50000,
            subtotal: 100000,
            porcentaje_iva: 10,
            productos: { cod_producto: 'PRD-1', descripcion: 'Producto 1' },
          },
        ],
      });

      const pdfBuffer = Buffer.from('pdf-mock');
      const originalFetch = (global as any).fetch;
      const fetchMock = jest.fn().mockResolvedValue({
        ok: true,
        arrayBuffer: async () => pdfBuffer,
      });
      (global as any).fetch = fetchMock;

      try {
        const result = await service.generatePdf('oc-1', 'emp-1');
        expect(fetchMock).toHaveBeenCalledTimes(1);
        expect(fetchMock.mock.calls[0][0]).toContain('/api/orden-compra/generate-pdf');
        expect(result.filename).toBe('orden-compra-001-001-0000001.pdf');
        expect(result.pdfBuffer).toEqual(Buffer.from('pdf-mock'));
      } finally {
        (global as any).fetch = originalFetch;
      }
    });
  });

  describe('getHistorial', () => {
    it('retorna evento mínimo de creación cuando no hay logs de auditoría', async () => {
      const { service } = createService();
      (service as any).prisma.orden_compra_cab.findFirst = jest.fn().mockResolvedValue({
        id: 'oc-1',
        numero_orden: 'OC-00000001',
        estado: 'pendiente',
        observaciones: null,
        created_at: new Date('2026-05-01T00:00:00.000Z'),
        updated_at: new Date('2026-05-01T00:00:00.000Z'),
        usuario: { id: 'u-1', nombres: 'Admin', apellidos: 'Demo', username: 'admin' },
      });
      (service as any).prisma.audit_logs.findMany = jest.fn().mockResolvedValue([]);
      (service as any).prisma.usuario.findMany = jest.fn().mockResolvedValue([]);

      const result = await service.getHistorial('oc-1', 'emp-1');
      expect(result.events.length).toBeGreaterThan(0);
      expect(String(result.events[0].accion)).toContain('create');
    });
  });
});
