/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// This is a cppunittest, rather than a gtest, in order to assert that no
// additional DLL needs to be linked in to use the function(s) tested herein.

#include <algorithm>
#include <iostream>
#include <optional>
#include <sstream>
#include <type_traits>

#include "mozmemory_stall.h"
#include "mozilla/Likely.h"

static bool TESTS_FAILED = false;

// Introduce iostream output operators for std::optional, for convenience's
// sake.
//
// (This is technically undefined behavior per [namespace.std], but it's
// unlikely to have any surprising effects when confined to this compilation
// unit.)
namespace std {
template <typename T>
std::ostream& operator<<(std::ostream& o, std::optional<T> const& s) {
  if (s) {
    return o << "std::optional{" << s.value() << "}";
  }
  return o << "std::nullopt";
}
std::ostream& operator<<(std::ostream& o, std::nullopt_t const& s) {
  return o << "std::nullopt";
}
}  // namespace std

// EXPECT_EQ
//
// Assert that two expressions are equal. Print them, and their values, on
// failure. (Based on the GTest macro of the same name.)
template <typename X, typename Y, size_t Xn, size_t Yn>
void AssertEqualImpl_(X&& x, Y&& y, const char* file, size_t line,
                      const char (&xStr)[Xn], const char (&yStr)[Yn],
                      const char* explanation = nullptr) {
  if (MOZ_LIKELY(x == y)) return;

  TESTS_FAILED = true;

  std::stringstream sstr;
  sstr << file << ':' << line << ": ";
  if (explanation) sstr << explanation << "\n\t";
  sstr << "expected " << xStr << " (" << x << ") == " << yStr << " (" << y
       << ")\n";
  std::cerr << sstr.str() << std::flush;
}

#define EXPECT_EQ(x, y)                                 \
  do {                                                  \
    AssertEqualImpl_(x, y, __FILE__, __LINE__, #x, #y); \
  } while (0)

// STATIC_ASSERT_VALUE_IS_OF_TYPE
//
// Assert that a value `v` is of type `t` (ignoring cv-qualification).
#define STATIC_ASSERT_VALUE_IS_OF_TYPE(v, t) \
  static_assert(std::is_same_v<std::remove_cv_t<decltype(v)>, t>)

// MockSleep
//
// Mock replacement for ::Sleep that merely logs its calls.
struct MockSleep {
  size_t calls = 0;
  size_t sum = 0;

  void operator()(size_t val) {
    ++calls;
    sum += val;
  }

  bool operator==(MockSleep const& that) const {
    return calls == that.calls && sum == that.sum;
  }
};
std::ostream& operator<<(std::ostream& o, MockSleep const& s) {
  return o << "MockSleep { count: " << s.calls << ", sum: " << s.sum << " }";
}

// MockAlloc
//
// Mock memory allocation mechanism. Eventually returns a value.
template <typename T>
struct MockAlloc {
  size_t count;
  T value;

  std::optional<T> operator()() {
    if (!count--) return value;
    return std::nullopt;
  }
};

int main() {
  using mozilla::StallSpecs;

  const StallSpecs stall = {.maxAttempts = 10, .delayMs = 50};

  // semantic test: stalls as requested but still yields a value,
  // up until it doesn't
  for (size_t i = 0; i < 20; ++i) {
    MockSleep sleep;
    auto const ret =
        stall.StallAndRetry(sleep, MockAlloc<int>{.count = i, .value = 5});
    STATIC_ASSERT_VALUE_IS_OF_TYPE(ret, std::optional<int>);

    if (i < 10) {
      EXPECT_EQ(ret, std::optional<int>(5));
    } else {
      EXPECT_EQ(ret, std::nullopt);
    }
    size_t const expectedCalls = std::min<size_t>(i + 1, 10);
    EXPECT_EQ(sleep,
              (MockSleep{.calls = expectedCalls, .sum = 50 * expectedCalls}));
  }

  // syntactic test: inline capturing lambda is accepted for aOperation
  {
    MockSleep sleep;
    std::optional<int> value{42};
    auto const ret = stall.StallAndRetry(sleep, [&]() { return value; });

    STATIC_ASSERT_VALUE_IS_OF_TYPE(ret, std::optional<int>);
    EXPECT_EQ(ret, std::optional(42));
    EXPECT_EQ(sleep, (MockSleep{.calls = 1, .sum = 50}));
  }

  // syntactic test: inline capturing lambda is accepted for aDelayFunc
  {
    MockSleep sleep;
    auto const ret =
        stall.StallAndRetry([&](size_t time) { sleep(time); },
                            MockAlloc<int>{.count = 0, .value = 105});

    STATIC_ASSERT_VALUE_IS_OF_TYPE(ret, std::optional<int>);
    EXPECT_EQ(ret, std::optional(105));
    EXPECT_EQ(sleep, (MockSleep{.calls = 1, .sum = 50}));
  }

  return TESTS_FAILED ? 1 : 0;
}
