// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef UI_BASE_INTERACTION_INTERACTIVE_TEST_INTERNAL_H_
#define UI_BASE_INTERACTION_INTERACTIVE_TEST_INTERNAL_H_

#include <memory>
#include <tuple>
#include <type_traits>

#include "base/callback_list.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/strings/strcat.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/rectify_callback.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/interaction/interaction_test_util.h"

namespace ui::test {

class InteractiveTestApi;

namespace internal {

// Element that is present during interactive tests that actions can bounce
// events off of.
DECLARE_ELEMENT_IDENTIFIER_VALUE(kInteractiveTestPivotElementId);
DECLARE_CUSTOM_ELEMENT_EVENT_TYPE(kInteractiveTestPivotEventType);

// Class that implements functionality for InteractiveTest* that should be
// hidden from tests that inherit the API.
class InteractiveTestPrivate {
 public:
  using MultiStep = std::vector<InteractionSequence::StepBuilder>;

  // Describes what should happen when an action isn't compatible with the
  // current build, platform, or environment. For example, not all tests are set
  // up to handle screenshots, and some Linux window managers cannot bring a
  // background window to the front.
  //
  // See chrome/test/interaction/README.md for best practices.
  enum class OnIncompatibleAction {
    // The test should fail. This is the default, and should be used in almost
    // all cases.
    kFailTest,
    // The sequence should abort immediately and the test should be skipped.
    // Use this when the remainder of the test would depend on the result of the
    // incompatible step. Good for smoke/regression tests that have known
    // incompatibilities but still need to be run in as many environments as
    // possible.
    kSkipTest,
    // As `kSkipTest`, but instead of marking the test as skipped, just stops
    // the test sequence. This is useful when the test cannot continue past the
    // problematic step, but you also want to preserve any non-fatal errors that
    // may have occurred up to that point (or check any conditions after the
    // test stops).
    kHaltTest,
    // The failure should be ignored and the test should continue.
    // Use this when the step does not affect the outcome of the test, such as
    // taking an incidental screenshot in a test job that doesn't support
    // screenshots.
    kIgnoreAndContinue,
  };

  explicit InteractiveTestPrivate(
      std::unique_ptr<InteractionTestUtil> test_util);
  virtual ~InteractiveTestPrivate();
  InteractiveTestPrivate(const InteractiveTestPrivate&) = delete;
  void operator=(const InteractiveTestPrivate&) = delete;

  InteractionTestUtil& test_util() { return *test_util_; }

  OnIncompatibleAction on_incompatible_action() const {
    return on_incompatible_action_;
  }

  bool sequence_skipped() const { return sequence_skipped_; }

  // Possibly fails or skips a sequence based on the result of an action
  // simulation.
  void HandleActionResult(InteractionSequence* seq,
                          const TrackedElement* el,
                          const std::string& operation_name,
                          ActionResult result);

  // Gets the pivot element for the specified context, which must exist.
  TrackedElement* GetPivotElement(ElementContext context) const;

  // Call this method during test SetUp(), or SetUpOnMainThread() for browser
  // tests.
  virtual void DoTestSetUp();

  // Call this method during test TearDown(), or TearDownOnMainThread() for
  // browser tests.
  virtual void DoTestTearDown();

  // Called when the sequence ends, but before we break out of the run loop
  // in RunTestSequenceImpl().
  virtual void OnSequenceComplete();
  virtual void OnSequenceAborted(const InteractionSequence::AbortedData& data);

  // Sets a callback that is called if the test sequence fails instead of
  // failing the current test. Should only be called in tests that are testing
  // InteractiveTestApi or descendant classes.
  void set_aborted_callback_for_testing(
      InteractionSequence::AbortedCallback aborted_callback_for_testing) {
    aborted_callback_for_testing_ = std::move(aborted_callback_for_testing);
  }

  // Places a callback in the message queue to bounce an event off of the pivot
  // element, then responds by executing `task`.
  template <typename T>
  static MultiStep PostTask(const base::StringPiece& description, T&& task);

 private:
  friend class ui::test::InteractiveTestApi;

  // Prepare for a sequence to start.
  void Init(ElementContext initial_context);

  // Clean up after a sequence.
  void Cleanup();

  // Note when a new element appears; we may update the context list.
  void OnElementAdded(TrackedElement* el);

  // Maybe adds a pivot element for the given context.
  void MaybeAddPivotElement(ElementContext context);

  // Tracks whether a sequence succeeded or failed.
  bool success_ = false;

  // Specifies how an incompatible action should be handled.
  OnIncompatibleAction on_incompatible_action_ =
      OnIncompatibleAction::kFailTest;
  std::string on_incompatible_action_reason_;

