// 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_VIEWS_INTERACTION_INTERACTIVE_VIEWS_TEST_H_
#define UI_VIEWS_INTERACTION_INTERACTIVE_VIEWS_TEST_H_

#include <functional>
#include <memory>
#include <string>
#include <type_traits>
#include <utility>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_piece_forward.h"
#include "base/strings/stringprintf.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_test_util.h"
#include "ui/base/interaction/interactive_test.h"
#include "ui/base/interaction/interactive_test_internal.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/interaction/interaction_test_util_mouse.h"
#include "ui/views/interaction/interactive_views_test_internal.h"
#include "ui/views/test/views_test_base.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"

namespace views::test {

// Provides interactive test functionality for Views.
//
// Interactive tests use InteractionSequence, ElementTracker, and
// InteractionTestUtil to provide a common library of concise test methods. This
// convenience API is nicknamed "Kombucha" (see
// //chrome/test/interaction/README.md for more information).
//
// This class is not a test fixture; it is a mixin that can be added to existing
// test classes using `InteractiveViewsTestT<T>` - or just use
// `InteractiveViewsTest`, which *is* a test fixture (preferred; see below).
//
// To use Kombucha for in-process browser tests, instead see:
// //chrome/test/interaction/interactive_browser_test.h
class InteractiveViewsTestApi : public ui::test::InteractiveTestApi {
 public:
  InteractiveViewsTestApi();
  ~InteractiveViewsTestApi() override;

  // Returns an object that can be used to inject mouse inputs. Generally,
  // prefer to use methods like MoveMouseTo, MouseClick, and DragMouseTo.
  InteractionTestUtilMouse& mouse_util() { return test_impl().mouse_util(); }

  // Shorthand to convert a tracked element into a View. The element should be
  // a views::TrackedElementViews and of type `T`.
  template <typename T = View>
  static T* AsView(ui::TrackedElement* el);
  template <typename T = View>
  static const T* AsView(const ui::TrackedElement* el);

  // Runs a test InteractionSequence from a series of Steps or StepBuilders with
  // RunSynchronouslyForTesting(). Hooks both the completed and aborted
  // callbacks to ensure completion, and prints an error on failure. The context
  // will be pulled from `context_widget()`.
  template <typename... Args>
  bool RunTestSequence(Args&&... steps);

  // Naming views:
  //
  // The following methods name a view (to be referred to later in the test
  // sequence by name) based on some kind of rule or relationship. The View need
  // not have an ElementIdentifier assigned ahead of time, so this is useful for
  // finding random or dynamically-created views.
  //
  // For example:
  //
  //   RunTestSequence(
  //     ...
  //     NameView(kThirdTabName,
  //              base::BindLambdaForTesting([&](){
  //                return browser_view->tabstrip()->tab_at(3);
  //              }))
  //     WithElement(kThirdTabName, ...)
  //     ...
  //   );
  //
  // How the view is named will depend on which version of the method you use;
  // the

  // Determines if a view matches some predicate.
  using ViewMatcher = base::RepeatingCallback<bool(const View*)>;

  // Specifies a View not relative to any particular other View.
  using AbsoluteViewSpecifier = absl::variant<
      // Specify a view that is known at the time the sequence is created. The
      // View must persist until the step executes.
      View*,
      // Specify a view pointer that will be valid by the time the step
      // executes. Use `std::ref()` to wrap the pointer that will receive the
      // value.
      std::reference_wrapper<View*>,
      // Find and return a view based on an arbitrary rule.
      base::OnceCallback<View*()>>;

  // Specifies a view relative to its parent.
  using ChildViewSpecifier = absl::variant<
      // The index of the child in the parent view. An out of bounds index will
      // generate an error.
      size_t,
      // Specifies a filter that is applied to the children; the first child
      // view to satisfy the filter (i.e. return true) is named.
      ViewMatcher>;

  // Methods that name views.

