export const description = `
copyToTexture with HTMLCanvasElement and OffscreenCanvas sources.
`;

import { makeTestGroup } from '../../../common/framework/test_group.js';
import { skipTestCase } from '../../../common/util/util.js';
import { kCanvasAlphaModes } from '../../capability_info.js';
import {
  getBaseFormatForRegularTextureFormat,
  isTextureFormatPossiblyUsableWithCopyExternalImageToTexture,
  kRegularTextureFormats,
  RegularTextureFormat,
} from '../../format_info.js';
import { TextureUploadingUtils } from '../../util/copy_to_texture.js';
import { CanvasType, kAllCanvasTypes, createCanvas } from '../../util/create_elements.js';
import { TexelCompareOptions } from '../../util/texture/texture_ok.js';

class F extends TextureUploadingUtils {
  init2DCanvasContentWithColorSpace({
    width,
    height,
    colorSpace,
  }: {
    width: number;
    height: number;
    colorSpace: 'srgb' | 'display-p3';
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, 'onscreen', width, height);

    let canvasContext = null;
    canvasContext = canvas.getContext('2d', { colorSpace });

    if (canvasContext === null) {
      this.skip('onscreen canvas 2d context not available');
    }

    if (
      typeof canvasContext.getContextAttributes === 'undefined' ||
      typeof canvasContext.getContextAttributes().colorSpace === 'undefined'
    ) {
      this.skip('color space attr is not supported for canvas 2d context');
    }

    const SOURCE_PIXEL_BYTES = 4;
    const imagePixels = new Uint8ClampedArray(SOURCE_PIXEL_BYTES * width * height);

    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 153;

    let pixelStartPos = 0;
    // Red;
    for (let i = 0; i < rectHeight; ++i) {
      for (let j = 0; j < rectWidth; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 255;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 0;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Lime;
    for (let i = 0; i < rectHeight; ++i) {
      for (let j = rectWidth; j < width; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 0;
        imagePixels[pixelStartPos + 1] = 255;
        imagePixels[pixelStartPos + 2] = 0;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Blue
    for (let i = rectHeight; i < height; ++i) {
      for (let j = 0; j < rectWidth; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 0;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 255;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    // Fuchsia
    for (let i = rectHeight; i < height; ++i) {
      for (let j = rectWidth; j < width; ++j) {
        pixelStartPos = (i * width + j) * SOURCE_PIXEL_BYTES;
        imagePixels[pixelStartPos] = 255;
        imagePixels[pixelStartPos + 1] = 0;
        imagePixels[pixelStartPos + 2] = 255;
        imagePixels[pixelStartPos + 3] = alphaValue;
      }
    }

    const imageData = new ImageData(imagePixels, width, height, { colorSpace });
    if (typeof imageData.colorSpace === 'undefined') {
      this.skip('color space attr is not supported for ImageData');
    }

    const ctx = canvasContext as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
    ctx.putImageData(imageData, 0, 0);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
    };
  }

  // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
  init2DCanvasContent({
    canvasType,
    width,
    height,
  }: {
    canvasType: CanvasType;
    width: number;
    height: number;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    let canvasContext = null;
    canvasContext = canvas.getContext('2d') as CanvasRenderingContext2D;

    if (canvasContext === null) {
      this.skip(canvasType + ' canvas 2d context not available');
    }

    const ctx = canvasContext;
    this.paint2DCanvas(ctx, width, height, 0.6);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackFor2DCanvas(canvasContext, width, height),
    };
  }

  private paint2DCanvas(
    ctx: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
    width: number,
    height: number,
    alphaValue: number
  ) {
    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    // Red
    ctx.fillStyle = `rgba(255, 0, 0, ${alphaValue})`;
    ctx.fillRect(0, 0, rectWidth, rectHeight);
    // Lime
    ctx.fillStyle = `rgba(0, 255, 0, ${alphaValue})`;
    ctx.fillRect(rectWidth, 0, width - rectWidth, rectHeight);
    // Blue
    ctx.fillStyle = `rgba(0, 0, 255, ${alphaValue})`;
    ctx.fillRect(0, rectHeight, rectWidth, height - rectHeight);
    // Fuchsia
    ctx.fillStyle = `rgba(255, 0, 255, ${alphaValue})`;
    ctx.fillRect(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
  }

  // MAINTENANCE_TODO: Cache the generated canvas to avoid duplicated initialization.
  initGLCanvasContent({
    canvasType,
    contextName,
    width,
    height,
    premultiplied,
  }: {
    canvasType: CanvasType;
    contextName: 'webgl' | 'webgl2';
    width: number;
    height: number;
    premultiplied: boolean;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    // MAINTENANCE_TODO: Workaround for @types/offscreencanvas missing an overload of
    // `OffscreenCanvas.getContext` that takes `string` or a union of context types.
    const gl = (canvas as HTMLCanvasElement).getContext(contextName, {
      premultipliedAlpha: premultiplied,
    }) as WebGLRenderingContext | WebGL2RenderingContext | null;

    if (gl === null) {
      this.skip(canvasType + ' canvas ' + contextName + ' context not available');
    }
    this.trackForCleanup(gl);

    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 0.6;
    const colorValue = premultiplied ? alphaValue : 1.0;

    // For webgl/webgl2 context canvas, if the context created with premultipliedAlpha attributes,
    // it means that the value in drawing buffer is premultiplied or not. So we should set
    // premultipliedAlpha value for premultipliedAlpha true gl context and unpremultipliedAlpha value
    // for the premultipliedAlpha false gl context.
    gl.enable(gl.SCISSOR_TEST);
    gl.scissor(0, 0, rectWidth, rectHeight);
    gl.clearColor(colorValue, 0.0, 0.0, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(rectWidth, 0, width - rectWidth, rectHeight);
    gl.clearColor(0.0, colorValue, 0.0, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(0, rectHeight, rectWidth, height - rectHeight);
    gl.clearColor(0.0, 0.0, colorValue, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.scissor(rectWidth, rectHeight, width - rectWidth, height - rectHeight);
    gl.clearColor(colorValue, colorValue, colorValue, alphaValue);
    gl.clear(gl.COLOR_BUFFER_BIT);

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackForWebGLCanvas(gl, width, height),
    };
  }

  private getDataToInitSourceWebGPUCanvas(
    width: number,
    height: number,
    alphaMode: GPUCanvasAlphaMode
  ): Uint8ClampedArray {
    const rectWidth = Math.floor(width / 2);
    const rectHeight = Math.floor(height / 2);

    const alphaValue = 153;
    // Always output [153, 153, 153, 153]. When the alphaMode is...
    //   - premultiplied: the readback is CSS `rgba(255, 255, 255, 60%)`.
    //   - opaque: the readback is CSS `rgba(153, 153, 153, 100%)`.
    // getExpectedReadbackForWebGPUCanvas matches this.
    const colorValue = alphaValue;

    // BGRA8Unorm texture
    const initialData = new Uint8ClampedArray(4 * width * height);
    const maxRectHeightIndex = width * rectHeight;
    for (let pixelIndex = 0; pixelIndex < initialData.length / 4; ++pixelIndex) {
      const index = pixelIndex * 4;

      // Top-half two rectangles
      if (pixelIndex < maxRectHeightIndex) {
        // top-left side rectangle
        if (pixelIndex % width < rectWidth) {
          // top-left side rectangle
          initialData[index] = colorValue;
          initialData[index + 1] = 0;
          initialData[index + 2] = 0;
          initialData[index + 3] = alphaValue;
        } else {
          // top-right side rectangle
          initialData[index] = 0;
          initialData[index + 1] = colorValue;
          initialData[index + 2] = 0;
          initialData[index + 3] = alphaValue;
        }
      } else {
        // Bottom-half two rectangles
        // bottom-left side rectangle
        if (pixelIndex % width < rectWidth) {
          initialData[index] = 0;
          initialData[index + 1] = 0;
          initialData[index + 2] = colorValue;
          initialData[index + 3] = alphaValue;
        } else {
          // bottom-right side rectangle
          initialData[index] = colorValue;
          initialData[index + 1] = colorValue;
          initialData[index + 2] = colorValue;
          initialData[index + 3] = alphaValue;
        }
      }
    }
    return initialData;
  }

  initSourceWebGPUCanvas({
    device,
    canvasType,
    width,
    height,
    alphaMode,
  }: {
    device: GPUDevice;
    canvasType: CanvasType;
    width: number;
    height: number;
    alphaMode: GPUCanvasAlphaMode;
  }): {
    canvas: HTMLCanvasElement | OffscreenCanvas;
    expectedSourceData: Uint8ClampedArray;
  } {
    const canvas = createCanvas(this, canvasType, width, height);

    const gpuContext = canvas.getContext('webgpu');

    if (!(gpuContext instanceof GPUCanvasContext)) {
      this.skip(canvasType + ' canvas webgpu context not available');
    }

    gpuContext.configure({
      device,
      format: 'bgra8unorm',
      usage: GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC,
      alphaMode,
    });

    // BGRA8Unorm texture
    const initialData = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);
    const canvasTexture = gpuContext.getCurrentTexture();
    device.queue.writeTexture(
      { texture: canvasTexture },
      initialData,
      {
        bytesPerRow: width * 4,
        rowsPerImage: height,
      },
      {
        width,
        height,
        depthOrArrayLayers: 1,
      }
    );

    return {
      canvas,
      expectedSourceData: this.getExpectedReadbackForWebGPUCanvas(width, height, alphaMode),
    };
  }

  private getExpectedReadbackFor2DCanvas(
    context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
    width: number,
    height: number
  ): Uint8ClampedArray {
    // Always read back the raw data from canvas
    return context.getImageData(0, 0, width, height).data;
  }

  private getExpectedReadbackForWebGLCanvas(
    gl: WebGLRenderingContext | WebGL2RenderingContext,
    width: number,
    height: number
  ): Uint8ClampedArray {
    const bytesPerPixel = 4;

    const sourcePixels = new Uint8ClampedArray(width * height * bytesPerPixel);
    gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, sourcePixels);

    return this.doFlipY(sourcePixels, width, height, bytesPerPixel);
  }

  private getExpectedReadbackForWebGPUCanvas(
    width: number,
    height: number,
    alphaMode: GPUCanvasAlphaMode
  ): Uint8ClampedArray {
    const bytesPerPixel = 4;

    const rgbaPixels = this.getDataToInitSourceWebGPUCanvas(width, height, alphaMode);

    // The source canvas has bgra8unorm back resource. We
    // swizzle the channels to align with 2d/webgl canvas and
    // clear alpha to 255 (1.0) when context alphaMode
    // is set to opaque (follow webgpu spec).
    for (let i = 0; i < height; ++i) {
      for (let j = 0; j < width; ++j) {
        const pixelPos = i * width + j;
        const r = rgbaPixels[pixelPos * bytesPerPixel + 2];
        if (alphaMode === 'opaque') {
          rgbaPixels[pixelPos * bytesPerPixel + 3] = 255;
        }

        rgbaPixels[pixelPos * bytesPerPixel + 2] = rgbaPixels[pixelPos * bytesPerPixel];
        rgbaPixels[pixelPos * bytesPerPixel] = r;
      }
    }

    return rgbaPixels;
  }

  doCopyContentsTest(
    source: HTMLCanvasElement | OffscreenCanvas,
    expectedSourceImage: Uint8ClampedArray,
    p: {
      width: number;
      height: number;
      dstColorFormat: RegularTextureFormat;
      srcDoFlipYDuringCopy: boolean;
      srcPremultiplied: boolean;
      dstPremultiplied: boolean;
    }
  ) {
    const dst = this.createTextureTracked({
      size: {
        width: p.width,
        height: p.height,
        depthOrArrayLayers: 1,
      },
      format: p.dstColorFormat,
      usage:
        GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    // Construct expected value for different dst color format
    const baseFormat = getBaseFormatForRegularTextureFormat(p.dstColorFormat);
    const expFormat = baseFormat ?? p.dstColorFormat;

    // For 2d canvas, get expected pixels with getImageData(), which returns unpremultiplied
    // values.
    const expectedDestinationImage = this.getExpectedDstPixelsFromSrcPixels({
      srcPixels: expectedSourceImage,
      srcOrigin: [0, 0],
      srcSize: [p.width, p.height],
      dstOrigin: [0, 0],
      dstSize: [p.width, p.height],
      subRectSize: [p.width, p.height],
      format: expFormat,
      flipSrcBeforeCopy: false,
      srcDoFlipYDuringCopy: p.srcDoFlipYDuringCopy,
      conversion: {
        srcPremultiplied: p.srcPremultiplied,
        dstPremultiplied: p.dstPremultiplied,
      },
    });

    this.doTestAndCheckResult(
      { source, origin: { x: 0, y: 0 }, flipY: p.srcDoFlipYDuringCopy },
      {
        texture: dst,
        origin: { x: 0, y: 0 },
        colorSpace: 'srgb',
        premultipliedAlpha: p.dstPremultiplied,
      },
      expectedDestinationImage,
      { width: p.width, height: p.height, depthOrArrayLayers: 1 },
      // 1.0 and 0.6 are representable precisely by all formats except rgb10a2unorm, but
      // allow diffs of 1ULP since that's the generally-appropriate threshold.
      { maxDiffULPsForNormFormat: 1, maxDiffULPsForFloatFormat: 1 }
    );
  }
}

export const g = makeTestGroup(F);

g.test('copy_contents_from_2d_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with 2d context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with '2d'.
  Use fillRect(2d context) to render red rect for top-left,
  green rect for top-right, blue rect for bottom-left and white for bottom-right.

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Valid 2d context type
  - Valid dstColorFormat of copyExternalImageToTexture()
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases)
  - TODO(#913): color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('dstColorFormat', kRegularTextureFormats)
      .filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .fn(t => {
    const { width, height, canvasType, dstAlphaMode, dstColorFormat } = t.params;
    t.skipIfTextureFormatNotSupported(dstColorFormat);
    t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);

    const { canvas, expectedSourceData } = t.init2DCanvasContent({
      canvasType,
      width,
      height,
    });

    t.doCopyContentsTest(canvas, expectedSourceData, {
      srcPremultiplied: false,
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('copy_contents_from_gl_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with webgl/webgl2 context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with webgl'/'webgl2'.
  Use scissor + clear to render red rect for top-left, green rect
  for top-right, blue rect for bottom-left and white for bottom-right.
  And do premultiply alpha in advance if the webgl/webgl2 context is created
  with premultipliedAlpha : true.

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Valid webgl/webgl2 context type
  - Valid dstColorFormat of copyExternalImageToTexture()
  - Valid source image alphaMode
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo'(named 'srcDoFlipYDuringCopy' in cases)
  - TODO: color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('contextName', ['webgl', 'webgl2'] as const)
      .combine('dstColorFormat', kRegularTextureFormats)
      .filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
      .combine('srcPremultiplied', [true, false])
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .fn(t => {
    const {
      width,
      height,
      canvasType,
      contextName,
      srcPremultiplied,
      dstAlphaMode,
      dstColorFormat,
    } = t.params;
    t.skipIfTextureFormatNotSupported(dstColorFormat);
    t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);

    const { canvas, expectedSourceData } = t.initGLCanvasContent({
      canvasType,
      contextName,
      width,
      height,
      premultiplied: srcPremultiplied,
    });

    t.doCopyContentsTest(canvas, expectedSourceData, {
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('copy_contents_from_gpu_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with webgpu context
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with 'webgpu'.
  Use writeTexture to copy pixels to back buffer. The results are:
  red rect for top-left, green rect for top-right, blue rect for bottom-left
  and white for bottom-right.

  TODO: Actually test alphaMode = opaque.
  And do premultiply alpha in advance if the webgpu context is created
  with alphaMode="premultiplied".

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Source WebGPU Canvas lives in the same GPUDevice or different GPUDevice as test
  - Valid dstColorFormat of copyExternalImageToTexture()
  - TODO: test more source image alphaMode
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo'(named 'srcDoFlipYDuringCopy' in cases)
  - TODO: color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('srcAndDstInSameGPUDevice', [true, false])
      .combine('dstColorFormat', kRegularTextureFormats)
      .filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
      // .combine('srcAlphaMode', kCanvasAlphaModes)
      .combine('srcAlphaMode', ['premultiplied'] as const)
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .beforeAllSubcases(t => {
    t.usesMismatchedDevice();
  })
  .fn(t => {
    const {
      width,
      height,
      canvasType,
      srcAndDstInSameGPUDevice,
      srcAlphaMode,
      dstAlphaMode,
      dstColorFormat,
    } = t.params;
    t.skipIfTextureFormatNotSupported(dstColorFormat);
    t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);

    const device = srcAndDstInSameGPUDevice ? t.device : t.mismatchedDevice;
    const { canvas: source, expectedSourceData } = t.initSourceWebGPUCanvas({
      device,
      canvasType,
      width,
      height,
      alphaMode: srcAlphaMode,
    });

    t.doCopyContentsTest(source, expectedSourceData, {
      srcPremultiplied: srcAlphaMode === 'premultiplied',
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('copy_contents_from_bitmaprenderer_context_canvas')
  .desc(
    `
  Test HTMLCanvasElement and OffscreenCanvas with ImageBitmapRenderingContext
  can be copied to WebGPU texture correctly.

  It creates HTMLCanvasElement/OffscreenCanvas with 'bitmaprenderer'.
  First, use fillRect(2d context) to render red rect for top-left,
  green rect for top-right, blue rect for bottom-left and white for bottom-right on a
  2d context canvas and create imageBitmap with that canvas. Use transferFromImageBitmap()
  to render the imageBitmap to source canvas.

  Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
  of dst texture, and read the contents out to compare with the canvas contents.

  Provide premultiplied input if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
  is set to 'true' and unpremultiplied input if it is set to 'false'.

  If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
  is flipped.

  The tests covers:
  - Valid canvas type
  - Valid ImageBitmapRendering context type
  - Valid dstColorFormat of copyExternalImageToTexture()
  - Valid dest alphaMode
  - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases)
  - TODO(#913): color space tests need to be added

  And the expected results are all passed.
  `
  )
  .params(u =>
    u
      .combine('canvasType', kAllCanvasTypes)
      .combine('dstColorFormat', kRegularTextureFormats)
      .filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
      .combine('dstAlphaMode', kCanvasAlphaModes)
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15])
      .combine('height', [1, 2, 4, 15])
  )
  .fn(async t => {
    const { width, height, canvasType, dstAlphaMode, dstColorFormat } = t.params;
    t.skipIfTextureFormatNotSupported(dstColorFormat);
    t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);

    const canvas = createCanvas(t, canvasType, width, height);

    const imageBitmapRenderingContext = canvas.getContext('bitmaprenderer');

    if (!(imageBitmapRenderingContext instanceof ImageBitmapRenderingContext)) {
      skipTestCase(canvasType + ' canvas imageBitmap rendering context not available');
    }

    const { canvas: sourceContentCanvas, expectedSourceData } = t.init2DCanvasContent({
      canvasType,
      width,
      height,
    });

    const imageBitmap = await createImageBitmap(sourceContentCanvas, { premultiplyAlpha: 'none' });
    imageBitmapRenderingContext.transferFromImageBitmap(imageBitmap);

    t.doCopyContentsTest(canvas, expectedSourceData, {
      srcPremultiplied: false,
      dstPremultiplied: dstAlphaMode === 'premultiplied',
      ...t.params,
    });
  });

g.test('color_space_conversion')
  .desc(
    `
    Test HTMLCanvasElement with 2d context can created with 'colorSpace' attribute.
    Using CopyExternalImageToTexture to copy from such type of canvas needs
    to do color space converting correctly.

    It creates HTMLCanvasElement/OffscreenCanvas with '2d' and 'colorSpace' attributes.
    Use fillRect(2d context) to render red rect for top-left,
    green rect for top-right, blue rect for bottom-left and white for bottom-right.

    Then call copyExternalImageToTexture() to do a full copy to the 0 mipLevel
    of dst texture, and read the contents out to compare with the canvas contents.

    Provide premultiplied input if 'premultipliedAlpha' in 'GPUCopyExternalImageDestInfo'
    is set to 'true' and unpremultiplied input if it is set to 'false'.

    If 'flipY' in 'GPUCopyExternalImageSourceInfo' is set to 'true', copy will ensure the result
    is flipped.

    If color space from source input and user defined dstTexture color space are different, the
    result must convert the content to user defined color space

    The tests covers:
    - Valid dstColorFormat of copyExternalImageToTexture()
    - Valid dest alphaMode
    - Valid 'flipY' config in 'GPUCopyExternalImageSourceInfo' (named 'srcDoFlipYDuringCopy' in cases)
    - Valid 'colorSpace' config in 'dstColorSpace'

    And the expected results are all passed.

    TODO: Enhance test data with colors that aren't always opaque and fully saturated.
    TODO: Consider refactoring src data setup with TexelView.writeTextureData.
  `
  )
  .params(u =>
    u
      .combine('srcColorSpace', ['srgb', 'display-p3'] as const)
      .combine('dstColorSpace', ['srgb', 'display-p3'] as const)
      .combine('dstColorFormat', kRegularTextureFormats)
      .filter(t => isTextureFormatPossiblyUsableWithCopyExternalImageToTexture(t.dstColorFormat))
      .combine('dstPremultiplied', [true, false])
      .combine('srcDoFlipYDuringCopy', [true, false])
      .beginSubcases()
      .combine('width', [1, 2, 4, 15, 255, 256])
      .combine('height', [1, 2, 4, 15, 255, 256])
  )
  .fn(t => {
    const {
      width,
      height,
      srcColorSpace,
      dstColorSpace,
      dstColorFormat,
      dstPremultiplied,
      srcDoFlipYDuringCopy,
    } = t.params;
    t.skipIfTextureFormatNotSupported(dstColorFormat);
    t.skipIfTextureFormatPossiblyNotUsableWithCopyExternalImageToTexture(dstColorFormat);
    const { canvas, expectedSourceData } = t.init2DCanvasContentWithColorSpace({
      width,
      height,
      colorSpace: srcColorSpace,
    });

    const dst = t.createTextureTracked({
      size: { width, height },
      format: dstColorFormat,
      usage:
        GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
    });

    const expectedDestinationImage = t.getExpectedDstPixelsFromSrcPixels({
      srcPixels: expectedSourceData,
      srcOrigin: [0, 0],
      srcSize: [width, height],
      dstOrigin: [0, 0],
      dstSize: [width, height],
      subRectSize: [width, height],
      // copyExternalImageToTexture does not perform gamma-encoding into `-srgb` formats.
      format: getBaseFormatForRegularTextureFormat(dstColorFormat) ?? dstColorFormat,
      flipSrcBeforeCopy: false,
      srcDoFlipYDuringCopy,
      conversion: {
        srcPremultiplied: false,
        dstPremultiplied,
        srcColorSpace,
        dstColorSpace,
      },
    });

    const texelCompareOptions: TexelCompareOptions = {
      maxFractionalDiff: 0,
      maxDiffULPsForNormFormat: 1,
    };
    if (srcColorSpace !== dstColorSpace) {
      if (dstColorFormat.endsWith('32float')) {
        texelCompareOptions.maxFractionalDiff = 0.0003;
      } else if (dstColorFormat.endsWith('16float')) {
        texelCompareOptions.maxFractionalDiff = 0.0007;
      } else if (dstColorFormat === 'rg11b10ufloat') {
        texelCompareOptions.maxFractionalDiff = 0.015;
      } else {
        texelCompareOptions.maxFractionalDiff = 0.001;
      }
    } else {
      texelCompareOptions.maxDiffULPsForFloatFormat = 1;
    }

    t.doTestAndCheckResult(
      { source: canvas, origin: { x: 0, y: 0 }, flipY: srcDoFlipYDuringCopy },
      {
        texture: dst,
        origin: { x: 0, y: 0 },
        colorSpace: dstColorSpace,
        premultipliedAlpha: dstPremultiplied,
      },
      expectedDestinationImage,
      { width, height, depthOrArrayLayers: 1 },
      texelCompareOptions
    );
  });
