const sinon = require('sinon');
const { assert, skip, test, module: describe, only } = require('qunit');
const { GPU } = require('../../src');

describe('internal: recycling');

function testImmutableKernelTextureRecycling(precision, mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function(v) {
    return v[0] + 1;
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
    precision,
  });
  let result = kernel([0]);
  const newTextureSpy = sinon.spy(kernel.texture.constructor.prototype, 'newTexture');
  for (let i = 0; i < 10; i++) {
    let lastResult = result;
    result = kernel(result);
    lastResult.delete();
  }
  assert.deepEqual(result.toArray(), new Float32Array([11]));
  assert.equal(newTextureSpy.callCount, 1);
  assert.equal(gpu.kernels.length, 2);
  newTextureSpy.restore();
  gpu.destroy();
}

test('immutable single precision kernel auto', () => {
  testImmutableKernelTextureRecycling('single')
});

test('immutable single precision kernel gpu', () => {
  testImmutableKernelTextureRecycling('single', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable single precision kernel webgl', () => {
  testImmutableKernelTextureRecycling('single', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable single precision kernel webgl2', () => {
  testImmutableKernelTextureRecycling('single', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable single precision kernel headlessgl', () => {
  testImmutableKernelTextureRecycling('single', 'headlessgl');
});

test('immutable unsigned precision kernel auto', () => {
  testImmutableKernelTextureRecycling('unsigned')
});

test('immutable unsigned precision kernel gpu', () => {
  testImmutableKernelTextureRecycling('unsigned', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable unsigned precision kernel webgl', () => {
  testImmutableKernelTextureRecycling('unsigned', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable unsigned precision kernel webgl2', () => {
  testImmutableKernelTextureRecycling('unsigned', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable unsigned precision headlessgl', () => {
  testImmutableKernelTextureRecycling('unsigned', 'headlessgl');
});

function testImmutableMappedKernelTextureRecycling(precision, mode) {
  const gpu = new GPU({ mode });
  function oneOff(value) {
    return value;
  }
  const kernel = gpu.createKernelMap({
    oneOffValue: oneOff
  },function(value1, value2) {
    oneOff(value2[0] - 1);
    return value1[0] + 1;
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
    precision,
  });
  let map = kernel([0], [11]);
  const newTextureSpy = sinon.spy(kernel.texture.constructor.prototype, 'newTexture');
  for (let i = 0; i < 10; i++) {
    let lastResults = map;
    map = kernel(map.result, map.oneOffValue);
    lastResults.result.delete();
    lastResults.oneOffValue.delete();
  }
  assert.deepEqual(map.result.toArray(), new Float32Array([11]));
  assert.deepEqual(map.oneOffValue.toArray(), new Float32Array([0]));
  assert.equal(newTextureSpy.callCount, 2);
  assert.equal(gpu.kernels.length, 2);
  newTextureSpy.restore();
  gpu.destroy();
}

test('immutable single precision mapped kernel auto', () => {
  testImmutableMappedKernelTextureRecycling('single')
});

test('immutable single precision mapped kernel gpu', () => {
  testImmutableMappedKernelTextureRecycling('single', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable single precision mapped kernel webgl', () => {
  testImmutableMappedKernelTextureRecycling('single', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable single precision mapped kernel webgl2', () => {
  testImmutableMappedKernelTextureRecycling('single', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable single precision mapped kernel headlessgl', () => {
  testImmutableMappedKernelTextureRecycling('single', 'headlessgl');
});

test('immutable unsigned precision mapped kernel auto', () => {
  testImmutableMappedKernelTextureRecycling('unsigned')
});

test('immutable unsigned precision mapped kernel gpu', () => {
  testImmutableMappedKernelTextureRecycling('unsigned', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable unsigned precision mapped kernel webgl', () => {
  testImmutableMappedKernelTextureRecycling('unsigned', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable unsigned precision mapped kernel webgl2', () => {
  testImmutableMappedKernelTextureRecycling('unsigned', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable unsigned precision mapped kernel headlessgl', () => {
  testImmutableMappedKernelTextureRecycling('unsigned', 'headlessgl');
});

function testImmutableTextureDelete(precision, done, mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function() {
    return this.thread.x;
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
    precision,
  });
  const result = kernel();
  assert.equal(result.texture._refs, 2);
  const clone1 = result.clone();
  assert.equal(result.texture._refs, 3);
  const clone2 = result.clone();
  assert.equal(result.texture._refs, 4);
  const clone3 = result.clone();
  assert.equal(result.texture._refs, 5);
  const clone4 = result.clone();
  assert.equal(result.texture._refs, 6);
  const clone5 = result.clone();
  assert.equal(result.texture._refs, 7);

  clone1.delete();
  assert.equal(result.texture._refs, 6);
  clone2.delete();
  assert.equal(result.texture._refs, 5);
  clone3.delete();
  assert.equal(result.texture._refs, 4);
  clone4.delete();
  assert.equal(result.texture._refs, 3);
  clone5.delete();
  assert.equal(result.texture._refs, 2);
  result.delete();
  assert.equal(result.texture._refs, 1);
  const spy = sinon.spy(kernel.kernel.context, 'deleteTexture');
  gpu.destroy()
    .then(() => {
      assert.equal(result.texture._refs, 0);
      assert.equal(spy.callCount, 1);
      assert.ok(spy.calledWith(result.texture));
      spy.restore();
      done();
    });
}

test('immutable single precision texture delete auto', t => {
  testImmutableTextureDelete('single', t.async());
});

test('immutable single precision texture delete gpu', t => {
  testImmutableTextureDelete('single', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable single precision texture delete webgl', t => {
  testImmutableTextureDelete('single', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable single precision texture delete webgl2', t => {
  testImmutableTextureDelete('single', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable single precision texture delete headlessgl', t => {
  testImmutableTextureDelete('single', t.async(), 'headlessgl');
});

test('immutable unsigned precision texture delete auto', t => {
  testImmutableTextureDelete('unsigned', t.async() );
});

test('immutable unsigned precision texture delete gpu', t => {
  testImmutableTextureDelete('unsigned', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable unsigned precision texture delete webgl', t => {
  testImmutableTextureDelete('unsigned', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable unsigned precision texture delete webgl2', t => {
  testImmutableTextureDelete('unsigned', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable unsigned precision texture delete headlessgl', t => {
  testImmutableTextureDelete('unsigned', t.async(), 'headlessgl');
});

function testImmutableKernelTextureDoesNotLeak(precision, done, mode) {
  const gpu = new GPU({ mode });
  const toTexture = gpu.createKernel(function(value) {
    return value[this.thread.x];
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
    precision,
  });
  const one = toTexture([1]);
  assert.equal(one.texture._refs, 2); // one's texture will be used in two places at first, in one and toTexture.texture
  assert.equal(toTexture.texture.texture, one.texture); // very important, a clone was mode, but not a deep clone
  assert.notEqual(one, toTexture.texture);
  const two = toTexture([2]);
  assert.equal(one.texture._refs, 1); // was tracked on toTexture.texture, and deleted
  assert.equal(toTexture.texture.texture, two.texture);
  assert.notEqual(toTexture.texture.texture, one.texture);
  assert.equal(two.texture._refs, 2);
  one.delete();
  two.delete();
  assert.equal(one.texture._refs, 0);
  assert.equal(two.texture._refs, 1); // still used by toTexture.texture
  two.delete(); // already deleted
  assert.equal(two.texture._refs, 1); // still used by toTexture
  gpu.destroy()
    .then(() => {
      assert.equal(two.texture._refs, 0);
      done();
    });
}

test('immutable unsigned precision kernel.texture does not leak auto', t => {
  testImmutableKernelTextureDoesNotLeak('unsigned', t.async());
});

test('immutable unsigned precision kernel.texture does not leak gpu', t => {
  testImmutableKernelTextureDoesNotLeak('unsigned', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable unsigned precision kernel.texture does not leak webgl', t => {
  testImmutableKernelTextureDoesNotLeak('unsigned', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable unsigned precision kernel.texture does not leak webgl2', t => {
  testImmutableKernelTextureDoesNotLeak('unsigned', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable unsigned precision kernel.texture does not leak headlessgl', t => {
  testImmutableKernelTextureDoesNotLeak('unsigned', t.async(), 'headlessgl');
});

test('immutable single precision kernel.texture does not leak auto', t => {
  testImmutableKernelTextureDoesNotLeak('single', t.async());
});

test('immutable single precision kernel.texture does not leak gpu', t => {
  testImmutableKernelTextureDoesNotLeak('single', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable single precision kernel.texture does not leak webgl', t => {
  testImmutableKernelTextureDoesNotLeak('single', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable single precision kernel.texture does not leak webgl2', t => {
  testImmutableKernelTextureDoesNotLeak('single', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable single precision kernel.texture does not leak headlessgl', t => {
  testImmutableKernelTextureDoesNotLeak('single', t.async(), 'headlessgl');
});

function testImmutableKernelMappedTexturesDoesNotLeak(precision, done, mode) {
  const gpu = new GPU({ mode });
  function saveValue(value) {
    return value;
  }
  const toTextures = gpu.createKernelMap([saveValue],function(value1, value2) {
    saveValue(value1[this.thread.x]);
    return value2[this.thread.x];
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
    precision,
  });
  const { result: one, 0: two } = toTextures([1], [2]);
  assert.equal(one.texture._refs, 2); // one's texture will be used in two places at first, in one and toTexture.texture
  assert.equal(two.texture._refs, 2); // one's texture will be used in two places at first, in one and toTexture.mappedTextures[0]
  assert.equal(toTextures.texture.texture, one.texture); // very important, a clone was mode, but not a deep clone
  assert.equal(toTextures.mappedTextures[0].texture, two.texture); // very important, a clone was mode, but not a deep clone
  assert.notEqual(one, toTextures.texture);
  assert.notEqual(one, toTextures.mappedTextures[0]);
  const { result: three, 0: four } = toTextures([3], [4]);
  assert.equal(one.texture._refs, 1); // was tracked on toTexture.texture, and deleted
  assert.equal(two.texture._refs, 1); // was tracked on toTexture.mappedTextures[0], and deleted
  assert.equal(toTextures.texture.texture, three.texture);
  assert.equal(toTextures.mappedTextures[0].texture, four.texture);
  assert.notEqual(toTextures.texture.texture, one.texture);
  assert.notEqual(toTextures.mappedTextures[0].texture, two.texture);
  assert.equal(three.texture._refs, 2);
  assert.equal(four.texture._refs, 2);
  one.delete();
  two.delete();
  three.delete();
  four.delete();
  assert.equal(one.texture._refs, 0);
  assert.equal(two.texture._refs, 0);
  assert.equal(three.texture._refs, 1); // still used by toTexture.texture
  assert.equal(four.texture._refs, 1); // still used by toTexture.mappedTextures[0]
  three.delete(); // already deleted
  four.delete(); // already deleted
  assert.equal(three.texture._refs, 1); // still used by toTexture
  assert.equal(four.texture._refs, 1); // still used by toTexture
  gpu.destroy()
    .then(() => {
      assert.equal(one.texture._refs, 0);
      assert.equal(two.texture._refs, 0);
      assert.equal(three.texture._refs, 0);
      assert.equal(four.texture._refs, 0);
      done();
    });
}

test('immutable unsigned precision kernel.mappedTextures does not leak auto', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('unsigned', t.async());
});

test('immutable unsigned precision kernel.mappedTextures does not leak gpu', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('unsigned', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable unsigned precision kernel.mappedTextures does not leak webgl', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('unsigned', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable unsigned precision kernel.mappedTextures does not leak webgl2', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('unsigned', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable unsigned precision kernel.mappedTextures does not leak headlessgl', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('unsigned', t.async(), 'headlessgl');
});

test('immutable single precision kernel.mappedTextures does not leak auto', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('single', t.async());
});

test('immutable single precision kernel.mappedTextures does not leak gpu', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('single', t.async(), 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('immutable single precision kernel.mappedTextures does not leak webgl', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('single', t.async(), 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('immutable single precision kernel.mappedTextures does not leak webgl2', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('single', t.async(), 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('immutable single precision kernel.mappedTextures does not leak headlessgl', t => {
  testImmutableKernelMappedTexturesDoesNotLeak('single', t.async(), 'headlessgl');
});

function testCloning(mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function(value) {
    return value[0] + 1;
  }, { output: [1], pipeline: true });
  const texture = kernel([1]);
  const { size } = texture;

  // set size to something unique, for tracking
  texture.size = [size[0] + 0.1, size[1] + 0.2];
  texture.cloneTexture();
  assert.equal(texture._framebuffer.width, size[0] + 0.1);
  assert.equal(texture._framebuffer.height, size[1] + 0.2);
  gpu.destroy();
}

(GPU.isWebGLSupported ? test : skip)('cloning sets up framebuffer with correct size webgl', () => {
  testCloning('webgl');
});

(GPU.isWebGL2Supported ? test : skip)('cloning sets up framebuffer with correct size webgl2', () => {
  testCloning('webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('cloning sets up framebuffer with correct size headlessgl', () => {
  testCloning('headlessgl');
});

function testMutableLeak(mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function() {
    return 1;
  }, {
    output: [1],
    pipeline: true
  });
  kernel.build();
  const cloneTextureSpy = sinon.spy(kernel.texture.constructor.prototype, 'beforeMutate');
  const texture1 = kernel();
  const texture2 = kernel();
  assert.equal(cloneTextureSpy.callCount, 0);
  assert.equal(texture1.texture._refs, 1);
  assert.ok(texture1 === texture2);
  cloneTextureSpy.restore();
  gpu.destroy();
}

test('test mutable leak auto', () => {
  testMutableLeak();
});

test('test mutable leak gpu', () => {
  testMutableLeak('gpu');
});

(GPU.isWebGLSupported ? test : skip)('test mutable leak webgl', () => {
  testMutableLeak('webgl');
});

(GPU.isWebGL2Supported ? test : skip)('test mutable leak webgl2', () => {
  testMutableLeak('webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('test mutable leak headlessgl', () => {
  testMutableLeak('headlessgl');
});

describe('internal: cpu recycling behaviour');

test('recycle CPU array', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return this.thread.x + v[0];
  }, {
    output: [1],
    pipeline: true,
    immutable: false,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0], 1);
  const result2 = kernel(new Float32Array([2]));

  assert.equal(result1[0], 2);
  assert.equal(result1, result2);
  gpu.destroy();
});

test('recycle CPU matrix', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return (this.thread.x + (this.thread.y * this.output.x)) + v[0];
  }, {
    output: [2, 2],
    pipeline: true,
    immutable: false,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0][0], 1);
  assert.equal(result1[0][1], 2);
  assert.equal(result1[1][0], 3);
  assert.equal(result1[1][1], 4);
  const result2 = kernel(new Float32Array([2]));
  assert.equal(result1[0][0], 2);
  assert.equal(result1[0][1], 3);
  assert.equal(result1[1][0], 4);
  assert.equal(result1[1][1], 5);

  assert.equal(result1, result2);
  gpu.destroy();
});

test('recycle CPU cube', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return (this.thread.x + (this.thread.y * this.output.x) + (this.thread.z * this.output.y * this.output.x)) + v[0];
  }, {
    output: [2, 2, 2],
    pipeline: true,
    immutable: false,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0][0][0], 1);
  assert.equal(result1[0][0][1], 2);
  assert.equal(result1[0][1][0], 3);
  assert.equal(result1[0][1][1], 4);
  assert.equal(result1[1][0][0], 5);
  assert.equal(result1[1][0][1], 6);
  assert.equal(result1[1][1][0], 7);
  assert.equal(result1[1][1][1], 8);
  const result2 = kernel(new Float32Array([2]));
  assert.equal(result1[0][0][0], 2);
  assert.equal(result1[0][0][1], 3);
  assert.equal(result1[0][1][0], 4);
  assert.equal(result1[0][1][1], 5);
  assert.equal(result1[1][0][0], 6);
  assert.equal(result1[1][0][1], 7);
  assert.equal(result1[1][1][0], 8);
  assert.equal(result1[1][1][1], 9);
  assert.equal(result1, result2);
  gpu.destroy();
});

describe('internal: cpu non-recycling behaviour');

test('non-recycle CPU array', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return this.thread.x + v[0];
  }, {
    output: [1],
    pipeline: true,
    immutable: true,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0], 1);
  const result2 = kernel(new Float32Array([2]));

  assert.equal(result1[0], 1);
  assert.equal(result2[0], 2);
  assert.notEqual(result1, result2);
  gpu.destroy();
});

test('non-recycle CPU matrix', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return (this.thread.x + (this.thread.y * this.output.x)) + v[0];
  }, {
    output: [2, 2],
    pipeline: true,
    immutable: true,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0][0], 1);
  assert.equal(result1[0][1], 2);
  assert.equal(result1[1][0], 3);
  assert.equal(result1[1][1], 4);
  const result2 = kernel(new Float32Array([2]));
  // untouched
  assert.equal(result1[0][0], 1);
  assert.equal(result1[0][1], 2);
  assert.equal(result1[1][0], 3);
  assert.equal(result1[1][1], 4);

  assert.equal(result2[0][0], 2);
  assert.equal(result2[0][1], 3);
  assert.equal(result2[1][0], 4);
  assert.equal(result2[1][1], 5);

  assert.notEqual(result1, result2);
  gpu.destroy();
});

test('non-recycle CPU cube', () => {
  const gpu = new GPU({ mode: 'cpu' });
  const kernel = gpu.createKernel(function(v) {
    return (this.thread.x + (this.thread.y * this.output.x) + (this.thread.z * this.output.y * this.output.x)) + v[0];
  }, {
    output: [2, 2, 2],
    pipeline: true,
    immutable: true,
  });
  const result1 = kernel(new Float32Array([1]));
  assert.equal(result1[0][0][0], 1);
  assert.equal(result1[0][0][1], 2);
  assert.equal(result1[0][1][0], 3);
  assert.equal(result1[0][1][1], 4);
  assert.equal(result1[1][0][0], 5);
  assert.equal(result1[1][0][1], 6);
  assert.equal(result1[1][1][0], 7);
  assert.equal(result1[1][1][1], 8);
  const result2 = kernel(new Float32Array([2]));
  // untouched
  assert.equal(result1[0][0][0], 1);
  assert.equal(result1[0][0][1], 2);
  assert.equal(result1[0][1][0], 3);
  assert.equal(result1[0][1][1], 4);
  assert.equal(result1[1][0][0], 5);
  assert.equal(result1[1][0][1], 6);
  assert.equal(result1[1][1][0], 7);
  assert.equal(result1[1][1][1], 8);

  assert.equal(result2[0][0][0], 2);
  assert.equal(result2[0][0][1], 3);
  assert.equal(result2[0][1][0], 4);
  assert.equal(result2[0][1][1], 5);
  assert.equal(result2[1][0][0], 6);
  assert.equal(result2[1][0][1], 7);
  assert.equal(result2[1][1][0], 8);
  assert.equal(result2[1][1][1], 9);
  assert.notEqual(result1, result2);
  gpu.destroy();
});

function testSameSourceDestinationFromResultThrows(error, precision, mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function(value) {
    return value[this.thread.x] + 1;
  }, {
    output: [1],
    pipeline: true,
    immutable: false,
    precision,
  });
  let result = kernel([0]);
  assert.equal((result.toArray ? result.toArray() : result)[0], 1);
  assert.throws(() => kernel(result), error);
  gpu.destroy();
}

const gpuError = new Error('Source and destination textures are the same.  Use immutable = true and manually cleanup kernel output texture memory with texture.delete()');
const cpuError = new Error('Source and destination arrays are the same.  Use immutable = true');

test('single precision same source and destination from result mutable throws auto', () => {
  testSameSourceDestinationFromResultThrows(gpuError,'single');
});

test('single precision same source and destination from result mutable throws gpu', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'single', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('single precision same source and destination from result mutable throws webgl', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'single', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('single precision same source and destination from result mutable throws webgl2', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'single', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('single precision same source and destination from result mutable throws headlessgl', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'single', 'headlessgl');
});

test('single precision same source and destination from result mutable throws cpu', () => {
  testSameSourceDestinationFromResultThrows(cpuError, 'single', 'cpu');
});

test('unsigned precision same source and destination from result mutable throws auto', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'unsigned');
});

test('unsigned precision same source and destination from result mutable throws gpu', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'unsigned', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('unsigned precision same source and destination from result mutable throws webgl', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'unsigned', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('unsigned precision same source and destination from result mutable throws webgl2', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'unsigned', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('unsigned precision same source and destination from result mutable throws headlessgl', () => {
  testSameSourceDestinationFromResultThrows(gpuError, 'unsigned', 'headlessgl');
});

test('unsigned precision same source and destination from result mutable throws cpu', () => {
  testSameSourceDestinationFromResultThrows(cpuError, 'unsigned', 'cpu');
});

function testSameSourceDestinationFromMappedResultThrows(error, precision, mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernelMap({
    mappedResult: function map(v) {
      return v;
    }
  }, function(value) {
    return map(value[this.thread.x] + 1);
  }, {
    output: [1],
    pipeline: true,
    immutable: false,
    precision,
  });
  let { result, mappedResult } = kernel([0]);
  assert.equal((mappedResult.toArray ? mappedResult.toArray() : mappedResult)[0], 1);
  assert.throws(() => kernel(mappedResult), error);
  assert.throws(() => kernel(result), error);
  gpu.destroy();
}

test('single precision same source and destination from mapped result mutable throws auto', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'single');
});

test('single precision same source and destination from mapped result mutable throws gpu', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'single', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('single precision same source and destination from mapped result mutable throws webgl', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'single', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('single precision same source and destination from mapped result mutable throws webgl2', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'single', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('single precision same source and destination from mapped result mutable throws headlessgl', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'single', 'headlessgl');
});

test('single precision same source and destination from mapped result mutable throws cpu', () => {
  testSameSourceDestinationFromMappedResultThrows(cpuError, 'single', 'cpu');
});

test('unsigned precision same source and destination from mapped result mutable throws auto', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'unsigned');
});

test('unsigned precision same source and destination from mapped result mutable throws gpu', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'unsigned', 'gpu');
});

(GPU.isWebGLSupported ? test : skip)('unsigned precision same source and destination from mapped result mutable throws webgl', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'unsigned', 'webgl');
});

(GPU.isWebGL2Supported ? test : skip)('unsigned precision same source and destination from mapped result mutable throws webgl2', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'unsigned', 'webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('unsigned precision same source and destination from mapped result mutable throws headlessgl', () => {
  testSameSourceDestinationFromMappedResultThrows(gpuError, 'unsigned', 'headlessgl');
});

test('unsigned precision same source and destination from mapped result mutable throws cpu', () => {
  testSameSourceDestinationFromMappedResultThrows(cpuError, 'unsigned', 'cpu');
});

function testOutputTextureIsClonedWhenRecompiling(mode) {
  const gpu = new GPU({ mode });
  const kernel = gpu.createKernel(function(value) {
    return value[this.thread.x] + 1;
  }, { output: [1], immutable: true, pipeline: true });
  const result1 = kernel([1]);
  assert.equal(result1.toArray()[0], 2);
  const result2 = kernel(result1);
  result1.delete();
  assert.equal(result2.toArray()[0], 3);
  result2.delete();
  const result3 = kernel([3]);
  assert.equal(result3.toArray()[0], 4);
  const result4 = kernel(result3);
  result3.delete();
  assert.equal(result4.toArray()[0], 5);
  result4.delete();
  gpu.destroy();
}

test('output texture is cloned when recompiling auto', () => {
  testOutputTextureIsClonedWhenRecompiling();
});

test('output texture is cloned when recompiling gpu', () => {
  testOutputTextureIsClonedWhenRecompiling('gpu');
});

(GPU.isWebGLSupported ? test : skip)('output texture is cloned when recompiling webgl', () => {
  testOutputTextureIsClonedWhenRecompiling('webgl');
});

(GPU.isWebGL2Supported ? test : skip)('output texture is cloned when recompiling webgl2', () => {
  testOutputTextureIsClonedWhenRecompiling('webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('output texture is cloned when recompiling headlessgl', () => {
  testOutputTextureIsClonedWhenRecompiling('headlessgl');
});

function testMappedOutputTextureIsClonedWhenRecompiling(mode) {
  const gpu = new GPU({ mode });
  function setValue(value) {
    return value * 10;
  }
  const kernel = gpu.createKernelMap({
    value: setValue,
  },function(value1, value2) {
    setValue(value2[this.thread.x]);
    return value1[this.thread.x] + 1;
  }, { output: [1], immutable: true, pipeline: true });
  const map1 = kernel([1], [1]);
  assert.equal(map1.result.toArray()[0], 2);
  assert.equal(map1.value.toArray()[0], 10);
  const map2 = kernel(map1.result, map1.value);
  map1.result.delete();
  map1.value.delete();
  assert.equal(map2.result.toArray()[0], 3);
  assert.equal(map2.value.toArray()[0], 100);
  map2.value.delete();
  map2.result.delete();
  const map3 = kernel([3], [3]);
  assert.equal(map3.result.toArray()[0], 4);
  assert.equal(map3.value.toArray()[0], 30);
  const map4 = kernel(map3.result, map3.value);
  map3.result.delete();
  map3.value.delete();
  assert.equal(map4.result.toArray()[0], 5);
  assert.equal(map4.value.toArray()[0], 300);
  map4.result.delete();
  map4.value.delete();
  gpu.destroy();
}

test('mapped output texture is cloned when recompiling auto', () => {
  testMappedOutputTextureIsClonedWhenRecompiling();
});

test('mapped output texture is cloned when recompiling gpu', () => {
  testMappedOutputTextureIsClonedWhenRecompiling('gpu');
});

(GPU.isWebGLSupported ? test : skip)('mapped output texture is cloned when recompiling webgl', () => {
  testMappedOutputTextureIsClonedWhenRecompiling('webgl');
});

(GPU.isWebGL2Supported ? test : skip)('mapped output texture is cloned when recompiling webgl2', () => {
  testMappedOutputTextureIsClonedWhenRecompiling('webgl2');
});

(GPU.isHeadlessGLSupported ? test : skip)('mapped output texture is cloned when recompiling headlessgl', () => {
  testMappedOutputTextureIsClonedWhenRecompiling('headlessgl');
});