// 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 "base/command_line.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "extensions/common/api/commands/commands_handler.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/manifest_constants.h"
#include "extensions/common/manifest_test.h"
#include "extensions/common/warnings_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/accelerators/command.h"

namespace extensions {

namespace errors = manifest_errors;

using CommandsManifestTest = ManifestTest;

#if BUILDFLAG(IS_MAC)
constexpr int kControlKey = ui::EF_COMMAND_DOWN;
#else
constexpr int kControlKey = ui::EF_CONTROL_DOWN;
#endif

TEST_F(CommandsManifestTest, CommandManifestParseCommandsBrowserAction) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Browser Action",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_browser_action": {
               "suggested_key": "Alt+Shift+F",
               "description": "browser action"
             }
           },
           "browser_action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const ui::CommandMap* commands =
      CommandsInfo::GetNamedCommands(extension.get());
  ASSERT_TRUE(commands);
  EXPECT_EQ(1u, commands->size());
  auto iter = commands->begin();
  const ui::Command* named_command = &(*iter).second;
  EXPECT_EQ("feature1", named_command->command_name());
  EXPECT_EQ(u"desc", named_command->description());
  const ui::Accelerator ctrl_shift_f =
      ui::Accelerator(ui::VKEY_F, kControlKey | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(ctrl_shift_f, named_command->accelerator());

  const Command* browser_action =
      CommandsInfo::GetBrowserActionCommand(extension.get());
  ASSERT_TRUE(browser_action);
  EXPECT_EQ("_execute_browser_action", browser_action->command_name());
  EXPECT_EQ(u"", browser_action->description());
  const ui::Accelerator alt_shift_f =
      ui::Accelerator(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(alt_shift_f, browser_action->accelerator());

  EXPECT_FALSE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

TEST_F(CommandsManifestTest, CommandManifestParseCommandsPageAction) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - page Action",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_page_action": {
               "suggested_key": "Ctrl+F",
               "description": ""
             }
           },
           "page_action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const ui::CommandMap* commands =
      CommandsInfo::GetNamedCommands(extension.get());
  ASSERT_TRUE(commands);
  EXPECT_EQ(1u, commands->size());
  auto iter = commands->begin();
  const ui::Command* named_command = &(*iter).second;
  EXPECT_EQ("feature1", named_command->command_name());
  EXPECT_EQ(u"desc", named_command->description());

  const Command* page_action =
      CommandsInfo::GetPageActionCommand(extension.get());
  ASSERT_TRUE(page_action);
  EXPECT_EQ("_execute_page_action", page_action->command_name());
  EXPECT_EQ(u"", page_action->description());
  const ui::Accelerator ctrl_f = ui::Accelerator(ui::VKEY_F, kControlKey);
  EXPECT_EQ(ctrl_f, page_action->accelerator());

  EXPECT_FALSE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

TEST_F(CommandsManifestTest, CommandManifestParseCommandsAction) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Action",
           "manifest_version": 3,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_action": {
               "suggested_key": "Ctrl+G",
               "description": ""
             }
           },
           "action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const ui::CommandMap* commands =
      CommandsInfo::GetNamedCommands(extension.get());
  ASSERT_TRUE(commands);
  EXPECT_EQ(1u, commands->size());
  auto iter = commands->begin();
  const ui::Command* named_command = &(*iter).second;
  EXPECT_EQ("feature1", named_command->command_name());
  EXPECT_EQ(u"desc", named_command->description());

  const Command* action = CommandsInfo::GetActionCommand(extension.get());
  ASSERT_TRUE(action);
  EXPECT_EQ("_execute_action", action->command_name());
  EXPECT_EQ(u"", action->description());
  const ui::Accelerator ctrl_g = ui::Accelerator(ui::VKEY_G, kControlKey);
  EXPECT_EQ(ctrl_g, action->accelerator());

  EXPECT_FALSE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that when only a custom action command is specified we create a
