export const description = `
Validation tests for the map-state of mappable buffers used in submitted command buffers.

Tests every operation that has a dependency on a buffer
  - writeBuffer
  - copyB2B {src,dst}
  - copyB2T
  - copyT2B

Test those operations against buffers in the following states:
  - Unmapped
  - In the process of mapping
  - mapped
  - mapped with a mapped range queried
  - unmapped after mapping
  - mapped at creation

Also tests every order of operations combination of mapping operations and command recording
operations to ensure the mapping state is only considered when a command buffer is submitted.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';

class F extends AllFeaturesMaxLimitsGPUTest {
  async runBufferDependencyTest(usage: number, callback: Function): Promise<void> {
    const bufferDesc = {
      size: 8,
      usage,
      mappedAtCreation: false,
    };

    const mapMode = usage & GPUBufferUsage.MAP_READ ? GPUMapMode.READ : GPUMapMode.WRITE;

    // Create a mappable buffer, and one that will remain unmapped for comparison.
    const mappableBuffer = this.createBufferTracked(bufferDesc);
    const unmappedBuffer = this.createBufferTracked(bufferDesc);

    // Run the given operation before the buffer is mapped. Should succeed.
    callback(mappableBuffer);

    // Map the buffer
    const mapPromise = mappableBuffer.mapAsync(mapMode);

    // Run the given operation while the buffer is in the process of mapping. Should fail.
    this.expectValidationError(() => {
      callback(mappableBuffer);
    });

    // Run on a different, unmapped buffer. Should succeed.
    callback(unmappedBuffer);

    await mapPromise;

    // Run the given operation when the buffer is finished mapping with no getMappedRange. Should fail.
    this.expectValidationError(() => {
      callback(mappableBuffer);
    });

    // Run on a different, unmapped buffer. Should succeed.
    callback(unmappedBuffer);

    // Run the given operation when the buffer is mapped with getMappedRange. Should fail.
    mappableBuffer.getMappedRange();
    this.expectValidationError(() => {
      callback(mappableBuffer);
    });

    // Unmap the buffer and run the operation. Should succeed.
    mappableBuffer.unmap();
    callback(mappableBuffer);

    // Create a buffer that's mappedAtCreation.
    bufferDesc.mappedAtCreation = true;
    const mappedBuffer = this.createBufferTracked(bufferDesc);

    // Run the operation with the mappedAtCreation buffer. Should fail.
    this.expectValidationError(() => {
      callback(mappedBuffer);
    });

    // Run on a different, unmapped buffer. Should succeed.
    callback(unmappedBuffer);

    // Unmap the mappedAtCreation buffer and run the operation. Should succeed.
    mappedBuffer.unmap();
    callback(mappedBuffer);
  }
}

export const g = makeTestGroup(F);

g.test('writeBuffer')
  .desc(`Test that an outstanding mapping will prevent writeBuffer calls.`)
  .fn(async t => {
    const data = new Uint32Array([42]);

    await t.runBufferDependencyTest(
      GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
      (buffer: GPUBuffer) => {
        t.queue.writeBuffer(buffer, 0, data);
      }
    );
  });

g.test('copyBufferToBuffer')
  .desc(
    `
  Test that an outstanding mapping will prevent copyBufferToTexture commands from submitting,
  both when used as the source and destination.`
  )
  .fn(async t => {
    const sourceBuffer = t.createBufferTracked({
      size: 8,
      usage: GPUBufferUsage.COPY_SRC,
    });

    const destBuffer = t.createBufferTracked({
      size: 8,
      usage: GPUBufferUsage.COPY_DST,
    });

    await t.runBufferDependencyTest(
      GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
      (buffer: GPUBuffer) => {
        const commandEncoder = t.device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(buffer, 0, destBuffer, 0, 4);
        t.queue.submit([commandEncoder.finish()]);
      }
    );

    await t.runBufferDependencyTest(
      GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
      (buffer: GPUBuffer) => {
        const commandEncoder = t.device.createCommandEncoder();
        commandEncoder.copyBufferToBuffer(sourceBuffer, 0, buffer, 0, 4);
        t.queue.submit([commandEncoder.finish()]);
      }
    );
  });

g.test('copyBufferToTexture')
  .desc(
    `Test that an outstanding mapping will prevent copyBufferToTexture commands from submitting.`
  )
  .fn(async t => {
    const size = { width: 1, height: 1 };

    const texture = t.createTextureTracked({
      size,
      format: 'rgba8unorm',
      usage: GPUTextureUsage.COPY_DST,
    });

    await t.runBufferDependencyTest(
      GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
      (buffer: GPUBuffer) => {
        const commandEncoder = t.device.createCommandEncoder();
        commandEncoder.copyBufferToTexture({ buffer }, { texture }, size);
        t.queue.submit([commandEncoder.finish()]);
      }
    );
  });

g.test('copyTextureToBuffer')
  .desc(
    `Test that an outstanding mapping will prevent copyTextureToBuffer commands from submitting.`
  )
  .fn(async t => {
    const size = { width: 1, height: 1 };

    const texture = t.createTextureTracked({
      size,
      format: 'rgba8unorm',
      usage: GPUTextureUsage.COPY_SRC,
    });

    await t.runBufferDependencyTest(
      GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
      (buffer: GPUBuffer) => {
        const commandEncoder = t.device.createCommandEncoder();
        commandEncoder.copyTextureToBuffer({ texture }, { buffer }, size);
        t.queue.submit([commandEncoder.finish()]);
      }
    );
  });

g.test('map_command_recording_order')
  .desc(
    `
