// Copyright 2012 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/exclusive_access/exclusive_access_test.h"

#include <optional>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_web_contents_delegate/browser_web_contents_delegate.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_bubble.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/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/exclusive_access/keyboard_lock_controller.h"
#include "chrome/browser/ui/exclusive_access/pointer_lock_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/input/native_web_keyboard_event.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_permission_controller.h"
#include "content/public/test/test_navigation_observer.h"
#include "exclusive_access_controller_base.h"
#include "exclusive_access_manager.h"
#include "exclusive_access_test.h"
#include "extensions/common/extension.h"
#include "third_party/blink/public/mojom/frame/fullscreen.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/animation/animation_test_api.h"

using content::WebContents;

const char ExclusiveAccessTest::kFullscreenKeyboardLockHTML[] =
    "/fullscreen_keyboardlock/fullscreen_keyboardlock.html";

const char ExclusiveAccessTest::kFullscreenPointerLockHTML[] =
    "/fullscreen_pointerlock/fullscreen_pointerlock.html";

MockExclusiveAccessController::MockExclusiveAccessController(
    ExclusiveAccessManager* manager)
    : ExclusiveAccessControllerBase(manager) {}

MockExclusiveAccessController::~MockExclusiveAccessController() = default;

bool MockExclusiveAccessController::HandleUserPressedEscape() {
  escape_pressed_count_++;
  return false;
}

ExclusiveAccessTest::ExclusiveAccessTest() {
  // It is important to disable system keyboard lock as low-level test utilities
  // may install a keyboard hook to listen for keyboard events and having an
  // active system hook may cause issues with that mechanism.
  scoped_feature_list_.InitWithFeatures({}, {features::kSystemKeyboardLock});
}

ExclusiveAccessTest::~ExclusiveAccessTest() = default;

void ExclusiveAccessTest::SetUpOnMainThread() {
  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(true);

  permission_controller_ =
      std::make_unique<content::MockPermissionController>();
  ON_CALL(*permission_controller_, RequestPermissionsFromCurrentDocument)
      .WillByDefault(
          [](content::RenderFrameHost* render_frame_host,
             content::PermissionRequestDescription request_description,
             base::OnceCallback<void(
                 const std::vector<content::PermissionResult>&)> callback) {
            std::move(callback).Run(std::vector<content::PermissionResult>(
                request_description.permissions.size(),
                content::PermissionResult(
                    content::PermissionStatus::GRANTED,
                    content::PermissionStatusSource::UNSPECIFIED)));
          });

  GetExclusiveAccessManager()
      ->permission_manager()
      .set_permission_controller_for_test(permission_controller_.get());
  GetExclusiveAccessManager()
      ->pointer_lock_controller()
      ->bubble_hide_callback_for_test_ = base::BindRepeating(
      &ExclusiveAccessTest::OnBubbleHidden, weak_ptr_factory_.GetWeakPtr(),
      &pointer_lock_bubble_hide_reason_recorder_);
  GetExclusiveAccessManager()
      ->keyboard_lock_controller()
      ->bubble_hide_callback_for_test_ = base::BindRepeating(
      &ExclusiveAccessTest::OnBubbleHidden, weak_ptr_factory_.GetWeakPtr(),
      &keyboard_lock_bubble_hide_reason_recorder_);

  mock_controller_ = std::make_unique<MockExclusiveAccessController>(
      GetExclusiveAccessManager());
  GetExclusiveAccessManager()->exclusive_access_controllers_for_test().insert(
      mock_controller_.get());
}

void ExclusiveAccessTest::TearDownOnMainThread() {
  ExclusiveAccessBubbleViews::set_skip_presentation_delay_for_testing(false);

  GetExclusiveAccessManager()
      ->pointer_lock_controller()
      ->bubble_hide_callback_for_test_ =
      base::RepeatingCallback<void(ExclusiveAccessBubbleHideReason)>();
  GetExclusiveAccessManager()
      ->keyboard_lock_controller()
      ->bubble_hide_callback_for_test_ =
      base::RepeatingCallback<void(ExclusiveAccessBubbleHideReason)>();

  GetExclusiveAccessManager()->exclusive_access_controllers_for_test().erase(
      mock_controller_.get());
  mock_controller_.reset();
}

// static
bool ExclusiveAccessTest::IsBubbleDownloadNotification(
    ExclusiveAccessBubble* bubble) {
  return bubble->params_.has_download;
}

