// 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.

// Common validation fixtures for unit tests

#ifndef TEST_VAL_VAL_FIXTURES_H_
#define TEST_VAL_VAL_FIXTURES_H_

#include <memory>
#include <string>

#include "source/val/validation_state.h"
#include "spirv-tools/libspirv.h"
#include "test/test_fixture.h"
#include "test/unit_spirv.h"

namespace spvtest {

template <typename T>
class ValidateBase : public ::testing::Test,
                     public ::testing::WithParamInterface<T> {
 public:
  ValidateBase();

  virtual void TearDown();

  // Returns the a spv_const_binary struct
  spv_const_binary get_const_binary();

  // Assembles the given SPIR-V text, checks that it fails to assemble,
  // and returns resulting diagnostic.  No internal state is updated.
  // Setting the desired_result to SPV_SUCCESS is used to allow all results
  std::string CompileFailure(std::string code,
                             spv_target_env env = SPV_ENV_UNIVERSAL_1_0,
                             spv_result_t desired_result = SPV_SUCCESS);

  // Checks that 'code' is valid SPIR-V text representation and stores the
  // binary version for further method calls.
  void CompileSuccessfully(std::string code,
                           spv_target_env env = SPV_ENV_UNIVERSAL_1_0);

  // Overwrites the word at index 'index' with the given word.
  // For testing purposes, it is often useful to be able to manipulate the
  // assembled binary before running the validator on it.
  // This function overwrites the word at the given index with a new word.
  void OverwriteAssembledBinary(uint32_t index, uint32_t word);

  // Performs validation on the SPIR-V code.
  spv_result_t ValidateInstructions(spv_target_env env = SPV_ENV_UNIVERSAL_1_0);

  // Performs validation. Returns the status and stores validation state into
  // the vstate_ member.
  spv_result_t ValidateAndRetrieveValidationState(
      spv_target_env env = SPV_ENV_UNIVERSAL_1_0);

  // Destroys the stored binary.
  void DestroyBinary() {
    spvBinaryDestroy(binary_);
    binary_ = nullptr;
  }

  // Destroys the stored diagnostic.
  void DestroyDiagnostic() {
    spvDiagnosticDestroy(diagnostic_);
    diagnostic_ = nullptr;
  }

  void SetAssembleOptions(uint32_t options) { assemble_options_ = options; }

  std::string getDiagnosticString();
  spv_position_t getErrorPosition();
  spv_validator_options getValidatorOptions();

  spv_binary binary_;
  spv_diagnostic diagnostic_;
  spv_validator_options options_;
  std::unique_ptr<spvtools::val::ValidationState_t> vstate_;
  uint32_t assemble_options_ = SPV_TEXT_TO_BINARY_OPTION_NONE;
};

template <typename T>
ValidateBase<T>::ValidateBase() : binary_(nullptr), diagnostic_(nullptr) {
  // Initialize to default command line options. Different tests can then
  // specialize specific options as necessary.
  options_ = spvValidatorOptionsCreate();
}

template <typename T>
spv_const_binary ValidateBase<T>::get_const_binary() {
  return spv_const_binary(binary_);
}

template <typename T>
void ValidateBase<T>::TearDown() {
  if (diagnostic_) {
    spvDiagnosticPrint(diagnostic_);
  }
  DestroyBinary();
  DestroyDiagnostic();
  spvValidatorOptionsDestroy(options_);
}

template <typename T>
std::string ValidateBase<T>::CompileFailure(std::string code,
                                            spv_target_env env,
                                            spv_result_t desired_result) {
  spv_diagnostic diagnostic = nullptr;
  spv_result_t actual_result =
      spvTextToBinary(ScopedContext(env).context, code.c_str(), code.size(),
                      &binary_, &diagnostic);
  EXPECT_NE(SPV_SUCCESS, actual_result);
  // optional check for exact result
  if (desired_result != SPV_SUCCESS) {
    EXPECT_EQ(actual_result, desired_result);
  }
  std::string result(diagnostic->error);
  spvDiagnosticDestroy(diagnostic);
  return result;
}

template <typename T>
void ValidateBase<T>::CompileSuccessfully(std::string code,
                                          spv_target_env env) {
  DestroyBinary();
  spv_diagnostic diagnostic = nullptr;
  ScopedContext context(env);
  auto status =
      spvTextToBinaryWithOptions(context.context, code.c_str(), code.size(),
                                 assemble_options_, &binary_, &diagnostic);
  EXPECT_EQ(SPV_SUCCESS, status)
      << "ERROR: " << diagnostic->error
      << "\nSPIR-V could not be compiled into binary:\n"
      << code;
  ASSERT_EQ(SPV_SUCCESS, status);
  spvDiagnosticDestroy(diagnostic);
}

template <typename T>
void ValidateBase<T>::OverwriteAssembledBinary(uint32_t index, uint32_t word) {
  ASSERT_TRUE(index < binary_->wordCount)
      << "OverwriteAssembledBinary: The given index is larger than the binary "
         "word count.";
  binary_->code[index] = word;
}

template <typename T>
spv_result_t ValidateBase<T>::ValidateInstructions(spv_target_env env) {
  DestroyDiagnostic();
  if (binary_ == nullptr) {
    fprintf(stderr,
            "ERROR: Attempting to validate a null binary, did you forget to "
            "call CompileSuccessfully?");
    fflush(stderr);
  }
  assert(binary_ != nullptr);
  return spvValidateWithOptions(ScopedContext(env).context, options_,
                                get_const_binary(), &diagnostic_);
}

template <typename T>
spv_result_t ValidateBase<T>::ValidateAndRetrieveValidationState(
    spv_target_env env) {
  DestroyDiagnostic();
  return spvtools::val::ValidateBinaryAndKeepValidationState(
      ScopedContext(env).context, options_, get_const_binary()->code,
      get_const_binary()->wordCount, &diagnostic_, &vstate_);
}

template <typename T>
std::string ValidateBase<T>::getDiagnosticString() {
  return diagnostic_ == nullptr ? std::string()
                                : std::string(diagnostic_->error);
}

template <typename T>
spv_validator_options ValidateBase<T>::getValidatorOptions() {
  return options_;
}

template <typename T>
spv_position_t ValidateBase<T>::getErrorPosition() {
  return diagnostic_ == nullptr ? spv_position_t() : diagnostic_->position;
}

}  // namespace spvtest

