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

// This file contains UI interactive tests for the extensions commands API.
// For non-UI interactive tests, see extension_keybinding_browsertest.cc.

#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/browser_window_util.h"
#include "chrome/browser/extensions/commands/command_service.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/extensions/extension_action_view_model.h"
#include "chrome/browser/ui/extensions/extensions_container.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/javascript_test_observer.h"
#include "extensions/browser/extension_action.h"
#include "extensions/browser/extension_action_manager.h"
#include "extensions/browser/test_event_router_observer.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/api/extension_action/action_info_test_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "third_party/blink/public/common/switches.h"

#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "chrome/test/base/ui_test_utils.h"
#include "ui/base/base_window.h"
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)

using content::WebContents;

namespace extensions {

namespace {

// Name of the command for the "basics" test extension.
constexpr char kBasicsShortcutCommandName[] = "toggle-feature";

#if BUILDFLAG(IS_MAC)
constexpr char kBookmarkKeybinding[] = "Command+D";
#else
constexpr char kBookmarkKeybinding[] = "Ctrl+D";
#endif  // BUILDFLAG(IS_MAC)

bool SendBookmarkKeyPressSync(BrowserWindowInterface* browser) {
  return ui_test_utils::SendKeyPressSync(browser, ui::VKEY_D,
#if BUILDFLAG(IS_MAC)
                                         false, false, false, true
#else
      true, false, false, false
#endif
  );
}

// Programmatically (from the extension) disables the action globally.
void DisableActionGlobally(Profile* profile, const Extension& extension) {
  // Omitting the tab_id parameter disables the action globally.
  static constexpr char kScriptTemplate[] =
      R"(chrome.action.disable(() => {
             chrome.test.sendScriptResult(
                 chrome.runtime.lastError ?
                     chrome.runtime.lastError.message :
                     'success');
           });)";

  base::Value set_result = browsertest_util::ExecuteScriptInBackgroundPage(
      profile, extension.id(), kScriptTemplate);
  EXPECT_EQ("success", set_result);
}

// Programmatically (from the extension) sets the action of |extension| to be
// enabled on the tab with the given |tab_id|.
//
// In Manifest V3, the chrome.pageAction and chrome.browserAction APIs are
// unified into chrome.action. This method simulates the old 'pageAction'
// behavior of a button becoming clickable/colored on a specific tab, which
// is achieved by calling chrome.action.enable(tabId).
void SetActionEnabledOnTab(Profile* profile,
                           const Extension& extension,
                           int tab_id) {
  ExtensionActionManager* action_manager = ExtensionActionManager::Get(profile);
  const ExtensionAction* extension_action =
      action_manager->GetExtensionAction(extension);
  ASSERT_TRUE(extension_action);

  // In MV3, the old concept of 'visibility' is replaced by 'enabled state'.
  // This initial check verifies the action is currently disabled (grayed out).
  EXPECT_FALSE(extension_action->GetIsVisible(tab_id));

  const ActionInfo* action_info =
      ActionInfo::GetExtensionActionInfo(&extension);
  ASSERT_TRUE(action_info);

  std::string script;
  if (action_info->type == ActionInfo::Type::kPage) {
    script = base::StringPrintf(
        R"(chrome.pageAction.show(%d, () => {
           chrome.test.sendScriptResult(
               chrome.runtime.lastError ?
                   chrome.runtime.lastError.message :
                   'success');
         });)",
        tab_id);
  } else {
    script = base::StringPrintf(
        R"(chrome.action.enable(%d, () => {
                 chrome.test.sendScriptResult(
                     chrome.runtime.lastError ?
                         chrome.runtime.lastError.message :
                         'success');
               });)",
        tab_id);
  }

  base::Value set_result = browsertest_util::ExecuteScriptInBackgroundPage(
      profile, extension.id(), script);

  EXPECT_EQ("success", set_result);

  // After enabling, the internal state (GetIsVisible) should reflect that
  // the action is now active (clickable/colored) for the specified tab.
  EXPECT_TRUE(extension_action->GetIsVisible(tab_id));
}

