// 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.

#include <memory>
#include <string>

#include "base/json/json_reader.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/web_contents/web_contents_impl.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/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_content_browser_client.h"
#include "content/shell/common/shell_switches.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/scroll/scroll_into_view_params.mojom.h"
#include "third_party/re2/src/re2/re2.h"
#include "ui/events/event_constants.h"
#include "url/gurl.h"

#if defined(USE_AURA)
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#endif

#define EXPECT_TRUE_OR_FAIL(condition) \
  EXPECT_TRUE(condition);              \
  if (!condition)                      \
    return false;

namespace content {

namespace {

// Test variants

// kLocalFrame will force all remote frames in a test to be local.
enum TestFrameType { kLocalFrame, kRemoteFrame };

// Tests run with both Left-to-Right and Right-to-Left writing modes.
enum TestWritingMode { kLTR, kRTL };

// What kind of scroll into view to invoke, via JavaScript binding
// (element.scrollIntoView), using the InputHandler
// ScrollFocusedEditableNodeIntoView method, or via setting an OSK inset.
enum TestInvokeMethod { kJavaScript, kInputHandler, kAuraOnScreenKeyboard };

[[maybe_unused]] std::string DescribeFrameType(
    const testing::TestParamInfo<TestFrameType>& info) {
  std::string frame_type;
  switch (info.param) {
    case kLocalFrame: {
      frame_type = "LocalFrame";
    } break;
    case kRemoteFrame: {
      frame_type = "RemoteFrame";
    } break;
  }
  return frame_type;
}

blink::mojom::FrameWidgetInputHandler* GetInputHandler(FrameTreeNode* node) {
  return node->current_frame_host()
      ->GetRenderWidgetHost()
      ->GetFrameWidgetInputHandler();
}

// Will block from the destructor until a ScrollFocusedEditableNodeIntoView has
// completed. This must be called with the root frame tree node since that's
// where the ScrollIntoView and PageScaleAnimation will bubble to.
class ScopedFocusScrollWaiter {
 public:
  explicit ScopedFocusScrollWaiter(FrameTreeNode* node) {
    DCHECK(node->IsOutermostMainFrame());
    GetInputHandler(node)->WaitForPageScaleAnimationForTesting(
        run_loop_.QuitClosure());
  }

  ~ScopedFocusScrollWaiter() { run_loop_.Run(); }

 private:
  base::RunLoop run_loop_;
};

// While this is in scope, causes the TextInputManager of the given WebContents
// to always return nullptr. This effectively blocks the IME from receiving any
// events from the renderer. Note: RenderWidgetHostViewBase caches this value
// so for this to work it must be constructed before the target page is
// constructed.
class ScopedSuppressImeEvents {
 public:
  explicit ScopedSuppressImeEvents(WebContentsImpl* web_contents)
      : web_contents_(web_contents->GetWeakPtr()) {
    web_contents->set_suppress_ime_events_for_testing(true);
  }

  ~ScopedSuppressImeEvents() {
    if (!web_contents_)
      return;

    static_cast<WebContentsImpl*>(web_contents_.get())
        ->set_suppress_ime_events_for_testing(false);
  }

  base::WeakPtr<WebContents> web_contents_;
};

// Interceptor that can be used to verify calls to
// ScrollRectToVisibleInParentFrame on the LocalFrameHost interface.
class ScrollRectToVisibleInParentFrameInterceptor
    : public blink::mojom::LocalFrameHostInterceptorForTesting {
 public:
  ScrollRectToVisibleInParentFrameInterceptor() = default;
  ~ScrollRectToVisibleInParentFrameInterceptor() override = default;

  void Init(RenderFrameHostImpl* render_frame_host) {
    render_frame_host_ = render_frame_host;
    std::ignore = render_frame_host_->local_frame_host_receiver_for_testing()
                      .SwapImplForTesting(this);
  }

  blink::mojom::LocalFrameHost* GetForwardingInterface() override {
    return render_frame_host_;
  }

  void ScrollRectToVisibleInParentFrame(
      const gfx::RectF& rect_to_scroll,
      blink::mojom::ScrollIntoViewParamsPtr params) override {
    has_called_method_ = true;
  }

  bool HasCalledScrollRectToVisibleInParentFrame() const {
    return has_called_method_;
  }

 private:
  raw_ptr<RenderFrameHostImpl> render_frame_host_;
  bool has_called_method_ = false;
};

// Test harness for ScrollIntoView related browser tests. These tests are
// mainly concerned with behavior of scroll into view related functionality
// across remote frames. This harness depends on
// cross_site_scroll_into_view_factory.html, which is based on
// cross_site_iframe_factory.html.
//
// cross_site_scroll_into_view_factory.html builds a frame tree from its given
// argument, allowing only a single child frame in each frame. The inner most
// frame adds an <input> element which can be used to call
// ScrollFocusedEditableNodeIntoView.
//
// Each test starts by performing a non-scrolling focus on the <input> element.
// It then performs a scroll into view (either via JavaScript bindings or
// content API) and ensures the caret is within a vertically centered band of
// the viewport.
class ScrollIntoViewBrowserTestBase : public ContentBrowserTest {
 public:
  ScrollIntoViewBrowserTestBase() = default;
  ~ScrollIntoViewBrowserTestBase() override = default;