  // Tracks whether a sequence is skipped. Will only be set if
  // `skip_on_unsupported_operation` is true.
  bool sequence_skipped_ = false;

  // Used to simulate input to UI elements.
  std::unique_ptr<InteractionTestUtil> test_util_;

  // Used to keep track of valid contexts.
  base::CallbackListSubscription context_subscription_;

  // Used to relay events to trigger follow-up steps.
  std::map<ElementContext, std::unique_ptr<TrackedElement>> pivot_elements_;

  // Overrides the default test failure behavior to test the API itself.
  InteractionSequence::AbortedCallback aborted_callback_for_testing_;
};

// Specifies an element either by ID or by name.
using ElementSpecifier = absl::variant<ElementIdentifier, base::StringPiece>;

// Applies `matcher` to `value` and returns the result; on failure a useful
// error message is printed using `test_name`, `value`, and `matcher`.
//
// Steps which use this method will fail if it returns false, printing out the
// details of the step in the usual way.
template <typename T>
bool MatchAndExplain(const base::StringPiece& test_name,
                     testing::Matcher<T>& matcher,
                     T&& value) {
  if (matcher.Matches(value))
    return true;
  std::ostringstream oss;
  oss << test_name << " failed.\nExpected: ";
  matcher.DescribeTo(&oss);
  oss << "\nActual: " << testing::PrintToString(value);
  LOG(ERROR) << oss.str();
  return false;
}

// static
template <typename T>
InteractiveTestPrivate::MultiStep InteractiveTestPrivate::PostTask(
    const base::StringPiece& description,
    T&& task) {
  MultiStep result;
  result.emplace_back(std::move(
      InteractionSequence::StepBuilder()
          .SetDescription(base::StrCat({description, ": PostTask()"}))
          .SetElementID(kInteractiveTestPivotElementId)
          .SetStartCallback(base::BindOnce([](ui::TrackedElement* el) {
            base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
                FROM_HERE,
                base::BindOnce(
                    [](ElementIdentifier id, ElementContext context) {
                      auto* const el =
                          ui::ElementTracker::GetElementTracker()
                              ->GetFirstMatchingElement(id, context);
                      if (el) {
                        ui::ElementTracker::GetFrameworkDelegate()
                            ->NotifyCustomEvent(el,
                                                kInteractiveTestPivotEventType);
                      }
                      // If there is no pivot element, the test sequence has
                      // been aborted and there's no need to send an additional
                      // error.
                    },
                    el->identifier(), el->context()));
          }))));
  result.emplace_back(std::move(
      InteractionSequence::StepBuilder()
          .SetDescription(base::StrCat({description, ": WaitForComplete()"}))
          .SetElementID(kInteractiveTestPivotElementId)
          .SetContext(InteractionSequence::ContextMode::kFromPreviousStep)
          .SetType(InteractionSequence::StepType::kCustomEvent,
                   kInteractiveTestPivotEventType)
          .SetStartCallback(
              base::RectifyCallback<InteractionSequence::StepStartCallback>(
                  std::move(task)))));
  return result;
}

template <typename T>
constexpr bool IsCallbackValue = base::IsBaseCallback<T>::value;

template <typename T, typename SFINAE = void>
struct IsCallable {
  static constexpr bool value = false;
};

template <typename T>
struct IsCallable<T, std::void_t<decltype(&T::operator())>> {
  static constexpr bool value = true;
};

template <typename T>
constexpr bool IsCallableValue = IsCallable<std::remove_reference_t<T>>::value;

template <typename T, typename SFINAE = void>
struct IsFunctionPointer {
  static constexpr bool value = false;
};

template <typename R, typename... Args>
struct IsFunctionPointer<R (*)(Args...), void> {
  static constexpr bool value = true;
};

template <typename T>
constexpr bool IsFunctionPointerValue = IsFunctionPointer<T>::value;

// Uses SFINAE to choose the correct implementation for `MaybeBind`.
template <typename F, typename SFINAE = void>
struct MaybeBindHelper;

// Callbacks are already callbacks, so can be returned as-is.
template <typename F>
struct MaybeBindHelper<F, std::enable_if_t<IsCallbackValue<F>>> {
  template <class G>
  static auto MaybeBind(G&& function) {
    return std::forward<G>(function);
  }
};

// Callable objects with state can only be bound with
// base::BindLambdaForTesting.
template <typename F>
struct MaybeBindHelper<
    F,
    std::enable_if_t<IsCallableValue<F> && !std::is_empty_v<F>>> {
  template <class G>
  static auto MaybeBind(G&& function) {
    return base::BindLambdaForTesting(std::forward<G>(function));
  }
};

// Function pointers and empty callable objects can be bound using
// base::BindOnce.
template <typename F>
struct MaybeBindHelper<
    F,
    std::enable_if_t<(IsCallableValue<F> && std::is_empty_v<F>) ||
                     IsFunctionPointerValue<F>>> {
  template <class G>
  static auto MaybeBind(G&& function) {
    return base::BindOnce(std::forward<G>(function));
  }
};

// base::DoNothing() is compatible with callbacks, so return it as-is.
template <>
struct MaybeBindHelper<decltype(base::DoNothing()), void> {
  static auto MaybeBind(decltype(base::DoNothing()) function) {
    return function;
  }
};

// Optionally converts `function` to something that is compatible with a
// base::OnceCallback.
template <typename F>
auto MaybeBind(F&& function) {
  return MaybeBindHelper<F>::MaybeBind(std::forward<F>(function));
}

// Helper struct that captures information about what signature a function-like
// object would have if it were bound.
template <typename F>
struct MaybeBindTypeHelper {
  using CallbackType = std::invoke_result_t<decltype(&MaybeBind<F>), F>;
  using ReturnType = typename CallbackType::ResultType;
  using Signature = typename CallbackType::RunType;
};

// DoNothing always has a void return type but no defined signature.
template <>
struct MaybeBindTypeHelper<decltype(base::DoNothing())> {
  using ReturnType = void;
};

template <typename T>
struct ArgsExtractor;

template <typename R, typename... Args>
struct ArgsExtractor<R(Args...)> {
  using holder = std::tuple<Args...>;
};

template <typename F>
using ReturnTypeOf = typename MaybeBindTypeHelper<F>::ReturnType;

template <size_t N, typename F>
using NthArgumentOf = std::tuple_element_t<
    N,
    typename ArgsExtractor<typename MaybeBindTypeHelper<F>::Signature>::holder>;

// Implementation for HasSignature that uses SFINAE to check whether the
// signature of a callable object `F` matches signature `S`.
template <typename F, typename S>
struct HasSignatureHelper {
  static constexpr bool value =
      std::is_same_v<typename MaybeBindTypeHelper<F>::Signature, S>;
};

// DoNothing() can match any signature that returns void.
template <typename... Args>
struct HasSignatureHelper<decltype(base::DoNothing()), void(Args...)> {
  static constexpr bool value = true;
};

template <typename F, typename S>
constexpr bool HasSignature = HasSignatureHelper<F, S>::value;

// Requires that `F` resolves to some kind of callable object with call
// signature `S`; causes a compile failure on mismatch.
template <typename F, typename S>
using RequireSignature = std::enable_if_t<HasSignature<F, S>>;

template <typename F, typename S>
struct HasCompatibleSignatureHelper;

// This is the leaf state for the recursive compatibility computation; see
// below.
template <typename F, typename R>
struct HasCompatibleSignatureHelper<F, R()> {
  static constexpr bool value = HasSignature<F, R()>;
};

// Implementation for `HasCompatibleSignature` and `RequireCompatibleSignature`.
//
// This removes arguments one by one from the left of the target signature `S`
// to see if `F` has that signature. The recursion stops when one matches, or
// when the arg list is empty (in which case the leaf state is hit, above).
template <typename F, typename R, typename A, typename... Args>
struct HasCompatibleSignatureHelper<F, R(A, Args...)> {
  static constexpr bool value =
      HasSignature<F, R(A, Args...)> ||
      HasCompatibleSignatureHelper<F, R(Args...)>::value;
};

template <typename F, typename S>
constexpr bool HasCompatibleSignature =
    HasCompatibleSignatureHelper<F, S>::value;

// Requires that `F` resolves to some kind of callable object whose signature
// can be rectified to `S`; see `base::RectifyCallback` for more information.
// (Basically, `F` can omit arguments from the left of `S`; these arguments
// will be ignored.)
template <typename F, typename S>
using RequireCompatibleSignature =
    std::enable_if_t<HasCompatibleSignature<F, S>>;

// Converts an ElementSpecifier to an element ID or name and sets it onto
// `builder`.
void SpecifyElement(ui::InteractionSequence::StepBuilder& builder,
                    ElementSpecifier element);

std::string DescribeElement(ElementSpecifier spec);

InteractionSequence::Builder BuildSubsequence(
    InteractiveTestPrivate::MultiStep steps);

}  // namespace internal

}  // namespace ui::test

#endif  // UI_BASE_INTERACTION_INTERACTIVE_TEST_INTERNAL_H_