// Sends a keypress with the given |keyboard_code| to the specified |extension|.
// If |expect_dispatch| is true, expects pageAction.onClicked to be dispatched
// to the extension. Otherwise, expects it is not sent.
void SendKeyPressToAction(BrowserWindowInterface* browser,
                          const Extension& extension,
                          ui::KeyboardCode keyboard_code,
                          const char* event_name,
                          bool expect_dispatch) {
  ExtensionTestMessageListener click_listener("clicked");
  click_listener.set_extension_id(extension.id());

  Profile* profile = browser->GetProfile();
  EventRouter* event_router = EventRouter::Get(profile);
  TestEventRouterObserver event_tracker(event_router);
  // Activate the shortcut (Alt+Shift+F).
  if (!ui_test_utils::SendKeyPressSync(browser, keyboard_code, false, true,
                                       true, false)) {
    ADD_FAILURE() << "Could not send key press!";
    return;
  }
  if (expect_dispatch) {
    event_tracker.WaitForDispatchedEventWithName(event_name);
  } else {
    base::RunLoop().RunUntilIdle();
    // Check that the event was dispatched if and only if we expected it to be.
    EXPECT_FALSE(event_tracker.dispatched_events().contains(event_name));
  }

  // Do a round-trip to the extension renderer. This serves as a pseudo-
  // RunUntilIdle()-type of method for the extension renderer itself, since
  // test.sendMessage() is FIFO.
  // This allows us to return the result of click_listener.was_satisfied(),
  // rather than using WaitUntilSatisfied(), which in turn allows this method
  // to exercise both the case of expecting dispatch and expecting *not* to
  // dispatch.
  static constexpr char kScript[] =
      R"(chrome.test.sendMessage(
             'run loop hack',
             () => {
               chrome.test.sendScriptResult('success');
             });)";
  base::Value set_result = browsertest_util::ExecuteScriptInBackgroundPage(
      profile, extension.id(), kScript);
  EXPECT_EQ("success", set_result);
  EXPECT_EQ(expect_dispatch, click_listener.was_satisfied());
}

// Given an |action_type|, returns the corresponding command key.
const char* GetCommandKeyForActionType(ActionInfo::Type action_type) {
  const char* command_key = nullptr;
  switch (action_type) {
    case ActionInfo::Type::kBrowser:
      command_key = manifest_values::kBrowserActionCommandEvent;
      break;
    case ActionInfo::Type::kPage:
      command_key = manifest_values::kPageActionCommandEvent;
      break;
    case ActionInfo::Type::kAction:
      command_key = manifest_values::kActionCommandEvent;
      break;
  }

  return command_key;
}

}  // namespace

class CommandsApiTest : public ExtensionApiTest {
 public:
  CommandsApiTest() {
    scoped_feature_list_.InitAndDisableFeature(
        features::kExtensionsPinnedByDefault);
  }
  ~CommandsApiTest() override = default;

  void SetUpOnMainThread() override {
    ExtensionApiTest::SetUpOnMainThread();
#if BUILDFLAG(IS_MAC)
    // ExtensionKeybindingRegistryViews doesn't get registered until BrowserView
    // is activated at least once.
    // TODO(crbug.com/41386956): Registry creation should happen independent of
    // activation. Focus manager lifetime may make this tricky to untangle.
    // TODO(crbug.com/40486728): Reassess after activation is restored in the
    // focus manager.
    ui_test_utils::BrowserActivationWaiter waiter(GetBrowserWindowInterface());
    ASSERT_TRUE(
        ui_test_utils::BringBrowserWindowToFront(GetBrowserWindowInterface()));
    waiter.WaitForActivation();
    ASSERT_TRUE(GetBrowserWindowInterface()->GetWindow()->IsActive());
#endif
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    ExtensionApiTest::SetUpCommandLine(command_line);
    // Some builders are flaky due to slower loading interacting with
    // deferred commits. This primarily impacts chromeos for the test
    // CommandsApiTest.ContinuePropagation.
    command_line->AppendSwitch(blink::switches::kAllowPreCommitInput);
  }

