// Copyright 2013 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/extensions/commands/command_service.h"

#include <utility>

#include "base/strings/stringprintf.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/api/extension_action/action_info.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/test/test_extension_dir.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/accelerators/command_constants.h"
#include "ui/events/keycodes/keyboard_codes.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace {
const char kBasicBrowserActionKeybinding[] = "Ctrl+Shift+F";
const char kBasicNamedKeybinding[] = "Ctrl+Shift+Y";
const char kBasicNamedKeybinding2[] = "Ctrl+Shift+X";
const char kBasicAlternateKeybinding[] = "Ctrl+Shift+G";
const char kBasicNamedCommand[] = "toggle-feature";
const char kBasicNamedCommand2[] = "toggle-feature-2";
constexpr char kAltNKeybinding[] = "Alt+N";
constexpr char kAltZKeybinding[] = "Alt+Z";
constexpr char kExtensionId[] = "pgoakhfeplldmjheffidklpoklkppipp";
constexpr char kManifestTemplate[] = R"({
    "name": "An action to test if upgrading from MV2 to MV3 succeeds.",
    "version": "1.0",
    "manifest_version": %d,
    "commands": {
      "%s": {
        "suggested_key": "%s"
      }
    },
    "%s": {}})";

// Get another command platform, which is used for simulating a command has been
// assigned with a shortcut on another platform.
std::string GetAnotherCommandPlatform() {
#if BUILDFLAG(IS_WIN)
  return ui::kKeybindingPlatformMac;
#elif BUILDFLAG(IS_MAC)
  return ui::kKeybindingPlatformChromeOs;
#elif BUILDFLAG(IS_CHROMEOS)
  return ui::kKeybindingPlatformLinux;
#elif BUILDFLAG(IS_LINUX)
  return ui::kKeybindingPlatformWin;
#else
  return "";
#endif
}

// Parameters passed to CommandServiceMv3UpgradeTest parameterized tests.
struct ManifestCommandTestParameters {
  const char* action_name;          // e.g. "browser_action", "page_action".
  const char* action_command_name;  // e.g. "_execute_browser_action",
                                    // "_execute_page_action".
  // e.g. the associated folder below
  // //chrome/test/data/extensions/api_test/keybinding/update/ that contains the
  // unpacked extension files for webstore and .crx installs.
  const char* action_command_mv2_extension_path;
};

}  // namespace

