export const description = `
Stress tests covering robustness when available VRAM is exhausted.
`;

import { makeTestGroup } from '../../common/framework/test_group.js';
import { unreachable } from '../../common/util/util.js';
import { GPUConst } from '../../webgpu/constants.js';
import { GPUTest } from '../../webgpu/gpu_test.js';
import { exhaustVramUntilUnder64MB } from '../../webgpu/util/memory.js';

export const g = makeTestGroup(GPUTest);

function createBufferWithMapState(
  device: GPUDevice,
  size: number,
  mapState: GPUBufferMapState,
  mode: GPUMapModeFlags,
  mappedAtCreation: boolean
) {
  const mappable = mapState === 'unmapped';
  if (!mappable && !mappedAtCreation) {
    return device.createBuffer({
      size,
      usage: GPUBufferUsage.UNIFORM,
      mappedAtCreation,
    });
  }
  let buffer: GPUBuffer;
  switch (mode) {
    case GPUMapMode.READ:
      buffer = device.createBuffer({
        size,
        usage: GPUBufferUsage.MAP_READ,
        mappedAtCreation,
      });
      break;
    case GPUMapMode.WRITE:
      buffer = device.createBuffer({
        size,
        usage: GPUBufferUsage.MAP_WRITE,
        mappedAtCreation,
      });
      break;
    default:
      unreachable();
  }
  // If we want the buffer to be mappable and also mappedAtCreation, we call unmap on it now.
  if (mappable && mappedAtCreation) {
    buffer.unmap();
  }
  return buffer;
}

g.test('vram_oom')
  .desc(`Tests that we can allocate buffers until we run out of VRAM.`)
  .fn(async t => {
    await exhaustVramUntilUnder64MB(t.device);
  });

g.test('map_after_vram_oom')
  .desc(
    `Allocates tons of buffers and textures with varying mapping states (unmappable,
mappable, mapAtCreation, mapAtCreation-then-unmapped) until OOM; then attempts
to mapAsync all the mappable objects. The last buffer should be an error buffer so
mapAsync on it should reject and produce a validation error. `
  )
  .params(u =>
    u
      .combine('mapState', ['mapped', 'unmapped'] as GPUBufferMapState[])
      .combine('mode', [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE])
      .combine('mappedAtCreation', [true, false])
      .combine('unmapBeforeResolve', [true, false])
  )
  .fn(async t => {
    // Use a relatively large size to quickly hit OOM.
    const kSize = 512 * 1024 * 1024;

    const { mapState, mode, mappedAtCreation, unmapBeforeResolve } = t.params;
    const mappable = mapState === 'unmapped';
    const buffers: GPUBuffer[] = [];
    // Closure to call map and verify results on all of the buffers.
    const finish = async () => {
      if (mappable) {
        await Promise.all(buffers.map(value => value.mapAsync(mode)));
      } else {
        buffers.forEach(value => {
          t.expectValidationError(() => {
            void value.mapAsync(mode);
          });
        });
      }
      // Finally, destroy all the buffers to free the resources.
      buffers.forEach(buffer => buffer.destroy());
    };

    let errorBuffer: GPUBuffer;
    for (;;) {
      if (mappedAtCreation) {
        // When mappedAtCreation is true, OOM can happen on the client which throws a RangeError. In
        // this case, we don't do any validations on the OOM buffer.
        try {
          t.device.pushErrorScope('out-of-memory');
          const buffer = t.trackForCleanup(
            createBufferWithMapState(t.device, kSize, mapState, mode, mappedAtCreation)
          );
          if (await t.device.popErrorScope()) {
            errorBuffer = buffer;
            break;
          }
          buffers.push(buffer);
        } catch (ex) {
          t.expect(ex instanceof RangeError);
          await finish();
          return;
        }
      } else {
        t.device.pushErrorScope('out-of-memory');
        const buffer = t.trackForCleanup(
          createBufferWithMapState(t.device, kSize, mapState, mode, mappedAtCreation)
        );
        if (await t.device.popErrorScope()) {
          errorBuffer = buffer;
          break;
        }
        buffers.push(buffer);
      }
    }

    // Do some validation on the OOM buffer.
    let promise: Promise<void>;
    t.expectValidationError(() => {
      promise = errorBuffer.mapAsync(mode);
    });
    if (unmapBeforeResolve) {
      // Should reject with abort error because buffer will be unmapped
      // before validation check finishes.
      t.shouldReject('AbortError', promise!);
    } else {
      // Should also reject in addition to the validation error.
      t.shouldReject('OperationError', promise!);

      // Wait for validation error before unmap to ensure validation check
      // ends before unmap.
      try {
        await promise!;
        throw new Error('The promise should be rejected.');
      } catch {
        // Should cause an exception because the promise should be rejected.
      }
    }

    // Should throw an OperationError because the buffer is not mapped.
    // Note: not a RangeError because the state of the buffer is checked first.
    t.shouldThrow('OperationError', () => {
      errorBuffer.getMappedRange();
    });

    // Should't be a validation error even if the buffer failed to be mapped.
    errorBuffer.unmap();
    errorBuffer.destroy();

    // Finish the rest of the test w.r.t the mappable buffers.
    await finish();
  });

g.test('validation_vs_oom')
  .desc(
    `Tests that calls affected by both OOM and validation errors expose the
validation error with precedence.`
  )
  .unimplemented();

g.test('recovery')
  .desc(
    `Tests that after going VRAM-OOM, destroying allocated resources eventually
allows new resources to be allocated.`
  )
  .unimplemented();