 protected:
  bool IsGrantedForTab(const Extension* extension,
                       const content::WebContents* web_contents) {
    return extension->permissions_data()->HasAPIPermissionForTab(
        sessions::SessionTabHelper::IdForTab(web_contents).id(),
        mojom::APIPermissionID::kTab);
  }

  // Returns true if the extension with the given |extension_id| has an active
  // command associated with an action of the given |action_type|.
  bool HasActiveActionCommand(const ExtensionId& extension_id,
                              ActionInfo::Type action_type) {
    bool active = false;
    Command command;
    CommandService* const command_service = CommandService::Get(profile());
    bool found_command = command_service->GetExtensionActionCommand(
        extension_id, action_type, CommandService::ALL, &command, &active);
    return found_command && active;
  }

  // Navigates to a test URL and return the ID of the navigated tab.
  int NavigateToTestURLAndReturnTabId() {
    EXPECT_TRUE(NavigateToURL(
        GetActiveWebContents(),
        embedded_test_server()->GetURL("/extensions/test_file.txt")));
    return sessions::SessionTabHelper::FromWebContents(GetActiveWebContents())
        ->session_id()
        .id();
  }

  ExtensionActionViewModel* GetViewModelForId(const std::string& action_id) {
    // It's safe to static cast here, because these tests only deal with
    // extensions.
    return static_cast<ExtensionActionViewModel*>(
        ExtensionsContainer::From(*GetBrowserWindowInterface())
            ->GetActionForId(action_id));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class IncognitoCommandsApiTest : public CommandsApiTest,
                                 public testing::WithParamInterface<bool> {};

// A parameterized version to allow testing with different action types.
class ActionCommandsApiTest
    : public CommandsApiTest,
      public testing::WithParamInterface<ActionInfo::Type> {};

// Test the basic functionality of the Keybinding API:
// - That pressing the shortcut keys should perform actions (activate the
//   browser action or send an event).
// - Note: Page action keybindings are tested in PageAction test below.
// - The shortcut keys taken by one extension are not overwritten by the last
//   installed extension.
IN_PROC_BROWSER_TEST_F(CommandsApiTest, Basic) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("keybinding/basics")) << message_;
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;

  // Load this extension, which uses the same keybindings but sets the page
  // to different colors. This is so we can see that it doesn't interfere. We
  // don't test this extension in any other way (it should otherwise be
  // immaterial to this test).
  ASSERT_TRUE(RunExtensionTest("keybinding/conflicting")) << message_;

  // Test that there are two browser actions in the toolbar.
  auto* toolbar_model = ToolbarActionsModel::Get(profile());
  ASSERT_EQ(2u, toolbar_model->action_ids().size());

  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("/extensions/test_file.txt")));

  // activeTab shouldn't have been granted yet.
  WebContents* tab = GetActiveWebContents();
  ASSERT_TRUE(tab);

  EXPECT_FALSE(IsGrantedForTab(extension, tab));

  ExtensionTestMessageListener test_listener;  // Won't reply.

  // Activate the browser action shortcut (Ctrl+Shift+F).
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F,
      /*control=*/true, /*shift=*/true, /*alt=*/false, /*command=*/false));
  EXPECT_TRUE(test_listener.WaitUntilSatisfied());
  // activeTab should now be granted.
  EXPECT_TRUE(IsGrantedForTab(extension, tab));
  // Verify the command worked.
  EXPECT_EQ(std::string("basics browser action"), test_listener.message());

  test_listener.Reset();
  // Activate the command shortcut (Ctrl+Shift+Y).
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_Y,
      /*control=*/true, /*shift=*/true, /*alt=*/false, /*command=*/false));
  EXPECT_TRUE(test_listener.WaitUntilSatisfied());
  EXPECT_EQ(std::string(kBasicsShortcutCommandName), test_listener.message());
}

