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

#include <stddef.h>

#include <array>
#include <memory>
#include <utility>

#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/extensions/test_extension_environment.h"
#include "chrome/common/extensions/permissions/chrome_permission_message_provider.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/testing_profile.h"
#include "components/crx_file/id_util.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/permissions/permissions_test_util.h"
#include "extensions/browser/permissions/permissions_updater.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/manifest.h"
#include "extensions/common/manifest_handlers/permissions_parser.h"
#include "extensions/common/permissions/permission_set.h"
#include "extensions/common/permissions/permissions_data.h"
#include "extensions/common/permissions/permissions_info.h"
#include "extensions/common/permissions/usb_device_permission.h"
#include "extensions/common/permissions/usb_device_permission_data.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using extensions::mojom::APIPermissionID;

namespace extensions {

// Tests that ChromePermissionMessageProvider provides not only correct, but
// meaningful permission messages that coalesce correctly where appropriate.
// There are 3 types of permission messages that need to be tested:
//  1. The combined list of active permissions, displayed at install time (or
//     when the app has been disabled automatically and needs to be re-enabled)
//  2. The split list of active permissions, displayed in the App Info dialog,
//     where the optional permissions are individually revokable
//  3. The list of requested optional permissions, displayed in a prompt to the
//     user when the app requests these during runtime
// Some of these tests are prefixed AntiTest_, since they demonstrate existing
// problematic functionality. These tests are prefixed with AntiTest_ and will
// be changed as the correct behaviour is implemented. TODOs in the test explain
// the currently problematic behaviour.
class PermissionMessagesUnittest : public testing::Test {
 public:
  PermissionMessagesUnittest()
      : message_provider_(std::make_unique<ChromePermissionMessageProvider>()) {
  }

  PermissionMessagesUnittest(const PermissionMessagesUnittest&) = delete;
  PermissionMessagesUnittest& operator=(const PermissionMessagesUnittest&) =
      delete;

  ~PermissionMessagesUnittest() override = default;

 protected:
  void CreateAndInstallExtensionWithPermissions(
      base::ListValue required_permissions,
      base::ListValue optional_permissions) {
    app_ = ExtensionBuilder("Test")
               .SetManifestKey("permissions", std::move(required_permissions))
               .SetManifestKey("optional_permissions",
                               std::move(optional_permissions))
               .SetID(crx_file::id_util::GenerateId("extension"))
               .SetLocation(mojom::ManifestLocation::kInternal)
               .Build();
    env_.GetExtensionRegistrar()->AddExtension(app_.get());
  }

  // Returns the permission messages that would display in the prompt that
  // requests all the optional permissions for the current |app_|.
  std::vector<std::u16string> GetOptionalPermissionMessages() {
    std::unique_ptr<const PermissionSet> granted_permissions =
        env_.GetExtensionPrefs()->GetGrantedPermissions(app_->id());
    const PermissionSet& optional_permissions =
        PermissionsParser::GetOptionalPermissions(app_.get());
    std::unique_ptr<const PermissionSet> requested_permissions =
        PermissionSet::CreateDifference(optional_permissions,
                                        *granted_permissions);
    return GetMessages(*requested_permissions);
  }

  void GrantOptionalPermissions() {
    permissions_test_util::GrantOptionalPermissionsAndWaitForCompletion(
        env_.profile(), *app_,
        PermissionsParser::GetOptionalPermissions(app_.get()));
  }

  std::vector<std::u16string> active_permissions() {
    return GetMessages(app_->permissions_data()->active_permissions());
  }

  std::vector<std::u16string> required_permissions() {
    return GetMessages(PermissionsParser::GetRequiredPermissions(app_.get()));
  }

  std::vector<std::u16string> optional_permissions() {
    return GetMessages(PermissionsParser::GetOptionalPermissions(app_.get()));
  }

 private:
  std::vector<std::u16string> GetMessages(const PermissionSet& permissions) {
    std::vector<std::u16string> messages;
    for (const PermissionMessage& msg :
         message_provider_->GetPermissionMessages(
             message_provider_->GetAllPermissionIDs(permissions,
                                                    app_->GetType()))) {
      messages.push_back(msg.message());
    }
    return messages;
  }