  // Names a view relative to another view `relative_to` based on an arbitrary
  // rule. The resulting view does not need to be a descendant (or even an
  // ancestor) of `relative_to`.
  //
  // Your `find_callback` should take a pointer to a View or a derived type and
  // return a pointer to a View or derived type.
  template <typename C,
            typename V = internal::ViewArgType<0, C>,
            typename R = std::remove_cv_t<
                std::remove_pointer_t<ui::test::internal::ReturnTypeOf<C>>>,
            typename = ui::test::internal::RequireSignature<C, R*(V*)>>
  [[nodiscard]] static StepBuilder NameViewRelative(
      ElementSpecifier relative_to,
      base::StringPiece name,
      C&& find_callback);

  [[nodiscard]] static StepBuilder NameView(base::StringPiece name,
                                            AbsoluteViewSpecifier spec);

  [[nodiscard]] static StepBuilder NameChildView(ElementSpecifier parent,
                                                 base::StringPiece name,
                                                 ChildViewSpecifier spec);

  [[nodiscard]] static StepBuilder NameDescendantView(ElementSpecifier ancestor,
                                                      base::StringPiece name,
                                                      ViewMatcher matcher);

  // Names the `index` (0-indexed) child view of `parent` that is of type `V`.
  template <typename V>
  [[nodiscard]] static StepBuilder NameChildViewByType(ElementSpecifier parent,
                                                       base::StringPiece name,
                                                       size_t index = 0);

  // Names the `index` (0-indexed) descendant view of `parent` in depth-first
  // traversal order that is of type `V`.
  template <typename V>
  [[nodiscard]] static StepBuilder NameDescendantViewByType(
      ElementSpecifier ancestor,
      base::StringPiece name,
      size_t index = 0);

  // As WithElement(), but `view` should resolve to a TrackedElementViews
  // wrapping a view of type `V`.
  template <typename F,
            typename V = internal::ViewArgType<0, F>,
            typename = ui::test::internal::RequireSignature<F, void(V*)>>
  [[nodiscard]] static StepBuilder WithView(ElementSpecifier view,
                                            F&& function);

  // As CheckElement(), but `view` should resolve to a TrackedElementViews
  // wrapping a view of type `V`.
  template <typename F,
            typename V = internal::ViewArgType<0, F>,
            typename = ui::test::internal::RequireSignature<
                F,
                bool(V*)>>  // NOLINT(readability/casting)
  [[nodiscard]] static StepBuilder CheckView(ElementSpecifier view, F&& check);

  // As CheckView(), but checks that the result of calling `function` on `view`
  // matches `matcher`. If not, the mismatch is printed and the test fails.
  //
  // `matcher` should resolve or convert to type `Matcher<R>`.
  template <typename F,
            typename M,
            typename R = ui::test::internal::ReturnTypeOf<F>,
            typename V = internal::ViewArgType<0, F>,
            typename = ui::test::internal::RequireSignature<F, R(V*)>>
  [[nodiscard]] static StepBuilder CheckView(ElementSpecifier view,
                                             F&& function,
                                             M&& matcher);

  // As CheckView() but checks that `matcher` matches the value returned by
  // calling `property` on `view`. On failure, logs the matcher error and fails
  // the test.
  //
  // `matcher` must resolve or convert to type `Matcher<R>`.
  template <typename V, typename R, typename M>
  [[nodiscard]] static StepBuilder CheckViewProperty(ElementSpecifier view,
                                                     R (V::*property)() const,
                                                     M&& matcher);

  // Scrolls `view` into the visible viewport if it is currently scrolled
  // outside its container. The view must be otherwise present and visible.
  // Has no effect if the view is not in a scroll container.
  [[nodiscard]] static StepBuilder ScrollIntoView(ElementSpecifier view);

  // Indicates that the center point of the target element should be used for a
  // mouse move.
  struct CenterPoint {};

  // Function that returns a destination for a move or drag.
  using AbsolutePositionCallback = base::OnceCallback<gfx::Point()>;

