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

#include <string_view>
#include <tuple>
#include <utility>

#include "base/auto_reset.h"
#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/json/json_reader.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/input/actions_parser.h"
#include "content/common/input/synthetic_gesture.h"
#include "content/common/input/synthetic_gesture_controller.h"
#include "content/common/input/synthetic_gesture_params.h"
#include "content/common/input/synthetic_gesture_target.h"
#include "content/common/input/synthetic_pointer_action.h"
#include "content/common/input/synthetic_pointer_action_list_params.h"
#include "content/common/input/synthetic_smooth_scroll_gesture.h"
#include "content/common/input/synthetic_smooth_scroll_gesture_params.h"
#include "content/common/input/synthetic_tap_gesture.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/blink/blink_features.h"
#include "ui/events/gesture_detection/filtered_gesture_provider.h"
#include "ui/latency/latency_info.h"

#if BUILDFLAG(IS_ANDROID)
#include "content/browser/renderer_host/input/touch_selection_controller_input_observer.h"
#include "content/browser/renderer_host/render_widget_host_view_android.h"
#endif  // BUILDFLAG(IS_ANDROID)

using blink::WebInputEvent;

namespace {

constexpr char kTouchActionDataURL[] =
    "data:text/html;charset=utf-8,"
    "<!DOCTYPE html>"
    "<meta name='viewport' content='width=device-width'/>"
    "<style>"
    "html, body {"
    "  margin: 0;"
    "}"
    ".box {"
    "  height: 100px;"
    "  width: 100px;"
    "  background-color: red;"
    "}"
    ".ta-none {"
    "  touch-action: none;"
    "  background-color: green;"
    "}"
    ".spacer {"
    "  height: 10000px;"
    "  width: 100px;"
    "  background-color: blue;"
    "}"
    "</style>"
    "<div class=box></div>"
    "<div class='box ta-none'></div>"
    "<div class=spacer></div>"
    "<script>"
    "  window.eventCounts = "
    "    {touchstart:0, touchmove:0, touchend: 0, touchcancel:0};"
    "  function countEvent(e) { eventCounts[e.type]++; }"
    "  for (var evt in eventCounts) { "
    "    document.addEventListener(evt, countEvent); "
    "  }"
    "  document.title='ready';"
    "</script>";

constexpr char kTouchActionURLWithOverlapArea[] =
    "data:text/html;charset=utf-8,"
    "<!DOCTYPE html>"
    "<meta name='viewport' content='width=device-width'/>"
    "<style>"
    "html, body {"
    "  margin: 0;"
    "}"
    ".box {"
    "  box-sizing: border-box;"
    "  height: 100px;"
    "  width: 100px;"
    "  border: 2px solid blue;"
    "  position: absolute;"
    "  will-change: transform;"
    "}"
    ".spacer {"
    "  height: 10000px;"
    "  width: 10000px;"
    "}"
    ".ta-auto {"
    "  top: 52px;"
    "  left: 52px;"
    "  touch-action: auto;"
    "}"
    ".ta-pany {"
    "  top: 102px;"
    "  left: 2px;"
    "  touch-action: pan-y;"
    "}"
    ".ta-panx {"
    "  top: 2px;"
    "  left: 102px;"
    "  touch-action: pan-x;"
    "}"
    "</style>"
    "<div class='box ta-auto'></div>"
    "<div class='box ta-panx'></div>"
    "<div class='box ta-pany'></div>"
    "<div class=spacer></div>"
    "<script>"
    "  document.title='ready';"
    "</script>";

void GiveItSomeTime(int t) {
  base::RunLoop run_loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(t));
  run_loop.Run();
}

constexpr base::TimeDelta kNoJankTime = base::Milliseconds(0);
constexpr base::TimeDelta kShortJankTime = base::Milliseconds(100);
// 1200ms is larger than both desktop / mobile_touch_ack_timeout_delay in the
// PassthroughTouchEventQueue, which ensures timeout to be triggered.
constexpr base::TimeDelta kLongJankTime = base::Milliseconds(1200);
}  // namespace

namespace content {

class TouchActionBrowserTest : public ContentBrowserTest {
 public:
  TouchActionBrowserTest() = default;

  TouchActionBrowserTest(const TouchActionBrowserTest&) = delete;
  TouchActionBrowserTest& operator=(const TouchActionBrowserTest&) = delete;

  ~TouchActionBrowserTest() override = default;

  RenderWidgetHostImpl* GetWidgetHost() {
    return RenderWidgetHostImpl::From(shell()
                                          ->web_contents()
                                          ->GetPrimaryMainFrame()
                                          ->GetRenderViewHost()
                                          ->GetWidget());
  }