Test that the order of mapping a buffer relative to when commands are recorded that use it
  does not matter, as long as the buffer is unmapped when the commands are submitted.
  `
  )
  .paramsSubcasesOnly([
    {
      order: ['record', 'map', 'unmap', 'finish', 'submit'],
      mappedAtCreation: false,
      _shouldError: false,
    },
    {
      order: ['record', 'map', 'finish', 'unmap', 'submit'],
      mappedAtCreation: false,
      _shouldError: false,
    },
    {
      order: ['record', 'finish', 'map', 'unmap', 'submit'],
      mappedAtCreation: false,
      _shouldError: false,
    },
    {
      order: ['map', 'record', 'unmap', 'finish', 'submit'],
      mappedAtCreation: false,
      _shouldError: false,
    },
    {
      order: ['map', 'record', 'finish', 'unmap', 'submit'],
      mappedAtCreation: false,
      _shouldError: false,
    },
    {
      order: ['map', 'record', 'finish', 'submit', 'unmap'],
      mappedAtCreation: false,
      _shouldError: true,
    },
    {
      order: ['record', 'map', 'finish', 'submit', 'unmap'],
      mappedAtCreation: false,
      _shouldError: true,
    },
    {
      order: ['record', 'finish', 'map', 'submit', 'unmap'],
      mappedAtCreation: false,
      _shouldError: true,
    },
    { order: ['record', 'unmap', 'finish', 'submit'], mappedAtCreation: true, _shouldError: false },
    { order: ['record', 'finish', 'unmap', 'submit'], mappedAtCreation: true, _shouldError: false },
    { order: ['record', 'finish', 'submit', 'unmap'], mappedAtCreation: true, _shouldError: true },
  ] as const)
  .fn(async t => {
    const { order, mappedAtCreation, _shouldError: shouldError } = t.params;

    const buffer = t.createBufferTracked({
      size: 4,
      usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC,
      mappedAtCreation,
    });

    const targetBuffer = t.createBufferTracked({
      size: 4,
      usage: GPUBufferUsage.COPY_DST,
    });

    const commandEncoder = t.device.createCommandEncoder();
    let commandBuffer: GPUCommandBuffer;

    const steps = {
      record: () => {
        commandEncoder.copyBufferToBuffer(buffer, 0, targetBuffer, 0, 4);
      },
      map: async () => {
        await buffer.mapAsync(GPUMapMode.WRITE);
      },
      unmap: () => {
        buffer.unmap();
      },
      finish: () => {
        commandBuffer = commandEncoder.finish();
      },
      submit: () => {
        t.expectValidationError(() => {
          t.queue.submit([commandBuffer]);
        }, shouldError);
      },
    };

    for (const op of order) {
      await steps[op]();
    }
  });
