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

#include "chrome/browser/ui/fullscreen_keyboard_browsertest_base.h"

#include <memory>
#include <string>

#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "third_party/blink/public/common/switches.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "url/gurl.h"
#include "url/url_constants.h"

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

namespace {

// The html file to receive key events, prevent defaults and export all the
// events with "getKeyEventReport()" function. It has two magic keys: pressing
// "S" to enter fullscreen mode; pressing "X" to indicate the end of all the
// keys (see FinishTestAndVerifyResult() function).
constexpr char kFullscreenKeyboardLockHTML[] =
    "/fullscreen_keyboardlock/fullscreen_keyboardlock.html";

// On MacOSX command key is used for most of the shortcuts, so replace it with
// control to reduce the complexity of comparison of the results.
void NormalizeMetaKeyForMacOS(std::string* output) {
#if BUILDFLAG(IS_MAC)
  base::ReplaceSubstringsAfterOffset(output, 0, "MetaLeft", "ControlLeft");
#endif
}

}  // namespace

FullscreenKeyboardBrowserTestBase::FullscreenKeyboardBrowserTestBase() =
    default;

FullscreenKeyboardBrowserTestBase::~FullscreenKeyboardBrowserTestBase() =
    default;

net::EmbeddedTestServer*
FullscreenKeyboardBrowserTestBase::GetEmbeddedTestServer() {
  return embedded_test_server();
}

bool FullscreenKeyboardBrowserTestBase::IsActiveTabFullscreen() const {
  auto* contents = GetActiveWebContents();
  return contents->GetDelegate()->IsFullscreenForTabOrPending(contents);
}

bool FullscreenKeyboardBrowserTestBase::IsInBrowserFullscreen() const {
  return GetActiveBrowser()
      ->GetFeatures()
      .exclusive_access_manager()
      ->fullscreen_controller()
      ->IsFullscreenForBrowser();
}

content::WebContents* FullscreenKeyboardBrowserTestBase::GetActiveWebContents()
    const {
  return GetActiveBrowser()->GetTabStripModel()->GetActiveWebContents();
}

int FullscreenKeyboardBrowserTestBase::GetActiveTabIndex() const {
  return GetActiveBrowser()->GetTabStripModel()->active_index();
}

int FullscreenKeyboardBrowserTestBase::GetTabCount() const {
  return GetActiveBrowser()->GetTabStripModel()->count();
}

size_t FullscreenKeyboardBrowserTestBase::GetBrowserCount() const {
  return GlobalBrowserCollection::GetInstance()->GetSize();
}

BrowserWindowInterface* FullscreenKeyboardBrowserTestBase::GetActiveBrowser()
    const {
  return GetLastActiveBrowserWindowInterfaceWithAnyProfile();
}

BrowserWindowInterface*
FullscreenKeyboardBrowserTestBase::CreateNewBrowserInstance() {
  BrowserWindowInterface* const first_instance = GetActiveBrowser();
  ui_test_utils::BrowserCreatedObserver creation_observer;
  EXPECT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_N));
  BrowserWindowInterface* const second_instance = creation_observer.Wait();
  ui_test_utils::WaitForBrowserSetLastActive(second_instance);
  EXPECT_NE(first_instance, second_instance);

  return second_instance;
}

void FullscreenKeyboardBrowserTestBase::FocusOnLastActiveBrowser() {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(GetActiveBrowser()));
}

void FullscreenKeyboardBrowserTestBase::WaitForBrowserCount(size_t expected) {
  while (GetBrowserCount() != expected) {
    base::RunLoop().RunUntilIdle();
  }
}

void FullscreenKeyboardBrowserTestBase::WaitForTabCount(int expected) {
  while (GetTabCount() != expected) {
    base::RunLoop().RunUntilIdle();
  }
}

void FullscreenKeyboardBrowserTestBase::WaitForActiveTabIndex(int expected) {
  while (GetActiveTabIndex() != expected) {
    base::RunLoop().RunUntilIdle();
  }
}

