// Copyright 2023 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/supervised_user/supervised_user_extensions_manager.h"

#include <string>
#include <vector>

#include "base/test/gtest_util.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/supervised_user/supervised_user_extensions_metrics_recorder.h"
#include "chrome/browser/supervised_user/supervised_user_test_util.h"
#include "chrome/test/base/testing_profile.h"
#include "components/prefs/pref_service.h"
#include "components/supervised_user/core/browser/supervised_user_utils.h"
#include "components/supervised_user/core/common/pref_names.h"
#include "components/version_info/version_info.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/manifest_constants.h"
#include "testing/gtest/include/gtest/gtest.h"

using extensions::Extension;

class SupervisedUserExtensionsManagerTestBase
    : public extensions::ExtensionServiceTestBase {
 public:
  SupervisedUserExtensionsManagerTestBase()
      : channel_(version_info::Channel::DEV) {}
  ~SupervisedUserExtensionsManagerTestBase() override = default;

  void SetUp() override {
    ExtensionServiceTestBase::SetUp();
    ExtensionServiceInitParams params;
    params.profile_is_supervised = true;
    InitializeExtensionService(std::move(params));
    // Flush the message loop, to ensure that credentials have been loaded in
    // Identity Manager.
    base::RunLoop().RunUntilIdle();
  }

  void TearDown() override {
    // Flush the message loop, to ensure all posted tasks run.
    base::RunLoop().RunUntilIdle();
    manager_.reset();
    ExtensionServiceTestBase::TearDown();
  }

  void CheckLocalApprovalMigrationForDesktopState(
      supervised_user::LocallyParentApprovedExtensionsMigrationState
          expected_migration_state) {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
    auto* prefs = profile()->GetPrefs();
    CHECK(prefs);
    EXPECT_EQ(static_cast<int>(expected_migration_state),
              prefs->GetInteger(
                  prefs::kLocallyParentApprovedExtensionsMigrationState));
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  }

  scoped_refptr<const extensions::Extension> MakeThemeExtension() {
    base::DictValue source;
    source.Set(extensions::manifest_keys::kName, "Theme");
    source.Set(extensions::manifest_keys::kTheme, base::DictValue());
    source.Set(extensions::manifest_keys::kVersion, "1.0");
    extensions::ExtensionBuilder builder;
    scoped_refptr<const extensions::Extension> extension =
        builder.SetManifest(std::move(source)).Build();
    return extension;
  }

  scoped_refptr<const extensions::Extension> MakeExtension(
      std::string name = "Extension") {
    scoped_refptr<const extensions::Extension> extension =
        extensions::ExtensionBuilder(name).Build();
    return extension;
  }

  void MakeSupervisedUserExtensionsManager() {
    manager_ = std::make_unique<extensions::SupervisedUserExtensionsManager>(
        profile());
  }

  extensions::ScopedCurrentChannel channel_;
  std::unique_ptr<extensions::SupervisedUserExtensionsManager> manager_;
};

class SupervisedUserExtensionsManagerTest
    : public SupervisedUserExtensionsManagerTestBase {};

TEST_F(SupervisedUserExtensionsManagerTest,
       ExtensionManagementPolicyProviderWithoutSUInitiatedInstalls) {
  MakeSupervisedUserExtensionsManager();
  supervised_user_test_util::
      SetSupervisedUserExtensionsMayRequestPermissionsPref(profile(), false);
  ASSERT_TRUE(profile()->IsChild());

  // Check that a supervised user can install and uninstall a theme even if
  // they are not allowed to install extensions.
  {
    scoped_refptr<const extensions::Extension> theme = MakeThemeExtension();

    std::u16string error_1;
    EXPECT_TRUE(manager_->UserMayLoad(theme.get(), &error_1));
    EXPECT_TRUE(error_1.empty());

    std::u16string error_2;
    EXPECT_FALSE(manager_->MustRemainInstalled(theme.get(), &error_2));
    EXPECT_TRUE(error_2.empty());
  }

  scoped_refptr<const extensions::Extension> extension = MakeExtension();
  // Installations are always allowed.
  std::u16string error_1;
  EXPECT_TRUE(manager_->UserMayLoad(extension.get(), &error_1));
  EXPECT_TRUE(error_1.empty());

  base::test::TestFuture<extensions::ManagementPolicy::Decision> future;
  manager_->UserMayInstall(extension, future.GetCallback());
  EXPECT_TRUE(future.Get().allowed);
  EXPECT_TRUE(future.Get().error.empty());

  std::u16string error_2;
  EXPECT_FALSE(manager_->MustRemainInstalled(extension.get(), &error_2));
  EXPECT_TRUE(error_2.empty());

#if DCHECK_IS_ON()
  EXPECT_FALSE(manager_->GetDebugPolicyProviderName().empty());
#endif
}