  extensions::TestExtensionEnvironment env_;
  std::unique_ptr<ChromePermissionMessageProvider> message_provider_;
  scoped_refptr<const Extension> app_;
};

// If an app has both the 'history' and 'tabs' permission, one should hide the
// other (the 'history' permission has superset permissions).
TEST_F(PermissionMessagesUnittest, HistoryHidesTabsMessage) {
  CreateAndInstallExtensionWithPermissions(
      base::ListValue().Append("tabs").Append("history"), base::ListValue());

  ASSERT_EQ(1U, required_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            required_permissions()[0]);

  ASSERT_EQ(0U, optional_permissions().size());
}

// If an app requests the 'history' permission, but already has the 'tabs'
// permission, only the new coalesced message is displayed.
TEST_F(PermissionMessagesUnittest, MixedPermissionMessagesCoalesceOnceGranted) {
  CreateAndInstallExtensionWithPermissions(base::ListValue().Append("tabs"),
                                           base::ListValue().Append("history"));

  ASSERT_EQ(1U, required_permissions().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      required_permissions()[0]);

  ASSERT_EQ(1U, optional_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            optional_permissions()[0]);

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      active_permissions()[0]);

  ASSERT_EQ(1U, GetOptionalPermissionMessages().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            GetOptionalPermissionMessages()[0]);

  GrantOptionalPermissions();

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            active_permissions()[0]);
}

// AntiTest: This behavior should be changed and improved.
// If an app requests the 'tabs' permission but already has the 'history'
// permission, a prompt is displayed. However, no prompt should appear at all,
// since 'tabs' is a subset of 'history' and the final list of permissions are
// not affected by this grant.
TEST_F(PermissionMessagesUnittest,
       AntiTest_PromptCanRequestSubsetOfAlreadyGrantedPermissions) {
  CreateAndInstallExtensionWithPermissions(base::ListValue().Append("history"),
                                           base::ListValue().Append("tabs"));

  ASSERT_EQ(1U, required_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            required_permissions()[0]);

  ASSERT_EQ(1U, optional_permissions().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      optional_permissions()[0]);

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            active_permissions()[0]);

  // TODO(sashab): This prompt should display no permissions, since READ is a
  // subset permission of WRITE.
  ASSERT_EQ(1U, GetOptionalPermissionMessages().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      GetOptionalPermissionMessages()[0]);

  GrantOptionalPermissions();

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_WRITE_ON_ALL_DEVICES),
            active_permissions()[0]);
}

// AntiTest: This behavior should be changed and improved.
// If an app requests the 'sessions' permission, nothing is displayed in the
// permission request prompt. However, the required permissions for the app are
// actually modified, so the prompt *should* display a message to prevent this
// permission from being granted for free.
TEST_F(PermissionMessagesUnittest,
       AntiTest_PromptCanBeEmptyButCausesChangeInPermissions) {
  CreateAndInstallExtensionWithPermissions(
      base::ListValue().Append("tabs"), base::ListValue().Append("sessions"));

  ASSERT_EQ(1U, required_permissions().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      required_permissions()[0]);

  ASSERT_EQ(0U, optional_permissions().size());

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(
      l10n_util::GetStringUTF16(IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ),
      active_permissions()[0]);

  // TODO(sashab): This prompt should display the sessions permission message,
  // as well as warn the user that it can affect the existing 'tab' permission.
  ASSERT_EQ(0U, GetOptionalPermissionMessages().size());

  GrantOptionalPermissions();

  ASSERT_EQ(1U, active_permissions().size());
  EXPECT_EQ(l10n_util::GetStringUTF16(
                IDS_EXTENSION_PROMPT_WARNING_HISTORY_READ_ON_ALL_DEVICES),
            active_permissions()[0]);
}

class USBDevicePermissionMessagesTest : public testing::Test {
 public:
  USBDevicePermissionMessagesTest()
      : message_provider_(new ChromePermissionMessageProvider()) {}
  ~USBDevicePermissionMessagesTest() override = default;

  PermissionMessages GetMessages(const PermissionIDSet& permissions) {
    return message_provider_->GetPermissionMessages(permissions);
  }

 private:
  std::unique_ptr<ChromePermissionMessageProvider> message_provider_;
};