IN_PROC_BROWSER_TEST_F(CommandsApiTest, InactivePageActionDoesntTrigger) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("keybinding/page_action")) << message_;
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;

  const int tab_id = NavigateToTestURLAndReturnTabId();

  ExtensionActionManager* action_manager =
      ExtensionActionManager::Get(profile());
  const ExtensionAction* extension_action =
      action_manager->GetExtensionAction(*extension);
  ASSERT_TRUE(extension_action);

  // Explicitly disable the action globally to mimic the MV2 page_action's
  // initial hidden/disabled state.
  DisableActionGlobally(profile(), *extension);
  EXPECT_FALSE(extension_action->GetIsVisible(tab_id));

  // If the page action is disabled, the event shouldn't be dispatched.
  bool expect_dispatch = false;
  SendKeyPressToAction(GetBrowserWindowInterface(), *extension, ui::VKEY_F,
                       "action.onClicked", expect_dispatch);
}

// Tests that a page action that is unpinned and only shown within the
// extensions menu will still properly trigger when the keybinding is used.
IN_PROC_BROWSER_TEST_F(CommandsApiTest, UnpinnedPageActionTriggers) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("keybinding/page_action")) << message_;
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;

  // Explicitly disable the action globally to mimic the MV2 page_action's
  // initial hidden/disabled state.
  DisableActionGlobally(profile(), *extension);

  auto* toolbar_model = ToolbarActionsModel::Get(profile());
#if !BUILDFLAG(IS_ANDROID)
  RunScheduledLayouts();
#endif
  EXPECT_FALSE(toolbar_model->IsActionPinned(extension->id()));

  const int tab_id = NavigateToTestURLAndReturnTabId();
  SetActionEnabledOnTab(profile(), *extension, tab_id);

  constexpr bool kExpectDispatch = true;
  SendKeyPressToAction(GetBrowserWindowInterface(), *extension, ui::VKEY_F,
                       "action.onClicked", kExpectDispatch);
}

IN_PROC_BROWSER_TEST_F(CommandsApiTest, ActionOverrideChromeShortcut) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("keybinding/page_action")) << message_;
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;

  // Explicitly disable the action globally to mimic the MV2 page_action's
  // initial hidden state.
  DisableActionGlobally(profile(), *extension);

  CommandService* command_service = CommandService::Get(profile());
// Simulate the user setting the keybinding to override the print shortcut.
#if BUILDFLAG(IS_MAC)
  std::string print_shortcut = "Command+P";
#else
  std::string print_shortcut = "Ctrl+P";
#endif
  command_service->UpdateKeybindingPrefs(
      extension->id(), manifest_values::kActionCommandEvent, print_shortcut);

  const int tab_id = NavigateToTestURLAndReturnTabId();

  SetActionEnabledOnTab(profile(), *extension, tab_id);

  ExtensionTestMessageListener test_listener;  // Won't reply.
  test_listener.set_extension_id(extension->id());

  // Note: The following incantation uses too many custom bits to comfortably
  // fit into SendKeyPressToAction(); do it manually.
  bool control_is_modifier = false;
  bool command_is_modifier = false;
#if BUILDFLAG(IS_MAC)
  command_is_modifier = true;
#else
  control_is_modifier = true;
#endif

  // Activate the omnibox. This checks to ensure that the extension shortcut
  // still works even if the WebContents isn't focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_L, control_is_modifier, false,
      false, command_is_modifier));

  // Activate the shortcut.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_P, control_is_modifier, false,
      false, command_is_modifier));

  EXPECT_TRUE(test_listener.WaitUntilSatisfied());
  EXPECT_EQ("clicked", test_listener.message());
}