TEST_F(SupervisedUserExtensionsManagerTest,
       ExtensionManagementPolicyProviderWithSUInitiatedInstalls) {
  MakeSupervisedUserExtensionsManager();
  // Enable child users to initiate extension installs by simulating the
  // toggling of "Skip parent approval to install extensions" to disabled.
  supervised_user_test_util::SetSkipParentApprovalToInstallExtensionsPref(
      profile(), false);

  ASSERT_TRUE(profile()->IsChild());

  // The supervised user should be able to load and uninstall the extensions
  // they install.
  {
    scoped_refptr<const extensions::Extension> extension = MakeExtension();

    std::u16string error;
    EXPECT_TRUE(manager_->UserMayLoad(extension.get(), &error));
    EXPECT_TRUE(error.empty());

    std::u16string error_2;
    EXPECT_FALSE(manager_->MustRemainInstalled(extension.get(), &error_2));
    EXPECT_TRUE(error_2.empty());

    extensions::disable_reason::DisableReason reason =
        extensions::disable_reason::DISABLE_NONE;
    EXPECT_TRUE(manager_->MustRemainDisabled(extension.get(), &reason));
    EXPECT_EQ(extensions::disable_reason::DISABLE_CUSTODIAN_APPROVAL_REQUIRED,
              reason);

    std::u16string error_3;
    EXPECT_TRUE(manager_->UserMayModifySettings(extension.get(), &error_3));
    EXPECT_TRUE(error_3.empty());

    base::test::TestFuture<extensions::ManagementPolicy::Decision> future;
    manager_->UserMayInstall(extension, future.GetCallback());
    EXPECT_TRUE(future.Get().allowed);
    EXPECT_TRUE(future.Get().error.empty());
  }

#if DCHECK_IS_ON()
  EXPECT_FALSE(manager_->GetDebugPolicyProviderName().empty());
#endif
}

// Tests that on Desktop (Win/Linux/Mac) platforms,
// present extensions will be marked as locally parent-approved
// when the SupervisedUserExtensionsManager is created for a supervised user.
TEST_F(SupervisedUserExtensionsManagerTest,
       MigrateExtensionsToLocallyApproved) {
  ASSERT_TRUE(profile()->IsChild());
  base::HistogramTester histogram_tester;

  // Register the extensions.
  scoped_refptr<const Extension> approved_extn =
      MakeExtension("extension_test_1");
  scoped_refptr<const Extension> locally_approved_extn =
      MakeExtension("local_extension_test_1");
  registrar()->AddExtension(approved_extn);
  registrar()->AddExtension(locally_approved_extn);

  // Mark one extension as already parent-approved in the corresponding
  // preference.
  auto* prefs = profile()->GetPrefs();
  CHECK(prefs);
  base::DictValue approved_extensions;
  approved_extensions.Set(approved_extn->id(), true);
  prefs->SetDict(prefs::kSupervisedUserApprovedExtensions,
                 std::move(approved_extensions));

  // Create the object under test.
  MakeSupervisedUserExtensionsManager();

  bool has_local_approval_migration_run = false;
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  auto expected_migration_state =
      supervised_user::LocallyParentApprovedExtensionsMigrationState::kComplete;
  has_local_approval_migration_run = true;
  EXPECT_EQ(
      static_cast<int>(expected_migration_state),
      prefs->GetInteger(prefs::kLocallyParentApprovedExtensionsMigrationState));
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

  // The already approved extension should be allowed and not part of the
  // local-approved list.
  const base::DictValue& local_approved_extensions_pref =
      prefs->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions);
  EXPECT_FALSE(local_approved_extensions_pref.contains(approved_extn->id()));
  EXPECT_TRUE(manager_->IsExtensionAllowed(*approved_extn));

  // The extensions approved in the migration should be allowed and part
  // of the local-approved list.
  int approved_extensions_count = has_local_approval_migration_run ? 1 : 0;
  EXPECT_EQ(
      has_local_approval_migration_run,
      local_approved_extensions_pref.contains(locally_approved_extn->id()));
  EXPECT_EQ(has_local_approval_migration_run,
            manager_->IsExtensionAllowed(*locally_approved_extn));
  histogram_tester.ExpectBucketCount(
      SupervisedUserExtensionsMetricsRecorder::kExtensionsHistogramName,
      SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
          kLocalApprovalGranted,
      approved_extensions_count);
}