bool ExclusiveAccessTest::RequestKeyboardLock(bool esc_key_locked) {
  WebContents* tab = browser()->GetTabStripModel()->GetActiveWebContents();

  // If the caller defines |esc_key_locked| as true then we create a set of
  // locked keys which includes the escape key, this will require the user/test
  // to press and hold escape to exit fullscreen.  If |esc_key_locked| is false,
  // then we create a set of keys that does not include escape (we arbitrarily
  // chose the 'a' key) which means the user/test can just press escape to exit
  // fullscreen.
  std::optional<base::flat_set<ui::DomCode>> codes;
  if (esc_key_locked) {
    codes = base::flat_set<ui::DomCode>({ui::DomCode::ESCAPE});
  } else {
    codes = base::flat_set<ui::DomCode>({ui::DomCode::US_A});
  }

  bool success = false;
  base::RunLoop run_loop;
  base::OnceCallback<void(blink::mojom::KeyboardLockRequestResult)> callback =
      base::BindOnce(
          [](bool* success, base::RunLoop* run_loop,
             blink::mojom::KeyboardLockRequestResult result) {
            *success =
                result == blink::mojom::KeyboardLockRequestResult::kSuccess;
            run_loop->Quit();
          },
          &success, &run_loop);
  content::RequestKeyboardLock(tab, std::move(codes), std::move(callback));
  run_loop.Run();
  return success;
}

void ExclusiveAccessTest::RequestToLockPointer(bool user_gesture,
                                               bool last_unlocked_by_target) {
  WebContents* tab = browser()->GetTabStripModel()->GetActiveWebContents();
  PointerLockController* pointer_lock_controller =
      GetExclusiveAccessManager()->pointer_lock_controller();
  pointer_lock_controller->fake_pointer_lock_for_test_ = true;
  base::RunLoop run_loop;
  pointer_lock_controller->set_lock_state_callback_for_test(
      run_loop.QuitClosure());
  BrowserWebContentsDelegate::From(browser())->RequestPointerLock(
      tab, user_gesture, last_unlocked_by_target);
  run_loop.Run();
  pointer_lock_controller->fake_pointer_lock_for_test_ = false;
}

void ExclusiveAccessTest::SetWebContentsGrantedSilentPointerLockPermission() {
  GetExclusiveAccessManager()
      ->pointer_lock_controller()
      ->web_contents_granted_silent_pointer_lock_permission_ =
      browser()->GetTabStripModel()->GetActiveWebContents();
}

void ExclusiveAccessTest::CancelKeyboardLock() {
  WebContents* tab = browser()->GetTabStripModel()->GetActiveWebContents();
  content::CancelKeyboardLock(tab);
}

void ExclusiveAccessTest::LostPointerLock() {
  BrowserWebContentsDelegate::From(browser())->LostPointerLock();
}

bool ExclusiveAccessTest::SendEscapeToExclusiveAccessManager(bool is_key_down) {
  input::NativeWebKeyboardEvent event(
      is_key_down ? blink::WebInputEvent::Type::kRawKeyDown
                  : blink::WebInputEvent::Type::kKeyUp,
      blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  event.windows_key_code = ui::VKEY_ESCAPE;
  return GetExclusiveAccessManager()->HandleUserKeyEvent(event);
}

bool ExclusiveAccessTest::IsFullscreenForBrowser() {
  return GetFullscreenController()->IsFullscreenForBrowser();
}

bool ExclusiveAccessTest::IsWindowFullscreenForTabOrPending() {
  return GetFullscreenController()->IsWindowFullscreenForTabOrPending();
}

void ExclusiveAccessTest::GoBack() {
  content::TestNavigationObserver observer(
      browser()->GetTabStripModel()->GetActiveWebContents(), 1);
  chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB);
  observer.Wait();
}

void ExclusiveAccessTest::Reload() {
  content::TestNavigationObserver observer(
      browser()->GetTabStripModel()->GetActiveWebContents(), 1);
  chrome::Reload(browser(), WindowOpenDisposition::CURRENT_TAB);
  observer.Wait();
}

void ExclusiveAccessTest::EnterActiveTabFullscreen() {
  WebContents* tab = browser()->GetTabStripModel()->GetActiveWebContents();
  ui_test_utils::FullscreenWaiter waiter(browser(), {.tab_fullscreen = true});
  BrowserWebContentsDelegate::From(browser())->EnterFullscreenModeForTab(
      tab->GetPrimaryMainFrame(), {});
  waiter.Wait();
}