  void OnSyntheticGestureCompleted(SyntheticGesture::Result result) {
    EXPECT_EQ(SyntheticGesture::GESTURE_FINISHED, result);
    run_loop_->Quit();
  }

 protected:
  void LoadURL(std::string_view touch_action_url) {
    const GURL data_url(std::move(touch_action_url));
    EXPECT_TRUE(NavigateToURL(shell(), data_url));

    RenderWidgetHostImpl* host = GetWidgetHost();
    frame_observer_ = std::make_unique<RenderFrameSubmissionObserver>(
        host->render_frame_metadata_provider());
    host->GetView()->SetSize(gfx::Size(400, 400));

    std::u16string ready_title(u"ready");
    TitleWatcher watcher(shell()->web_contents(), ready_title);
    std::ignore = watcher.WaitAndGetTitle();

    // We need to wait until hit test data is available. We use our own
    // HitTestRegionObserver here because we have the RenderWidgetHostImpl
    // available.
    HitTestRegionObserver observer(host->GetFrameSinkId());
    observer.WaitForHitTestData();
  }

  // ContentBrowserTest:
  void SetUpCommandLine(base::CommandLine* cmd) override {
    cmd->AppendSwitchASCII(switches::kTouchEventFeatureDetection,
                           switches::kTouchEventFeatureDetectionEnabled);
  }

  // ContentBrowserTest:
  void PostRunTestOnMainThread() override {
    // Delete this before the WebContents is destroyed.
    frame_observer_.reset();
    ContentBrowserTest::PostRunTestOnMainThread();
  }

  void JankMainThread(base::TimeDelta delta) {
    std::string script = "var end = performance.now() + ";
    script.append(base::NumberToString(delta.InMilliseconds()));
    script.append("; while (performance.now() < end) ; ");
    EXPECT_TRUE(ExecJs(shell(), script));
  }

  double GetScrollTop() {
    return EvalJs(shell(), "document.scrollingElement.scrollTop")
        .ExtractDouble();
  }

  double GetScrollLeft() {
    return EvalJs(shell(), "document.scrollingElement.scrollLeft")
        .ExtractDouble();
  }

  bool URLLoaded() {
    std::u16string ready_title(u"ready");
    TitleWatcher watcher(shell()->web_contents(), ready_title);
    const std::u16string title = watcher.WaitAndGetTitle();
    return title == ready_title;
  }

  // In this test, we first jank the main thread for 1200ms. Then we let the
  // first finger scroll along the x-direction, on a pan-y area, for 1 second.
  // While the first finger is still scrolling, we let the second finger
  // touching the same area and scroll along the same direction. We purposely
  // trigger touch ack timeout for the first finger touch. All we need to ensure
  // is that the second finger also scrolled.
  // TODO(bokan): This test isn't doing what's described. For one thing, the
  // JankMainThread function will block the caller as well as the main thread
  // so we're actually waiting 1.8s before starting the second scroll, by which
  // point the first scroll has finished. Additionally, we can only run one
  // synthetic gesture at a time so queueing two gestures will produce
  // back-to-back scrolls rather than one two fingered scroll.
  void DoTwoFingerTouchScroll(
      bool wait_until_scrolled,
      const gfx::Vector2d& expected_scroll_position_after_scroll) {
    SyntheticSmoothScrollGestureParams params1;
    params1.gesture_source_type =
        content::mojom::GestureSourceType::kTouchInput;
    params1.anchor = gfx::PointF(25, 125);
    params1.distances.push_back(gfx::Vector2dF(-5, 0));
    params1.prevent_fling = true;
    params1.speed_in_pixels_s = 5;

    SyntheticSmoothScrollGestureParams params2;
    params2.gesture_source_type =
        content::mojom::GestureSourceType::kTouchInput;
    params2.anchor = gfx::PointF(25, 125);
    params2.distances.push_back(gfx::Vector2dF(-50, 0));

    run_loop_ = std::make_unique<base::RunLoop>();

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticSmoothScrollGesture>(params1),
        base::DoNothing());