  virtual bool IsForceLocalFrames() const = 0;
  virtual bool IsWritingModeLTR() const = 0;
  virtual TestInvokeMethod GetInvokeMethod() const = 0;
  virtual net::EmbeddedTestServer* server() { return embedded_test_server(); }

  void SetUpOnMainThread() override {
    ContentBrowserTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(server()->Start());

    suppress_ime_ = std::make_unique<ScopedSuppressImeEvents>(web_contents());
  }

  void TearDownOnMainThread() override {
    suppress_ime_.reset();

    ContentBrowserTest::TearDownOnMainThread();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ContentBrowserTest::SetUpCommandLine(command_line);
    IsolateAllSitesForTesting(command_line);

    // Need this to control page scale factor via script or check for root
    // scroller.
    command_line->AppendSwitch(switches::kExposeInternalsForTesting);
  }

  WebContentsImpl* web_contents() {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  FrameTreeNode* InnerMostFrameTreeNode() {
    FrameTreeNode* inner_most_node = nullptr;
    ForEachFrameFromRootToInnerMost(
        [&inner_most_node](FrameTreeNode* node) { inner_most_node = node; });
    return inner_most_node;
  }

  FrameTreeNode* RootFrameTreeNode() {
    return web_contents()->GetPrimaryFrameTree().root();
  }

  // Gets the bounding client rect from the element returned via the given query
  // string (i.e. as found via document.querySelector).
  gfx::RectF GetClientRect(FrameTreeNode* node, std::string query) {
    auto result = EvalJs(node, JsReplace(R"JS(
      JSON.stringify(document.querySelector($1).getBoundingClientRect());
    )JS",
                                         query));
    absl::optional<base::Value> value =
        base::JSONReader::Read(result.ExtractString());
    CHECK(value.has_value());
    CHECK(value->is_dict());

    const base::Value::Dict& dict = value->GetDict();
    absl::optional<double> x = dict.FindDouble("x");
    absl::optional<double> y = dict.FindDouble("y");
    absl::optional<double> width = dict.FindDouble("width");
    absl::optional<double> height = dict.FindDouble("height");

    CHECK(x);
    CHECK(y);
    CHECK(width);
    CHECK(height);

    return gfx::RectF(*x, *y, *width, *height);
  }

  gfx::RectF GetLayoutViewportRect() {
    return GetClientRect(RootFrameTreeNode(), ".layoutViewport");
  }

  struct VisualViewport {
    // These are the values coming from the window.visualViewport object. Note:
    // the width/height are _relative to the root frame_, meaning they decrease
    // as `scale` increases.
    double offset_left;
    double offset_top;
    double width;
    double height;
    double scale;
    double page_left;
    double page_top;

    // This is the _unscaled_ rect computed from values above.
    gfx::RectF rect;
  };