// For Vulkan testing.
// Allows test parameter test to list all possible VUIDs with a delimiter that
// is then split here to check if one VUID was in the error message
MATCHER_P(AnyVUID, vuid_set, "VUID from the set is in error message") {
  // use space as delimiter because clang-format will properly line break VUID
  // strings which is important the entire VUID is in a single line for script
  // to scan
  std::string delimiter = " ";
  std::string token;
  std::string vuids = std::string(vuid_set);
  size_t position;

  // Catch case were someone accidentally left spaces by trimming string
  // clang-format off
  vuids.erase(std::find_if(vuids.rbegin(), vuids.rend(), [](unsigned char c) {
    return (c != ' ');
  }).base(), vuids.end());
  vuids.erase(vuids.begin(), std::find_if(vuids.begin(), vuids.end(), [](unsigned char c) {
    return (c != ' ');
  }));
  // clang-format on

  do {
    position = vuids.find(delimiter);
    if (position != std::string::npos) {
      token = vuids.substr(0, position);
      vuids.erase(0, position + delimiter.length());
    } else {
      token = vuids.substr(0);  // last item
    }

    // arg contains diagnostic message
    if (arg.find(token) != std::string::npos) {
      return true;
    }
  } while (position != std::string::npos);
  return false;
}

#endif  // TEST_VAL_VAL_FIXTURES_H_