    JankMainThread(kLongJankTime);
    GiveItSomeTime(800);

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticSmoothScrollGesture>(params2),
        base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                       base::Unretained(this)));

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop_->Run();
    run_loop_.reset();

    CheckScrollOffset(wait_until_scrolled,
                      expected_scroll_position_after_scroll);
  }

  // Generate touch events for a synthetic scroll from |point| for |distance|.
  void DoTouchScrollAndCheckScrollHeight(
      const gfx::Point& point,
      const gfx::Vector2d& distance,
      bool wait_until_scrolled,
      int expected_scroll_height_after_scroll,
      const gfx::Vector2d& expected_scroll_position_after_scroll,
      const base::TimeDelta& jank_time) {
    EXPECT_EQ(expected_scroll_height_after_scroll,
              EvalJs(shell(), "document.documentElement.scrollHeight"));
    DoTouchScroll(point, distance, wait_until_scrolled,
                  expected_scroll_position_after_scroll, jank_time);
  }

  void DoTouchScroll(const gfx::Point& point,
                     const gfx::Vector2d& distance,
                     bool wait_until_scrolled,
                     const gfx::Vector2d& expected_scroll_position_after_scroll,
                     const base::TimeDelta& jank_time) {
    DCHECK(URLLoaded());
    EXPECT_EQ(0, GetScrollTop());

    EnsureInitializedForSyntheticGestures();

    float page_scale_factor =
        frame_observer_->LastRenderFrameMetadata().page_scale_factor;
    if (page_scale_factor == 0)
      page_scale_factor = 1.0f;
    gfx::PointF touch_point(point);
    if (page_scale_factor != 1.0f) {
      touch_point.set_x(touch_point.x() * page_scale_factor);
      touch_point.set_y(touch_point.y() * page_scale_factor);
    }
    SyntheticSmoothScrollGestureParams params;
    params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput;
    params.anchor = touch_point;
    params.distances.push_back(-distance);
    // Set the speed to very high so that there is one GSU only.
    // It seems that when the speed is too high, it has a race with the timeout
    // test.
    if (jank_time != kLongJankTime) {
      params.speed_in_pixels_s = 1000000;
    }

    run_loop_ = std::make_unique<base::RunLoop>();

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticSmoothScrollGesture>(params),
        base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                       base::Unretained(this)));

    if (jank_time > base::Milliseconds(0))
      JankMainThread(jank_time);

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop_->Run();
    run_loop_.reset();

    CheckScrollOffset(wait_until_scrolled,
                      expected_scroll_position_after_scroll);
  }

  void DoTwoFingerPan() {
    DCHECK(URLLoaded());

    const std::string pointer_actions_json = R"HTML(
        [{"source": "touch", "id": 0,
              "actions": [
                { "name": "pointerDown", "x": 10, "y": 125 },
                { "name": "pointerMove", "x": 10, "y": 155 },
                { "name": "pointerUp" }]},
             {"source": "touch", "id": 1,
              "actions": [
                { "name": "pointerDown", "x": 15, "y": 125 },
                { "name": "pointerMove", "x": 15, "y": 155 },
                { "name": "pointerUp"}]}]
        )HTML";

    ASSERT_OK_AND_ASSIGN(
        auto parsed_json,
        base::JSONReader::ReadAndReturnValueWithError(
            pointer_actions_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS));
    ActionsParser actions_parser(std::move(parsed_json));

    ASSERT_TRUE(actions_parser.Parse());

    run_loop_ = std::make_unique<base::RunLoop>();

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticPointerAction>(
            actions_parser.pointer_action_params()),
        base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                       base::Unretained(this)));

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop_->Run();
    run_loop_.reset();
  }

  void DoTouchCancel() {
    DCHECK(URLLoaded());
    const std::string pointer_actions_json = R"HTML(
        [{"source": "touch", "id": 0,
              "actions": [
              { "name": "pointerDown", "x": 50, "y": 50 },
              { "name": "pointerCancel"}]}]
        )HTML";

    ASSERT_OK_AND_ASSIGN(
        auto parsed_json,
        base::JSONReader::ReadAndReturnValueWithError(
            pointer_actions_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS));
    ActionsParser actions_parser(std::move(parsed_json));

    ASSERT_TRUE(actions_parser.Parse());

    run_loop_ = std::make_unique<base::RunLoop>();

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticPointerAction>(
            actions_parser.pointer_action_params()),
        base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                       base::Unretained(this)));

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop_->Run();
    run_loop_.reset();
  }

  // Generate touch events for a double tap and drag zoom gesture at
  // coordinates (50, 50).
  void DoDoubleTapDragZoom() {
    DCHECK(URLLoaded());

    const std::string pointer_actions_json = R"HTML(
        [{
          "source": "touch",
          "actions": [
            { "name": "pointerDown", "x": 50, "y": 50 },
            { "name": "pointerUp" },
            { "name": "pause", "duration": 50 },
            { "name": "pointerDown", "x": 50, "y": 50 },
            { "name": "pointerMove", "x": 50, "y": 150 },
            { "name": "pointerUp" }
          ]
        }]
        )HTML";

    UNSAFE_BUFFERS(ASSERT_OK_AND_ASSIGN(
        auto parsed_json,
        base::JSONReader::ReadAndReturnValueWithError(
            pointer_actions_json, base::JSON_PARSE_CHROMIUM_EXTENSIONS)));
    ActionsParser actions_parser(std::move(parsed_json));

    ASSERT_TRUE(actions_parser.Parse());

    run_loop_ = std::make_unique<base::RunLoop>();

    GetWidgetHost()->QueueSyntheticGesture(
        std::make_unique<SyntheticPointerAction>(
            actions_parser.pointer_action_params()),
        base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                       base::Unretained(this)));

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop_->Run();
    run_loop_.reset();
  }

  // Sends a no-op gesture to the page to ensure the SyntheticGestureController
  // is initialized. We need to do this if the first sent gesture happens when
  // the main thread is janked, otherwise the initialization won't happen
  // because of the blocked main thread.
  void EnsureInitializedForSyntheticGestures() {
    DCHECK(URLLoaded());

    base::RunLoop run_loop;

    GetWidgetHost()->EnsureReadyForSyntheticGestures(
        base::BindLambdaForTesting([&]() { run_loop.Quit(); }));

    // Runs until we get the OnSyntheticGestureCompleted callback
    run_loop.Run();
  }

  void CheckScrollOffset(
      bool wait_until_scrolled,
      const gfx::Vector2d& expected_scroll_position_after_scroll) {
    gfx::PointF default_scroll_offset;
    gfx::PointF root_scroll_offset =
        frame_observer_->LastRenderFrameMetadata().root_scroll_offset.value_or(
            default_scroll_offset);

    int scroll_top, scroll_left;
    if (!wait_until_scrolled) {
      scroll_top = root_scroll_offset.y();
      scroll_left = root_scroll_offset.x();
    } else {
      // GetScrollTop() and GetScrollLeft() goes through the main thread, here
      // we want to make sure that the compositor already scrolled before asking
      // the main thread.
      while (root_scroll_offset.y() <
                 expected_scroll_position_after_scroll.y() / 2 ||
             root_scroll_offset.x() <
                 expected_scroll_position_after_scroll.x() / 2) {
        frame_observer_->WaitForMetadataChange();
        root_scroll_offset =
            frame_observer_->LastRenderFrameMetadata()
                .root_scroll_offset.value_or(default_scroll_offset);
      }
      // Check the scroll offset
      scroll_top = GetScrollTop();
      scroll_left = GetScrollLeft();
    }

    // It seems that even if the compositor frame has scrolled half of the
    // expected scroll offset, the Blink side scroll offset may not yet be
    // updated, so here we expect it to at least have scrolled.
    // TODO(crbug.com/40601223): this can be resolved by fixing this bug.
    if (expected_scroll_position_after_scroll.y() > 0)
      EXPECT_GT(scroll_top, 0);
    if (expected_scroll_position_after_scroll.x() > 0)
      EXPECT_GT(scroll_left, 0);
  }

  std::unique_ptr<base::RunLoop> run_loop_;

 private:
  std::unique_ptr<RenderFrameSubmissionObserver> frame_observer_;
};

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_DefaultAuto DISABLED_DefaultAuto
#else
#define MAYBE_DefaultAuto DefaultAuto
#endif
//
// Verify the test infrastructure works - we can touch-scroll the page and get a
// touchcancel as expected.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_DefaultAuto) {
  LoadURL(kTouchActionDataURL);

  bool wait_until_scrolled = false;
  DoTouchScrollAndCheckScrollHeight(gfx::Point(50, 50), gfx::Vector2d(0, 45),
                                    wait_until_scrolled, 10200,
                                    gfx::Vector2d(0, 45), kNoJankTime);

  EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchstart"));
  EXPECT_GE(EvalJs(shell(), "eventCounts.touchmove").ExtractInt(), 1);
  EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchend"));
  EXPECT_EQ(0, EvalJs(shell(), "eventCounts.touchcancel"));
}