void FullscreenKeyboardBrowserTestBase::WaitForInactiveTabIndex(int expected) {
  while (GetActiveTabIndex() == expected) {
    base::RunLoop().RunUntilIdle();
  }
}

void FullscreenKeyboardBrowserTestBase::StartFullscreenLockPage() {
  // Ensures the initial states.
  ASSERT_EQ(1, GetTabCount());
  ASSERT_EQ(0, GetActiveTabIndex());
  ASSERT_EQ(1U, GetBrowserCount());
  // Add a second tab for counting and focus purposes.
  ASSERT_TRUE(
      AddTabAtIndex(1, GURL(url::kAboutBlankURL), ui::PAGE_TRANSITION_LINK));
  ASSERT_EQ(2, GetTabCount());
  ASSERT_EQ(1U, GetBrowserCount());

  if (!GetEmbeddedTestServer()->Started()) {
    ASSERT_TRUE(GetEmbeddedTestServer()->Start());
  }
  ui_test_utils::NavigateToURLWithDisposition(
      GetActiveBrowser(),
      GetEmbeddedTestServer()->GetURL(kFullscreenKeyboardLockHTML),
      WindowOpenDisposition::CURRENT_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
}

void FullscreenKeyboardBrowserTestBase::SendShortcut(ui::KeyboardCode key,
                                                     bool shift /* = false */) {
#if BUILDFLAG(IS_MAC)
  const bool control_modifier = false;
  const bool command_modifier = true;
#else
  const bool control_modifier = true;
  const bool command_modifier = false;
#endif
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), key,
                                              control_modifier, shift, false,
                                              command_modifier));

  expected_result_ += ui::KeycodeConverter::DomCodeToCodeString(
      ui::UsLayoutKeyboardCodeToDomCode(key));
  expected_result_ += " ctrl:";
  expected_result_ += base::ToString(control_modifier);
  expected_result_ += " shift:";
  expected_result_ += base::ToString(shift);
  expected_result_ += " alt:false";
  expected_result_ += " meta:";
  expected_result_ += base::ToString(command_modifier);
  expected_result_ += '\n';
}

void FullscreenKeyboardBrowserTestBase::SendShiftShortcut(
    ui::KeyboardCode key) {
  ASSERT_NO_FATAL_FAILURE(SendShortcut(key, true));
}

void FullscreenKeyboardBrowserTestBase::SendFullscreenShortcutAndWait() {
  // On MacOSX, entering and exiting fullscreen are not synchronous. So we wait
  // for the observer to notice the change of fullscreen state.
  bool current = GetActiveBrowser()
                     ->GetFeatures()
                     .exclusive_access_manager()
                     ->context()
                     ->IsFullscreen();
  ui_test_utils::FullscreenWaiter waiter(
      GetActiveBrowser(), current
                              ? ui_test_utils::FullscreenWaiter::kNoFullscreen
                              : ui_test_utils::FullscreenWaiter::Expectation{
                                    .browser_fullscreen = true});
// Enter fullscreen.
#if BUILDFLAG(IS_MAC)
  // On MACOSX, Command + Control + F is used.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_F,
                                              true, false, false, true));
#elif BUILDFLAG(IS_CHROMEOS)
  // A dedicated fullscreen key is used on Chrome OS, so send a fullscreen
  // command directly instead, to avoid constructing the key press.
  ASSERT_TRUE(chrome::ExecuteCommand(GetActiveBrowser(), IDC_FULLSCREEN));
#else
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_F11,
                                              false, false, false, false));
#endif

// Mac fullscreen is simulated in tests and is performed synchronously with the
// keyboard events. As a result, content doesn't actually know it has entered
// fullscreen. For more details, see ScopedFakeNSWindowFullscreen.
// TODO(crbug.com/41385780): Remove this once ScopedFakeNSWindowFullscreen fires
// OnFullscreenStateChanged.
#if !BUILDFLAG(IS_MAC)
  waiter.Wait();