  // Specifies an absolute position for a mouse move or drag that does not need
  // a reference element.
  using AbsolutePositionSpecifier = absl::variant<
      // Use this specific position. This value is stored when the sequence is
      // created; use gfx::Point* if you want to capture a point during sequence
      // execution.
      gfx::Point,
      // As above, but the position is read from the memory address on execution
      // instead of copied when the test sequence is constructed. Use this when
      // you want to calculate and cache a point during test execution for later
      // use. The pointer must remain valid through the end of the test.
      gfx::Point*,
      // Use the return value of the supplied callback
      AbsolutePositionCallback>;

  // Specifies how the `reference_element` should be used (or not) to generate a
  // target point for a mouse move.
  using RelativePositionCallback =
      base::OnceCallback<gfx::Point(ui::TrackedElement* reference_element)>;

  // Specifies how the target position of a mouse operation (in screen
  // coordinates) will be determined.
  using RelativePositionSpecifier = absl::variant<
      // Default to the centerpoint of the reference element, which should be a
      // views::View.
      CenterPoint,
      // Use the return value of the supplied callback.
      RelativePositionCallback>;

  // Move the mouse to the specified `position` in screen coordinates. The
  // `reference` element will be used based on how `position` is specified.
  [[nodiscard]] StepBuilder MoveMouseTo(AbsolutePositionSpecifier position);
  [[nodiscard]] StepBuilder MoveMouseTo(
      ElementSpecifier reference,
      RelativePositionSpecifier position = CenterPoint());

  // Clicks mouse button `button` at the current cursor position.
  [[nodiscard]] StepBuilder ClickMouse(
      ui_controls::MouseButton button = ui_controls::LEFT,
      bool release = true);

  // Depresses the left mouse button at the current cursor position and drags to
  // the target `position`. The `reference` element will be used based on how
  // `position` is specified.
  [[nodiscard]] StepBuilder DragMouseTo(AbsolutePositionSpecifier position,
                                        bool release = true);
  [[nodiscard]] StepBuilder DragMouseTo(
      ElementSpecifier reference,
      RelativePositionSpecifier position = CenterPoint(),
      bool release = true);

  // Releases the specified mouse button. Use when you previously called
  // ClickMouse() or DragMouseTo() with `release` = false.
  [[nodiscard]] StepBuilder ReleaseMouse(
      ui_controls::MouseButton button = ui_controls::LEFT);

  // As IfElement(), but `condition` takes a single argument that is a const
  // View pointer. If `element` is not a view of type V, then the test will
  // fail.
  template <typename C,
            typename T,
            typename U = MultiStep,
            typename V = internal::ViewArgType<0, C>,
            typename = ui::test::internal::RequireSignature<
                C,
                bool(const V*)>>  // NOLINT(readability/casting)
  [[nodiscard]] static StepBuilder IfView(ElementSpecifier element,
                                          C&& condition,
                                          T&& then_steps,
                                          U&& else_steps = MultiStep());

  // As IfElementMatches(), but `function` takes a single argument that is a
  // const View pointer. If `element` is not a view of type V, then the test
  // will fail.
  template <typename F,
            typename M,
            typename T,
            typename U = MultiStep,
            typename R = ui::test::internal::ReturnTypeOf<F>,
            typename V = internal::ViewArgType<0, F>,
            typename = ui::test::internal::RequireSignature<F, R(const V*)>>
  [[nodiscard]] static StepBuilder IfViewMatches(ElementSpecifier element,
                                                 F&& function,
                                                 M&& matcher,
                                                 T&& then_steps,
                                                 U&& else_steps = MultiStep());

  // Executes `then_steps` if `property` of the view `element` (which must be of
  // the correct View type) matches `matcher`, otherwise executes `else_steps`.
  //
  // Note that bare literal strings cannot be passed as `matcher` for properties
  // with string values, you will need to either explicitly pass a
  // std::[u16]string or explicitly construct a testing::Eq matcher.
  template <typename R,
            typename M,
            typename V,
            typename T,
            typename U = MultiStep>
  [[nodiscard]] static StepBuilder IfViewPropertyMatches(
      ElementSpecifier element,
      R (V::*property)() const,
      M&& matcher,
      T&& then_steps,
      U&& else_steps = MultiStep());

