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

#include "base/run_loop.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/pdf/pdf_extension_test_base.h"
#include "chrome/browser/pdf/pdf_extension_test_util.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/omnibox/omnibox_next_features.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/pdf/browser/pdf_frame_util.h"
#include "content/public/browser/browser_plugin_guest_manager.h"
#include "content/public/browser/focused_node_details.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/focus_changed_observer.h"
#include "content/public/test/hit_test_region_observer.h"
#include "content/public/test/test_utils.h"
#include "extensions/browser/api/extensions_api_client.h"
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/pdf_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-shared.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "url/gurl.h"

#if defined(TOOLKIT_VIEWS) && defined(USE_AURA)
#include "content/public/browser/touch_selection_controller_client_manager.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event.h"
#include "ui/events/gesture_event_details.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/test/geometry_util.h"
#include "ui/gfx/selection_bound.h"
#include "ui/touch_selection/touch_selection_controller.h"
#include "ui/views/touchui/touch_selection_menu_views.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#endif  // defined(TOOLKIT_VIEWS) && defined(USE_AURA)

namespace {

using ::pdf_extension_test_util::ConvertPageCoordToScreenCoord;
using ::pdf_extension_test_util::SetInputFocusOnPlugin;

class PDFExtensionInteractiveUITest : public base::test::WithFeatureOverride,
                                      public PDFExtensionTestBase {
 public:
  PDFExtensionInteractiveUITest()
      : base::test::WithFeatureOverride(chrome_pdf::features::kPdfOopif) {}

  void SetUpInProcessBrowserTestFixture() override {
    PDFExtensionTestBase::SetUpInProcessBrowserTestFixture();

    // TODO(crbug.com/452061489): Fix tests that fail when the WebUI Omnibox is
    // enabled and then remove this.
    webui_omnibox_feature_list_.InitWithFeatures(
        /*enabled_features=*/{},
        /*disabled_features=*/
        {omnibox::internal::kWebUIOmniboxPopup,
         omnibox::internal::kWebUIOmniboxAimPopup});
  }

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

    content::IsolateAllSitesForTesting(command_line);
  }

  content::FocusedNodeDetails TabAndWait(content::WebContents* web_contents,
                                         bool forward) {
    content::FocusChangedObserver focus_observer(web_contents);
    if (!ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_TAB,
                                         /*control=*/false,
                                         /*shift=*/!forward,
                                         /*alt=*/false,
                                         /*command=*/false)) {
      ADD_FAILURE() << "Failed to send key press";
      return {};
    }
    return focus_observer.Wait();
  }

  bool UseOopif() const override { return GetParam(); }

 private:
  base::test::ScopedFeatureList webui_omnibox_feature_list_;
};

class TabChangedWaiter : public TabStripModelObserver {
 public:
  explicit TabChangedWaiter(BrowserWindowInterface* browser) {
    browser->GetTabStripModel()->AddObserver(this);
  }
  TabChangedWaiter(const TabChangedWaiter&) = delete;
  TabChangedWaiter& operator=(const TabChangedWaiter&) = delete;
  ~TabChangedWaiter() override = default;

  void Wait() { run_loop_.Run(); }

  // TabStripModelObserver:
  void OnTabStripModelChanged(
      TabStripModel* tab_strip_model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    if (change.type() == TabStripModelChange::kSelectionOnly)
      run_loop_.Quit();
  }

 private:
  base::RunLoop run_loop_;
};

}  // namespace

// TODO(crbug.com/333802743): re-enable the test
// For crbug.com/40113375
IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest,
                       DISABLED_CtrlPageUpDownSwitchesTabs) {
  content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost(
      embedded_test_server()->GetURL("/pdf/test.pdf"));
  ASSERT_TRUE(extension_host);

  auto* tab_strip_model = browser()->tab_strip_model();
  ASSERT_EQ(2, tab_strip_model->count());
  EXPECT_EQ(1, tab_strip_model->active_index());

  SetInputFocusOnPlugin(extension_host, GetEmbedderWebContents());

  {
    TabChangedWaiter tab_changed_waiter(browser());
    ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_NEXT,
                                                /*control=*/true,
                                                /*shift=*/false,
                                                /*alt=*/false,
                                                /*command=*/false));
    tab_changed_waiter.Wait();
  }
  ASSERT_EQ(2, tab_strip_model->count());
  EXPECT_EQ(0, tab_strip_model->active_index());

  {
    TabChangedWaiter tab_changed_waiter(browser());
    ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_PRIOR,
                                                /*control=*/true,
                                                /*shift=*/false,
                                                /*alt=*/false,
                                                /*command=*/false));
    tab_changed_waiter.Wait();
  }
  ASSERT_EQ(2, tab_strip_model->count());
  EXPECT_EQ(1, tab_strip_model->active_index());
}

IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, FocusForwardTraversal) {
  content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost(
      embedded_test_server()->GetURL("/pdf/test.pdf#toolbar=0"));
  ASSERT_TRUE(extension_host);
  auto* target_web_contents =
      content::WebContents::FromRenderFrameHost(extension_host);

  // Tab in.
  content::FocusedNodeDetails details =
      TabAndWait(target_web_contents, /*forward=*/true);
  EXPECT_EQ(blink::mojom::FocusType::kForward, details.focus_type);

  // Tab out.
  details = TabAndWait(target_web_contents, /*forward=*/true);
  EXPECT_EQ(blink::mojom::FocusType::kNone, details.focus_type);
}

IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, FocusReverseTraversal) {
  content::RenderFrameHost* extension_host = LoadPdfInNewTabGetExtensionHost(
      embedded_test_server()->GetURL("/pdf/test.pdf#toolbar=0"));
  ASSERT_TRUE(extension_host);
  auto* target_web_contents =
      content::WebContents::FromRenderFrameHost(extension_host);

  // Tab in.
  content::FocusedNodeDetails details =
      TabAndWait(target_web_contents, /*forward=*/false);
  EXPECT_EQ(blink::mojom::FocusType::kBackward, details.focus_type);

  // Tab out.
  details = TabAndWait(target_web_contents, /*forward=*/false);
  EXPECT_EQ(blink::mojom::FocusType::kNone, details.focus_type);
}

// Regression test for https://crbug.com/326275041
IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest, SpaceKeyInForm) {
  content::RenderFrameHost* extension_host = LoadPdfGetExtensionHost(
      embedded_test_server()->GetURL("/pdf/text_form.pdf"));
  ASSERT_TRUE(extension_host);
  content::RenderFrameHost* plugin_host =
      pdf_frame_util::FindPdfChildFrame(extension_host);
  ASSERT_TRUE(plugin_host);

  static constexpr char kFocusChangeScript[] = R"(
    var form_focus_changed = false;
    const plugin = document.querySelector('embed');
    plugin.addEventListener('message', e => {
      if (e.data.type === 'formFocusChange') {
        form_focus_changed = true;
      }
    });
  )";
  ASSERT_TRUE(content::ExecJs(plugin_host, kFocusChangeScript));

  // Since the PDF contains a text form that takes up the entire PDF page, this
  // clicks into that form.
  auto* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);
  SetInputFocusOnPlugin(extension_host, embedder_web_contents);

  // Wait for the input event to propagate into `plugin_host`.
  while (true) {
    // content::EvalJs() uses a base::RunLoop internally, so do not use
    // base::test::RunUntil() here to avoid a double run loop.
    if (content::EvalJs(plugin_host, "form_focus_changed").ExtractBool()) {
      break;
    }
  }

  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      browser(), ui::VKEY_SPACE, /*control=*/false, /*shift=*/false,
      /*alt=*/false, /*command=*/false));

  content::RenderWidgetHostView* view = plugin_host->GetView();
  ASSERT_TRUE(view);
  EXPECT_TRUE(view->GetSelectedText().empty());

  // Wait for the key press to propagate from the browser to the PDF renderer.
  // Keep calling SelectAll(), as it may arrive ahead of the key press event.
  // Then wait for the text selection update to propagate from the PDF renderer
  // back to the browser.
  //
  // If the space key press did not register at all, this hangs.
  EXPECT_TRUE(base::test::RunUntil([embedder_web_contents, view]() {
    embedder_web_contents->SelectAll();
    return view->GetSelectedText() == u" ";
  }));
}

#if defined(TOOLKIT_VIEWS) && defined(USE_AURA)
namespace {

// Simulates a touch press event and touch release event on `contents` at
// `screen_pos`. Waits for the PDF viewer to notify `listener_host` that text
// has been selected in the PDF.
views::Widget* TouchSelectText(content::WebContents* contents,
                               content::RenderFrameHost* listener_host,
                               const gfx::Point& screen_pos) {
  views::NamedWidgetShownWaiter waiter(views::test::AnyWidgetTestPasskey{},
                                       "TouchSelectionMenuViews");
  content::SimulateTouchEventAt(contents, ui::EventType::kTouchPressed,
                                screen_pos);

  EXPECT_EQ(true, content::EvalJs(
                      listener_host,
                      "new Promise(resolve => {"
                      "  window.addEventListener('message', function(event) {"
                      "    if (event.data.type == 'touchSelectionOccurred')"
                      "      resolve(true);"
                      "  });"
                      "});"));

  content::SimulateTouchEventAt(contents, ui::EventType::kTouchReleased,
                                screen_pos);
  return waiter.WaitIfNeededAndGet();
}

}  // namespace