namespace extensions {

using CommandServiceTest = ExtensionApiTest;
// Test class for testing keybinding changes across MV2->MV3 reloads/updates.
class CommandServiceMv3UpgradeTest
    : public ExtensionApiTest,
      public testing::WithParamInterface<ManifestCommandTestParameters> {
 protected:
  // Create a new extension from a manifest for MV2 and verify its command
  // shortcuts are setup correctly.
  std::string LoadExtensionMv2(const char* suggested_keybinding);
  // Reload the extension from a manifest for MV3 and verify its command
  // shortcut has the new suggested keybinding.
  void ReloadExtensionMv3(const std::string& id,
                          const char* new_suggested_keybinding);
  // Install an extension with `id`. `install_from_crx` = false means it will
  // install from webstore.
  void InstallExtensionMv2(const std::string& id,
                           bool install_from_crx = false);
  // Update the extension with `id` (from webstore or via crx) from the
  // `mv3_extension_upgrade_folder`.
  void UpdateExtensionMv3(const std::string& id,
                          const char* mv3_extension_upgrade_folder);
  // Modify the browser action keybinding as if it was modified by the user.
  void ChangeMv2CommandKeybinding(const std::string& id,
                                  const char* new_keybinding);
  bool CommandHasAltPlusKeybinding(const std::string& id,
                                   const char* action_command_name,
                                   const ui::KeyboardCode key);
  bool CommandHasNoKeybinding(const std::string& id,
                              const char* action_command_name);
  // Checks that extension with `id` no longer has MV2 command action
  // keybindings and now has an MV3 keybinding of Alt + `keyboard_code`.
  void CheckMv3UpgradeHasExpectedKeybindings(
      const std::string& id,
      const ui::KeyboardCode keyboard_code);

  TestExtensionDir& test_dir() { return test_dir_; }
  CommandService* command_service() { return CommandService::Get(profile()); }

 private:
  TestExtensionDir test_dir_;
  base::FilePath test_path_;
};

std::string CommandServiceMv3UpgradeTest::LoadExtensionMv2(
    const char* suggested_keybinding) {
  ManifestCommandTestParameters test_params = GetParam();
  test_dir().WriteManifest(
      base::StringPrintf(kManifestTemplate, 2, test_params.action_command_name,
                         suggested_keybinding, test_params.action_name));

  // Install MV2 of the extension.
  const Extension* extension = LoadExtension(
      test_dir().UnpackedPath(), {.wait_for_registration_stored = true});
  if (!extension) {
    ADD_FAILURE() << "Couldn't load extension successfully for test setup.";
  }
  const std::string unpacked_extension_id = extension->id();
  EXPECT_TRUE(extension_registry()->enabled_extensions().Contains(
      unpacked_extension_id));

  // Verify it has an MV2 action command of Alt+N.
  EXPECT_TRUE(CommandHasAltPlusKeybinding(unpacked_extension_id.c_str(),
                                          test_params.action_command_name,
                                          ui::VKEY_N));
  // Verify MV3 action command is set to nothing before we upgrade.
  EXPECT_TRUE(CommandHasNoKeybinding(unpacked_extension_id.c_str(),
                                     manifest_values::kActionCommandEvent));
  return unpacked_extension_id;
}

void CommandServiceMv3UpgradeTest::ReloadExtensionMv3(
    const std::string& id,
    const char* new_suggested_keybinding) {
  test_dir().WriteManifest(base::StringPrintf(
      kManifestTemplate, 3, manifest_values::kActionCommandEvent,
      new_suggested_keybinding, manifest_keys::kAction));
  ReloadExtension(id);
  EXPECT_TRUE(extension_registry()->enabled_extensions().GetByID(id));
}

void CommandServiceMv3UpgradeTest::InstallExtensionMv2(const std::string& id,
                                                       bool install_from_crx) {
  // TODO(jlulejian): Try to make these test class members for less repetition.
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir scoped_temp_dir;
  base::FilePath pem_path =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("keybinding.pem");
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  ManifestCommandTestParameters test_params = GetParam();
  base::FilePath path_mv2 = PackExtensionWithOptions(
      test_data_dir_.AppendASCII("keybinding")
          .AppendASCII("update")
          .AppendASCII(test_params.action_command_mv2_extension_path),
      scoped_temp_dir.GetPath().AppendASCII("v1.crx"), pem_path,
      base::FilePath());
  ASSERT_FALSE(path_mv2.empty());

  // Install MV2 of the extension.
  if (install_from_crx) {
    ASSERT_TRUE(InstallExtension(path_mv2, 1));
  } else {
    ASSERT_TRUE(InstallExtensionFromWebstore(path_mv2, 1));
  }
  EXPECT_TRUE(extension_registry()->enabled_extensions().GetByID(id));

  // Verify it has an MV2 action command of Alt+N.
  EXPECT_TRUE(CommandHasAltPlusKeybinding(id, test_params.action_command_name,
                                          ui::VKEY_N));
  // Verify MV3 action command is set to nothing before we upgrade.
  EXPECT_TRUE(CommandHasNoKeybinding(id, manifest_values::kActionCommandEvent));
}

void CommandServiceMv3UpgradeTest::UpdateExtensionMv3(
    const std::string& id,
    const char* mv3_extension_upgrade_folder) {
  // TODO(jlulejian): Try to make these test class members for less repetition.
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir scoped_temp_dir;
  base::FilePath pem_path =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("keybinding.pem");
  ASSERT_TRUE(scoped_temp_dir.CreateUniqueTempDir());

  base::FilePath path_mv3 =
      PackExtensionWithOptions(test_data_dir_.AppendASCII("keybinding")
                                   .AppendASCII("update")
                                   .AppendASCII(mv3_extension_upgrade_folder),
                               scoped_temp_dir.GetPath().AppendASCII("v2.crx"),
                               pem_path, base::FilePath());
  ASSERT_FALSE(path_mv3.empty());

  // Update to MV3 with an action command instead of a browser action.
  ASSERT_TRUE(UpdateExtension(id, path_mv3, 0));
  EXPECT_TRUE(extension_registry()->enabled_extensions().GetByID(id));
}

void CommandServiceMv3UpgradeTest::ChangeMv2CommandKeybinding(
    const std::string& id,
    const char* new_keybinding) {
  const char* action_command_name = GetParam().action_command_name;
  Command action_command =
      command_service()->FindCommandByName(id, action_command_name);
  command_service()->UpdateKeybindingPrefs(id, action_command_name,
                                           new_keybinding);
}

void CommandServiceMv3UpgradeTest::CheckMv3UpgradeHasExpectedKeybindings(
    const std::string& id,
    const ui::KeyboardCode keyboard_code) {
  // Verify both MV2 commands are set to nothing.
  EXPECT_TRUE(
      CommandHasNoKeybinding(id, manifest_values::kBrowserActionCommandEvent));
  EXPECT_TRUE(
      CommandHasNoKeybinding(id, manifest_values::kPageActionCommandEvent));

  // Verify extension now has an MV3 action command of Alt+<keyboard_code>.
  EXPECT_TRUE(CommandHasAltPlusKeybinding(
      id, manifest_values::kActionCommandEvent, keyboard_code));
}

bool CommandServiceMv3UpgradeTest::CommandHasAltPlusKeybinding(
    const std::string& id,
    const char* action_command_name,
    const ui::KeyboardCode key) {
  ui::Accelerator accelerator = command_service()
                                    ->FindCommandByName(id, action_command_name)
                                    .accelerator();
  return key == accelerator.key_code() && accelerator.IsAltDown() &&
         !accelerator.IsShiftDown() && !accelerator.IsCtrlDown();
}
bool CommandServiceMv3UpgradeTest::CommandHasNoKeybinding(
    const std::string& id,
    const char* action_command_name) {
  ui::Accelerator accelerator = command_service()
                                    ->FindCommandByName(id, action_command_name)
                                    .accelerator();
  return ui::VKEY_UNKNOWN == accelerator.key_code();
}

// Tests that a suggested keybinding for an action command in MV2 persists when
// the extension is reloaded (unpacked) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       ActionCommandUpgradesDuringReload) {
  const std::string unpacked_extension_id = LoadExtensionMv2(
      /*suggested_keybinding=*/kAltNKeybinding);
  ReloadExtensionMv3(unpacked_extension_id,
                     /*new_suggested_keybinding=*/kAltNKeybinding);
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(unpacked_extension_id,
                                        /*keyboard_code=*/ui::VKEY_N);
}