  // Sets the context widget. Must be called before RunTestSequence() or any of
  // the mouse functions.
  void SetContextWidget(Widget* context_widget);
  Widget* context_widget() { return context_widget_; }

 protected:
  explicit InteractiveViewsTestApi(
      std::unique_ptr<internal::InteractiveViewsTestPrivate> private_test_impl);

 private:
  using FindViewCallback = base::OnceCallback<View*(View*)>;
  static FindViewCallback GetFindViewCallback(AbsoluteViewSpecifier spec);
  static FindViewCallback GetFindViewCallback(ChildViewSpecifier spec);

  // Recursively finds an element that matches `matcher` starting with (but
  // not including) `from`. If `recursive` is true, searches all descendants,
  // otherwise searches children.
  static views::View* FindMatchingView(const views::View* from,
                                       ViewMatcher& matcher,
                                       bool recursive);

  // Converts a *PositionSpecifier to an appropriate *PositionCallback.
  static RelativePositionCallback GetPositionCallback(
      AbsolutePositionSpecifier spec);
  static RelativePositionCallback GetPositionCallback(
      RelativePositionSpecifier spec);

  internal::InteractiveViewsTestPrivate& test_impl() {
    return static_cast<internal::InteractiveViewsTestPrivate&>(
        InteractiveTestApi::private_test_impl());
  }

  // Creates the follow-up step for a mouse action.
  StepBuilder CreateMouseFollowUpStep(const base::StringPiece& description);

  base::raw_ptr<Widget, DanglingUntriaged> context_widget_ = nullptr;
};

// Template that adds InteractiveViewsTestApi to any test fixture. Prefer to use
// InteractiveViewsTest unless you specifically need to inherit from another
// test class.
//
// You must call SetContextWidget() before using RunTestSequence() or any of the
// mouse actions.
//
// See //chrome/test/interaction/README.md for usage.
template <typename T>
class InteractiveViewsTestT : public T, public InteractiveViewsTestApi {
 public:
  template <typename... Args>
  explicit InteractiveViewsTestT(Args&&... args)
      : T(std::forward<Args>(args)...) {}

  ~InteractiveViewsTestT() override = default;

 protected:
  void SetUp() override {
    T::SetUp();
    private_test_impl().DoTestSetUp();
  }