TEST_F(USBDevicePermissionMessagesTest, SingleDevice) {
  {
    const char16_t kMessage[] =
        u"Access any PVR Mass Storage from HUMAX Co., Ltd. via USB";

    auto permission_list =
        base::ListValue().Append(base::Value::FromUniquePtrValue(
            UsbDevicePermissionData(0x02ad, 0x138c, -1, -1).ToValue()));
    base::Value permission_value = base::Value(std::move(permission_list));

    UsbDevicePermission permission(
        PermissionsInfo::GetInstance()->GetByID(APIPermissionID::kUsbDevice));
    ASSERT_TRUE(permission.FromValue(&permission_value, nullptr, nullptr));

    PermissionMessages messages = GetMessages(permission.GetPermissions());
    ASSERT_EQ(1U, messages.size());
    EXPECT_EQ(kMessage, messages.front().message());
  }
  {
    const char16_t kMessage[] = u"Access USB devices from HUMAX Co., Ltd.";

    base::ListValue permission_list;
    permission_list.Append(base::Value::FromUniquePtrValue(
        UsbDevicePermissionData(0x02ad, 0x138d, -1, -1).ToValue()));
    base::Value permission_value = base::Value(std::move(permission_list));

    UsbDevicePermission permission(
        PermissionsInfo::GetInstance()->GetByID(APIPermissionID::kUsbDevice));
    ASSERT_TRUE(permission.FromValue(&permission_value, nullptr, nullptr));

    PermissionMessages messages = GetMessages(permission.GetPermissions());
    ASSERT_EQ(1U, messages.size());
    EXPECT_EQ(kMessage, messages.front().message());
  }
  {
    const char16_t kMessage[] = u"Access USB devices from an unknown vendor";

    base::ListValue permission_list;
    permission_list.Append(base::Value::FromUniquePtrValue(
        UsbDevicePermissionData(0x02ae, 0x138d, -1, -1).ToValue()));
    base::Value permission_value = base::Value(std::move(permission_list));

    UsbDevicePermission permission(
        PermissionsInfo::GetInstance()->GetByID(APIPermissionID::kUsbDevice));
    ASSERT_TRUE(permission.FromValue(&permission_value, nullptr, nullptr));

    PermissionMessages messages = GetMessages(permission.GetPermissions());
    ASSERT_EQ(1U, messages.size());
    EXPECT_EQ(kMessage, messages.front().message());
  }
}

TEST_F(USBDevicePermissionMessagesTest, MultipleDevice) {
  const char16_t kMessage[] = u"Access any of these USB devices";
  auto kDetails = std::to_array<const char*>({
      "PVR Mass Storage from HUMAX Co., Ltd.",
      "unknown devices from HUMAX Co., Ltd.",
      "devices from an unknown vendor",
  });

  // Prepare data set
  base::ListValue permission_list;
  permission_list.Append(base::Value::FromUniquePtrValue(
      UsbDevicePermissionData(0x02ad, 0x138c, -1, -1).ToValue()));
  // This device's product ID is not in Chrome's database.
  permission_list.Append(base::Value::FromUniquePtrValue(
      UsbDevicePermissionData(0x02ad, 0x138d, -1, -1).ToValue()));
  // This additional unknown product will be collapsed into the entry above.
  permission_list.Append(base::Value::FromUniquePtrValue(
      UsbDevicePermissionData(0x02ad, 0x138e, -1, -1).ToValue()));
  // This device's vendor ID is not in Chrome's database.
  permission_list.Append(base::Value::FromUniquePtrValue(
      UsbDevicePermissionData(0x02ae, 0x138d, -1, -1).ToValue()));
  // This additional unknown vendor will be collapsed into the entry above.
  permission_list.Append(base::Value::FromUniquePtrValue(
      UsbDevicePermissionData(0x02af, 0x138d, -1, -1).ToValue()));

  base::Value permission_value = base::Value(std::move(permission_list));

  UsbDevicePermission permission(
      PermissionsInfo::GetInstance()->GetByID(APIPermissionID::kUsbDevice));
  ASSERT_TRUE(permission.FromValue(&permission_value, nullptr, nullptr));

  PermissionMessages messages = GetMessages(permission.GetPermissions());
  ASSERT_EQ(1U, messages.size());
  EXPECT_EQ(kMessage, messages.front().message());
  const std::vector<std::u16string>& submessages =
      messages.front().submessages();
  ASSERT_EQ(std::size(kDetails), submessages.size());
  for (size_t i = 0; i < submessages.size(); i++)
    EXPECT_EQ(base::ASCIIToUTF16(kDetails[i]), submessages[i]);
}

}  // namespace extensions