// This test validates that an extension cannot request a shortcut that is
// already in use by Chrome.
IN_PROC_BROWSER_TEST_F(CommandsApiTest, DontOverwriteSystemShortcuts) {
  ASSERT_TRUE(embedded_test_server()->Start());

#if BUILDFLAG(ENABLE_EXTENSIONS)
  // TODO(crbug.com/405219115): Currently, on android, we can assume the browser
  // window is at front, but nice to remove the assumption by unifying the
  // code with other platform.
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
#endif

  ASSERT_TRUE(RunExtensionTest("keybinding/dont_overwrite_system")) << message_;

  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("/extensions/test_file.txt")));

  // Activate the regular shortcut (Alt+Shift+F).
  ExtensionTestMessageListener alt_shift_f_listener("alt_shift_f");
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, false, true, true, false));
  EXPECT_TRUE(alt_shift_f_listener.WaitUntilSatisfied());

  // Try to activate the Ctrl+F shortcut (shouldn't work).
  // Since keypresses are sent synchronously, we can check this by first sending
  // Ctrl+F (which shouldn't work), followed by Alt+Shift+F (which should work),
  // and listening for both. If, by the time we receive the Alt+Shift+F
  // response, we haven't received a response for Ctrl+F, it is safe to say we
  // won't receive one.
  ExtensionTestMessageListener ctrl_f_listener("ctrl_f");
  alt_shift_f_listener.Reset();
  // Send Ctrl+F.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, true, false, false, false));
  // Send Alt+Shift+F.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, false, true, true, false));
  EXPECT_TRUE(alt_shift_f_listener.WaitUntilSatisfied());
  EXPECT_FALSE(ctrl_f_listener.was_satisfied());
}

// This test validates that user-set override of the Chrome bookmark shortcut in
// an extension that does not request it does supersede the same keybinding by
// web pages.
IN_PROC_BROWSER_TEST_F(CommandsApiTest,
                       OverwriteBookmarkShortcutByUserOverridesWebKeybinding) {
  ASSERT_TRUE(embedded_test_server()->Start());

#if BUILDFLAG(ENABLE_EXTENSIONS)
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
#endif

  ASSERT_TRUE(RunExtensionTest("keybinding/basics")) << message_;

  CommandService* command_service = CommandService::Get(profile());

  const Extension* extension = GetSingleLoadedExtension();
  // Simulate the user setting the keybinding to Ctrl+D.
  command_service->UpdateKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent,
                                         kBookmarkKeybinding);

  ASSERT_TRUE(
      NavigateToURL(GetActiveWebContents(),
                    embedded_test_server()->GetURL(
                        "/extensions/test_file_with_ctrl-d_keybinding.html")));

  ExtensionTestMessageListener test_listener;
  // Activate the shortcut (Ctrl+D) which should be handled by the extension.
  ASSERT_TRUE(SendBookmarkKeyPressSync(GetBrowserWindowInterface()));
  EXPECT_TRUE(test_listener.WaitUntilSatisfied());
  EXPECT_EQ(std::string("basics browser action"), test_listener.message());
}

#if BUILDFLAG(IS_WIN)
// Currently this feature is implemented on Windows only.
#define MAYBE_AllowDuplicatedMediaKeys AllowDuplicatedMediaKeys
#else
#define MAYBE_AllowDuplicatedMediaKeys DISABLED_AllowDuplicatedMediaKeys
#endif