// Verify that touching a touch-action: none region disables scrolling and
// enables all touch events to be sent.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER)
#define MAYBE_TouchActionNone DISABLED_TouchActionNone
#else
#define MAYBE_TouchActionNone TouchActionNone
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_TouchActionNone) {
  LoadURL(kTouchActionDataURL);

  bool wait_until_scrolled = false;
  DoTouchScrollAndCheckScrollHeight(gfx::Point(50, 150), gfx::Vector2d(0, 45),
                                    wait_until_scrolled, 10200,
                                    gfx::Vector2d(0, 0), kNoJankTime);

  EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchstart"));
  EXPECT_GE(EvalJs(shell(), "eventCounts.touchmove").ExtractInt(), 1);
  EXPECT_EQ(1, EvalJs(shell(), "eventCounts.touchend"));
  EXPECT_EQ(0, EvalJs(shell(), "eventCounts.touchcancel"));
}

// Tests that when double-tap zoom is disabled, the GestureTapUnconfirmed event
// is converted to a GestureTap.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       GestureTapUnconfirmedNotSentWhenDoubleTapIsDisabled) {
  const std::string kDoubleTapZoomDataURL = R"HTML(
    data:text/html,<!DOCTYPE html>
    <style>
      html, body {
        margin: 0;
      }
      .target {
        width: 100vw;
        height: 100vh;
        touch-action: pan-y;
      }
      .spacer { height: 10000px; }
    </style>
    <div class=target></div>
    <div class=spacer></div>
    <script>
      document.title='ready';
    </script>)HTML";
  LoadURL(kDoubleTapZoomDataURL);

  // A touch cancel to initialize gesture provider in Aura.
  DoTouchCancel();

  ui::GestureDetector* gesture_detector =
      GetWidgetHost()
          ->GetView()
          ->GetFilteredGestureProviderForTesting()
          ->GetGestureDetectorForTesting();
  auto task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  gesture_detector->SetGestureTimeoutHandlerTaskRunnerForTesting(task_runner);

  GestureTapEventObserver tap_observer;
  GetWidgetHost()->AddInputEventObserver(&tap_observer);

  run_loop_ = std::make_unique<base::RunLoop>();

  SyntheticTapGestureParams params;
  params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput;
  params.position = gfx::PointF(50, 50);
  params.duration_ms = 100;
  GetWidgetHost()->QueueSyntheticGesture(
      std::make_unique<SyntheticTapGesture>(params),
      base::BindOnce(&TouchActionBrowserTest::OnSyntheticGestureCompleted,
                     base::Unretained(this)));

  run_loop_->Run();

  // After the tap gesture has been completed, the tap event should have been
  // seen exactly once.
  EXPECT_EQ(1, tap_observer.num_gesture_tap_seen());

  // Advance the task runner clock by timeout delay, to make sure the tap
  // timeout task runs.
  task_runner->FastForwardBy(gesture_detector->GetDoubleTapTimeoutForTesting());

  // No extra tap is seen by input observers.
  EXPECT_EQ(1, tap_observer.num_gesture_tap_seen());

  GetWidgetHost()->RemoveInputEventObserver(&tap_observer);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanYMainThreadJanky DISABLED_PanYMainThreadJanky
