export const description = `
Pipeline creation validation tests for immediate data size mismatches.

Validates that creating a pipeline fails if the shader uses immediate data
larger than the immediateSize specified in the pipeline layout, or larger than
maxImmediateSize if layout is 'auto'.
`;

import { makeTestGroup } from '../../../../common/framework/test_group.js';
import { getGPU } from '../../../../common/util/navigator_gpu.js';
import { assert, range, supportsImmediateData } from '../../../../common/util/util.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../gpu_test.js';
import * as vtu from '../validation_test_utils.js';

export const g = makeTestGroup(AllFeaturesMaxLimitsGPUTest);

/**
 * Generate shader code for a given stage with the specified immediate data size.
 * If size is 0, the shader has no immediate data.
 */
function makeShaderCode(size: number, stage: 'compute' | 'vertex' | 'fragment'): string {
  if (size === 0) {
    switch (stage) {
      case 'compute':
        return `@compute @workgroup_size(1) fn main_compute() {}`;
      case 'vertex':
        return `@vertex fn main_vertex() -> @builtin(position) vec4<f32> { return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
      case 'fragment':
        return `@fragment fn main_fragment() -> @location(0) vec4<f32> { return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
    }
  }
  const numFields = size / 4;
  const fields = range(numFields, i => `m${i}: u32`).join(', ');
  const structDecl = `struct Immediates { ${fields} }\nvar<immediate> data: Immediates;`;
  switch (stage) {
    case 'compute':
      return `${structDecl}\nfn use_data() { _ = data.m0; }\n@compute @workgroup_size(1) fn main_compute() { use_data(); }`;
    case 'vertex':
      return `${structDecl}\n@vertex fn main_vertex() -> @builtin(position) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 0.0, 0.0, 1.0); }`;
    case 'fragment':
      return `${structDecl}\n@fragment fn main_fragment() -> @location(0) vec4<f32> { _ = data.m0; return vec4<f32>(0.0, 1.0, 0.0, 1.0); }`;
  }
}

g.test('pipeline_creation_immediate_size_mismatch')
  .desc(
    `
    Validate that creating a compute or render pipeline fails if the shader uses
    immediate data larger than the immediateSize specified in the pipeline layout,
    or larger than maxImmediateSize if layout is 'auto'.
    Also validates that using less or equal size is allowed.

    For compute pipelines, stageASize is the compute stage size (stageBSize is unused).
    For render pipelines, stageASize is the vertex stage size and stageBSize is the
    fragment stage size.
    `
  )
  .params(u =>
    u
      .combine('pipelineType', ['compute', 'render'] as const)
      .combine('isAsync', [true, false])
      .beginSubcases()
      .expandWithParams(function* (p) {
        yield { stageASize: 16, stageBSize: 16, layoutSize: 16 }; // Equal
        yield { stageASize: 12, stageBSize: 12, layoutSize: 16 }; // Shader smaller
        yield { stageASize: 20, stageBSize: 20, layoutSize: 16 }; // Shader larger (small diff)
        yield { stageASize: 32, stageBSize: 32, layoutSize: 16 }; // Shader larger
        yield { stageASize: 'max', stageBSize: 0, layoutSize: 'auto' }; // StageA at limit
        yield { stageASize: 'exceedLimits', stageBSize: 0, layoutSize: 'auto' }; // StageA exceeds

        if (p.pipelineType === 'render') {
          yield { stageASize: 0, stageBSize: 'max', layoutSize: 'auto' }; // StageB at limit
          yield { stageASize: 'max', stageBSize: 'max', layoutSize: 'auto' }; // Both at limit
          yield { stageASize: 0, stageBSize: 'exceedLimits', layoutSize: 'auto' }; // StageB exceeds
        }
      })
  )
  .fn(t => {
    t.skipIf(!supportsImmediateData(getGPU(t.rec)), 'Immediate data not supported');

    const { pipelineType, isAsync, stageASize, stageBSize, layoutSize } = t.params;

    const maxImmediateSize = t.device.limits.maxImmediateSize;
    assert(maxImmediateSize !== undefined);

    const resolveSize = (sizeDescriptor: number | string): number => {
      if (typeof sizeDescriptor === 'number') return sizeDescriptor;
      if (sizeDescriptor === 'max') return maxImmediateSize;
      if (sizeDescriptor === 'exceedLimits') return maxImmediateSize + 4;
      return 0;
    };

    const resolvedStageASize = resolveSize(stageASize);
    const resolvedStageBSize = resolveSize(stageBSize);

    // Ensure the test's fixed sizes fit within the device limit.
    if (stageASize !== 'exceedLimits') {
      assert(
        resolvedStageASize <= maxImmediateSize,
        `stageASize (${resolvedStageASize}) must be <= maxImmediateSize (${maxImmediateSize})`
      );
    }
    if (stageBSize !== 'exceedLimits') {
      assert(
        resolvedStageBSize <= maxImmediateSize,
        `stageBSize (${resolvedStageBSize}) must be <= maxImmediateSize (${maxImmediateSize})`
      );
    }

    // Build pipeline layout.
    let layout: GPUPipelineLayout | 'auto';
    let validSize: number;

    if (layoutSize === 'auto') {
      layout = 'auto';
      validSize = maxImmediateSize;
    } else {
      layout = t.device.createPipelineLayout({
        bindGroupLayouts: [],
        immediateSize: layoutSize as number,
      });
      validSize = layoutSize as number;
    }

    const stageAExceedsLimit = resolvedStageASize > validSize;
    const stageBExceedsLimit = resolvedStageBSize > validSize;
    const shouldError = stageAExceedsLimit || stageBExceedsLimit;

    if (pipelineType === 'compute') {
      const code = makeShaderCode(resolvedStageASize, 'compute');

      vtu.doCreateComputePipelineTest(t, isAsync, !shouldError, {
        layout,
        compute: { module: t.device.createShaderModule({ code }) },
      });
    } else {
      const vertexCode = makeShaderCode(resolvedStageASize, 'vertex');
      const fragmentCode = makeShaderCode(resolvedStageBSize, 'fragment');

      vtu.doCreateRenderPipelineTest(t, isAsync, !shouldError, {
        layout,
        vertex: { module: t.device.createShaderModule({ code: vertexCode }) },
        fragment: {
          module: t.device.createShaderModule({ code: fragmentCode }),
          targets: [{ format: 'rgba8unorm' }],
        },
      });
    }
  });