// Test that media keys go to all extensions that register for them.
IN_PROC_BROWSER_TEST_F(CommandsApiTest, MAYBE_AllowDuplicatedMediaKeys) {
  ResultCatcher catcher;
  ASSERT_TRUE(RunExtensionTest("keybinding/non_global_media_keys_0"))
      << message_;
  ASSERT_TRUE(catcher.GetNextResult());
  ASSERT_TRUE(RunExtensionTest("keybinding/non_global_media_keys_1"))
      << message_;
  ASSERT_TRUE(catcher.GetNextResult());

  // Activate the Media Stop key.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetBrowserWindowInterface(),
                                              ui::VKEY_MEDIA_STOP, false, false,
                                              false, false));

  // We should get two success result.
  ASSERT_TRUE(catcher.GetNextResult());
  ASSERT_TRUE(catcher.GetNextResult());
}

#if BUILDFLAG(IS_CHROMEOS) && !defined(NDEBUG)
// TODO(dtseng): Test times out on Chrome OS debug. See crbug.com/40383617.
#define MAYBE_ContinuePropagation DISABLED_ContinuePropagation
#else
#define MAYBE_ContinuePropagation ContinuePropagation
#endif

IN_PROC_BROWSER_TEST_F(CommandsApiTest, MAYBE_ContinuePropagation) {
  // Setup the environment.
  ASSERT_TRUE(embedded_test_server()->Start());
#if BUILDFLAG(ENABLE_EXTENSIONS)
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
#endif
  ASSERT_TRUE(RunExtensionTest("keybinding/continue_propagation")) << message_;
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("/extensions/test_file.txt")));

  ResultCatcher catcher;

  // Activate the shortcut (Ctrl+Shift+F). The page should capture the
  // keystroke and not the extension since |onCommand| has no event listener
  // initially.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, true, true, false, false));
  ASSERT_TRUE(catcher.GetNextResult());

  // Now, the extension should have added an |onCommand| event listener.
  // Send the same key, but the |onCommand| listener should now receive it.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, true, true, false, false));
  ASSERT_TRUE(catcher.GetNextResult());

  // The extension should now have removed its |onCommand| event listener.
  // Finally, the page should again receive the key.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_F, true, true, false, false));
  ASSERT_TRUE(catcher.GetNextResult());
}

// Test is only applicable on Chrome OS.
#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(CommandsApiTest, ChromeOSConversions) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
  ASSERT_TRUE(RunExtensionTest("keybinding/chromeos_conversions")) << message_;
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), embedded_test_server()->GetURL("/extensions/test_file.txt")));

  ResultCatcher catcher;

  // Send all expected keys (Search+Shift+{Left, Up, Right, Down}).
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_LEFT, false,
                                              true, false, true));
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_UP, false,
                                              true, false, true));
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_RIGHT, false,
                                              true, false, true));
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_DOWN, false,
                                              true, false, true));

  ASSERT_TRUE(catcher.GetNextResult());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