#else
#define MAYBE_PanYMainThreadJanky PanYMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanYMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  bool wait_until_scrolled = false;
  DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(0, 45),
                                    wait_until_scrolled, 10000,
                                    gfx::Vector2d(0, 45), kShortJankTime);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXMainThreadJanky DISABLED_PanXMainThreadJanky
#else
#define MAYBE_PanXMainThreadJanky PanXMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  bool wait_until_scrolled = false;
  DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 25), gfx::Vector2d(45, 0),
                                    wait_until_scrolled, 10000,
                                    gfx::Vector2d(45, 0), kShortJankTime);
}

#if BUILDFLAG(IS_ANDROID)
#define MAYBE_PanXAtYAreaWithTimeout PanXAtYAreaWithTimeout
#else
#define MAYBE_PanXAtYAreaWithTimeout DISABLED_PanXAtYAreaWithTimeout
#endif
// When touch ack timeout is triggered, the panx gesture will be allowed even
// though we touch the pany area.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXAtYAreaWithTimeout) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(45, 0),
                                    true, 10000, gfx::Vector2d(45, 0),
                                    kLongJankTime);
}

#if BUILDFLAG(IS_ANDROID)
#define MAYBE_TwoFingerPanXAtYAreaWithTimeout TwoFingerPanXAtYAreaWithTimeout
#else
#define MAYBE_TwoFingerPanXAtYAreaWithTimeout \
  DISABLED_TwoFingerPanXAtYAreaWithTimeout
#endif
// When touch ack timeout is triggered, the panx gesture will be allowed even
// though we touch the pany area.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       MAYBE_TwoFingerPanXAtYAreaWithTimeout) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTwoFingerTouchScroll(true, gfx::Vector2d(20, 0));
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXYMainThreadJanky DISABLED_PanXYMainThreadJanky
#else
#define MAYBE_PanXYMainThreadJanky PanXYMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_PanXYMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  bool wait_until_scrolled = false;
  DoTouchScrollAndCheckScrollHeight(gfx::Point(75, 60), gfx::Vector2d(45, 45),
                                    wait_until_scrolled, 10000,
                                    gfx::Vector2d(45, 45), kShortJankTime);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXYAtXAreaMainThreadJanky DISABLED_PanXYAtXAreaMainThreadJanky
#else
#define MAYBE_PanXYAtXAreaMainThreadJanky PanXYAtXAreaMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       MAYBE_PanXYAtXAreaMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 25), gfx::Vector2d(45, 20),
                                    true, 10000, gfx::Vector2d(45, 0),
                                    kShortJankTime);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXYAtYAreaMainThreadJanky DISABLED_PanXYAtYAreaMainThreadJanky