// default action command for the action type for MV2.
TEST_F(CommandsManifestTest,
       CommandManifestParseCommandsOnlyCustomCommandGetsDefault_MV2) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Only Custom Commands",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             }
           },
           "browser_action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const ui::CommandMap* commands =
      CommandsInfo::GetNamedCommands(extension.get());
  ASSERT_TRUE(commands);
  EXPECT_EQ(1u, commands->size());
  auto iter = commands->begin();
  const ui::Command* named_command = &(*iter).second;
  EXPECT_EQ("feature1", named_command->command_name());
  EXPECT_EQ(u"desc", named_command->description());

  const Command* browser_action =
      CommandsInfo::GetBrowserActionCommand(extension.get());
  ASSERT_TRUE(browser_action);
  EXPECT_EQ("",
            browser_action->AcceleratorToString(browser_action->accelerator()));

  EXPECT_FALSE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that when only a custom action command is specified we create a
// default action command for the action type for MV3.
TEST_F(CommandsManifestTest,
       CommandManifestParseCommandsOnlyCustomCommandGetsDefault_MV3) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Only Custom Commands - MV3",
           "manifest_version": 3,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             }
           },
           "action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const ui::CommandMap* commands =
      CommandsInfo::GetNamedCommands(extension.get());
  ASSERT_TRUE(commands);
  EXPECT_EQ(1u, commands->size());
  auto iter = commands->begin();
  const ui::Command* named_command = &(*iter).second;
  EXPECT_EQ("feature1", named_command->command_name());
  EXPECT_EQ(u"desc", named_command->description());

  const Command* action = CommandsInfo::GetActionCommand(extension.get());
  ASSERT_TRUE(action);
  EXPECT_EQ("", action->AcceleratorToString(action->accelerator()));

  EXPECT_FALSE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that only the correct action command (_execute_browser_action) is
// used from the manifest for MV2, but others are ignored and we install a
// warning for the incorrect command. See https://crbug.com/1353210.
TEST_F(CommandsManifestTest,
       CommandManifestIgnoreInvalidActionCommandsAndInstallWarning_MV2) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Multiple Action Commands",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_browser_action": {
               "suggested_key": "Alt+Shift+F",
               "description": "browser action"
             },
             "_execute_page_action": {
               "suggested_key": "Ctrl+F",
               "description": ""
             }
           },
           "browser_action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const Command* browser_action =
      CommandsInfo::GetBrowserActionCommand(extension.get());
  ASSERT_TRUE(browser_action);
  EXPECT_EQ("_execute_browser_action", browser_action->command_name());
  EXPECT_EQ(u"", browser_action->description());
  const ui::Accelerator alt_shift_f =
      ui::Accelerator(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(alt_shift_f, browser_action->accelerator());

  EXPECT_FALSE(CommandsInfo::GetPageActionCommand(extension.get()));
  EXPECT_FALSE(CommandsInfo::GetActionCommand(extension.get()));

  EXPECT_TRUE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that only the correct action command (_execute_action) is used
// from the manifest for MV3, but others are ignored and we install a warning
// for the incorrect command. See https://crbug.com/1353210.
TEST_F(CommandsManifestTest,
       CommandManifestIgnoreInvalidActionCommandsAndInstallWarning_MV3) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Multiple Action Commands - MV3",
           "manifest_version": 3,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_action": {
               "suggested_key": "Alt+Shift+F",
               "description": "browser action"
             },
             "_execute_page_action": {
               "suggested_key": "Ctrl+F",
               "description": ""
             }
           },
           "action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const Command* action = CommandsInfo::GetActionCommand(extension.get());
  ASSERT_TRUE(action);
  EXPECT_EQ("_execute_action", action->command_name());
  EXPECT_EQ(u"", action->description());
  const ui::Accelerator alt_shift_f =
      ui::Accelerator(ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_SHIFT_DOWN);
  EXPECT_EQ(alt_shift_f, action->accelerator());

  EXPECT_FALSE(CommandsInfo::GetBrowserActionCommand(extension.get()));
  EXPECT_FALSE(CommandsInfo::GetPageActionCommand(extension.get()));

  EXPECT_TRUE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that when only incorrect action commands are specified we install