  void TearDown() override {
    private_test_impl().DoTestTearDown();
    T::TearDown();
  }
};

// Convenience test fixture for Views tests that supports
// InteractiveViewsTestApi.
//
// You must call SetContextWidget() before using RunTestSequence() or any of the
// mouse actions.
//
// See //chrome/test/interaction/README.md for usage.
using InteractiveViewsTest = InteractiveViewsTestT<ViewsTestBase>;

// Template definitions:

// static
template <class T>
T* InteractiveViewsTestApi::AsView(ui::TrackedElement* el) {
  auto* const views_el = el->AsA<TrackedElementViews>();
  CHECK(views_el);
  T* const view = AsViewClass<T>(views_el->view());
  CHECK(view);
  return view;
}

// static
template <class T>
const T* InteractiveViewsTestApi::AsView(const ui::TrackedElement* el) {
  const auto* const views_el = el->AsA<TrackedElementViews>();
  CHECK(views_el);
  const T* const view = AsViewClass<T>(views_el->view());
  CHECK(view);
  return view;
}

template <typename... Args>
bool InteractiveViewsTestApi::RunTestSequence(Args&&... steps) {
  return RunTestSequenceInContext(
      ElementTrackerViews::GetContextForWidget(context_widget()),
      std::forward<Args>(steps)...);
}

// static
template <typename C, typename V, typename R, typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::NameViewRelative(
    ElementSpecifier relative_to,
    base::StringPiece name,
    C&& find_callback) {
  StepBuilder builder;
  builder.SetDescription(
      base::StringPrintf("NameViewRelative( \"%s\" )", name.data()));
  ui::test::internal::SpecifyElement(builder, relative_to);
  builder.SetMustBeVisibleAtStart(true);
  builder.SetStartCallback(base::BindOnce(
      [](base::OnceCallback<R*(V*)> find_callback, std::string name,
         ui::InteractionSequence* seq, ui::TrackedElement* el) {
        V* relative_to = nullptr;
        if (el->identifier() !=
            ui::test::internal::kInteractiveTestPivotElementId) {
          if (!el->IsA<TrackedElementViews>()) {
            LOG(ERROR) << "NameView(): Target element is not a View.";
            seq->FailForTesting();
            return;
          }
          View* const view = el->AsA<TrackedElementViews>()->view();
          if (!IsViewClass<V>(view)) {
            LOG(ERROR) << "NameView(): Target View is of type "
                       << view->GetClassName() << " but expected "
                       << V::MetaData()->type_name();
            seq->FailForTesting();
            return;
          }
          relative_to = AsViewClass<V>(view);
        }
        View* const result = std::move(find_callback).Run(relative_to);
        if (!result) {
          LOG(ERROR) << "NameView(): No View found.";
          seq->FailForTesting();
          return;
        }
        auto* const target_element =
            ElementTrackerViews::GetInstance()->GetElementForView(
                result, /* assign_temporary_id =*/true);
        if (!target_element) {
          LOG(ERROR)
              << "NameView(): attempting to name View that is not visible.";
          seq->FailForTesting();
          return;
        }
        seq->NameElement(target_element, name);
      },
      ui::test::internal::MaybeBind(std::forward<C>(find_callback)),
      std::string(name)));
  return builder;
}

// static
template <typename F, typename V, typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::WithView(
    ElementSpecifier view,
    F&& function) {
  StepBuilder builder;
  builder.SetDescription("WithView()");
  ui::test::internal::SpecifyElement(builder, view);
  builder.SetMustBeVisibleAtStart(true);
  builder.SetStartCallback(base::BindOnce(
      [](base::OnceCallback<void(V*)> function, ui::InteractionSequence* seq,
         ui::TrackedElement* el) { std::move(function).Run(AsView<V>(el)); },
      ui::test::internal::MaybeBind(std::forward<F>(function))));
  return builder;
}

// static
template <typename C, typename T, typename U, typename V, typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::IfView(
    ElementSpecifier element,
    C&& condition,
    T&& then_steps,
    U&& else_steps) {
  return std::move(
      IfElement(element,
                base::BindOnce(
                    [](base::OnceCallback<bool(const V*)> condition,
                       const ui::InteractionSequence* seq,
                       const ui::TrackedElement* el) {
                      const V* const view = el ? AsView<V>(el) : nullptr;
                      return std::move(condition).Run(view);
                    },
                    ui::test::internal::MaybeBind(std::forward<C>(condition))),
                std::forward<T>(then_steps), std::forward<U>(else_steps))
          .SetDescription("IfView()"));
}

// static
template <typename F,
          typename M,
          typename T,
          typename U,
          typename R,
          typename V,
          typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::IfViewMatches(
    ElementSpecifier element,
    F&& function,
    M&& matcher,
    T&& then_steps,
    U&& else_steps) {
  return std::move(
      IfElementMatches(
          element,
          base::BindOnce(
              [](base::OnceCallback<R(const V*)> condition,
                 const ui::InteractionSequence* seq,
                 const ui::TrackedElement* el) {
                const V* const view = el ? AsView<V>(el) : nullptr;
                return std::move(condition).Run(view);
              },
              ui::test::internal::MaybeBind(std::forward<F>(function))),
          testing::Matcher<R>(std::forward<M>(matcher)),
          std::forward<T>(then_steps), std::forward<U>(else_steps))
          .SetDescription("IfViewMatches()"));
}

// static
template <typename R, typename M, typename V, typename T, typename U>
ui::InteractionSequence::StepBuilder
InteractiveViewsTestApi::IfViewPropertyMatches(ElementSpecifier element,
                                               R (V::*property)() const,
                                               M&& matcher,
                                               T&& then_steps,
                                               U&& else_steps) {
  using Return = std::remove_cvref_t<R>;
  base::OnceCallback<Return(const V*)> function = base::BindOnce(
      [](R (V::*property)() const, const V* view) -> Return {
        return (view->*property)();
      },
      std::move(property));
  return std::move(
      IfViewMatches(element, std::move(function), std::forward<M>(matcher),
                    std::forward<T>(then_steps), std::forward<U>(else_steps))
          .SetDescription("IfViewPropertyMatches()"));
}

// static
template <typename V>
ui::InteractionSequence::StepBuilder
InteractiveViewsTestApi::NameChildViewByType(ElementSpecifier parent,
                                             base::StringPiece name,
                                             size_t index) {
  return std::move(
      NameChildView(parent, name,
                    base::BindRepeating(
                        [](size_t& index, const View* view) {
                          if (IsViewClass<V>(view)) {
                            if (index == 0) {
                              return true;
                            }
                            --index;
                          }
                          return false;
                        },
                        base::OwnedRef(index)))
          .SetDescription(base::StringPrintf(
              "NameChildViewByType<%s>( \"%s\" %zu )",
              V::MetaData()->type_name().c_str(), name.data(), index)));
}

// static
template <typename V>
ui::InteractionSequence::StepBuilder
InteractiveViewsTestApi::NameDescendantViewByType(ElementSpecifier ancestor,
                                                  base::StringPiece name,
                                                  size_t index) {
  return std::move(
      NameDescendantView(ancestor, name,
                         base::BindRepeating(
                             [](size_t& index, const View* view) {
                               if (IsViewClass<V>(view)) {
                                 if (index == 0) {
                                   return true;
                                 }
                                 --index;
                               }
                               return false;
                             },
                             base::OwnedRef(index)))
          .SetDescription(base::StringPrintf(
              "NameDescendantViewByType<%s>( \"%s\" %zu )",
              V::MetaData()->type_name().c_str(), name.data(), index)));
}

// static
template <typename F, typename, typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::CheckView(
    ElementSpecifier view,
    F&& check) {
  return CheckView(view, std::forward<F>(check), true);
}

// static
template <typename F, typename M, typename R, typename V, typename>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::CheckView(
    ElementSpecifier view,
    F&& function,
    M&& matcher) {
  StepBuilder builder;
  builder.SetDescription("CheckView()");
  ui::test::internal::SpecifyElement(builder, view);
  builder.SetStartCallback(base::BindOnce(
      [](base::OnceCallback<R(V*)> function, testing::Matcher<R> matcher,
         ui::InteractionSequence* seq, ui::TrackedElement* el) {
        if (!ui::test::internal::MatchAndExplain(
                "CheckView()", matcher,
                std::move(function).Run(AsView<V>(el)))) {
          seq->FailForTesting();
        }
      },
      ui::test::internal::MaybeBind(std::forward<F>(function)),
      testing::Matcher<R>(std::forward<M>(matcher))));
  return builder;
}

// static
template <typename V, typename R, typename M>
ui::InteractionSequence::StepBuilder InteractiveViewsTestApi::CheckViewProperty(
    ElementSpecifier view,
    R (V::*property)() const,
    M&& matcher) {
  StepBuilder builder;
  builder.SetDescription("CheckViewProperty()");
  ui::test::internal::SpecifyElement(builder, view);
  builder.SetStartCallback(base::BindOnce(
      [](R (V::*property)() const, testing::Matcher<R> matcher,
         ui::InteractionSequence* seq, ui::TrackedElement* el) {
        if (!ui::test::internal::MatchAndExplain(
                "CheckViewProperty()", matcher, (AsView<V>(el)->*property)())) {
          seq->FailForTesting();
        }
      },
      property, testing::Matcher<R>(std::forward<M>(matcher))));
  return builder;
}

}  // namespace views::test

#endif  // UI_VIEWS_INTERACTION_INTERACTIVE_VIEWS_TEST_H_