#else
#define MAYBE_PanXYAtYAreaMainThreadJanky PanXYAtYAreaMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       MAYBE_PanXYAtYAreaMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTouchScrollAndCheckScrollHeight(gfx::Point(25, 125), gfx::Vector2d(20, 45),
                                    true, 10000, gfx::Vector2d(0, 45),
                                    kShortJankTime);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky \
  DISABLED_PanXYAtAutoYOverlapAreaMainThreadJanky
#else
#define MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky \
  PanXYAtAutoYOverlapAreaMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       MAYBE_PanXYAtAutoYOverlapAreaMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTouchScrollAndCheckScrollHeight(gfx::Point(75, 125), gfx::Vector2d(20, 45),
                                    true, 10000, gfx::Vector2d(0, 45),
                                    kShortJankTime);
}

// TODO(crbug.com/40236573): Fix Mac failures.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER) ||       \
    defined(MEMORY_SANITIZER) || defined(LEAK_SANITIZER) || \
    defined(THREAD_SANITIZER) || BUILDFLAG(IS_MAC)
#define MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky \
  DISABLED_PanXYAtAutoXOverlapAreaMainThreadJanky
#else
#define MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky \
  PanXYAtAutoXOverlapAreaMainThreadJanky
#endif
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest,
                       MAYBE_PanXYAtAutoXOverlapAreaMainThreadJanky) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTouchScrollAndCheckScrollHeight(gfx::Point(125, 75), gfx::Vector2d(45, 20),
                                    true, 10000, gfx::Vector2d(45, 0),
                                    kShortJankTime);
}

// TODO(crbug.com/41422733): Make this test work on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_TwoFingerPanYDisallowed DISABLED_TwoFingerPanYDisallowed
#else
#define MAYBE_TwoFingerPanYDisallowed TwoFingerPanYDisallowed
#endif
// Test that two finger panning is treated as pinch zoom and is disallowed when
// touching the pan-y area.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, MAYBE_TwoFingerPanYDisallowed) {
  LoadURL(kTouchActionURLWithOverlapArea);

  DoTwoFingerPan();
  CheckScrollOffset(true, gfx::Vector2d(0, 0));
}

namespace {

const std::string kDoubleTapZoomDataURL = R"HTML(
    data:text/html,<!DOCTYPE html>
    <meta name='viewport' content='width=device-width'/>
    <style>
      html, body {
        margin: 0;
      }
      .spacer { height: 10000px; }
      .touchaction { width: 75px; height: 75px; touch-action: none; }
    </style>
    <div class="touchaction"></div>
    <div class=spacer></div>
    <script>
      document.title='ready';
    </script>)HTML";

}  // namespace

// Test that |touch-action: none| correctly blocks a double-tap and drag zoom
// gesture.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTest, BlockDoubleTapDragZoom) {
  LoadURL(kDoubleTapZoomDataURL.c_str());

  ASSERT_EQ(1, EvalJs(shell(), "window.visualViewport.scale"));

  DoDoubleTapDragZoom();

  EXPECT_EQ(1, EvalJs(shell(), "window.visualViewport.scale"));
}

namespace {

constexpr char kContentEditableDataURL[] = R"HTML(
    data:text/html,<!DOCTYPE html>
    <meta name='viewport' content='width=device-width'/>
    <style>
    html, body {
      margin: 0;
    }
    </style>
    <div id='container' contenteditable style='height: 200px'>
      11111111111111111111111111111111
    </div>
    <div class=spacer style='height: 10000px'></div>
    <script>
      let container = document.getElementById('container');
      container.focus();
      let textNode = container.childNodes[0];
      window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32);
      document.title='ready';
    </script>)HTML";

constexpr char kContentEditableHorizontalScrollableDataURL[] = R"HTML(
    data:text/html,<!DOCTYPE html>
    <meta name='viewport' content='width=device-width'/>
    <style>
    html, body {
      margin: 0;
    }
    %23scroller {
      height: 220px;
      width: 100px;
      white-space: nowrap;
      overflow-x: scroll;
    }
    %23container {
      height: 200px;
      width: 500px;
      display: inline-block;
    }
    </style>
    <div id="scroller">
      <div id='container' contenteditable>
        11111111111111111111111111111111
      </div>
    </div>
    <div class=spacer style='height: 10000px'></div>
    <script>
      let container = document.getElementById('container');
      container.focus();
      let textNode = container.childNodes[0];
      window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32);
      document.title='ready';
    </script>)HTML";

