// 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_INTERACTION_TEST_UTIL_MOUSE_H_
#define UI_VIEWS_INTERACTION_INTERACTION_TEST_UTIL_MOUSE_H_

#include <list>
#include <memory>
#include <set>
#include <utility>

#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#include "third_party/abseil-cpp/absl/types/variant.h"
#include "ui/base/test/ui_controls.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/native_widget_types.h"

namespace views {

class Widget;

namespace test {

// Class which provides useful primitives for controlling the mouse and then
// cleaning up mouse state (even if a test fails). As this object does control
// the mouse, do not create multiple simultaneous instances, and strongly prefer
// to use it only in test suites such as interactive_ui_tests where a single
// test can control the mouse at a time.
class InteractionTestUtilMouse {
 public:
  // Construct for a particular window or browser. This is required because the
  // util object may need access to a drag controller, which is most easily
  // accessed via the window.
  explicit InteractionTestUtilMouse(Widget* widget);

  ~InteractionTestUtilMouse();
  InteractionTestUtilMouse(const InteractionTestUtilMouse&) = delete;
  void operator=(const InteractionTestUtilMouse&) = delete;

  // These represent mouse gestures of different types. They are implementation
  // details; prefer to use the static factory methods below.
  using MouseButtonGesture =
      std::pair<ui_controls::MouseButton, ui_controls::MouseButtonState>;
  using MouseMoveGesture = gfx::Point;
  using MouseGesture = absl::variant<MouseMoveGesture, MouseButtonGesture>;
  using MouseGestures = std::list<MouseGesture>;

  // These factory methods create individual or compound gestures. They can be
  // chained together. Prefer these to directly constructing a MouseGesture.
  static MouseGesture MoveTo(gfx::Point point);
  static MouseGesture MouseDown(ui_controls::MouseButton button);
  static MouseGesture MouseUp(ui_controls::MouseButton button);
  static MouseGestures Click(ui_controls::MouseButton button);
  static MouseGestures DragAndHold(gfx::Point destination);
  static MouseGestures DragAndRelease(gfx::Point destination);

  // Set or get touch mode.
  //
  // `SetTouchMode(true)` returns false if touch is not supported. If it
  // succeeds, subsequent mouse inputs will be converted to equivalent touch
  // inputs.
  //
  // Notes:
  //  - This is an experimental feature and the API is subject to change.
  //     - See tracking bug at crbug.com/1428292 for current status.
  //  - Currently only Ash Chrome is supported.
  //  - Hover is not yet supported, only tap [up/down] and drag.
  //    - Moves without a finger down affect the next tap input but do not send
  //      events.
  //
  // To use this in an InteractiveViewsTest or InteractiveBrowserTest, use the
  // following syntax:
  //
  //   Check([this](){ return test_impl().mouse_util().SetTouchMode(true); })
  //
  // Afterwards, you can use mouse verbs as normal and they will convert to
  // equivalent touch inputs. We suggest using `Check()` so that the test will
  // fail if it's accidentally run on a system that doesn't yet support it.
  //
  // Alternatively, you can write a parameterized test which selectively tries
  // the test in touch-on and touch-off modes for platforms that support it, but
  // only in touch-off mode for those that don't. In these cases, the `Check()`
  // above changes to something like `...SetTouchMode(GetParam())`.
  bool SetTouchMode(bool touch_mode);
  bool GetTouchMode() const;

  // Perform the gesture or gestures specified, returns true on success.
  template <typename... Args>
  bool PerformGestures(gfx::NativeWindow window_hint, Args... gestures);

  // Cancels any pending actions and cleans up any resulting mouse state (i.e.
  // releases any buttons which were pressed).
  void CancelAllGestures();

 private:
  explicit InteractionTestUtilMouse(gfx::NativeWindow window);

  // Helper methods for adding gestures to a gesture list.
  static void AddGestures(MouseGestures& gestures, MouseGesture to_add);
  static void AddGestures(MouseGestures& gestures, MouseGestures to_add);

  bool PerformGesturesImpl(MouseGestures gestures,
                           gfx::NativeWindow window_hint);

  bool ShouldCancelDrag() const;
  void CancelFutureDrag();
  void CancelDragNow();

  bool SendButtonPress(const MouseButtonGesture& gesture,
                       gfx::NativeWindow window_hint,
                       bool sync,
                       base::OnceClosure on_complete);
  bool SendMove(const MouseMoveGesture& gesture,
                gfx::NativeWindow window_hint,
                bool sync,
                base::OnceClosure on_complete);

  // The set of mouse buttons currently depressed. Used to clean up on abort.
  std::set<ui_controls::MouseButton> buttons_down_;

  // Whether gestures are being executed.
  bool performing_gestures_ = false;

  // Whether the current sequence is canceled.
  bool canceled_ = false;

  // Whether we're in touch mode. In touch mode, touch events are sent instead
  // of mouse events. Moves without fingers down will not send events (but see
  // `touch_hover_point_` below).
  bool touch_mode_ = false;

  // Tracks the next place touch input should take place. It is affected by all
  // moves, regardless of whether any fingers are down, and you can use MoveTo
  // with no fingers to reposition the point.
  gfx::Point touch_hover_point_;

#if defined(USE_AURA)
  // Whether the mouse is currently being dragged.
  bool dragging_ = false;

  // These are used in order to clean up extraneous drags on Aura platforms;
  // without this it is possible for a drag loop to start and not exit,
  // preventing a test from completing.
  class DragEnder;
  const std::unique_ptr<DragEnder> drag_ender_;
#endif

  base::WeakPtrFactory<InteractionTestUtilMouse> weak_ptr_factory_{this};
};

template <typename... Args>
bool InteractionTestUtilMouse::PerformGestures(gfx::NativeWindow window_hint,
                                               Args... gestures) {
  MouseGestures gesture_list;
  (AddGestures(gesture_list, std::move(gestures)), ...);
  return PerformGesturesImpl(std::move(gesture_list), window_hint);
}

}  // namespace test
}  // namespace views

#endif  // UI_VIEWS_INTERACTION_INTERACTION_TEST_UTIL_MOUSE_H_