  VisualViewport GetVisualViewport() {
    auto result = EvalJs(RootFrameTreeNode(), R"JS(
      JSON.stringify({
        offsetLeft: visualViewport.offsetLeft,
        offsetTop: visualViewport.offsetTop,
        width: visualViewport.width,
        height: visualViewport.height,
        scale: visualViewport.scale,
        pageLeft: visualViewport.pageLeft,
        pageTop: visualViewport.pageTop});
    )JS");

    absl::optional<base::Value> value =
        base::JSONReader::Read(result.ExtractString());
    CHECK(value.has_value());
    CHECK(value->is_dict());

    const base::Value::Dict& dict = value->GetDict();
    absl::optional<double> offset_left = dict.FindDouble("offsetLeft");
    absl::optional<double> offset_top = dict.FindDouble("offsetTop");
    absl::optional<double> width = dict.FindDouble("width");
    absl::optional<double> height = dict.FindDouble("height");
    absl::optional<double> scale = dict.FindDouble("scale");
    absl::optional<double> page_left = dict.FindDouble("pageLeft");
    absl::optional<double> page_top = dict.FindDouble("pageTop");

    CHECK(offset_left);
    CHECK(offset_top);
    CHECK(width);
    CHECK(height);
    CHECK(scale);
    CHECK(page_left);
    CHECK(page_top);

    VisualViewport values;
    values.offset_left = *offset_left;
    values.offset_top = *offset_top;
    values.width = *width;
    values.height = *height;
    values.scale = *scale;
    values.page_left = *page_left;
    values.page_top = *page_top;

    values.rect = gfx::RectF(
        gfx::PointF(),
        gfx::ScaleSize(gfx::SizeF(values.width, values.height), values.scale));

    return values;
  }

  // Gets the bounding rect of the caret (taken from the <input> element in the
  // inner-most frame) as it appears in the root most viewport.
  //
  // This accounts for clipping in each intervening frame.
  //
  // WARNING: This doesn't take transforms on the frames into account. It also
  // makes a guess on where the caret is, based on the writing-mode of the
  // document.
  gfx::RectF GetCaretRectInViewport() {
    FrameTreeNode* node = InnerMostFrameTreeNode();
    gfx::RectF rect = GetClientRect(node, "input");

    // Take either the left-most or right-most portion of the input box as an
    // estimate of the caret; based on the writing-mode of the page.
    constexpr float kCaretBoxWidth = 30.f;
    if (IsWritingModeLTR()) {
      rect.Inset(gfx::InsetsF::TLBR(0, 0, 0, rect.width() - kCaretBoxWidth));
    } else {
      rect.Inset(gfx::InsetsF::TLBR(0, rect.width() - kCaretBoxWidth, 0, 0));
    }

    EXPECT_EQ("", EvalJs(node, "document.querySelector('input').value"))
        << "Caret location is assumed based on empty <input> value";

    // If `node` is a child frame, we'll convert rect up the ancestor frame
    // chain, clipping to each frame rect.
    FrameTreeNode* frame =
        FrameTreeNode::From(node->GetParentOrOuterDocument());
    while (frame) {
      gfx::RectF parent_rect = GetClientRect(frame, "#childframe");
      rect.Offset(parent_rect.OffsetFromOrigin());

      rect = gfx::IntersectRects(parent_rect, rect);

      frame = FrameTreeNode::From(frame->GetParentOrOuterDocument());
    }

    gfx::RectF root_frame_rect = GetLayoutViewportRect();
    root_frame_rect.set_origin(gfx::PointF());

    rect = gfx::IntersectRects(root_frame_rect, rect);

    VisualViewport visual_viewport = GetVisualViewport();
    rect.Offset(-visual_viewport.offset_left, -visual_viewport.offset_top);
    rect.Scale(visual_viewport.scale);

    rect = gfx::IntersectRects(visual_viewport.rect, rect);

    return rect;
  }

  // Returns the rect within the visual viewport where, if the caret ends up in
  // after a scroll into view, we'll consider it a success.
  gfx::RectF GetAcceptableCaretRect() {
    gfx::RectF caret_in_viewport = GetCaretRectInViewport();
    VisualViewport visual_viewport = GetVisualViewport();

    gfx::RectF rect = visual_viewport.rect;

    // Vertically, the caret should be roughly centered (40px of wiggleroom,
    // e.g. for scrollbars, in either direction) in the viewport.
    const float kVerticalInset =
        ((rect.height() - caret_in_viewport.height()) / 2.f) - 40.f;

    // Horizontally, we're less picky, as long as the caret is in the viewport.
    // TODO(bokan): The constants used in
    // WebViewImpl::ComputeScaleAndScrollForEditableElementRects are somewhat
    // inscrutible and dimension dependent (which is a problem when this test
    // runs on Android and the width depends on the device). Ideally we'd be
    // able to ensure the caret appears in the right region of the viewport.
    const float kHorizontalInset = 0.f;

    rect.Inset(gfx::InsetsF::VH(kVerticalInset, kHorizontalInset));
    return rect;
  }