constexpr char kContentEditableNonPassiveHandlerDataURL[] = R"HTML(
    data:text/html,<!DOCTYPE html>
    <meta name='viewport' content='width=device-width'/>
    <style>
    html, body {
      margin: 0;
    }
    </style>
    <div id='container' contenteditable style='height: 200px'>
      11111111111111111111111111111111
    </div>
    <div class=spacer style='height: 10000px'></div>
    <script>
      let container = document.getElementById('container');
      container.focus();
      let textNode = container.childNodes[0];
      window.getSelection().setBaseAndExtent(textNode, 32, textNode, 32);
      container.addEventListener("touchstart", function(event) {
        event.preventDefault();
      }, {passive: false});
      document.title='ready';
    </script>)HTML";

constexpr char kInputTagCursorControl[] = R"HTML(
    data:text/html,<!DOCTYPE html>
    <meta name='viewport' content='width=device-width'/>
    <style>
    html, body {
      margin: 0;
    }
    input {
      height: 20px;
      padding: 0px;
      margin: 0px;
      border: 0px;
    }
    </style>
    <input type="text" id="container" value="11111111111111111111111111111111"
      size=%d>
    <div class=spacer style='height: 10000px'></div>
    <script>
      let container = document.getElementById('container');
      container.focus();
      container.setSelectionRange(32, 32);
      document.title='ready';
    </script>)HTML";

}  // namespace

class TouchActionBrowserTestEnableCursorControl
    : public TouchActionBrowserTest {
 public:
  TouchActionBrowserTestEnableCursorControl() {
    feature_list_.InitWithFeatures({::features::kSwipeToMoveCursor}, {});
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// Perform a horizontal swipe over an editable element from right to left.
// Ensure the swipe is interpreted as a cursor control movement, rather than a
// scroll, and changes the selection.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       BasicCursorControl) {
  if (!::features::IsSwipeToMoveCursorEnabled())
    return;
  LoadURL(kContentEditableDataURL);

  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset"));
  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset"));

  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  const int anchor_offset =
      EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt();

  EXPECT_EQ(anchor_offset,
            EvalJs(shell(), "window.getSelection().focusOffset"));
  EXPECT_GT(32, anchor_offset);
}

// Perform a horizontal swipe over an editable element from right to left (the
// element shift to left), the element is inside of a horizontal scroller.
// Ensure the swipe is interpreted as a normal scroll, selection should not be
// changed and scroll should happen.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       NoCursorControlForHorizontalScrollable) {
  if (!::features::IsSwipeToMoveCursorEnabled())
    return;
  LoadURL(kContentEditableHorizontalScrollableDataURL);

  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset"));
  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset"));

  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  const int anchor_offset =
      EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt();

  EXPECT_EQ(anchor_offset,
            EvalJs(shell(), "window.getSelection().focusOffset"));
  EXPECT_EQ(32, anchor_offset);
  EXPECT_LT(0.f,
            EvalJs(shell(), "document.getElementById('scroller').scrollLeft")
                .ExtractDouble());
}

// Perform a horizontal swipe over an editable element from right to left
// Ensure the swipe is not triggering cursor control.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       NoCursorControlForNonPassiveLisenter) {
  if (!::features::IsSwipeToMoveCursorEnabled())
    return;
  LoadURL(kContentEditableNonPassiveHandlerDataURL);

  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().anchorOffset"));
  EXPECT_EQ(32, EvalJs(shell(), "window.getSelection().focusOffset"));

  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  const int anchor_offset =
      EvalJs(shell(), "window.getSelection().anchorOffset").ExtractInt();

  EXPECT_EQ(anchor_offset,
            EvalJs(shell(), "window.getSelection().focusOffset"));
  EXPECT_EQ(32, anchor_offset);
}

// Perform a horizontal swipe over an input element from right to left.
// Ensure the swipe is interpreted as a cursor control movement, rather than a
// scroll, and changes the selection.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       CursorControlOnInput) {
  if (!::features::IsSwipeToMoveCursorEnabled())
    return;
  // input size larger than the text size, not horizontally scrollable.
  LoadURL(base::StringPrintf(kInputTagCursorControl, 40).c_str());

  EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart"));
  EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd"));

  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  const int selection_start =
      EvalJs(shell(), "container.selectionStart").ExtractInt();

  EXPECT_EQ(selection_start, EvalJs(shell(), "container.selectionEnd"));
  EXPECT_GT(32, selection_start);
}

// Perform a horizontal swipe over an horizontal scrollable input element from
// right to left. Ensure the swipe is doing scrolling other than cursor control.
IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       NoCursorControlOnHorizontalScrollableInput) {
  if (!::features::IsSwipeToMoveCursorEnabled())
    return;
  // Make the input size smaller than the text size, so it horizontally
  // scrollable.
  LoadURL(base::StringPrintf(kInputTagCursorControl, 20).c_str());

  EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart"));
  EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd"));

  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  const int selection_start =
      EvalJs(shell(), "container.selectionStart").ExtractInt();

  EXPECT_EQ(selection_start, EvalJs(shell(), "container.selectionEnd"));
  EXPECT_EQ(32, selection_start);
}