// a warning and set a default (for MV2). See https://crbug.com/1353210.
TEST_F(CommandsManifestTest,
       CommandManifestAllInvalidActionCommandsInstallWarning_MV2) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Multiple Action Commands But None Correct",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_action": {
               "suggested_key": "Alt+Shift+F",
               "description": "browser action"
             },
             "_execute_page_action": {
               "suggested_key": "Ctrl+F",
               "description": ""
             }
           },
           "browser_action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const Command* browser_action =
      CommandsInfo::GetBrowserActionCommand(extension.get());
  ASSERT_TRUE(browser_action);
  EXPECT_EQ("",
            browser_action->AcceleratorToString(browser_action->accelerator()));

  EXPECT_FALSE(CommandsInfo::GetActionCommand(extension.get()));
  EXPECT_FALSE(CommandsInfo::GetPageActionCommand(extension.get()));

  EXPECT_TRUE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

// Tests that when only incorrect execute commands are specified we install
// a warning and set a default (for MV3). See https://crbug.com/1353210.
TEST_F(CommandsManifestTest,
       CommandManifestAllInvalidActionCommandsInstallWarning_MV3) {
  static constexpr char kManifest[] =
      R"({
           "name": "Multiple Action Commands But None Correct - MV3",
           "manifest_version": 3,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+F",
               "description": "desc"
             },
             "_execute_browser_action": {
               "suggested_key": "Alt+Shift+F",
               "description": "browser action"
             },
             "_execute_page_action": {
               "suggested_key": "Ctrl+F",
               "description": ""
             }
           },
           "action" : {}
         })";

  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  ASSERT_TRUE(extension.get());

  const Command* action = CommandsInfo::GetActionCommand(extension.get());
  ASSERT_TRUE(action);
  EXPECT_EQ("", action->AcceleratorToString(action->accelerator()));

  EXPECT_FALSE(CommandsInfo::GetBrowserActionCommand(extension.get()));
  EXPECT_FALSE(CommandsInfo::GetPageActionCommand(extension.get()));

  EXPECT_TRUE(warnings_test_util::HasInstallWarning(
      extension,
      manifest_errors::kCommandActionIncorrectForManifestActionType));
}

TEST_F(CommandsManifestTest, CommandManifestShortcutsTooMany) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - too many commands with shortcuts",
           "manifest_version": 2,
           "version": "2",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+A",
               "description": "feature1"
             },
             "feature2": {
               "suggested_key": "Ctrl+B",
               "description": "feature2"
             },
             "feature3": {
               "suggested_key": "Ctrl+C",
               "description": "feature3"
             },
             "feature4": {
               "suggested_key": "Ctrl+D",
               "description": "feature4"
             },
             "feature5": {
               "suggested_key": "Ctrl+E",
               "description": "feature5"
             }
           }
         })";
  LoadAndExpectError(ManifestData::FromJSON(kManifest),
                     errors::kInvalidKeyBindingTooMany);
}

TEST_F(CommandsManifestTest, CommandManifestManyButWithinBounds) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - many commands, but not too many shortcuts",
           "manifest_version": 2,
           "version": "2",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+A",
               "description": "feature1"
             },
             "feature2": {
               "suggested_key": "Ctrl+B",
               "description": "feature2"
             },
             "feature3": {
               "suggested_key": "Ctrl+C",
               "description": "feature3"
             },
             "feature4": {
               "suggested_key": "Ctrl+D",
               "description": "feature4"
             },
             "feature5": {
               "description": "feature5"
             }
           }
         })";
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
}

TEST_F(CommandsManifestTest, CommandManifestAllowNumbers) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Numbers should be allowed",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+1",
               "description": "feature1"
             }
           }
         })";
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
}

