// Copyright 2018 Google LLC
//
// 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.
//
////////////////////////////////////////////////////////////////////////////////

#ifndef TINK_UTIL_TEST_MATCHERS_H_
#define TINK_UTIL_TEST_MATCHERS_H_

#include <ostream>
#include <string>
#include <type_traits>

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "absl/status/status.h"
#include "absl/strings/str_cat.h"
#include "tink/util/secret_data.h"
#include "tink/util/status.h"
#include "tink/util/statusor.h"

namespace crypto {
namespace tink {
namespace test {
namespace internal {

////////////////////////////////////////////////////////////
// Implementation of IsOkAndHolds().

// Monomorphic implementation of matcher IsOkAndHolds(m).  StatusOrType is a
// reference to StatusOr<T>.
template <typename StatusOrType>
class IsOkAndHoldsMatcherImpl
    : public ::testing::MatcherInterface<StatusOrType> {
 public:
  using value_type =
      typename std::remove_reference<StatusOrType>::type::value_type;

  template <typename InnerMatcher>
  explicit IsOkAndHoldsMatcherImpl(InnerMatcher&& inner_matcher)
      : inner_matcher_(::testing::SafeMatcherCast<const value_type&>(
            std::forward<InnerMatcher>(inner_matcher))) {}

  void DescribeTo(std::ostream* os) const override {
    *os << "is OK and has a value that ";
    inner_matcher_.DescribeTo(os);
  }

  void DescribeNegationTo(std::ostream* os) const override {
    *os << "isn't OK or has a value that ";
    inner_matcher_.DescribeNegationTo(os);
  }

  bool MatchAndExplain(
      StatusOrType actual_value,
      ::testing::MatchResultListener* result_listener) const override {
    if (!actual_value.ok()) {
      *result_listener << "which has status " << actual_value.status();
      return false;
    }

    ::testing::StringMatchResultListener inner_listener;
    const bool matches =
        inner_matcher_.MatchAndExplain(*actual_value, &inner_listener);
    const std::string inner_explanation = inner_listener.str();
    if (!inner_explanation.empty()) {
      *result_listener << "which contains value "
                       << ::testing::PrintToString(*actual_value) << ", "
                       << inner_explanation;
    }
    return matches;
  }

 private:
  const ::testing::Matcher<const value_type&> inner_matcher_;
};

// Implements IsOkAndHolds(m) as a polymorphic matcher.
template <typename InnerMatcher>
class IsOkAndHoldsMatcher {
 public:
  explicit IsOkAndHoldsMatcher(InnerMatcher inner_matcher)
      : inner_matcher_(std::move(inner_matcher)) {}

  // Converts this polymorphic matcher to a monomorphic matcher of the
  // given type.  StatusOrType can be either StatusOr<T> or a
  // reference to StatusOr<T>.
  template <typename StatusOrType>
  operator ::testing::Matcher<StatusOrType>() const {  // NOLINT
    return ::testing::Matcher<StatusOrType>(
        new IsOkAndHoldsMatcherImpl<const StatusOrType&>(inner_matcher_));
  }

 private:
  const InnerMatcher inner_matcher_;
};

template <typename T>
inline const absl::Status& GetStatus(const absl::StatusOr<T>& s) {
  return s.status();
}

inline const absl::Status& GetStatus(const absl::Status& s) { return s; }

}  // namespace internal

// Matches an absl::StatusOk() value.
// This is better than EXPECT_TRUE(status.ok())
// because the error message is a part of the failure message.
MATCHER(IsOk,
        absl::StrCat(negation ? "isn't" : "is", " a Status with an OK value")) {
  const absl::Status& status = internal::GetStatus(arg);
  if (status.ok()) {
    return true;
  }
  *result_listener << ::testing::PrintToString(status);
  return false;
}

// Returns a gMock matcher that matches a StatusOr<> whose status is
// OK and whose value matches the inner matcher.
template <typename InnerMatcher>
internal::IsOkAndHoldsMatcher<typename std::decay<InnerMatcher>::type>
IsOkAndHolds(InnerMatcher&& inner_matcher) {
  return internal::IsOkAndHoldsMatcher<typename std::decay<InnerMatcher>::type>(
      std::forward<InnerMatcher>(inner_matcher));
}

// Matches Status, StatusOr<>, or a reference to either of them, with the
// specified `code` as code().
MATCHER_P(StatusIs, code,
          "is a Status with a " + absl::StatusCodeToString(code) + " code") {
  const absl::Status& status = internal::GetStatus(arg);
  if (status.code() == code) {
    return true;
  }
  *result_listener << ::testing::PrintToString(status);
  return false;
}

// Matches Status, StatusOr<>, or a reference to either of them, whose code()
// equals `code`, and whose message() matches `message_macher`.
MATCHER_P2(StatusIs, code, message_matcher, "") {
  const absl::Status& status = internal::GetStatus(arg);
  return (status.code() == code) &&
         testing::Matches(message_matcher)(std::string(status.message()));
}

// Matches a Keyset::Key with `key`.
MATCHER_P(EqualsKey, key, "is equals to the expected key") {
  if (arg.key_id() == key.key_id() && arg.status() == key.status() &&
      arg.output_prefix_type() == key.output_prefix_type() &&
      arg.key_data().type_url() == key.key_data().type_url() &&
      arg.key_data().key_material_type() ==
          key.key_data().key_material_type() &&
      arg.key_data().value() == key.key_data().value()) {
    return true;
  }
  *result_listener << "Expected: " << arg.key_id() << ", "
                   << arg.output_prefix_type() << arg.key_data().type_url()
                   << ", " << arg.key_data().key_material_type() << ", "
                   << arg.key_data().value();
  *result_listener << "\nActual: " << key.key_id() << ", "
                   << key.output_prefix_type() << key.key_data().type_url()
                   << ", " << key.key_data().key_material_type() << ", "
                   << key.key_data().value();
  return false;
}

MATCHER_P(EqualsSecretData, data, "is equal to the expected secret data") {
  if (::crypto::tink::util::SecretDataEquals(arg, data)) {
    return true;
  }
  *result_listener << "Expected: "
                   << ::crypto::tink::util::SecretDataAsStringView(arg);
  *result_listener << "\nActual: "
                   << ::crypto::tink::util::SecretDataAsStringView(data);
  return false;
}

}  // namespace test
}  // namespace tink
}  // namespace crypto

#endif  // TINK_UTIL_TEST_MATCHERS_H_