#if BUILDFLAG(IS_ANDROID)
class ScrollBeginObserver : public RenderWidgetHost::InputEventObserver {
 public:
  ScrollBeginObserver(RenderWidgetHost& rwh,
                      RenderWidgetHostViewAndroid& rwhv_android,
                      base::OnceClosure quit_closure)
      : rwh_(rwh),
        rwhv_android_(rwhv_android),
        quit_closure_(std::move(quit_closure)) {
    rwh_->AddInputEventObserver(this);
  }

  ~ScrollBeginObserver() override { rwh_->RemoveInputEventObserver(this); }

  void OnInputEvent(const RenderWidgetHost&,
                    const blink::WebInputEvent& event,
                    InputEventSource) override {
    if (event.GetType() != blink::WebInputEvent::Type::kGestureScrollBegin) {
      return;
    }
    // Insertion handles were active due to first scroll and should become
    // inactive after view is removed from hierarchy.
    EXPECT_EQ(rwhv_android_->touch_selection_controller()->active_status(),
              ui::TouchSelectionController::ActiveStatus::kInsertionActive);
    rwhv_android_->UpdateNativeViewTree(nullptr, nullptr);
    EXPECT_EQ(rwhv_android_->touch_selection_controller()->active_status(),
              ui::TouchSelectionController::ActiveStatus::kInactive);
  }

  void OnInputEventAck(const RenderWidgetHost&,
                       blink::mojom::InputEventResultSource,
                       blink::mojom::InputEventResultState,
                       const blink::WebInputEvent& event) override {
    if (event.GetType() != blink::WebInputEvent::Type::kGestureScrollBegin) {
      return;
    }
    // Post a task so that it's guaranteed
    // TouchSelectionControllerInputObserver would have also processed this
    // problematic ack which comes after view having already been removed from
    // hierarchy.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(quit_closure_));
  }

 private:
  raw_ref<RenderWidgetHost> rwh_;
  raw_ref<RenderWidgetHostViewAndroid> rwhv_android_;
  base::OnceClosure quit_closure_;
};

IN_PROC_BROWSER_TEST_F(TouchActionBrowserTestEnableCursorControl,
                       CursorControlDetachViewMidScroll) {
  if (!::features::IsSwipeToMoveCursorEnabled()) {
    return;
  }

  LoadURL(base::StringPrintf(kInputTagCursorControl, 40).c_str());

  EXPECT_EQ(32, EvalJs(shell(), "container.selectionStart"));
  EXPECT_EQ(32, EvalJs(shell(), "container.selectionEnd"));

  // Do a scroll over input field to activate insertion handle.
  DoTouchScroll(gfx::Point(85, 5), gfx::Vector2d(40, 0),
                /* wait_until_scrolled*/ false, gfx::Vector2d(0, 0),
                kNoJankTime);

  auto* rwhv_android =
      static_cast<RenderWidgetHostViewAndroid*>(GetWidgetHost()->GetView());
  ASSERT_TRUE(rwhv_android);
  EXPECT_EQ(rwhv_android->touch_selection_controller()->active_status(),
            ui::TouchSelectionController::ActiveStatus::kInsertionActive);

  base::RunLoop run_loop;
  ScrollBeginObserver observer(*GetWidgetHost(), *rwhv_android,
                               run_loop.QuitClosure());

  float page_scale_factor = GetWidgetHost()
                                ->render_frame_metadata_provider()
                                ->LastRenderFrameMetadata()
                                .page_scale_factor;
  if (page_scale_factor == 0) {
    page_scale_factor = 1.0f;
  }
  gfx::PointF touch_point(85, 5);
  touch_point.Scale(page_scale_factor);

  SyntheticSmoothScrollGestureParams params;
  params.gesture_source_type = content::mojom::GestureSourceType::kTouchInput;
  params.anchor = touch_point;
  params.distances.emplace_back(-80, 0);

  GetWidgetHost()->QueueSyntheticGesture(
      std::make_unique<SyntheticSmoothScrollGesture>(params),
      base::DoNothing());

  // We expect the run loop be exited when ScrollBeginObserver processes
  // ScrollBegin ack.
  run_loop.Run();
  EXPECT_TRUE(rwhv_android->GetTouchSelectionControllerInputObserver()
                  ->HasSeenScrollBeginAckForTesting());
}
#endif  // BUILDFLAG(IS_ANDROID)

}  // namespace content