TEST_F(CommandsManifestTest, CommandManifestRejectJustShift) {
  static constexpr char kManifest[] =
      R"({
           "name": "Command test - Shift on its own is not a valid modifier",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Shift+A",
               "description": "feature1"
             }
           }
         })";
  LoadAndExpectError(ManifestData::FromJSON(kManifest),
                     errors::kInvalidKeyBinding);
}

TEST_F(CommandsManifestTest, BrowserActionSynthesizesCommand) {
  static constexpr char kManifest[] =
      R"({
           "name": "A simple browser action that defines no extension command",
           "version": "1.0",
           "manifest_version": 2,
           "browser_action": {
             "default_title": "Make this page red"
           }
         })";
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  // An extension with a browser action but no extension command specified
  // should get a command assigned to it.
  const extensions::Command* command =
      CommandsInfo::GetBrowserActionCommand(extension.get());
  ASSERT_TRUE(command);
  EXPECT_EQ(ui::VKEY_UNKNOWN, command->accelerator().key_code());
}

// An extension with an action but no extension command specified should get a
// command assigned to it.
TEST_F(CommandsManifestTest, ActionSynthesizesCommand) {
  static constexpr char kManifest[] =
      R"({
           "name": "Synthesize action shortcut if commands key is missing",
           "version": "1.0",
           "manifest_version": 3,
           "action": {
             "default_title": "Test"
           }
         })";
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
  const Command* command = CommandsInfo::GetActionCommand(extension.get());
  ASSERT_TRUE(command);
  EXPECT_EQ(ui::VKEY_UNKNOWN, command->accelerator().key_code());
}

// This test makes sure that the "commands" feature and the "commands.global"
// property load properly.
TEST_F(CommandsManifestTest, LoadsOnStable) {
  static constexpr char kManifest1[] =
      R"({
           "name": "Command test - Extension with Command",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+0",
               "description": "desc"
             }
           }
         })";
  scoped_refptr<Extension> extension1 =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest1));

  static constexpr char kManifest2[] =
      R"({
           "name": "Command test - App with Command",
           "manifest_version": 2,
           "version": "1",
           "app": {
             "background": { "scripts": ["background.js"] }
           },
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+0",
               "description": "desc"
             }
           }
         })";
  scoped_refptr<Extension> extension2 =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest2));

  static constexpr char kManifest3[] =
      R"({
           "name": "Command test - Extension with Command that is global",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+0",
               "description": "desc",
               "global": true
             }
           }
         })";
  scoped_refptr<Extension> extension3 =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest3));

  static constexpr char kManifest4[] =
      R"({
           "name": "Command test - App with Command that is global",
           "manifest_version": 2,
           "version": "1",
           "app": {
             "background": { "scripts": ["background.js"] }
           },
           "commands": {
             "feature1": {
               "suggested_key": "Ctrl+Shift+0",
               "description": "desc",
               "global": true
             }
           }
         })";
  scoped_refptr<Extension> extension4 =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest4));
}

TEST_F(CommandsManifestTest, CommandManifestShouldNotCountMediaKeys) {
  // Media keys shouldn't count towards the max of four shortcuts per extension.
  static constexpr char kManifest[] =
      R"({
           "name": "mediaKeys",
           "manifest_version": 2,
           "version": "1",
           "commands": {
             "MediaNextTrack": {
               "suggested_key": "MediaNextTrack",
               "description": "MediaNextTrack"
             },
             "MediaPlayPause": {
               "suggested_key": "MediaPlayPause",
               "description": "MediaPlayPause"
             },
             "MediaPrevTrack": {
               "suggested_key": "MediaPrevTrack",
               "description": "MediaPrevTrack"
             },
             "MediaStop": {
               "suggested_key": "MediaStop",
               "description": "MediaStop"
             },
             "feature1": {
               "suggested_key": "Ctrl+A",
               "description": "feature1"
             },
             "feature2": {
               "suggested_key": "Ctrl+B",
               "description": "feature2"
             }
           }
         })";
  scoped_refptr<Extension> extension =
      LoadAndExpectSuccess(ManifestData::FromJSON(kManifest));
}

}  // namespace extensions