// Tests that extensions missing parent approval are granted parent approval
// on their installation, when the "Extensions" Family Link toggle is ON.
TEST_F(SupervisedUserExtensionsManagerTest,
       GrantParentApprovalOnInstallationWhenExtensionsToggleOn) {
  ASSERT_TRUE(profile()->IsChild());
  base::HistogramTester histogram_tester;

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  // Mark the migration done to avoid any interference with the one-off
  // migration.
  auto* prefs = profile()->GetPrefs();
  CHECK(prefs);
  prefs->SetInteger(
      prefs::kLocallyParentApprovedExtensionsMigrationState,
      static_cast<int>(
          supervised_user::LocallyParentApprovedExtensionsMigrationState::
              kComplete));
#endif

  // Create the object under test.
  MakeSupervisedUserExtensionsManager();

  // Set the Extensions switch to OFF. The extensions installed on this mode,
  // should be pending approval and disabled.
  supervised_user_test_util::SetSkipParentApprovalToInstallExtensionsPref(
      profile(), false);

  // Install an extension.
  scoped_refptr<const Extension> extn_with_switch_off =
      MakeExtension("extension_test_1");
  registrar()->OnExtensionInstalled(extn_with_switch_off.get(),
                                    /*page_ordinal=*/syncer::StringOrdinal());

  extensions::disable_reason::DisableReason reason;
  EXPECT_FALSE(manager_->IsExtensionAllowed(*extn_with_switch_off.get()));
  EXPECT_TRUE(
      manager_->MustRemainDisabled(extn_with_switch_off.get(), &reason));

  // Set the Extensions switch to ON. Install another extension which should be
  // granted parental approval by the end of the installation.
  supervised_user_test_util::SetSkipParentApprovalToInstallExtensionsPref(
      profile(), true);

  // Toggling the "Extensions" results in granting approval to the existing
  // extension.
  int approved_extensions_count = 1;
  histogram_tester.ExpectBucketCount(
      SupervisedUserExtensionsMetricsRecorder::kExtensionsHistogramName,
      SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
          kApprovalGrantedByDefault,
      approved_extensions_count);

  // Install an extension.
  scoped_refptr<const Extension> extn_with_switch_on =
      MakeExtension("extension_test_2");
  registrar()->OnExtensionInstalled(extn_with_switch_on.get(),
                                    /*page_ordinal=*/syncer::StringOrdinal());

  EXPECT_TRUE(manager_->IsExtensionAllowed(*extn_with_switch_on.get()));
  EXPECT_TRUE(
      !manager_->MustRemainDisabled(extn_with_switch_on.get(), &reason));
  EXPECT_TRUE(profile()
                  ->GetPrefs()
                  ->GetDict(prefs::kSupervisedUserApprovedExtensions)
                  .contains(extn_with_switch_on->id()));

  histogram_tester.ExpectBucketCount(
      SupervisedUserExtensionsMetricsRecorder::kExtensionsHistogramName,
      SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
          kApprovalGrantedByDefault,
      approved_extensions_count + 1);
  // The migration to locally approved extensions has occurred before we
  // installed any extensions, so not local approvals have been granted.
  histogram_tester.ExpectBucketCount(
      SupervisedUserExtensionsMetricsRecorder::kExtensionsHistogramName,
      SupervisedUserExtensionsMetricsRecorder::UmaExtensionState::
          kLocalApprovalGranted,
      0);
}