// Tests that a suggested keybinding for an action command in MV2 persists when
// the extension is updated from the webstore and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       ActionCommandUpgradesDuringWebstoreUpdate) {
  InstallExtensionMv2(kExtensionId);
  UpdateExtensionMv3(kExtensionId,
                     /*mv3_extension_upgrade_folder=*/"v2_upgrade");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_N);
}

// Tests that a suggested keybinding for an action command in MV2 persists when
// the extension is updated (via .crx) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       ActionCommandUpgradesDuringCrxUpdate) {
  InstallExtensionMv2(kExtensionId, /*install_from_crx=*/true);
  UpdateExtensionMv3(kExtensionId,
                     /*mv3_extension_upgrade_folder=*/"v2_upgrade");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_N);
}

// Tests that a user modified keybinding for an action command in MV2 persists
// when the extension is reloaded (unpacked) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       UserModifiedActionCommandSurvivesReload) {
  const std::string unpacked_extension_id = LoadExtensionMv2(
      /*suggested_keybinding=*/kAltNKeybinding);
  ChangeMv2CommandKeybinding(unpacked_extension_id,
                             /*new_keybinding=*/kAltZKeybinding);
  ReloadExtensionMv3(unpacked_extension_id,
                     /*new_suggested_keybinding=*/kAltNKeybinding);
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(unpacked_extension_id,
                                        /*keyboard_code=*/ui::VKEY_Z);
}