  // Modifies the frame tree string as needed for different test parameters.
  GURL GetMainURLForFrameTree(std::string frame_tree_string) {
    // To make things simple, remove any whitespace or empty attribute lists.
    re2::RE2::GlobalReplace(&frame_tree_string, "\\s*", "");
    re2::RE2::GlobalReplace(&frame_tree_string, "{}", "");

    // If we're in a local frame test variant, replace all site strings with
    // "siteA".
    if (IsForceLocalFrames()) {
      re2::RE2::GlobalReplace(&frame_tree_string, "site[A-Z]", "siteA");
    }

    // For RTL tests, add {RTL} attribute on each frame.
    if (!IsWritingModeLTR()) {
      // Prepend RTL to any existing attribute lists.
      re2::RE2::GlobalReplace(&frame_tree_string, "{(.*?)}", "{RTL,\\1}");

      // Add an attribute list with RTL to sites without an existing list.
      {
        std::string regex =
            // Match any site name (store in capture group 1).
            "(site[A-Z])"

            // That's followed by a non-{ character or line-end (store in
            // capture group 2).
            "([^{]|$)";

        re2::RE2::GlobalReplace(&frame_tree_string, regex, "\\1{RTL}\\2");
      }
    }

    return server()->GetURL(
        "a.test", base::StrCat({"/cross_site_scroll_into_view_factory.html?",
                                frame_tree_string}));
  }

  // Simualte a keyboard coming up, insetting the viewport by its height.
  void SetAuraOnScreenKeyboardInset(int keyboard_height) {
#if defined(USE_AURA)
    RenderWidgetHostViewBase* inner_most_view = InnerMostFrameTreeNode()
                                                    ->current_frame_host()
                                                    ->GetRenderWidgetHost()
                                                    ->GetView();

    RenderWidgetHostViewBase* root_view = inner_most_view->GetRootView();

    // Set the pointer type to simulate the keyboard appearing as a result of
    // the user tapping on an editable element.
    root_view->SetLastPointerType(ui::EventPointerType::kTouch);
    root_view->SetInsets(gfx::Insets::TLBR(0, 0, keyboard_height, 0));
#else
    NOTREACHED();
#endif
  }

  // Calls `func` with each FrameTreeNode in the page, starting from the root
  // and descending into the inner most frame, traversing frame tree boundaries
  // such as fenced frames/portals.
  template <typename Function>
  void ForEachFrameFromRootToInnerMost(const Function& func) {
    FrameTreeNode* node = web_contents()->GetPrimaryFrameTree().root();
    while (node) {
      bool is_proxy_for_inner_frame_tree =
          node->current_frame_host()->inner_tree_main_frame_tree_node_id() !=
          FrameTreeNode::kFrameTreeNodeInvalidId;

      // The functor isn't called for the placeholder FrameTreeNode, it'll be
      // called on the inner tree's root.
      if (!is_proxy_for_inner_frame_tree)
        func(node);

      if (node->child_count()) {
        CHECK_EQ(
            node->current_frame_host()->inner_tree_main_frame_tree_node_id(),
            FrameTreeNode::kFrameTreeNodeInvalidId);
        // These tests never have multiple child frames.
        CHECK_EQ(node->child_count(), 1ul);
        node = node->child_at(0);
      } else if (is_proxy_for_inner_frame_tree) {
        CHECK_EQ(node->child_count(), 0ul);
        node = FrameTreeNode::GloballyFindByID(
            node->current_frame_host()->inner_tree_main_frame_tree_node_id());
      } else {
        node = nullptr;
      }
    }
  }

