// Copyright (c) 2015-2016 The Khronos Group Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Assembler tests for instructions in the "Type-Declaration" section of the
// SPIR-V spec.

#include <string>
#include <vector>

#include "gmock/gmock.h"
#include "test/test_fixture.h"
#include "test/unit_spirv.h"

namespace spvtools {
namespace {

using spvtest::EnumCase;
using spvtest::MakeInstruction;
using ::testing::Eq;

// Test Dim enums via OpTypeImage

using DimTest =
    spvtest::TextToBinaryTestBase<::testing::TestWithParam<EnumCase<spv::Dim>>>;

TEST_P(DimTest, AnyDim) {
  const std::string input =
      "%1 = OpTypeImage %2 " + GetParam().name() + " 2 3 0 4 Rgba8\n";
  EXPECT_THAT(CompiledInstructions(input),
              Eq(MakeInstruction(spv::Op::OpTypeImage,
                                 {1, 2, (uint32_t)GetParam().value(), 2, 3, 0,
                                  4, (uint32_t)spv::ImageFormat::Rgba8})));

  // Check the disassembler as well.
  EXPECT_THAT(EncodeAndDecodeSuccessfully(input), Eq(input));
}

// clang-format off
#define CASE(NAME) {spv::Dim::NAME, #NAME}
#define CASE1(DIM, NAME) {spv::Dim::DIM, #NAME}
INSTANTIATE_TEST_SUITE_P(
    TextToBinaryDim, DimTest,
    ::testing::ValuesIn(std::vector<EnumCase<spv::Dim>>{
        CASE1(Dim1D, 1D),
        CASE1(Dim2D, 2D),
        CASE1(Dim3D, 3D),
        CASE(Cube),
        CASE(Rect),
        CASE(Buffer),
        CASE(SubpassData),
        CASE(TileImageDataEXT),
    }));
#undef CASE
// clang-format on

TEST_F(DimTest, WrongDim) {
  EXPECT_THAT(CompileFailure("%i = OpTypeImage %t xxyyzz 1 2 3 4 R8"),
              Eq("Invalid dimensionality 'xxyyzz'."));
}

// Test ImageFormat enums via OpTypeImage

using ImageFormatTest = spvtest::TextToBinaryTestBase<
    ::testing::TestWithParam<EnumCase<spv::ImageFormat>>>;

TEST_P(ImageFormatTest, AnyImageFormatAndNoAccessQualifier) {
  const std::string input =
      "%1 = OpTypeImage %2 1D 2 3 0 4 " + GetParam().name() + "\n";
  EXPECT_THAT(CompiledInstructions(input),
              Eq(MakeInstruction(spv::Op::OpTypeImage,
                                 {1, 2, (uint32_t)spv::Dim::Dim1D, 2, 3, 0, 4,
                                  GetParam().value()})));
  // Check the disassembler as well.
  EXPECT_THAT(EncodeAndDecodeSuccessfully(input), Eq(input));
}

// clang-format off
#define CASE(NAME) {spv::ImageFormat::NAME, #NAME}
INSTANTIATE_TEST_SUITE_P(
    TextToBinaryImageFormat, ImageFormatTest,
    ::testing::ValuesIn(std::vector<EnumCase<spv::ImageFormat>>{
        CASE(Unknown),
        CASE(Rgba32f),
        CASE(Rgba16f),
        CASE(R32f),
        CASE(Rgba8),
        CASE(Rgba8Snorm),
        CASE(Rg32f),
        CASE(Rg16f),
        CASE(R11fG11fB10f),
        CASE(R16f),
        CASE(Rgba16),
        CASE(Rgb10A2),
        CASE(Rg16),
        CASE(Rg8),
        CASE(R16),
        CASE(R8),
        CASE(Rgba16Snorm),
        CASE(Rg16Snorm),
        CASE(Rg8Snorm),
        CASE(R16Snorm),
        CASE(R8Snorm),
        CASE(Rgba32i),
        CASE(Rgba16i),
        CASE(Rgba8i),
        CASE(R32i),
        CASE(Rg32i),
        CASE(Rg16i),
        CASE(Rg8i),
        CASE(R16i),
        CASE(R8i),
        CASE(Rgba32ui),
        CASE(Rgba16ui),
        CASE(Rgba8ui),
        CASE(R32ui),
        CASE(Rgb10a2ui),
        CASE(Rg32ui),
        CASE(Rg16ui),
        CASE(Rg8ui),
        CASE(R16ui),
        CASE(R8ui),
    }));
#undef CASE
// clang-format on

TEST_F(ImageFormatTest, WrongFormat) {
  EXPECT_THAT(CompileFailure("%r = OpTypeImage %t 1D  2 3 0 4 xxyyzz"),
              Eq("Invalid image format 'xxyyzz'."));
}

// Test AccessQualifier enums via OpTypeImage.
using ImageAccessQualifierTest = spvtest::TextToBinaryTestBase<
    ::testing::TestWithParam<EnumCase<spv::AccessQualifier>>>;

TEST_P(ImageAccessQualifierTest, AnyAccessQualifier) {
  const std::string input =
      "%1 = OpTypeImage %2 1D 2 3 0 4 Rgba8 " + GetParam().name() + "\n";
  EXPECT_THAT(CompiledInstructions(input),
              Eq(MakeInstruction(
                  spv::Op::OpTypeImage,
                  {1, 2, (uint32_t)spv::Dim::Dim1D, 2, 3, 0, 4,
                   (uint32_t)spv::ImageFormat::Rgba8, GetParam().value()})));
  // Check the disassembler as well.
  EXPECT_THAT(EncodeAndDecodeSuccessfully(input), Eq(input));
}

// clang-format off
#define CASE(NAME) {spv::AccessQualifier::NAME, #NAME}
INSTANTIATE_TEST_SUITE_P(
    AccessQualifier, ImageAccessQualifierTest,
    ::testing::ValuesIn(std::vector<EnumCase<spv::AccessQualifier>>{
      CASE(ReadOnly),
      CASE(WriteOnly),
      CASE(ReadWrite),
    }));
// clang-format on
#undef CASE

// Test AccessQualifier enums via OpTypePipe.

using OpTypePipeTest = spvtest::TextToBinaryTestBase<
    ::testing::TestWithParam<EnumCase<spv::AccessQualifier>>>;

TEST_P(OpTypePipeTest, AnyAccessQualifier) {
  const std::string input = "%1 = OpTypePipe " + GetParam().name() + "\n";
  EXPECT_THAT(
      CompiledInstructions(input),
      Eq(MakeInstruction(spv::Op::OpTypePipe, {1, GetParam().value()})));
  // Check the disassembler as well.
  EXPECT_THAT(EncodeAndDecodeSuccessfully(input), Eq(input));
}

// clang-format off
#define CASE(NAME) {spv::AccessQualifier::NAME, #NAME}
INSTANTIATE_TEST_SUITE_P(
    TextToBinaryTypePipe, OpTypePipeTest,
    ::testing::ValuesIn(std::vector<EnumCase<spv::AccessQualifier>>{
                            CASE(ReadOnly),
                            CASE(WriteOnly),
                            CASE(ReadWrite),
    }));
#undef CASE
// clang-format on

TEST_F(OpTypePipeTest, WrongAccessQualifier) {
  EXPECT_THAT(CompileFailure("%1 = OpTypePipe xxyyzz"),
              Eq("Invalid access qualifier 'xxyyzz'."));
}

using OpTypeForwardPointerTest = spvtest::TextToBinaryTest;

#define CASE(storage_class)                                                    \
  do {                                                                         \
    EXPECT_THAT(                                                               \
        CompiledInstructions("OpTypeForwardPointer %pt " #storage_class),      \
        Eq(MakeInstruction(spv::Op::OpTypeForwardPointer,                      \
                           {1, (uint32_t)spv::StorageClass::storage_class}))); \
  } while (0)

TEST_F(OpTypeForwardPointerTest, ValidStorageClass) {
  CASE(UniformConstant);
  CASE(Input);
  CASE(Uniform);
  CASE(Output);
  CASE(Workgroup);
  CASE(CrossWorkgroup);
  CASE(Private);
  CASE(Function);
  CASE(Generic);
  CASE(PushConstant);
  CASE(AtomicCounter);
  CASE(Image);
  CASE(StorageBuffer);
  CASE(TileImageEXT);
}

#undef CASE

TEST_F(OpTypeForwardPointerTest, MissingType) {
  EXPECT_THAT(CompileFailure("OpTypeForwardPointer"),
              Eq("Expected operand for OpTypeForwardPointer instruction, but "
                 "found the end of the stream."));
}

TEST_F(OpTypeForwardPointerTest, MissingClass) {
  EXPECT_THAT(CompileFailure("OpTypeForwardPointer %pt"),
              Eq("Expected operand for OpTypeForwardPointer instruction, but "
                 "found the end of the stream."));
}

TEST_F(OpTypeForwardPointerTest, WrongClass) {
  EXPECT_THAT(CompileFailure("OpTypeForwardPointer %pt xxyyzz"),
              Eq("Invalid storage class 'xxyyzz'."));
}

using OpSizeOfTest = spvtest::TextToBinaryTest;

// We should be able to assemble it.  Validation checks are in another test
// file.
TEST_F(OpSizeOfTest, OpcodeAssemblesInV10) {
  EXPECT_THAT(
      CompiledInstructions("%1 = OpSizeOf %2 %3", SPV_ENV_UNIVERSAL_1_0),
      Eq(MakeInstruction(spv::Op::OpSizeOf, {1, 2, 3})));
}

TEST_F(OpSizeOfTest, ArgumentCount) {
  EXPECT_THAT(
      CompileFailure("OpSizeOf", SPV_ENV_UNIVERSAL_1_1),
      Eq("Expected <result-id> at the beginning of an instruction, found "
         "'OpSizeOf'."));
  EXPECT_THAT(CompileFailure("%res = OpSizeOf OpNop", SPV_ENV_UNIVERSAL_1_1),
              Eq("Expected operand for OpSizeOf instruction, but found the "
                 "next instruction instead."));
  EXPECT_THAT(
      CompiledInstructions("%1 = OpSizeOf %2 %3", SPV_ENV_UNIVERSAL_1_1),
      Eq(MakeInstruction(spv::Op::OpSizeOf, {1, 2, 3})));
  EXPECT_THAT(
      CompileFailure("%1 = OpSizeOf %2 %3 44 55 ", SPV_ENV_UNIVERSAL_1_1),
      Eq("Expected <opcode> or <result-id> at the beginning of an instruction, "
         "found '44'."));
}

TEST_F(OpSizeOfTest, ArgumentTypes) {
  EXPECT_THAT(CompileFailure("%1 = OpSizeOf 2 %3", SPV_ENV_UNIVERSAL_1_1),
              Eq("Expected id to start with %."));
  EXPECT_THAT(CompileFailure("%1 = OpSizeOf %2 \"abc\"", SPV_ENV_UNIVERSAL_1_1),
              Eq("Expected id to start with %."));
}

struct FloatEncodingWidthCase {
  std::string input;
  bool expect_pass;
};

using FloatEncodingWidthTest = spvtest::TextToBinaryTestBase<
    ::testing::TestWithParam<FloatEncodingWidthCase>>;

TEST_P(FloatEncodingWidthTest, Samples) {
  const auto& param = GetParam();
  if (param.expect_pass) {
    CompileSuccessfully(param.input);
  } else {
    auto err = CompileFailure(param.input);
    EXPECT_THAT(err, testing::HasSubstr("Invalid bit width"));
    EXPECT_THAT(err, testing::HasSubstr("for floating point encoding"));
  }
}

INSTANTIATE_TEST_SUITE_P(
    TextToBinaryFloatWidth, FloatEncodingWidthTest,
    ::testing::ValuesIn(std::vector<FloatEncodingWidthCase>{
        {"%1 = OpTypeFloat 32", true},
        {"%1 = OpTypeFloat 64", true},
        {"%1 = OpTypeFloat 16", true},
        {"%1 = OpTypeFloat 8", true},
        // bfloat16
        {"%1 = OpTypeFloat 0 BFloat16KHR", false},
        {"%1 = OpTypeFloat 1 BFloat16KHR", false},
        {"%1 = OpTypeFloat 15 BFloat16KHR", false},
        {"%1 = OpTypeFloat 16 BFloat16KHR", true},
        {"%1 = OpTypeFloat 17 BFloat16KHR", false},
        {"%1 = OpTypeFloat 32 BFloat16KHR", false},
        {"%1 = OpTypeFloat 64 BFloat16KHR", false},
        {"%1 = OpTypeFloat 100 BFloat16KHR", false},
        // fp8 E5M2
        {"%1 = OpTypeFloat 0 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 1 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 7 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 8 Float8E5M2EXT", true},
        {"%1 = OpTypeFloat 9 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 16 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 32 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 64 Float8E5M2EXT", false},
        {"%1 = OpTypeFloat 100 Float8E4M3EXT", false},
        // fp8 E4M3
        {"%1 = OpTypeFloat 0 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 1 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 7 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 8 Float8E4M3EXT", true},
        {"%1 = OpTypeFloat 9 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 16 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 32 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 64 Float8E4M3EXT", false},
        {"%1 = OpTypeFloat 100 Float8E4M3EXT", false},
    }));

// TODO(dneto): OpTypeVoid
// TODO(dneto): OpTypeBool
// TODO(dneto): OpTypeInt
// TODO(dneto): OpTypeFloat
// TODO(dneto): OpTypeVector
// TODO(dneto): OpTypeMatrix
// TODO(dneto): OpTypeImage
// TODO(dneto): OpTypeSampler
// TODO(dneto): OpTypeSampledImage
// TODO(dneto): OpTypeArray
// TODO(dneto): OpTypeRuntimeArray
// TODO(dneto): OpTypeStruct
// TODO(dneto): OpTypeOpaque
// TODO(dneto): OpTypePointer
// TODO(dneto): OpTypeFunction
// TODO(dneto): OpTypeEvent
// TODO(dneto): OpTypeDeviceEvent
// TODO(dneto): OpTypeReserveId
// TODO(dneto): OpTypeQueue

}  // namespace
}  // namespace spvtools