// Tests that a user-modified keybinding for an action command in MV2 persists
// when the extension is updated from the webstore and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       UserModifiedCommandSurvivesWebstoreUpdate) {
  InstallExtensionMv2(kExtensionId);
  ChangeMv2CommandKeybinding(kExtensionId,
                             /*new_keybinding=*/kAltZKeybinding);
  UpdateExtensionMv3(kExtensionId,
                     /*mv3_extension_upgrade_folder=*/"v2_upgrade");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_Z);
}

// Tests that a user modified keybinding for an action command in MV2 persists
// when the extension is updated (via .crx) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       UserModifiedCommandSurvivesCrxUpdate) {
  InstallExtensionMv2(kExtensionId, /*install_from_crx=*/true);
  ChangeMv2CommandKeybinding(kExtensionId,
                             /*new_keybinding=*/kAltZKeybinding);
  UpdateExtensionMv3(kExtensionId,
                     /*mv3_extension_upgrade_folder=*/"v2_upgrade");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_Z);
}

// Tests that a modified suggested keybinding for an action command in MV2
// persists when the extension is reloaded (unpacked) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       SuggestedKeyModifiedActionCommandSurvivesReload) {
  const std::string unpacked_extension_id = LoadExtensionMv2(
      /*suggested_keybinding=*/kAltNKeybinding);
  ReloadExtensionMv3(unpacked_extension_id,
                     /*new_suggested_keybinding=*/"Alt+U");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(unpacked_extension_id,
                                        /*keyboard_code=*/ui::VKEY_U);
}

// Tests that a modified suggested keybinding for an action command in MV2
// persists when the extension is updated (via webstore) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       SuggestedKeyModifiedActionCommandSurvivesWebstore) {
  InstallExtensionMv2(kExtensionId);
  UpdateExtensionMv3(kExtensionId, "v2_upgrade_reassign");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_U);
}

