import { Fixture } from '../common/framework/fixture.js';
import { getGPU } from '../common/util/navigator_gpu.js';
import { assert, raceWithRejectOnTimeout } from '../common/util/util.js';

/**
 * A test class to help test error scopes and uncapturederror events.
 */
export class ErrorTest extends Fixture {
  _device: GPUDevice | undefined = undefined;

  get device(): GPUDevice {
    assert(this._device !== undefined);
    return this._device;
  }

  override async init(): Promise<void> {
    await super.init();
    const gpu = getGPU(this.rec);
    const adapter = await gpu.requestAdapter();
    assert(adapter !== null);

    // We need to max out the adapter limits related to texture dimensions to more reliably cause an
    // OOM error when asked for it, so set that on the device now.
    const device = await this.requestDeviceTracked(adapter, {
      requiredLimits: {
        maxTextureDimension2D: adapter.limits.maxTextureDimension2D,
        maxTextureArrayLayers: adapter.limits.maxTextureArrayLayers,
      },
    });
    assert(device !== null);
    this._device = device;
  }

  /**
   * Generates an error of the given filter type. For now, the errors are generated by calling a
   * known code-path to cause the error. This can be updated in the future should there be a more
   * direct way to inject errors.
   */
  generateError(filter: GPUErrorFilter): void {
    switch (filter) {
      case 'out-of-memory':
        this.trackForCleanup(
          this.device.createTexture({
            // One of the largest formats. With the base limits, the texture will be 256 GiB.
            format: 'rgba32float',
            usage: GPUTextureUsage.COPY_DST,
            size: [
              this.device.limits.maxTextureDimension2D,
              this.device.limits.maxTextureDimension2D,
              this.device.limits.maxTextureArrayLayers,
            ],
          })
        );
        break;
      case 'validation':
        // Generating a validation error by passing in an invalid usage when creating a buffer.
        this.trackForCleanup(
          this.device.createBuffer({
            size: 1024,
            usage: 0xffff, // Invalid GPUBufferUsage
          })
        );
        break;
    }
    // MAINTENANCE_TODO: This is a workaround for Chromium not flushing. Remove when not needed.
    this.device.queue.submit([]);
  }

  /**
   * Checks whether the error is of the type expected given the filter.
   */
  isInstanceOfError(filter: GPUErrorFilter, error: GPUError | null): boolean {
    switch (filter) {
      case 'out-of-memory':
        return error instanceof GPUOutOfMemoryError;
      case 'validation':
        return error instanceof GPUValidationError;
      case 'internal':
        return error instanceof GPUInternalError;
    }
  }

  /**
   * Pop `count` error scopes, and assert they all return `null`. Chunks the
   * awaits so we only `Promise.all` 200 scopes at a time, instead of stalling
   * on a huge `Promise.all` all at once. This helps Chromium's "heartbeat"
   * mechanism know that the test is still running (and not hung).
   */
  async chunkedPopManyErrorScopes(count: number) {
    const promises = [];
    for (let i = 0; i < count; i++) {
      promises.push(this.device.popErrorScope());
      if (promises.length >= 200) {
        this.expect((await Promise.all(promises)).every(e => e === null));
        promises.length = 0;
      }
    }
    this.expect((await Promise.all(promises)).every(e => e === null));
  }

  /**
   * Expect an uncapturederror event to occur. Note: this MUST be awaited, because
   * otherwise it could erroneously pass by capturing an error from later in the test.
   */
  async expectUncapturedError(
    fn: Function,
    useOnuncapturederror = false
  ): Promise<GPUUncapturedErrorEvent> {
    return this.immediateAsyncExpectation(() => {
      const promise: Promise<GPUUncapturedErrorEvent> = new Promise(resolve => {
        const eventListener = (event: GPUUncapturedErrorEvent) => {
          // Don't emit error to console.
          event.preventDefault();
          // Unregister before resolving so we can be certain these are cleaned
          // up before the next test.
          if (useOnuncapturederror) {
            this.device.onuncapturederror = null;
          } else {
            this.device.removeEventListener('uncapturederror', eventListener);
          }

          this.debug(`Got uncaptured error event with ${event.error}`);
          resolve(event);
        };

        if (useOnuncapturederror) {
          this.device.onuncapturederror = eventListener;
        } else {
          this.device.addEventListener('uncapturederror', eventListener, { once: true });
        }
      });

      fn();

      const kTimeoutMS = 1000;
      return raceWithRejectOnTimeout(
        promise,
        kTimeoutMS,
        'Timeout occurred waiting for uncaptured error'
      );
    });
  }
}