void ExclusiveAccessTest::WaitForTabFullscreenExit() {
  ui_test_utils::FullscreenWaiter waiter(browser(), {.tab_fullscreen = false});
  waiter.Wait();
}

void ExclusiveAccessTest::WaitAndVerifyFullscreenState(bool browser_fullscreen,
                                                       bool tab_fullscreen) {
  ui_test_utils::FullscreenWaiter waiter(
      browser(), {.browser_fullscreen = browser_fullscreen,
                  .tab_fullscreen = tab_fullscreen});
  waiter.Wait();
}

void ExclusiveAccessTest::EnterExtensionInitiatedFullscreen() {
  ui_test_utils::FullscreenWaiter waiter(browser(),
                                         {.browser_fullscreen = true});
  static const char kExtensionId[] = "extension-id";
  GetExclusiveAccessManager()
      ->fullscreen_controller()
      ->ToggleBrowserFullscreenModeWithExtension(
          extensions::Extension::GetBaseURLFromExtensionId(kExtensionId));
  waiter.Wait();
}

bool ExclusiveAccessTest::IsEscKeyHoldTimerRunning() {
  return GetExclusiveAccessManager()->esc_key_hold_timer_for_test().IsRunning();
}

ExclusiveAccessBubbleType ExclusiveAccessTest::GetExclusiveAccessBubbleType() {
  return GetExclusiveAccessManager()->GetExclusiveAccessExitBubbleType();
}

ExclusiveAccessBubbleViews*
ExclusiveAccessTest::GetExclusiveAccessBubbleView() {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  return browser_view ? browser_view->GetExclusiveAccessBubble() : nullptr;
}

bool ExclusiveAccessTest::IsExclusiveAccessBubbleDisplayed() {
  return GetExclusiveAccessManager()
      ->context()
      ->IsExclusiveAccessBubbleDisplayed();
}

void ExclusiveAccessTest::FinishExclusiveAccessBubbleAnimation() {
  if (!GetExclusiveAccessBubbleView()) {
    return;
  }
  gfx::AnimationTestApi animation_api(
      GetExclusiveAccessBubbleView()->animation_for_test());
  base::TimeTicks far_future = base::TimeTicks::Now() + base::Seconds(1);
  animation_api.Step(far_future);
}

FullscreenController* ExclusiveAccessTest::GetFullscreenController() {
  return GetExclusiveAccessManager()->fullscreen_controller();
}

ExclusiveAccessManager* ExclusiveAccessTest::GetExclusiveAccessManager() {
  return ExclusiveAccessManager::From(browser());
}

void ExclusiveAccessTest::SetEscRepeatWindowLength(
    base::TimeDelta esc_repeat_window) {
  GetExclusiveAccessManager()->keyboard_lock_controller()->esc_repeat_window_ =
      esc_repeat_window;
}

void ExclusiveAccessTest::SetEscRepeatThresholdReachedCallback(
    base::OnceClosure callback) {
  GetExclusiveAccessManager()
      ->keyboard_lock_controller()
      ->esc_repeat_triggered_for_test_ = std::move(callback);
}

void ExclusiveAccessTest::SetEscRepeatTestTickClock(
    const base::TickClock* tick_clock_for_test) {
  GetExclusiveAccessManager()
      ->keyboard_lock_controller()
      ->esc_repeat_tick_clock_ = tick_clock_for_test;
}

void ExclusiveAccessTest::OnBubbleHidden(
    std::vector<ExclusiveAccessBubbleHideReason>* reason_recorder,
    ExclusiveAccessBubbleHideReason reason) {
  reason_recorder->push_back(reason);
}

void ExclusiveAccessTest::SetUserEscapeTimestampForTest(
    const base::TimeTicks timestamp) {
  GetExclusiveAccessManager()
      ->pointer_lock_controller()
      ->last_user_escape_time_ = timestamp;
}

void ExclusiveAccessTest::ExpectMockControllerReceivedEscape(int count) {
  EXPECT_EQ(count, mock_controller()->escape_pressed_count());
  mock_controller()->reset_escape_pressed_count();
}

void ExclusiveAccessTest::Wait(base::TimeDelta duration) {
  base::RunLoop run_loop;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, run_loop.QuitClosure(), duration);
  run_loop.Run();
}