#endif
}

void FullscreenKeyboardBrowserTestBase::SendJsFullscreenShortcutAndWait() {
  ui_test_utils::FullscreenWaiter waiter(GetActiveBrowser(),
                                         {.tab_fullscreen = true});
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_S,
                                              false, false, false, false));
  expected_result_ += "KeyS ctrl:false shift:false alt:false meta:false\n";
  waiter.Wait();
  ASSERT_TRUE(IsActiveTabFullscreen());
}

void FullscreenKeyboardBrowserTestBase::SendEscape() {
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetActiveBrowser(), ui::VKEY_ESCAPE, false, false, false, false));
  expected_result_ += "Escape ctrl:false shift:false alt:false meta:false\n";
}

void FullscreenKeyboardBrowserTestBase::
    SendEscapeAndWaitForExitingFullscreen() {
  ui_test_utils::FullscreenWaiter waiter(GetActiveBrowser(),
                                         {.tab_fullscreen = false});
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetActiveBrowser(), ui::VKEY_ESCAPE, false, false, false, false));
  waiter.Wait();
  ASSERT_FALSE(IsActiveTabFullscreen());
}

void FullscreenKeyboardBrowserTestBase::SendShortcutsAndExpectPrevented() {
  const int initial_active_index = GetActiveTabIndex();
  const int initial_tab_count = GetTabCount();
  const size_t initial_browser_count = GetBrowserCount();
  // The tab should not be closed.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_W));
  ASSERT_EQ(initial_tab_count, GetTabCount());
  // The window should not be closed.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_W));
  ASSERT_EQ(initial_browser_count, GetBrowserCount());
  // A new tab should not be created.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_T));
  ASSERT_EQ(initial_tab_count, GetTabCount());
  // A new window should not be created.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_N));
  ASSERT_EQ(initial_browser_count, GetBrowserCount());
  // A new incognito window should not be created.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_N));
  ASSERT_EQ(initial_browser_count, GetBrowserCount());
  // Last closed tab should not be restored.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_T));
  ASSERT_EQ(initial_tab_count, GetTabCount());
  // Browser should not switch to the next tab.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_TAB));
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());
  // Browser should not switch to the previous tab.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_TAB));
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());
}

void FullscreenKeyboardBrowserTestBase::SendShortcutsAndExpectNotPrevented(
    bool js_fullscreen) {
  const int initial_active_index = GetActiveTabIndex();
  const int initial_tab_count = GetTabCount();
  const size_t initial_browser_count = GetBrowserCount();
  const auto enter_fullscreen = [this, js_fullscreen]() {
    ASSERT_TRUE(
        ui_test_utils::BringBrowserWindowToFront(this->GetActiveBrowser()));
    if (js_fullscreen) {
      if (!this->IsActiveTabFullscreen()) {
        static const std::string page =
            "<html><head></head><body></body><script>"
            "document.addEventListener('keydown', "
            "    (e) => {"
            "      if (e.code == 'KeyS') { "
            "        document.body.webkitRequestFullscreen();"
            "      }"
            "    });"
            "</script></html>";
        ui_test_utils::NavigateToURLWithDisposition(
            this->GetActiveBrowser(), GURL("data:text/html," + page),
            WindowOpenDisposition::CURRENT_TAB,
            ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
        ASSERT_NO_FATAL_FAILURE(this->SendJsFullscreenShortcutAndWait());
      }
    } else {
      if (!this->IsInBrowserFullscreen()) {
        ASSERT_NO_FATAL_FAILURE(this->SendFullscreenShortcutAndWait());
      }
      ASSERT_TRUE(this->IsInBrowserFullscreen());
    }
  };

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // A new tab should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_T));
  WaitForTabCount(initial_tab_count + 1);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // The newly created tab should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_W));
  WaitForTabCount(initial_tab_count);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // A new tab should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_T));
  WaitForTabCount(initial_tab_count + 1);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // The previous tab should be focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_TAB,
                                              true, true, false, false));
  WaitForActiveTabIndex(initial_active_index);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // The newly created tab should be focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_TAB,
                                              true, false, false, false));
  WaitForInactiveTabIndex(initial_active_index);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // The newly created tab should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_W));
  WaitForTabCount(initial_tab_count);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // A new window should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_N));
  WaitForBrowserCount(initial_browser_count + 1);
  ASSERT_EQ(initial_browser_count + 1, GetBrowserCount());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());

  // The newly created window should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_W));
  WaitForBrowserCount(initial_browser_count);

  ASSERT_EQ(initial_browser_count, GetBrowserCount());
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  ASSERT_NO_FATAL_FAILURE(enter_fullscreen());
}

