#include <c10/util/Optional.h>

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <array>
#include <cstdint>
#include <string>

namespace {

using testing::Eq;
using testing::Ge;
using testing::Gt;
using testing::Le;
using testing::Lt;
using testing::Ne;
using testing::Not;

template <typename T>
class OptionalTest : public ::testing::Test {
 public:
  using optional = c10::optional<T>;
};

template <typename T>
T getSampleValue();

template <>
bool getSampleValue() {
  return true;
}

template <>
uint64_t getSampleValue() {
  return 42;
}

template <>
c10::IntArrayRef getSampleValue() {
  return {};
}

template <>
std::string getSampleValue() {
  return "hello";
}

using OptionalTypes = ::testing::Types<
    // 32-bit scalar optimization.
    bool,
    // Trivially destructible but not 32-bit scalar.
    uint64_t,
    // ArrayRef optimization.
    c10::IntArrayRef,
    // Non-trivial destructor.
    std::string>;

// This assert is also in Optional.cpp; including here too to make it
// more likely that we'll remember to port this optimization over when
// we move to std::optional.
static_assert(
    sizeof(c10::optional<c10::IntArrayRef>) == sizeof(c10::IntArrayRef),
    "c10::optional<IntArrayRef> should be size-optimized");

TYPED_TEST_SUITE(OptionalTest, OptionalTypes);

TYPED_TEST(OptionalTest, Empty) {
  typename TestFixture::optional empty;

  EXPECT_FALSE((bool)empty);
  EXPECT_FALSE(empty.has_value());

  // NOLINTNEXTLINE(hicpp-avoid-goto,cppcoreguidelines-avoid-goto)
  EXPECT_THROW(empty.value(), c10::bad_optional_access);
}

TYPED_TEST(OptionalTest, Initialized) {
  using optional = typename TestFixture::optional;

  const auto val = getSampleValue<TypeParam>();
  optional opt((val));
  auto copy(opt), moveFrom1(opt), moveFrom2(opt);
  optional move(std::move(moveFrom1));
  optional copyAssign;
  copyAssign = opt;
  optional moveAssign;
  moveAssign = std::move(moveFrom2);

  std::array<typename TestFixture::optional*, 5> opts = {
      &opt, &copy, &copyAssign, &move, &moveAssign};
  for (auto* popt : opts) {
    auto& opt = *popt;
    EXPECT_TRUE((bool)opt);
    EXPECT_TRUE(opt.has_value());

    EXPECT_EQ(opt.value(), val);
    EXPECT_EQ(*opt, val);
  }
}

class SelfCompareTest : public testing::TestWithParam<c10::optional<int>> {};

TEST_P(SelfCompareTest, SelfCompare) {
  c10::optional<int> x = GetParam();
  EXPECT_THAT(x, Eq(x));
  EXPECT_THAT(x, Le(x));
  EXPECT_THAT(x, Ge(x));
  EXPECT_THAT(x, Not(Ne(x)));
  EXPECT_THAT(x, Not(Lt(x)));
  EXPECT_THAT(x, Not(Gt(x)));
}

INSTANTIATE_TEST_SUITE_P(
    nullopt,
    SelfCompareTest,
    testing::Values(c10::nullopt));
INSTANTIATE_TEST_SUITE_P(
    int,
    SelfCompareTest,
    testing::Values(c10::make_optional(2)));

TEST(OptionalTest, Nullopt) {
  c10::optional<int> x = 2;

  EXPECT_THAT(c10::nullopt, Not(Eq(x)));
  EXPECT_THAT(x, Not(Eq(c10::nullopt)));

  EXPECT_THAT(x, Ne(c10::nullopt));
  EXPECT_THAT(c10::nullopt, Ne(x));

  EXPECT_THAT(x, Not(Lt(c10::nullopt)));
  EXPECT_THAT(c10::nullopt, Lt(x));

  EXPECT_THAT(x, Not(Le(c10::nullopt)));
  EXPECT_THAT(c10::nullopt, Le(x));

  EXPECT_THAT(x, Gt(c10::nullopt));
  EXPECT_THAT(c10::nullopt, Not(Gt(x)));

  EXPECT_THAT(x, Ge(c10::nullopt));
  EXPECT_THAT(c10::nullopt, Not(Ge(x)));
}

// Ensure comparisons work...
using CmpTestTypes = testing::Types<
    // between two optionals
    std::pair<c10::optional<int>, c10::optional<int>>,

    // between an optional and a value
    std::pair<c10::optional<int>, int>,
    // between a value and an optional
    std::pair<int, c10::optional<int>>,

    // between an optional and a differently typed value
    std::pair<c10::optional<int>, long>,
    // between a differently typed value and an optional
    std::pair<long, c10::optional<int>>>;
template <typename T>
class CmpTest : public testing::Test {};
TYPED_TEST_SUITE(CmpTest, CmpTestTypes);

TYPED_TEST(CmpTest, Cmp) {
  TypeParam pair = {2, 3};
  auto x = pair.first;
  auto y = pair.second;

  EXPECT_THAT(x, Not(Eq(y)));

  EXPECT_THAT(x, Ne(y));

  EXPECT_THAT(x, Lt(y));
  EXPECT_THAT(y, Not(Lt(x)));

  EXPECT_THAT(x, Le(y));
  EXPECT_THAT(y, Not(Le(x)));

  EXPECT_THAT(x, Not(Gt(y)));
  EXPECT_THAT(y, Gt(x));

  EXPECT_THAT(x, Not(Ge(y)));
  EXPECT_THAT(y, Ge(x));
}

} // namespace