// Tests that extensions missing parent approval are granted parent approval
// when the "Extensions" toggle is flipped to ON.
TEST_F(SupervisedUserExtensionsManagerTest,
       GrantParentApprovalOnExtensionsWhenExtensionsToggleSetToOn) {
  ASSERT_TRUE(profile()->IsChild());

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  // Mark the migration done to avoid any interference with the one-off
  // migration.
  auto* prefs = profile()->GetPrefs();
  CHECK(prefs);
  prefs->SetInteger(
      prefs::kLocallyParentApprovedExtensionsMigrationState,
      static_cast<int>(
          supervised_user::LocallyParentApprovedExtensionsMigrationState::
              kComplete));
#endif

  // Create the object under test.
  MakeSupervisedUserExtensionsManager();

  // Set the Extensions switch to OFF. The extensions installed on this mode,
  // should be pending approval and disabled.
  supervised_user_test_util::SetSkipParentApprovalToInstallExtensionsPref(
      profile(), false);

  // Install an extension.
  scoped_refptr<const Extension> extn_with_switch_off =
      MakeExtension("extension_test_1");
  registrar()->OnExtensionInstalled(extn_with_switch_off.get(),
                                    /*page_ordinal=*/syncer::StringOrdinal());

  extensions::disable_reason::DisableReason reason;
  EXPECT_FALSE(manager_->IsExtensionAllowed(*extn_with_switch_off.get()));
  EXPECT_TRUE(
      manager_->MustRemainDisabled(extn_with_switch_off.get(), &reason));

  // Set the Extensions switch to ON. The extension should have been granted
  // parent approval when the SkipParentApprovalToInstallExtension preference is
  // flipped.
  supervised_user_test_util::SetSkipParentApprovalToInstallExtensionsPref(
      profile(), true);

  EXPECT_TRUE(manager_->IsExtensionAllowed(*extn_with_switch_off.get()));
  EXPECT_TRUE(
      !manager_->MustRemainDisabled(extn_with_switch_off.get(), &reason));
  EXPECT_TRUE(profile()
                  ->GetPrefs()
                  ->GetDict(prefs::kSupervisedUserApprovedExtensions)
                  .contains(extn_with_switch_off->id()));
}

// Tests the local approval is revoked on uninstalling the extension or
// when the extension gains normal parental approval.
TEST_F(SupervisedUserExtensionsManagerTest, RevokeLocalApproval) {
  ASSERT_TRUE(profile()->IsChild());

  scoped_refptr<const Extension> locally_approved_extn1 =
      MakeExtension("extension_test_1");
  registrar()->AddExtension(locally_approved_extn1);
  scoped_refptr<const Extension> locally_approved_extn2 =
      MakeExtension("extension_test_2");
  registrar()->AddExtension(locally_approved_extn2);

  // Create the object under test.
  MakeSupervisedUserExtensionsManager();

  bool has_local_approval_migration_run = false;
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  has_local_approval_migration_run = true;
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

  auto* prefs = profile()->GetPrefs();
  CHECK(prefs);
  const base::DictValue& local_approved_extensions_pref =
      prefs->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions);
  EXPECT_EQ(
      has_local_approval_migration_run,
      local_approved_extensions_pref.contains(locally_approved_extn1->id()));
  EXPECT_EQ(
      has_local_approval_migration_run,
      registry()->enabled_extensions().Contains(locally_approved_extn1->id()));

  // Uninstalling the extension also removes the local approval.
  ASSERT_TRUE(registrar()->UninstallExtension(
      locally_approved_extn1->id(), extensions::UNINSTALL_REASON_FOR_TESTING,
      nullptr));
  EXPECT_FALSE(
      local_approved_extensions_pref.contains(locally_approved_extn1->id()));

  // Granting parent approval (typically from another client) removes the local
  // approval. The extension remains allowed.
  manager_->AddExtensionApproval(*locally_approved_extn2);
  EXPECT_FALSE(
      local_approved_extensions_pref.contains(locally_approved_extn2->id()));
  EXPECT_TRUE(manager_->IsExtensionAllowed(*locally_approved_extn2));
}

// Whether the local approval extension migration for
// Win/Mac/Linux platforms has already run at the beginning of
// each test case.
enum class LocalApprovalMigrationForDesktopState : int {
  kExecuted = 0,
  kPending = 1
};