// Validate parameters sent along with an extension event, in response to
// command being triggered.
IN_PROC_BROWSER_TEST_F(CommandsApiTest, TabParameter) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("keybinding/tab_parameter")) << message_;
  ASSERT_TRUE(NavigateToURL(
      GetActiveWebContents(),
      embedded_test_server()->GetURL("/extensions/test_file.txt")));
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;
  ResultCatcher catcher;
  EXPECT_TRUE(content::WaitForLoadStop(GetActiveWebContents()));
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(GetBrowserWindowInterface(),
                                              ui::VKEY_Y, true, true, false,
                                              false));  // Ctrl+Shift+Y
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Test Keybinding in incognito mode.
// Disabled because flaky: crbug.com/519947934
IN_PROC_BROWSER_TEST_P(IncognitoCommandsApiTest, DISABLED_IncognitoMode) {
  ASSERT_TRUE(embedded_test_server()->Start());

  bool is_incognito_enabled = GetParam();

  ASSERT_TRUE(RunExtensionTest("keybinding/basics", {},
                               {.allow_in_incognito = is_incognito_enabled}))
      << message_;

  // Open incognito window and navigate to test page.
  content::WebContents* incognito_web_contents = PlatformOpenURLOffTheRecord(
      profile(), embedded_test_server()->GetURL("/extensions/test_file.html"));

  ASSERT_TRUE(NavigateToURL(
      incognito_web_contents,
      embedded_test_server()->GetURL("/extensions/test_file.txt")));

  BrowserWindowInterface* incognito_browser =
      extensions::browser_window_util::GetBrowserForTabContents(
          *incognito_web_contents);
  ASSERT_TRUE(incognito_browser);

  TestEventRouterObserver test_observer(
      EventRouter::Get(incognito_browser->GetProfile()));

  // Activate the browser action shortcut (Ctrl+Shift+F).
  ExtensionTestMessageListener action_listener("basics browser action");
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(incognito_browser, ui::VKEY_F,
                                              true, true, false, false));
  if (is_incognito_enabled) {
    EXPECT_TRUE(action_listener.WaitUntilSatisfied());
  } else {
    base::RunLoop().RunUntilIdle();
  }
  EXPECT_EQ(is_incognito_enabled,
            test_observer.dispatched_events().contains("action.onClicked"));

  test_observer.ClearEvents();

  // Activate the command shortcut (Ctrl+Shift+Y).
  ExtensionTestMessageListener command_listener("toggle-feature");
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(incognito_browser, ui::VKEY_Y,
                                              true, true, false, false));
  if (is_incognito_enabled) {
    EXPECT_TRUE(action_listener.WaitUntilSatisfied());
  } else {
    base::RunLoop().RunUntilIdle();
  }
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(is_incognito_enabled,
            test_observer.dispatched_events().contains("commands.onCommand"));
}

// On Android platform, incognito config looks not working yet.
// TODO(crbug.com/405219115): Fix the incognito function.
INSTANTIATE_TEST_SUITE_P(All,
                         IncognitoCommandsApiTest,
#if BUILDFLAG(ENABLE_EXTENSIONS)
                         testing::Bool()
#else
                         testing::Values(false)
#endif
);

IN_PROC_BROWSER_TEST_P(ActionCommandsApiTest,
                       TriggeringCommandTriggersListener) {
  ASSERT_TRUE(embedded_test_server()->Start());

  const ActionInfo::Type action_type = GetParam();

  // Load a test extension that has a command that invokes the action, and sends
  // a message when the action is invoked.
  constexpr char kManifestTemplate[] = R"(
    {
      "name": "Extension Action Listener Test",
      "manifest_version": %d,
      "version": "0.1",
      "commands": {
        "%s": {
          "suggested_key": {
            "default": "Alt+Shift+U"
          }
        }
      },
      "%s": {},
      "background": { %s }
    }
  )";
  constexpr char kBackgroundScriptTemplate[] = R"(
      chrome.%s.onClicked.addListener(() => {
        chrome.test.sendMessage('clicked');
      });
      chrome.test.sendMessage('ready');
  )";
  const char* background_specification =
      action_type == ActionInfo::Type::kAction
          ? R"("service_worker": "background.js")"
          : R"("scripts": ["background.js"])";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(base::StringPrintf(
      kManifestTemplate, GetManifestVersionForActionType(action_type),
      GetCommandKeyForActionType(action_type),
      ActionInfo::GetManifestKeyForActionType(action_type),
      background_specification));
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"),
                     base::StringPrintf(kBackgroundScriptTemplate,
                                        GetAPINameForActionType(action_type)));

  ExtensionTestMessageListener listener("ready");
  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(listener.WaitUntilSatisfied());
  ASSERT_TRUE(HasActiveActionCommand(extension->id(), action_type));

  const int tab_id = NavigateToTestURLAndReturnTabId();

  // If the action is a page action, it's disabled by default. Show it.
  // NOTE: This block only runs for MV2 kPage and kBrowser types, or if you
  // explicitly use chrome.action.disable() for MV3 kAction type.
  if (action_type == ActionInfo::Type::kPage) {
    SetActionEnabledOnTab(profile(), *extension, tab_id);
    ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
  }

  ExtensionTestMessageListener click_listener("clicked");
  // Execute a trivial command in the service worker to ensure it's alive right
  // before sending the key press. This is critical for MV3 tests. This step is
  // only required for the MV3 'kAction' parameter.
  if (action_type == ActionInfo::Type::kAction) {
    base::Value result = browsertest_util::ExecuteScriptInBackgroundPage(
        profile(), extension->id(),
        "chrome.runtime.getPlatformInfo(() => "
        "chrome.test.sendScriptResult('success'));");
    EXPECT_EQ("success", result);
  }

  // Send the key press to trigger the command.
  EXPECT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_U, false, true, true, false));

  // The listener should now successfully receive the 'clicked' message.
  EXPECT_TRUE(click_listener.WaitUntilSatisfied());
}