// Tests that a modified suggested keybinding for an action command in MV2
// persists when the extension is updated (via .crx) and upgraded to MV3
// (_execute_action_command).
IN_PROC_BROWSER_TEST_P(CommandServiceMv3UpgradeTest,
                       SuggestedKeyModifiedActionCommandSurvivesCrx) {
  InstallExtensionMv2(kExtensionId, /*install_from_crx=*/true);
  UpdateExtensionMv3(kExtensionId,
                     /*mv3_extension_upgrade_folder=*/"v2_upgrade_reassign");
  ASSERT_FALSE(HasFatalFailure() && HasNonfatalFailure());

  CheckMv3UpgradeHasExpectedKeybindings(kExtensionId,
                                        /*keyboard_code=*/ui::VKEY_U);
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest, RemoveShortcutSurvivesUpdate) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir scoped_temp_dir;
  EXPECT_TRUE(scoped_temp_dir.CreateUniqueTempDir());
  base::FilePath pem_path = test_data_dir_.
      AppendASCII("keybinding").AppendASCII("keybinding.pem");
  base::FilePath path_v1 =
      PackExtensionWithOptions(test_data_dir_.AppendASCII("keybinding")
                                   .AppendASCII("update")
                                   .AppendASCII("v1"),
                               scoped_temp_dir.GetPath().AppendASCII("v1.crx"),
                               pem_path, base::FilePath());
  base::FilePath path_v2 =
      PackExtensionWithOptions(test_data_dir_.AppendASCII("keybinding")
                                   .AppendASCII("update")
                                   .AppendASCII("v2"),
                               scoped_temp_dir.GetPath().AppendASCII("v2.crx"),
                               pem_path, base::FilePath());

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

  // Install v1 of the extension.
  ASSERT_TRUE(InstallExtension(path_v1, 1));
  EXPECT_TRUE(registry->enabled_extensions().GetByID(kExtensionId));

  // Verify it has a command of Alt+Shift+F.
  ui::Accelerator accelerator =
      command_service
          ->FindCommandByName(kExtensionId,
                              manifest_values::kActionCommandEvent)
          .accelerator();
  EXPECT_EQ(ui::VKEY_F, accelerator.key_code());
  EXPECT_FALSE(accelerator.IsCtrlDown());
  EXPECT_TRUE(accelerator.IsShiftDown());
  EXPECT_TRUE(accelerator.IsAltDown());

  // Remove the keybinding.
  command_service->UpdateKeybindingPrefs(kExtensionId,
                                         manifest_values::kActionCommandEvent,
                                         /*keystroke=*/"");

  // Verify it got removed.
  accelerator = command_service
                    ->FindCommandByName(kExtensionId,
                                        manifest_values::kActionCommandEvent)
                    .accelerator();
  EXPECT_EQ(ui::VKEY_UNKNOWN, accelerator.key_code());

  // Update to version 2.
  EXPECT_TRUE(UpdateExtension(kExtensionId, path_v2, 0));
  EXPECT_TRUE(registry->enabled_extensions().GetByID(kExtensionId));

  // Verify it is still set to nothing.
  accelerator = command_service
                    ->FindCommandByName(kExtensionId,
                                        manifest_values::kActionCommandEvent)
                    .accelerator();
  EXPECT_EQ(ui::VKEY_UNKNOWN, accelerator.key_code());
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest,
                       RemoveKeybindingPrefsShouldBePlatformSpecific) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

  ScopedDictPrefUpdate updater(profile()->GetPrefs(),
                               prefs::kExtensionCommands);
  base::DictValue& bindings = updater.Get();

  // Simulate command |toggle-feature| has been assigned with a shortcut on
  // another platform.
  std::string anotherPlatformKey = GetAnotherCommandPlatform() + ":Alt+G";
  const char kNamedCommandName[] = "toggle-feature";
  base::DictValue keybinding;
  keybinding.Set("extension", extension->id());
  keybinding.Set("command_name", kNamedCommandName);
  keybinding.Set("global", false);
  bindings.Set(anotherPlatformKey, std::move(keybinding));

  CommandService* command_service = CommandService::Get(profile());
  command_service->RemoveKeybindingPrefs(extension->id(), kNamedCommandName);

  // Removal of keybinding preference should be platform-specific, so the key on
  // another platform should always remained.
  EXPECT_TRUE(bindings.Find(anotherPlatformKey));
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest,
                       GetExtensionActionCommandQueryAll) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

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

  {
    Command command;
    bool active = false;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ALL,
        &command, &active));

    EXPECT_EQ(kBasicBrowserActionKeybinding,
              Command::AcceleratorToString(command.accelerator()));
    EXPECT_TRUE(active);
  }

  command_service->UpdateKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent,
                                         kBasicAlternateKeybinding);

  {
    Command command;
    bool active = false;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ALL,
        &command, &active));

    EXPECT_EQ(kBasicAlternateKeybinding,
              Command::AcceleratorToString(command.accelerator()));
    EXPECT_TRUE(active);
  }

  command_service->RemoveKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent);

  {
    Command command;
    bool active = true;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ALL,
        &command, &active));

    EXPECT_EQ(kBasicBrowserActionKeybinding,
              Command::AcceleratorToString(command.accelerator()));
    EXPECT_FALSE(active);
  }

  command_service->UpdateKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent,
                                         /*keystroke=*/"");

  {
    Command command;
    bool active = true;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ALL,
        &command, &active));

    EXPECT_EQ(ui::VKEY_UNKNOWN, command.accelerator().key_code());
    EXPECT_FALSE(active);
  }
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest,
                       GetExtensionActionCommandQueryActive) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

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

  {
    Command command;
    bool active = false;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ACTIVE,
        &command, &active));

    EXPECT_EQ(kBasicBrowserActionKeybinding,
              Command::AcceleratorToString(command.accelerator()));
    EXPECT_TRUE(active);
  }

  command_service->UpdateKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent,
                                         kBasicAlternateKeybinding);

  {
    Command command;
    bool active = false;
    EXPECT_TRUE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ACTIVE,
        &command, &active));

    EXPECT_EQ(kBasicAlternateKeybinding,
              Command::AcceleratorToString(command.accelerator()));
    EXPECT_TRUE(active);
  }

  command_service->RemoveKeybindingPrefs(extension->id(),
                                         manifest_values::kActionCommandEvent);

  {
    Command command;
    bool active = false;
    EXPECT_FALSE(command_service->GetExtensionActionCommand(
        extension->id(), ActionInfo::Type::kAction, CommandService::ACTIVE,
        &command, &active));
  }
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest,
                       GetNamedCommandsQueryAll) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

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

  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ALL, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(kBasicNamedKeybinding,
              Command::AcceleratorToString(command.accelerator()));
  }

  command_service->UpdateKeybindingPrefs(
      extension->id(), kBasicNamedCommand, kBasicAlternateKeybinding);

  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ALL, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(kBasicAlternateKeybinding,
              Command::AcceleratorToString(command.accelerator()));
  }

  // Tests that removing the keybinding pref restores the default binding.
  command_service->RemoveKeybindingPrefs(extension->id(), kBasicNamedCommand);

  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ALL, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(kBasicNamedKeybinding,
              Command::AcceleratorToString(command.accelerator()));
  }

  // Tests that setting an empty keybinding string unsets the binding.
  // Make sure other bindings are unaffected.
  command_service->UpdateKeybindingPrefs(extension->id(), kBasicNamedCommand,
                                         /*keystroke=*/"");
  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ALL, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(ui::VKEY_UNKNOWN, command.accelerator().key_code());

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand2));
    ui::Command command2 = command_map[kBasicNamedCommand2];
    EXPECT_EQ(kBasicNamedKeybinding2,
              Command::AcceleratorToString(command2.accelerator()));
  }
}