void FullscreenKeyboardBrowserTestBase::VerifyShortcutsAreNotPrevented() {
  const int initial_active_index = GetActiveTabIndex();
  const int initial_tab_count = GetTabCount();
  const size_t initial_browser_count = GetBrowserCount();

  // A new tab should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_T));
  WaitForTabCount(initial_tab_count + 1);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  // The newly created tab should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_W));
  WaitForTabCount(initial_tab_count);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  // A new tab should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_T));
  WaitForTabCount(initial_tab_count + 1);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  // The previous tab should be focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_TAB,
                                              true, true, false, false));
  WaitForActiveTabIndex(initial_active_index);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  // The newly created tab should be focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_TAB,
                                              true, false, false, false));
  WaitForInactiveTabIndex(initial_active_index);
  ASSERT_NE(initial_active_index, GetActiveTabIndex());

  // The newly created tab should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_W));
  WaitForTabCount(initial_tab_count);
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());

  ui_test_utils::BrowserCreatedObserver creation_observer;
  // A new window should be created and focused.
  ASSERT_NO_FATAL_FAILURE(SendShortcut(ui::VKEY_N));
  Browser* new_browser = creation_observer.Wait();
  ui_test_utils::WaitForBrowserSetLastActive(new_browser);
  ASSERT_EQ(initial_browser_count + 1, GetBrowserCount());

  // The newly created window should be closed.
  ASSERT_NO_FATAL_FAILURE(SendShiftShortcut(ui::VKEY_W));
  WaitForBrowserCount(initial_browser_count);

  ASSERT_EQ(initial_browser_count, GetBrowserCount());
  ASSERT_EQ(initial_active_index, GetActiveTabIndex());
}

void FullscreenKeyboardBrowserTestBase::FinishTestAndVerifyResult() {
  // The renderer process receives key events through IPC channel,
  // SendKeyPressSync() cannot guarantee the JS has processed the key event it
  // sent. So we sent a KeyX to the webpage to indicate the end of the test
  // case. After processing this key event, web page is safe to send the record
  // back through window.domAutomationController.
  EXPECT_TRUE(ui_test_utils::SendKeyPressSync(GetActiveBrowser(), ui::VKEY_X,
                                              false, false, false, false));
  expected_result_ += "KeyX ctrl:false shift:false alt:false meta:false";
  std::string result =
      content::EvalJs(GetActiveWebContents(), "getKeyEventReport();")
          .ExtractString();
  NormalizeMetaKeyForMacOS(&result);
  NormalizeMetaKeyForMacOS(&expected_result_);
  base::TrimWhitespaceASCII(result, base::TRIM_ALL, &result);
  ASSERT_EQ(expected_result_, result);
}

std::string FullscreenKeyboardBrowserTestBase::GetFullscreenFramePath() {
  return kFullscreenKeyboardLockHTML;
}

void FullscreenKeyboardBrowserTestBase::SetUpOnMainThread() {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(GetActiveBrowser()));
}

void FullscreenKeyboardBrowserTestBase::SetUpCommandLine(
    base::CommandLine* command_line) {
  BrowserTestBase::SetUpCommandLine(command_line);
  command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
}