#if BUILDFLAG(IS_ANDROID)
// TODO(crbug.com/405219115): For some reason this is causing flakiness issue
// on bot (but not yet locally reproducible). Investigate and fix the issue.
#define MAYBE_TriggeringCommandTriggersPopup \
  DISABLED_TriggeringCommandTriggersPopup
#else
#define MAYBE_TriggeringCommandTriggersPopup TriggeringCommandTriggersPopup
#endif

// Tests that triggering a command associated with an action opens an
// extension's popup.
IN_PROC_BROWSER_TEST_P(ActionCommandsApiTest,
                       MAYBE_TriggeringCommandTriggersPopup) {
  ASSERT_TRUE(embedded_test_server()->Start());

  const ActionInfo::Type action_type = GetParam();

  // Load an extension that specifies a command to invoke the action, and has
  // a default popup.
  constexpr char kManifestTemplate[] = R"(
    {
      "name": "Extension Action Listener Test",
      "manifest_version": %d,
      "version": "0.1",
      "commands": {
        "%s": {
          "suggested_key": {
            "default": "Alt+Shift+U"
          }
        }
      },
      "%s": {"default_popup": "popup.html"}
    }
  )";
  constexpr char kPopupHtml[] = R"(
      <!doctype html>
      <html>
        <script src="popup.js"></script>
      </html>
  )";
  constexpr char kPopupJs[] = "chrome.test.notifyPass();";

  TestExtensionDir test_dir;
  test_dir.WriteManifest(base::StringPrintf(
      kManifestTemplate, GetManifestVersionForActionType(action_type),
      GetCommandKeyForActionType(action_type),
      ActionInfo::GetManifestKeyForActionType(action_type)));
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.html"), kPopupHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("popup.js"), kPopupJs);

  const Extension* extension = LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);
  ASSERT_TRUE(HasActiveActionCommand(extension->id(), action_type));

  const int tab_id = NavigateToTestURLAndReturnTabId();

  if (action_type == ActionInfo::Type::kPage) {
    // Note: We don't use SetActionEnabledOnTab() here because it relies on a
    // background page, which this extension doesn't have.
    ExtensionActionManager::Get(profile())
        ->GetExtensionAction(*extension)
        ->SetIsVisible(tab_id, true);
    ASSERT_TRUE(WaitForPageActionVisibilityChangeTo(1));
  }

  // Invoke the action, and wait for the popup to show.
  ResultCatcher catcher;
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(
      GetBrowserWindowInterface(), ui::VKEY_U, false, true, true, false));
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();

  // Verify popup is shown.
  auto* view_model = GetViewModelForId(extension->id());
  EXPECT_TRUE(view_model->IsShowingPopup());
}

// In new platforms, we only supports manifest v3. kBrowser/kPage is for v2
// so excluded there.
INSTANTIATE_TEST_SUITE_P(All,
                         ActionCommandsApiTest,
                         testing::Values(
#if BUILDFLAG(ENABLE_EXTENSIONS)
                             ActionInfo::Type::kBrowser,
                             ActionInfo::Type::kPage,
#endif
                             ActionInfo::Type::kAction));

}  // namespace extensions