IN_PROC_BROWSER_TEST_F(CommandServiceTest, GetNamedCommandsQueryActive) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

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

  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ACTIVE, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(kBasicNamedKeybinding,
              Command::AcceleratorToString(command.accelerator()));
  }

  command_service->UpdateKeybindingPrefs(
      extension->id(), kBasicNamedCommand, kBasicAlternateKeybinding);

  {
    ui::CommandMap command_map;
    EXPECT_TRUE(command_service->GetNamedCommands(
        extension->id(), CommandService::ACTIVE, CommandService::ANY_SCOPE,
        &command_map));

    ASSERT_EQ(1u, command_map.count(kBasicNamedCommand));
    ui::Command command = command_map[kBasicNamedCommand];
    EXPECT_EQ(kBasicAlternateKeybinding,
              Command::AcceleratorToString(command.accelerator()));
  }

  command_service->RemoveKeybindingPrefs(extension->id(), kBasicNamedCommand);

  {
    ui::CommandMap command_map;
    command_service->GetNamedCommands(
        extension->id(), CommandService::ACTIVE, CommandService::ANY_SCOPE,
        &command_map);
    EXPECT_EQ(0u, command_map.count(kBasicNamedCommand));
  }
}

// Tests that removing a keybinding does not cause a crash if the extension's
// "commands" preference has been corrupted into a non-dictionary type.
// This ensures ClearSuggestedKeyWasAssignedPrefs safely handles a nullptr
// when attempting to retrieve and update the command dictionary.
IN_PROC_BROWSER_TEST_F(CommandServiceTest, CorruptedCommandsPrefDoesNotCrash) {
  base::FilePath extension_dir =
      test_data_dir_.AppendASCII("keybinding").AppendASCII("basics");
  const Extension* extension = InstallExtension(extension_dir, 1);
  ASSERT_TRUE(extension);

  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
  ASSERT_TRUE(prefs);

  // Break preferences by setting an invalid data type so updater.Get() returns
  // nullptr.
  prefs->UpdateExtensionPref(extension->id(), "commands",
                             base::Value(base::Value::Type::LIST));

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

  // Trigger ClearSuggestedKeyWasAssignedPrefs.
  command_service->RemoveKeybindingPrefs(extension->id(), kBasicNamedCommand);
}

// Test both browser and page actions for these tests.
INSTANTIATE_TEST_SUITE_P(BrowserAction,
                         CommandServiceMv3UpgradeTest,
                         testing::Values(ManifestCommandTestParameters{
                             manifest_keys::kBrowserAction,
                             manifest_values::kBrowserActionCommandEvent,
                             "v1_upgrade_browser_action"}));

INSTANTIATE_TEST_SUITE_P(PageAction,
                         CommandServiceMv3UpgradeTest,
                         testing::Values(ManifestCommandTestParameters{
                             manifest_keys::kPageAction,
                             manifest_values::kPageActionCommandEvent,
                             "v1_upgrade_page_action"}));

}  // namespace extensions
