export const description = `
setImmediates validation tests.
TODO(#4297): enable Float16Array
`;

import { makeTestGroup } from '../../../../../common/framework/test_group.js';
import { getGPU } from '../../../../../common/util/navigator_gpu.js';
import {
  kTypedArrayBufferViews,
  kTypedArrayBufferViewKeys,
  supportsImmediateData,
} from '../../../../../common/util/util.js';
import { AllFeaturesMaxLimitsGPUTest } from '../../../../gpu_test.js';
import { kProgrammableEncoderTypes } from '../../../../util/command_buffer_maker.js';

class SetImmediatesTest extends AllFeaturesMaxLimitsGPUTest {
  override async init() {
    await super.init();
    if (!supportsImmediateData(getGPU(this.rec))) {
      this.skip('setImmediates not supported');
    }
  }
}

export const g = makeTestGroup(SetImmediatesTest);

g.test('alignment')
  .desc('Tests that rangeOffset and contentSize must align to 4 bytes.')
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .combine('arrayType', kTypedArrayBufferViewKeys)
      .filter(p => p.arrayType !== 'Float16Array')
      .combineWithParams([
        // control case: rangeOffset 4 is aligned. contentByteSize 8 is aligned.
        { rangeOffset: 4, contentByteSize: 8 },
        // rangeOffset 6 is unaligned (6 % 4 !== 0).
        { rangeOffset: 6, contentByteSize: 8 },
        // contentByteSize 10 is unaligned (10 % 4 !== 0).
        // Note: This case will be skipped for types with element size > 2 (e.g. Uint32, Uint64)
        // because they cannot form a 10-byte array.
        { rangeOffset: 4, contentByteSize: 10 },
      ])
      .filter(({ arrayType, contentByteSize }) => {
        // Skip if the contentByteSize is not a multiple of the element size.
        // For example, we can't have 10 bytes if the element size is 4 or 8 bytes.
        const arrayConstructor = kTypedArrayBufferViews[arrayType];
        return contentByteSize % arrayConstructor.BYTES_PER_ELEMENT === 0;
      })
  )
  .fn(t => {
    const { encoderType, arrayType, rangeOffset, contentByteSize } = t.params;
    const arrayBufferType = kTypedArrayBufferViews[arrayType];
    const elementSize = arrayBufferType.BYTES_PER_ELEMENT;
    const elementCount = contentByteSize / elementSize;

    const isRangeOffsetAligned = rangeOffset % 4 === 0;
    const isContentSizeAligned = contentByteSize % 4 === 0;

    const { encoder, validateFinish } = t.createEncoder(encoderType);
    const data = new arrayBufferType(elementCount);

    t.shouldThrow(isContentSizeAligned ? false : 'OperationError', () => {
      encoder.setImmediates(rangeOffset, data, 0, elementCount);
    });

    validateFinish(isRangeOffsetAligned);
  });

g.test('overflow')
  .desc(
    `
    Tests that rangeOffset + contentSize or dataOffset + size is handled correctly if it exceeds limits.
    `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .combine('arrayType', kTypedArrayBufferViewKeys)
      .filter(p => p.arrayType !== 'Float16Array')
      .combineWithParams([
        // control case
        { rangeOffset: 0, dataOffset: 0, elementCount: 4, _expectedError: null },
        // elementCount 0
        { rangeOffset: 0, dataOffset: 0, elementCount: 0, _expectedError: null },
        // rangeOffset + contentSize overflows
        {
          rangeOffset: 2 ** 31 - 8,
          dataOffset: 0,
          elementCount: 4,
          _expectedError: 'validation',
        },
        {
          rangeOffset: 2 ** 32 - 8,
          dataOffset: 0,
          elementCount: 4,
          _expectedError: 'validation',
        },
        // dataOffset + size overflows
        {
          rangeOffset: 0,
          dataOffset: 2 ** 31 - 1,
          elementCount: 4,
          _expectedError: 'OperationError',
        },
        {
          rangeOffset: 0,
          dataOffset: 2 ** 32 - 1,
          elementCount: 4,
          _expectedError: 'OperationError',
        },
      ])
  )
  .fn(t => {
    const { encoderType, arrayType, rangeOffset, dataOffset, elementCount, _expectedError } =
      t.params;
    const arrayBufferType = kTypedArrayBufferViews[arrayType];

    const { encoder, validateFinish } = t.createEncoder(encoderType);
    const data = new arrayBufferType(elementCount);

    const doSetImmediates = () => {
      encoder.setImmediates(rangeOffset, data, dataOffset, elementCount);
    };

    if (_expectedError === 'OperationError') {
      t.shouldThrow('OperationError', doSetImmediates);
    } else {
      doSetImmediates();
      validateFinish(_expectedError === null);
    }
  });

g.test('out_of_bounds')
  .desc(
    `
    Tests that rangeOffset + contentSize is greater than maxImmediateSize (Validation Error)
    and contentSize is larger than data size (OperationError).
    `
  )
  .params(u =>
    u //
      .combine('encoderType', kProgrammableEncoderTypes)
      .combine('arrayType', kTypedArrayBufferViewKeys)
      .filter(p => p.arrayType !== 'Float16Array')
      .combineWithParams([
        // control case
        { rangeOffsetDelta: 0, dataLengthDelta: 0 },
        // rangeOffset + contentSize > maxImmediateSize
        { rangeOffsetDelta: 4, dataLengthDelta: 0 },
        // dataOffset + size > data.length
        { rangeOffsetDelta: 0, dataLengthDelta: -1 },
      ])
  )
  .fn(t => {
    const { encoderType, arrayType, rangeOffsetDelta, dataLengthDelta } = t.params;
    const arrayBufferType = kTypedArrayBufferViews[arrayType];
    const elementSize = arrayBufferType.BYTES_PER_ELEMENT;

    const maxImmediateSize = t.device.limits.maxImmediateSize;
    if (maxImmediateSize === undefined) {
      t.skip('maxImmediateSize not found');
    }

    // We want contentByteSize to be aligned to 4 bytes to avoid alignment errors.
    // We use 8 bytes to cover all types including BigUint64 (8 bytes).
    const elementCount = elementSize >= 8 ? 1 : 8 / elementSize;
    const contentByteSize = elementCount * elementSize;

    const rangeOffset = maxImmediateSize - contentByteSize + rangeOffsetDelta;
    const dataLength = elementCount + dataLengthDelta;

    const data = new arrayBufferType(dataLength);

    const { encoder, validateFinish } = t.createEncoder(encoderType);

    const rangeOverLimit = rangeOffset + contentByteSize > maxImmediateSize;
    const dataOverLimit = elementCount > dataLength;

    t.shouldThrow(dataOverLimit ? 'OperationError' : false, () => {
      encoder.setImmediates(rangeOffset, data, 0, elementCount);
    });

    if (!dataOverLimit) {
      validateFinish(!rangeOverLimit);
    }
  });