// On text selection, a touch selection menu should pop up. On clicking ellipsis
// icon on the menu, the context menu should open up.
IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest,
                       ContextMenuOpensFromTouchSelectionMenu) {
  const GURL url = embedded_test_server()->GetURL("/pdf/text_large.pdf");
  content::RenderFrameHost* extension_host =
      LoadPdfInNewTabGetExtensionHost(url);
  ASSERT_TRUE(extension_host);

  content::WaitForHitTestData(extension_host);

  content::WebContents* contents = GetActiveWebContents();

  // For GuestView PDF viewer, the listener host can be the PDF embedder host.
  // For OOPIF PDF viewer, the listener host can't be the embedder host, since
  // the PDF extension host doesn't send it messages. Instead, the listener host
  // can be the extension host and listen for messages from the PDF content
  // host.
  content::RenderFrameHost* listener_host =
      UseOopif() ? extension_host : contents->GetPrimaryMainFrame();
  const gfx::Point point_in_root_coords =
      extension_host->GetView()->TransformPointToRootCoordSpace(
          ConvertPageCoordToScreenCoord(extension_host, {12, 12}));
  views::Widget* widget =
      TouchSelectText(contents, listener_host, point_in_root_coords);
  ASSERT_TRUE(widget);
  views::View* menu = widget->GetContentsView();
  ASSERT_TRUE(menu);
  views::View* ellipsis_button = menu->GetViewByID(
      views::TouchSelectionMenuViews::ButtonViewId::kEllipsisButton);
  ASSERT_TRUE(ellipsis_button);
  ContextMenuWaiter context_menu_observer;
  ui::GestureEvent tap(0, 0, 0, ui::EventTimeForNow(),
                       ui::GestureEventDetails(ui::EventType::kGestureTap));
  ellipsis_button->OnGestureEvent(&tap);
  context_menu_observer.WaitForMenuOpenAndClose();

  // Verify that the expected context menu items are present.
  //
  // Note that the assertion below doesn't use exact matching via
  // testing::ElementsAre, because some platforms may include unexpected extra
  // elements (e.g. an extra separator and IDC=100 has been observed on some Mac
  // bots).
  EXPECT_THAT(
      context_menu_observer.GetCapturedCommandIds(),
      testing::IsSupersetOf(
          {IDC_CONTENT_CONTEXT_COPY, IDC_CONTENT_CONTEXT_SEARCHWEBFOR,
           IDC_PRINT, IDC_CONTENT_CONTEXT_ROTATECW,
           IDC_CONTENT_CONTEXT_ROTATECCW, IDC_CONTENT_CONTEXT_INSPECTELEMENT}));
}

// TODO(crbug.com/40847318): Deflake this test.
#if BUILDFLAG(IS_WIN)
#define MAYBE_TouchSelectionBounds DISABLED_TouchSelectionBounds
#else
#define MAYBE_TouchSelectionBounds TouchSelectionBounds
#endif  // BUILDFLAG(IS_WIN)
IN_PROC_BROWSER_TEST_P(PDFExtensionInteractiveUITest,
                       MAYBE_TouchSelectionBounds) {
  // Use test.pdf here because it has embedded font metrics. With a fixed zoom,
  // coordinates should be consistent across platforms.
  const GURL url = embedded_test_server()->GetURL("/pdf/test.pdf#zoom=100");
  content::RenderFrameHost* extension_host =
      LoadPdfInNewTabGetExtensionHost(url);
  ASSERT_TRUE(extension_host);
  content::RenderFrameHost* plugin_host =
      pdf_frame_util::FindPdfChildFrame(extension_host);
  ASSERT_TRUE(plugin_host);
  content::RenderWidgetHostView* view = plugin_host->GetView();
  ASSERT_TRUE(view);
  EXPECT_TRUE(view->GetSelectedText().empty());

  content::WaitForHitTestData(extension_host);

  content::WebContents* contents = GetActiveWebContents();

  // For GuestView PDF viewer, the listener host can be the PDF embedder host.
  // For OOPIF PDF viewer, the listener host can't be the embedder host, since
  // the PDF extension host doesn't send it messages. Instead, the listener host
  // can be the extension host and listen for messages from the PDF content
  // host.
  content::RenderFrameHost* listener_host =
      UseOopif() ? extension_host : contents->GetPrimaryMainFrame();
  views::Widget* widget = TouchSelectText(contents, listener_host, {473, 166});
  ASSERT_TRUE(widget);

  EXPECT_EQ(u"some", view->GetSelectedText());

  auto* touch_selection_controller =
      extension_host->GetView()
          ->GetTouchSelectionControllerClientManager()
          ->GetTouchSelectionController();

  gfx::SelectionBound start_bound = touch_selection_controller->start();
  EXPECT_EQ(gfx::SelectionBound::LEFT, start_bound.type());
  EXPECT_POINTF_NEAR(gfx::PointF(454.0f, 157.0f), start_bound.edge_start(),
                     1.0f);
  EXPECT_POINTF_NEAR(gfx::PointF(454.0f, 174.0f), start_bound.edge_end(), 1.0f);

  gfx::SelectionBound end_bound = touch_selection_controller->end();
  EXPECT_EQ(gfx::SelectionBound::RIGHT, end_bound.type());
  EXPECT_POINTF_NEAR(gfx::PointF(494.0f, 157.0f), end_bound.edge_start(), 1.0f);
  EXPECT_POINTF_NEAR(gfx::PointF(494.0f, 174.0f), end_bound.edge_end(), 1.0f);
}
#endif  // defined(TOOLKIT_VIEWS) && defined(USE_AURA)

// TODO(crbug.com/40268279): Stop testing both modes after OOPIF PDF viewer
// launches.
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(PDFExtensionInteractiveUITest);