// Tests the managed extensions' state when an existing profile becomes
// supervised.
class AddingSupervisionTest : public SupervisedUserExtensionsManagerTestBase,
                              public ::testing::WithParamInterface<
                                  LocalApprovalMigrationForDesktopState> {
 public:
  AddingSupervisionTest() = default;

  LocalApprovalMigrationForDesktopState
  GetInitialLocalApprovalMigrationForDesktopState() {
    return GetParam();
  }

  void MaybeMarkLocalApprovalMigrationDone() {
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
    if (GetInitialLocalApprovalMigrationForDesktopState() ==
        LocalApprovalMigrationForDesktopState::kExecuted) {
      auto* prefs = profile()->GetPrefs();
      CHECK(prefs);
      prefs->SetInteger(
          prefs::kLocallyParentApprovedExtensionsMigrationState,
          static_cast<int>(
              supervised_user::LocallyParentApprovedExtensionsMigrationState::
                  kComplete));
    }
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
  }
};

// Tests that on Desktop (Win/Linux/Mac) platforms, when:
// 1) parental controls on extensions apply for the first time (solution
// release) and
// 2) a supervised user sings-in on an existing unsupervised profile or
// the existing profile becomes supervised (Gellerized) then
// the existing extensions remain disabled and pending approval.
// The states of the present extensions should not be impacted by the local
// extension approval migration that is executed on feature release for Desktop
// platforms.
TEST_P(AddingSupervisionTest,
       DisableExistingExtensionsOnProfileBecomingSupervised) {
  MaybeMarkLocalApprovalMigrationDone();
  profile()->AsTestingProfile()->SetIsSupervisedProfile(false);
  ASSERT_TRUE(!profile()->IsChild());

  scoped_refptr<const Extension> existing_extension =
      MakeExtension("extension_test_2");
  registrar()->AddExtension(existing_extension);

  supervised_user::LocallyParentApprovedExtensionsMigrationState
      expected_migragtion_state = supervised_user::
          LocallyParentApprovedExtensionsMigrationState::kNeedToRun;
  if (GetInitialLocalApprovalMigrationForDesktopState() ==
      LocalApprovalMigrationForDesktopState::kExecuted) {
    expected_migragtion_state = supervised_user::
        LocallyParentApprovedExtensionsMigrationState::kComplete;
  }
  CheckLocalApprovalMigrationForDesktopState(expected_migragtion_state);

  // Create the object under test.
  MakeSupervisedUserExtensionsManager();

  expected_migragtion_state =
      supervised_user::LocallyParentApprovedExtensionsMigrationState::kComplete;
  CheckLocalApprovalMigrationForDesktopState(expected_migragtion_state);

  auto* prefs = profile()->GetPrefs();
  CHECK(prefs);
  const base::DictValue& local_approved_extensions_pref =
      prefs->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions);
  EXPECT_FALSE(
      local_approved_extensions_pref.contains(existing_extension->id()));
  EXPECT_TRUE(
      registry()->enabled_extensions().Contains(existing_extension->id()));

  // Make the user supervised.
  profile()->AsTestingProfile()->SetIsSupervisedProfile(true);
  ASSERT_TRUE(profile()->IsChild());

  const base::DictValue& local_approved_extensions_pref_post_migr =
      prefs->GetDict(prefs::kSupervisedUserLocallyParentApprovedExtensions);
  // Check that the pre-existing extensions should have been be excluded from
  // the local approval migration.
  EXPECT_FALSE(local_approved_extensions_pref_post_migr.contains(
      existing_extension->id()));
  EXPECT_FALSE(manager_->IsExtensionAllowed(*existing_extension));

  EXPECT_TRUE(
      registry()->disabled_extensions().Contains(existing_extension->id()));
}

INSTANTIATE_TEST_SUITE_P(
    All,
    AddingSupervisionTest,
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
    testing::Values(LocalApprovalMigrationForDesktopState::kExecuted,
                    LocalApprovalMigrationForDesktopState::kPending),
#else   // ChromeOS case
        // The local approval migration is not applicable for ChromeOS.
        // The test just needs to be executed once, the value of
        // LocalApprovalMigrationForDesktopState does not matter.
    testing::Values(LocalApprovalMigrationForDesktopState::kExecuted),
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
    [](const auto& info) {
      return std::string(
          info.param == LocalApprovalMigrationForDesktopState::kExecuted
              ? "PostReleaseSkipParentApprovalFeature"
              : "PreReleaseSkipParentApprovalFeature");
    });