  // Cross origin frames may throttle their lifecycle when not visible.
  // This method ensure each frame is brought into view and a frame produced to
  // ensure up-to-date layout.
  void EnsureAllFramesCompletedLifecycle() {
    // Wait until each frame presents a CompositorFrame and then scroll its
    // child frame (if it has one) into view, so that it is unthrottled and
    // able to generate and present CompositorFrames.
    ForEachFrameFromRootToInnerMost([](FrameTreeNode* node) {
      base::RunLoop loop;
      node->current_frame_host()->InsertVisualStateCallback(
          base::BindLambdaForTesting(
              [&loop](bool visual_state_updated) { loop.Quit(); }));
      loop.Run();

      EXPECT_TRUE(ExecJs(node, R"JS(
              if (document.getElementById('childframe'))
                document.getElementById('childframe').scrollIntoView()
          )JS"));
    });

    // Now that each frame has been in view and produced a frame, reset each
    // scroll offset.
    ForEachFrameFromRootToInnerMost([](FrameTreeNode* node) {
      EXPECT_TRUE(ExecJs(node, "window.scrollTo(0, 0)"));
    });
  }

  // For frame_tree syntax see tree_parser_util.js.
  // These tests place two additional restrictions to make some simplifying
  // assumptions:
  //
  //  * All site names must start with "site" and be followed by [A-Z].
  //  * Allow only one or zero children. That is, siteA(siteB) is valid but
  //    siteA(siteB, siteB) is not.
  //
  // For valid arguments, see comments in
  // cross_site_scroll_into_view_factory.html
  bool SetupTest(std::string frame_tree) {
    const GURL kMainUrl(GetMainURLForFrameTree(frame_tree));

    if (!NavigateToURL(shell(), kMainUrl))
      return false;

    EnsureAllFramesCompletedLifecycle();

    VisualViewport viewport = GetVisualViewport();
    double page_scale_factor_before = viewport.scale;
    double page_left_before = viewport.page_left;
    double page_top_before = viewport.page_top;

    if (GetInvokeMethod() == kInputHandler ||
        GetInvokeMethod() == kAuraOnScreenKeyboard) {
      // Focus the input for tests that rely on scrolling to a focused element
      // (i.e. via ScrollFocusedEditableNodeIntoView).  Use `preventScroll` to
      // avoid affecting the test via the automatic scrolling caused by focus.
      //
      // Note: normally, an IME (i.e. On-Screen Keyboard) can also attempt to
      // scroll into view (in fact, using ScrollFocusedEditableNodeIntoView
      // which we're trying to test). However, in order to reliably test this
      // across platforms this test harness suppresses IME events so that the
      // on-screen keyboard on a platform that uses one will not activate in
      // response to this. See ScopedSuppressImeEvents above.
      EXPECT_TRUE_OR_FAIL(ExecJs(InnerMostFrameTreeNode(), R"JS(
        document.querySelector('input').focus({preventScroll: true});
      )JS"));
    }

    // The test should start with fresh scroll and scale.
    viewport = GetVisualViewport();
    CHECK_EQ(viewport.scale, page_scale_factor_before);
    CHECK_EQ(viewport.page_left, page_left_before);
    CHECK_EQ(viewport.page_top, page_top_before);

    return true;
  }

  void RunTest() {
    switch (GetInvokeMethod()) {
      case kInputHandler: {
        ScopedFocusScrollWaiter wait_for_scroll_done(RootFrameTreeNode());

        GetInputHandler(InnerMostFrameTreeNode())
            ->ScrollFocusedEditableNodeIntoView();
      } break;
      case kAuraOnScreenKeyboard: {
        ScopedFocusScrollWaiter wait_for_scroll_done(RootFrameTreeNode());
        SetAuraOnScreenKeyboardInset(/*keyboard_height=*/400);
      } break;
      case kJavaScript: {
        RenderFrameSubmissionObserver frame_observer(web_contents());
        EXPECT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
          document.querySelector('input').scrollIntoView({
            behavior: 'instant',
            block: 'center',
            inline: 'center'
          })
        )JS"));
        frame_observer.WaitForScrollOffsetAtTop(
            /*expected_scroll_offset_at_top=*/false);
      } break;
    }

    gfx::RectF caret_in_viewport = GetCaretRectInViewport();
    gfx::RectF acceptable_rect = GetAcceptableCaretRect();

    EXPECT_TRUE(acceptable_rect.Contains(caret_in_viewport))
        << "Expected caret to within [" << acceptable_rect.ToString()
        << "] but caret is [" << caret_in_viewport.ToString() << "]";
  }

 private:
  std::unique_ptr<ScopedSuppressImeEvents> suppress_ime_;
};

// Runs tests in all combinations of Local/Remote frames,
// left-to-right/right-to-left writing modes, and scrollIntoView via
// element.scrollIntoView/InputHandler.ScrollFocusedEditableNodeIntoView. The
// kAuraOnScreenKeyboard is intentionally omitted as it is expected to be
// functionally equivalent to kInputHandler.
class ScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<
          std::tuple<TestFrameType, TestWritingMode, TestInvokeMethod>> {
 public:
  bool IsForceLocalFrames() const override {
    return std::get<0>(GetParam()) == kLocalFrame;
  }

  bool IsWritingModeLTR() const override {
    return std::get<1>(GetParam()) == kLTR;
  }

  TestInvokeMethod GetInvokeMethod() const override {
    return std::get<2>(GetParam());
  }

  static std::string DescribeParams(
      const testing::TestParamInfo<ParamType>& info) {
    auto [frame_type_param, writing_mode_param, invoke_method_param] =
        info.param;

    std::string frame_type;
    switch (frame_type_param) {
      case kLocalFrame: {
        frame_type = "LocalFrame";
      } break;
      case kRemoteFrame: {
        frame_type = "RemoteFrame";
      } break;
    }

    std::string writing_mode;
    switch (writing_mode_param) {
      case kLTR: {
        writing_mode = "LTR";
      } break;
      case kRTL: {
        writing_mode = "RTL";
      } break;
    }

    std::string invoke_method;
    switch (invoke_method_param) {
      case kJavaScript: {
        invoke_method = "JavaScript";
      } break;
      case kInputHandler: {
        invoke_method = "ScrollFocusedEditableNodeIntoView";
      } break;
      case kAuraOnScreenKeyboard: {
        invoke_method = "AuraOnScreenKeyboard";
      } break;
    }

    return base::StringPrintf("%s_%s_%s", frame_type.c_str(),
                              writing_mode.c_str(), invoke_method.c_str());
  }
};

// See comment in SetupTest for frame tree syntax.

// ScrollIntoViewBrowserTest runs with all combinations of multiple parameters
// to test the basic scroll into view machinery so each test instantiates 8
// cases. To avoid an explosion of tests, prefer to add new tests to a more
// specific suite unless the functionality it's testing is likely to differ
// across the various parameters and isn't already covered.

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInSingleNestedFrame) {
  ASSERT_TRUE(SetupTest("siteA(siteB)"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInLocalRoot) {
  ASSERT_TRUE(SetupTest("siteA(siteB(siteA))"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewBrowserTest, EditableInDoublyNestedFrame) {
  ASSERT_TRUE(SetupTest("siteA(siteB(siteC))"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(
    ScrollIntoViewBrowserTest,
    CrossesEditableInDoublyNestedFrameLocalAndRemoteBoundaries) {
  ASSERT_TRUE(SetupTest("siteA(siteA(siteB(siteB)))"));
  RunTest();
}

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    ScrollIntoViewBrowserTest,
    testing::Combine(testing::Values(kLocalFrame, kRemoteFrame),
                     testing::Values(kLTR, kRTL),
                     // kAuraOnScreenKeyboard is intentionally omitted as it is
                     // expected to be functionally equivalent to
                     // kInputHandler.
                     testing::Values(kJavaScript, kInputHandler)),
    ScrollIntoViewBrowserTest::DescribeParams);

#if defined(USE_AURA)

// Tests viewport insetting as a result of keyboard insets. Insetting is only
// used on Aura platforms. The OSK on Android resizes the entire view.
class InsetScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<TestFrameType> {
 public:
  bool IsForceLocalFrames() const override { return GetParam() == kLocalFrame; }
  bool IsWritingModeLTR() const override { return true; }
  TestInvokeMethod GetInvokeMethod() const override {
    return kAuraOnScreenKeyboard;
  }
};

// Ensure that insetting the viewport causes the visual viewport to be resized
// and focused editable scrolled into view. (https://crbug.com/927483)
IN_PROC_BROWSER_TEST_P(InsetScrollIntoViewBrowserTest,
                       InsetsCauseScrollToFocusedEditable) {
  ASSERT_TRUE(SetupTest("siteA(siteB(siteC))"));

  int contents_height = web_contents()->GetViewBounds().height();

  // Ensure the window height is large enough to accommodate the inset and leave
  // some space for a caret. Note: we can't just assume 800x600 because some
  // Windows 7 bots have less than 600px of workspace area available which
  // results in a smaller window.
  ASSERT_GT(contents_height, 450);

  int visual_viewport_height_before = GetVisualViewport().height;
  int layout_viewport_height_before = GetLayoutViewportRect().height();

  // We expect the viewport height to match the WebContents but allow some
  // fuzziness due to differing scrollbars and window decorations on different
  // platforms.
  const int kEpsilon = 30;
  EXPECT_NEAR(visual_viewport_height_before, contents_height, kEpsilon);
  EXPECT_NEAR(layout_viewport_height_before, contents_height, kEpsilon);
  EXPECT_EQ(1.f, GetVisualViewport().scale);

  RunTest();

  // The visualViewport should have been insetted by 400px but not the root
  // frame.
  EXPECT_EQ(visual_viewport_height_before - GetVisualViewport().height, 400);
  EXPECT_EQ(layout_viewport_height_before, GetLayoutViewportRect().height());
  EXPECT_EQ(1.f, GetVisualViewport().scale);

  // The rect where we expect the caret to appear must not not be below the
  // inset region.
  ASSERT_LT(GetAcceptableCaretRect().bottom(), 200);
}

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         InsetScrollIntoViewBrowserTest,
                         testing::Values(kLocalFrame, kRemoteFrame),
                         DescribeFrameType);

#endif

// Only Chrome Android performs a zoom when focusing an editable.
#if BUILDFLAG(IS_ANDROID)

constexpr double kMobileMinimumScale = 0.25;

// Tests zooming behaviors for ScrollFocusedEditableNodeIntoView. These tests
// runs only on Android since that's the only platorm that uses this behavior.
class ZoomScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<TestFrameType> {
 public:
  bool IsForceLocalFrames() const override { return GetParam() == kLocalFrame; }

  bool IsWritingModeLTR() const override { return true; }

  TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};

// A regular "desktop" site (i.e. no viewport <meta> tag) on Chrome Android
// should zoom in on a focused editable so that it's legible.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest, DesktopViewportMustZoom) {
  ASSERT_TRUE(SetupTest("siteA(siteB)"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // Without a viewport tag, the page is considered a "desktop" page so we
  // should enable zooming to a legible scale.
  EXPECT_NEAR(1, GetVisualViewport().scale, 0.05);
}

// Ensure that adding a `width=device-width` viewport <meta> tag disables the
// zooming behavior so that "mobile-friendly" pages do not zoom in on input
// boxes.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
                       MobileViewportDisablesZoom) {
  ASSERT_TRUE(SetupTest("siteA{MobileViewport}(siteB)"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // width=device-width must prevent the zooming behavior.
  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}

// Similar to above, an input in a touch-action region that disables pinch-zoom
// shouldn't cause zoom since it may trap the user at that zoom level.
IN_PROC_BROWSER_TEST_P(ZoomScrollIntoViewBrowserTest,
                       TouchActionNoneDisablesZoom) {
  ASSERT_TRUE(SetupTest("siteA(siteB{TouchActionNone})"));

  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);

  RunTest();

  // touch-action: none must prevent the zooming behavior since the user may
  // not be able to zoom back out.
  EXPECT_EQ(kMobileMinimumScale, GetVisualViewport().scale);
}

class RootScrollerScrollIntoViewBrowserTest
    : public ScrollIntoViewBrowserTestBase {
 public:
  bool IsForceLocalFrames() const override { return false; }
  bool IsWritingModeLTR() const override { return true; }
  TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
};

IN_PROC_BROWSER_TEST_F(RootScrollerScrollIntoViewBrowserTest,
                       FocusInRootScroller) {
  ASSERT_TRUE(SetupTest("siteA{RootScroller,MobileViewportNoZoom}"));

  // Root scroller is recomputed after a Blink lifecycle so ensure a frame is
  // produced to make sure the renderer has had time to evaluate the root
  // scroller.
  {
    base::RunLoop loop;
    shell()->web_contents()->GetPrimaryMainFrame()->InsertVisualStateCallback(
        base::BindLambdaForTesting(
            [&loop](bool visual_state_updated) { loop.Quit(); }));
    loop.Run();
  }

  ASSERT_EQ(1.0, GetVisualViewport().scale);
  ASSERT_EQ(
      true,
      EvalJs(
          InnerMostFrameTreeNode(),
          "window.internals.effectiveRootScroller(document).tagName == 'DIV'"));

  RunTest();
}

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         ZoomScrollIntoViewBrowserTest,
                         testing::Values(kLocalFrame, kRemoteFrame),
                         DescribeFrameType);
#endif

enum FencedFrameType { kFencedFrameMPArch, kFencedFrameShadowDOM };

[[maybe_unused]] std::string DescribeFencedFrameType(
    const testing::TestParamInfo<FencedFrameType>& info) {
  std::string impl_type;
  switch (info.param) {
    case kFencedFrameMPArch: {
      impl_type = "MPArch";
    } break;
    case kFencedFrameShadowDOM: {
      impl_type = "ShadowDOM";
    } break;
  }
  return impl_type;
}

// Tests scrollIntoView behaviors related to a fenced frame.
class ScrollIntoViewFencedFrameBrowserTest
    : public ScrollIntoViewBrowserTestBase,
      public ::testing::WithParamInterface<FencedFrameType> {
 public:
  ScrollIntoViewFencedFrameBrowserTest() {
    const char* impl_param =
        GetParam() == kFencedFrameMPArch ? "mparch" : "shadow_dom";
    feature_list_.InitWithFeaturesAndParameters(
        {{blink::features::kFencedFrames,
          {{"implementation_type", impl_param}}},
         {features::kPrivacySandboxAdsAPIsOverride, {}},
         {blink::features::kFencedFramesAPIChanges, {}}},
        {/* disabled_features */});
  }
  bool IsForceLocalFrames() const override { return false; }
  bool IsWritingModeLTR() const override { return true; }
  TestInvokeMethod GetInvokeMethod() const override { return kInputHandler; }
  net::EmbeddedTestServer* server() override { return &https_server_; }

  void SetUpOnMainThread() override {
    https_server_.ServeFilesFromSourceDirectory(GetTestDataFilePath());
    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    ScrollIntoViewBrowserTestBase::SetUpOnMainThread();
  }

 private:
  net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       SingleFencedFrame) {
  ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB)"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       NestedFencedFrames) {
  ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB{FencedFrame}(siteC))"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       LocalFrameInFencedFrame) {
  ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB(siteB))"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       RemoteFrameInFencedFrame) {
  ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB(siteC))"));

  // TODO(bokan): This is required due to a race in how page-level focus is
  // transferred. If the race is won by the page level focus notification then
  // it'll clobber the <input> focus and reset it to the main frame. In this
  // case, trying again will work because the fenced frame tree already has
  // page focus now so focusing it doesn't change page focus. See
  // https://crbug.com/1327439.
  {
    VisualViewport viewport = GetVisualViewport();
    double page_scale_factor_before = viewport.scale;

    EXPECT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
      document.querySelector('input').focus({preventScroll: true});
    )JS"));

    // The test should start with fresh scroll and scale.
    ASSERT_EQ(viewport.scale, page_scale_factor_before);
    ASSERT_EQ(viewport.page_left, 0);
    ASSERT_EQ(viewport.page_top, 0);
  }

  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       FencedFrameInRemoteFrame) {
  ASSERT_TRUE(SetupTest("siteA(siteB{FencedFrame}(siteC))"));
  RunTest();
}

IN_PROC_BROWSER_TEST_P(ScrollIntoViewFencedFrameBrowserTest,
                       ProgrammaticScrollIntoViewDoesntCrossFencedFrame) {
  ASSERT_TRUE(SetupTest("siteA{FencedFrame}(siteB)"));

  ScrollRectToVisibleInParentFrameInterceptor interceptor;
  interceptor.Init(InnerMostFrameTreeNode()->current_frame_host());

  ASSERT_EQ(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollX"));
  ASSERT_EQ(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollY"));
  ASSERT_TRUE(ExecJs(InnerMostFrameTreeNode(), R"JS(
    document.querySelector('input').scrollIntoView({
      behavior: 'instant',
      block: 'center',
      inline: 'center'
    })
  )JS"));
  ASSERT_LT(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollX"));
  ASSERT_LT(0, EvalJs(InnerMostFrameTreeNode(), "window.scrollY"));

  // Since bubbling to a parent frame happens synchronously in scrollIntoView,
  // once the fenced frame has visible scroll we can guarantee that, if it
  // tried bubbling the scroll to the parent the message must have been sent to
  // the browser by now.
  InnerMostFrameTreeNode()
      ->current_frame_host()
      ->local_frame_host_receiver_for_testing()
      .FlushForTesting();
  EXPECT_FALSE(interceptor.HasCalledScrollRectToVisibleInParentFrame());
}

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         ScrollIntoViewFencedFrameBrowserTest,
                         testing::Values(kFencedFrameMPArch,
                                         kFencedFrameShadowDOM),
                         DescribeFencedFrameType);

}  // namespace

}  // namespace content