export const description = `
Validation tests to all commands of GPUCommandEncoder, GPUComputePassEncoder, and
GPURenderPassEncoder when the encoder is not finished.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { keysOf } from '../../../../common/util/data_tables.js';
import { unreachable } from '../../../../common/util/util.js';
import { ValidationTest } from '../validation_test.js';

import { beginRenderPassWithQuerySet } from './queries/common.js';

class F extends ValidationTest {
  createRenderPipelineForTest(): GPURenderPipeline {
    return this.device.createRenderPipeline({
      layout: 'auto',
      vertex: {
        module: this.device.createShaderModule({
          code: `
            @vertex fn main() -> @builtin(position) vec4<f32> {
              return vec4<f32>();
            }
          `,
        }),
        entryPoint: 'main',
      },
      fragment: {
        module: this.device.createShaderModule({
          code: `@fragment fn main() {}`,
        }),
        entryPoint: 'main',
        targets: [{ format: 'rgba8unorm', writeMask: 0 }],
      },
    });
  }

  createBindGroupForTest(): GPUBindGroup {
    return this.device.createBindGroup({
      entries: [
        {
          binding: 0,
          resource: this.device.createSampler(),
        },
      ],
      layout: this.device.createBindGroupLayout({
        entries: [
          {
            binding: 0,
            visibility: GPUShaderStage.FRAGMENT,
            sampler: { type: 'filtering' },
          },
        ],
      }),
    });
  }
}

export const g = makeTestGroup(F);

// MAINTENANCE_TODO: Remove writeTimestamp from here once it's (hopefully) added back to the spec.
type EncoderCommands =
  | keyof Omit<GPUCommandEncoder, '__brand' | 'label' | 'finish'>
  | 'writeTimestamp';
const kEncoderCommandInfo: {
  readonly [k in EncoderCommands]: {};
} = {
  beginComputePass: {},
  beginRenderPass: {},
  clearBuffer: {},
  copyBufferToBuffer: {},
  copyBufferToTexture: {},
  copyTextureToBuffer: {},
  copyTextureToTexture: {},
  insertDebugMarker: {},
  popDebugGroup: {},
  pushDebugGroup: {},
  writeTimestamp: {},
  resolveQuerySet: {},
};
const kEncoderCommands = keysOf(kEncoderCommandInfo);

type RenderPassEncoderCommands = keyof Omit<GPURenderPassEncoder, '__brand' | 'label' | 'end'>;
const kRenderPassEncoderCommandInfo: {
  readonly [k in RenderPassEncoderCommands]: {};
} = {
  draw: {},
  drawIndexed: {},
  drawIndexedIndirect: {},
  drawIndirect: {},
  setIndexBuffer: {},
  setBindGroup: {},
  setVertexBuffer: {},
  setPipeline: {},
  setViewport: {},
  setScissorRect: {},
  setBlendConstant: {},
  setStencilReference: {},
  beginOcclusionQuery: {},
  endOcclusionQuery: {},
  executeBundles: {},
  pushDebugGroup: {},
  popDebugGroup: {},
  insertDebugMarker: {},
};
const kRenderPassEncoderCommands = keysOf(kRenderPassEncoderCommandInfo);

type RenderBundleEncoderCommands = keyof Omit<
  GPURenderBundleEncoder,
  '__brand' | 'label' | 'finish'
>;
const kRenderBundleEncoderCommandInfo: {
  readonly [k in RenderBundleEncoderCommands]: {};
} = {
  draw: {},
  drawIndexed: {},
  drawIndexedIndirect: {},
  drawIndirect: {},
  setPipeline: {},
  setBindGroup: {},
  setIndexBuffer: {},
  setVertexBuffer: {},
  pushDebugGroup: {},
  popDebugGroup: {},
  insertDebugMarker: {},
};
const kRenderBundleEncoderCommands = keysOf(kRenderBundleEncoderCommandInfo);

// MAINTENANCE_TODO: remove the deprecated 'dispatch' and 'dispatchIndirect' here once they're
// removed from `@webgpu/types`.
type ComputePassEncoderCommands = keyof Omit<
  GPUComputePassEncoder,
  '__brand' | 'label' | 'end' | 'dispatch' | 'dispatchIndirect'
>;
const kComputePassEncoderCommandInfo: {
  readonly [k in ComputePassEncoderCommands]: {};
} = {
  setBindGroup: {},
  setPipeline: {},
  dispatchWorkgroups: {},
  dispatchWorkgroupsIndirect: {},
  pushDebugGroup: {},
  popDebugGroup: {},
  insertDebugMarker: {},
};
const kComputePassEncoderCommands = keysOf(kComputePassEncoderCommandInfo);

g.test('non_pass_commands')
  .desc(
    `
  Test that functions of GPUCommandEncoder generate a validation error if the encoder is already
  finished.

  TODO: writeTimestamp is removed from the spec so it's skipped if it TypeErrors.
  `
  )
  .params(u =>
    u
      .combine('command', kEncoderCommands)
      .beginSubcases()
      .combine('finishBeforeCommand', [false, true])
  )
  .beforeAllSubcases(t => {
    switch (t.params.command) {
      case 'writeTimestamp':
        t.selectDeviceOrSkipTestCase('timestamp-query');
        break;
    }
  })
  .fn(t => {
    const { command, finishBeforeCommand } = t.params;

    const srcBuffer = t.device.createBuffer({
      size: 16,
      usage: GPUBufferUsage.COPY_SRC | GPUTextureUsage.COPY_DST,
    });
    const dstBuffer = t.device.createBuffer({
      size: 16,
      usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.QUERY_RESOLVE,
    });

    const textureSize = { width: 1, height: 1 };
    const textureFormat = 'rgba8unorm';
    const srcTexture = t.device.createTexture({
      size: textureSize,
      format: textureFormat,
      usage: GPUTextureUsage.COPY_SRC,
    });
    const dstTexture = t.device.createTexture({
      size: textureSize,
      format: textureFormat,
      usage: GPUTextureUsage.COPY_DST,
    });

    const querySet = t.device.createQuerySet({
      type: command === 'writeTimestamp' ? 'timestamp' : 'occlusion',
      count: 1,
    });

    const encoder = t.device.createCommandEncoder();

    if (finishBeforeCommand) encoder.finish();

    t.expectValidationError(() => {
      switch (command) {
        case 'beginComputePass':
          {
            encoder.beginComputePass();
          }
          break;
        case 'beginRenderPass':
          {
            encoder.beginRenderPass({ colorAttachments: [] });
          }
          break;
        case 'clearBuffer':
          {
            encoder.clearBuffer(dstBuffer, 0, 16);
          }
          break;
        case 'copyBufferToBuffer':
          {
            encoder.copyBufferToBuffer(srcBuffer, 0, dstBuffer, 0, 0);
          }
          break;
        case 'copyBufferToTexture':
          {
            encoder.copyBufferToTexture(
              { buffer: srcBuffer },
              { texture: dstTexture },
              textureSize
            );
          }
          break;
        case 'copyTextureToBuffer':
          {
            encoder.copyTextureToBuffer(
              { texture: srcTexture },
              { buffer: dstBuffer },
              textureSize
            );
          }
          break;
        case 'copyTextureToTexture':
          {
            encoder.copyTextureToTexture(
              { texture: srcTexture },
              { texture: dstTexture },
              textureSize
            );
          }
          break;
        case 'insertDebugMarker':
          {
            encoder.insertDebugMarker('marker');
          }
          break;
        case 'pushDebugGroup':
          {
            encoder.pushDebugGroup('group');
          }
          break;
        case 'popDebugGroup':
          {
            encoder.popDebugGroup();
          }
          break;
        case 'writeTimestamp':
          try {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            (encoder as any).writeTimestamp(querySet, 0);
          } catch (ex) {
            t.skipIf(ex instanceof TypeError, 'writeTimestamp is actually not available');
          }
          break;
        case 'resolveQuerySet':
          {
            encoder.resolveQuerySet(querySet, 0, 1, dstBuffer, 0);
          }
          break;
        default:
          unreachable();
      }
    }, finishBeforeCommand);
  });

g.test('render_pass_commands')
  .desc(
    `
    Test that functions of GPURenderPassEncoder generate a validation error if the encoder or the
    pass is already finished.

    - TODO: Consider testing: nothing before command, end before command, end+finish before command.
  `
  )
  .params(u =>
    u
      .combine('command', kRenderPassEncoderCommands)
      .beginSubcases()
      .combine('finishBeforeCommand', [false, true])
  )
  .fn(t => {
    const { command, finishBeforeCommand } = t.params;

    const querySet = t.device.createQuerySet({ type: 'occlusion', count: 1 });
    const encoder = t.device.createCommandEncoder();
    const renderPass = beginRenderPassWithQuerySet(t, encoder, querySet);

    const buffer = t.device.createBuffer({
      size: 12,
      usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.VERTEX,
    });

    const pipeline = t.createRenderPipelineForTest();

    const bindGroup = t.createBindGroupForTest();

    if (finishBeforeCommand) {
      renderPass.end();
      encoder.finish();
    }

    t.expectValidationError(() => {
      switch (command) {
        case 'draw':
          {
            renderPass.draw(1);
          }
          break;
        case 'drawIndexed':
          {
            renderPass.drawIndexed(1);
          }
          break;
        case 'drawIndirect':
          {
            renderPass.drawIndirect(buffer, 1);
          }
          break;
        case 'setIndexBuffer':
          {
            renderPass.setIndexBuffer(buffer, 'uint32');
          }
          break;
        case 'drawIndexedIndirect':
          {
            renderPass.drawIndexedIndirect(buffer, 0);
          }
          break;
        case 'setBindGroup':
          {
            renderPass.setBindGroup(0, bindGroup);
          }
          break;
        case 'setVertexBuffer':
          {
            renderPass.setVertexBuffer(1, buffer);
          }
          break;
        case 'setPipeline':
          {
            renderPass.setPipeline(pipeline);
          }
          break;
        case 'setViewport':
          {
            const kNumTestPoints = 8;
            const kViewportMinDepth = 0;
            const kViewportMaxDepth = 1;
            renderPass.setViewport(0, 0, kNumTestPoints, 0, kViewportMinDepth, kViewportMaxDepth);
          }
          break;
        case 'setScissorRect':
          {
            renderPass.setScissorRect(0, 0, 0, 0);
          }
          break;
        case 'setBlendConstant':
          {
            renderPass.setBlendConstant({ r: 1.0, g: 1.0, b: 1.0, a: 1.0 });
          }
          break;
        case 'setStencilReference':
          {
            renderPass.setStencilReference(0);
          }
          break;
        case 'beginOcclusionQuery':
          {
            renderPass.beginOcclusionQuery(0);
          }
          break;
        case 'endOcclusionQuery':
          {
            renderPass.endOcclusionQuery();
          }
          break;
        case 'executeBundles':
          {
            renderPass.executeBundles([]);
          }
          break;
        case 'pushDebugGroup':
          {
            encoder.pushDebugGroup('group');
          }
          break;
        case 'popDebugGroup':
          {
            encoder.popDebugGroup();
          }
          break;
        case 'insertDebugMarker':
          {
            encoder.insertDebugMarker('marker');
          }
          break;
        default:
          unreachable();
      }
    }, finishBeforeCommand);
  });

g.test('render_bundle_commands')
  .desc(
    `
    Test that functions of GPURenderBundleEncoder generate a validation error if the encoder or the
    pass is already finished.
  `
  )
  .params(u =>
    u
      .combine('command', kRenderBundleEncoderCommands)
      .beginSubcases()
      .combine('finishBeforeCommand', [false, true])
  )
  .fn(t => {
    const { command, finishBeforeCommand } = t.params;

    const buffer = t.device.createBuffer({
      size: 12,
      usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.VERTEX,
    });

    const pipeline = t.createRenderPipelineForTest();

    const bindGroup = t.createBindGroupForTest();

    const bundleEncoder = t.device.createRenderBundleEncoder({
      colorFormats: ['rgba8unorm'],
    });

    if (finishBeforeCommand) {
      bundleEncoder.finish();
    }

    t.expectValidationError(() => {
      switch (command) {
        case 'draw':
          {
            bundleEncoder.draw(1);
          }
          break;
        case 'drawIndexed':
          {
            bundleEncoder.drawIndexed(1);
          }
          break;
        case 'drawIndexedIndirect':
          {
            bundleEncoder.drawIndexedIndirect(buffer, 0);
          }
          break;
        case 'drawIndirect':
          {
            bundleEncoder.drawIndirect(buffer, 1);
          }
          break;
        case 'setPipeline':
          {
            bundleEncoder.setPipeline(pipeline);
          }
          break;
        case 'setBindGroup':
          {
            bundleEncoder.setBindGroup(0, bindGroup);
          }
          break;
        case 'setIndexBuffer':
          {
            bundleEncoder.setIndexBuffer(buffer, 'uint32');
          }
          break;
        case 'setVertexBuffer':
          {
            bundleEncoder.setVertexBuffer(1, buffer);
          }
          break;
        case 'pushDebugGroup':
          {
            bundleEncoder.pushDebugGroup('group');
          }
          break;
        case 'popDebugGroup':
          {
            bundleEncoder.popDebugGroup();
          }
          break;
        case 'insertDebugMarker':
          {
            bundleEncoder.insertDebugMarker('marker');
          }
          break;
        default:
          unreachable();
      }
    }, finishBeforeCommand);
  });

g.test('compute_pass_commands')
  .desc(
    `
    Test that functions of GPUComputePassEncoder generate a validation error if the encoder or the
    pass is already finished.

    - TODO: Consider testing: nothing before command, end before command, end+finish before command.
  `
  )
  .params(u =>
    u
      .combine('command', kComputePassEncoderCommands)
      .beginSubcases()
      .combine('finishBeforeCommand', [false, true])
  )
  .fn(t => {
    const { command, finishBeforeCommand } = t.params;

    const encoder = t.device.createCommandEncoder();
    const computePass = encoder.beginComputePass();

    const indirectBuffer = t.device.createBuffer({
      size: 12,
      usage: GPUBufferUsage.INDIRECT,
    });

    const computePipeline = t.createNoOpComputePipeline();

    const bindGroup = t.createBindGroupForTest();

    if (finishBeforeCommand) {
      computePass.end();
      encoder.finish();
    }

    t.expectValidationError(() => {
      switch (command) {
        case 'setBindGroup':
          {
            computePass.setBindGroup(0, bindGroup);
          }
          break;
        case 'setPipeline':
          {
            computePass.setPipeline(computePipeline);
          }
          break;
        case 'dispatchWorkgroups':
          {
            computePass.dispatchWorkgroups(0);
          }
          break;
        case 'dispatchWorkgroupsIndirect':
          {
            computePass.dispatchWorkgroupsIndirect(indirectBuffer, 0);
          }
          break;
        case 'pushDebugGroup':
          {
            computePass.pushDebugGroup('group');
          }
          break;
        case 'popDebugGroup':
          {
            computePass.popDebugGroup();
          }
          break;
        case 'insertDebugMarker':
          {
            computePass.insertDebugMarker('marker');
          }
          break;
        default:
          unreachable();
      }
    }, finishBeforeCommand);
  });
