// Copyright 2015 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/extension_service_test_with_install.h"

#include <algorithm>

#include "base/check.h"
#include "base/check_op.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/sync/extension_sync_data.h"
#include "chrome/browser/extensions/updater/extension_updater.h"
#include "chrome/browser/profiles/profile.h"
#include "components/sync/model/sync_change.h"
#include "components/sync/test/sync_change_processor_wrapper_for_test.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/crx_installer.h"
#include "extensions/browser/extension_creator.h"
#include "extensions/browser/extension_prefs.h"
#include "extensions/browser/load_error_reporter.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/verifier_formats.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using extensions::mojom::ManifestLocation;

namespace extensions {

namespace {

struct ExtensionsOrder {
  bool operator()(const scoped_refptr<const Extension>& a,
                  const scoped_refptr<const Extension>& b) {
    return a->name() < b->name();
  }
};

}  // namespace

ExtensionServiceTestWithInstall::ExtensionServiceTestWithInstall()
    : ExtensionServiceTestWithInstall(
          std::make_unique<content::BrowserTaskEnvironment>(
              base::test::TaskEnvironment::MainThreadType::IO)) {}

ExtensionServiceTestWithInstall::ExtensionServiceTestWithInstall(
    std::unique_ptr<content::BrowserTaskEnvironment> task_environment)
    : ExtensionServiceUserTestBase(std::move(task_environment)),
      installed_extension_(nullptr),
      was_update_(false),
      unloaded_reason_(UnloadedExtensionReason::UNDEFINED),
      expected_extensions_count_(0),
      override_external_install_prompt_(
          FeatureSwitch::prompt_for_external_extensions(),
          false) {}

ExtensionServiceTestWithInstall::~ExtensionServiceTestWithInstall() = default;

void ExtensionServiceTestWithInstall::TearDown() {
  registry_observation_.Reset();
  ExtensionServiceUserTestBase::TearDown();
}

void ExtensionServiceTestWithInstall::InitializeExtensionService(
    ExtensionServiceInitParams params) {
  ExtensionServiceTestBase::InitializeExtensionService(std::move(params));

  registry_observation_.Observe(registry());
}

// static
std::vector<std::u16string> ExtensionServiceTestWithInstall::GetErrors() {
  const std::vector<std::u16string>* errors =
      LoadErrorReporter::GetInstance()->GetErrors();
  std::vector<std::u16string> ret_val;

  for (const std::u16string& error : *errors) {
    std::string utf8_error = base::UTF16ToUTF8(error);
    if (utf8_error.find(".svn") == std::string::npos) {
      ret_val.push_back(error);
    }
  }

  // The tests rely on the errors being in a certain order, which can vary
  // depending on how filesystem iteration works.
  std::stable_sort(ret_val.begin(), ret_val.end());

  return ret_val;
}

void ExtensionServiceTestWithInstall::PackCRX(const base::FilePath& dir_path,
                                              const base::FilePath& pem_path,
                                              const base::FilePath& crx_path) {
  // Use the existing pem key, if provided.
  base::FilePath pem_output_path;
  if (pem_path.value().empty()) {
    pem_output_path = crx_path.DirName().AppendASCII("temp.pem");
  } else {
    ASSERT_TRUE(base::PathExists(pem_path));
  }

  ASSERT_TRUE(base::DeleteFile(crx_path));

  std::unique_ptr<ExtensionCreator> creator(new ExtensionCreator());
  ASSERT_TRUE(creator->Run(dir_path,
                           crx_path,
                           pem_path,
                           pem_output_path,
                           ExtensionCreator::kOverwriteCRX));

  ASSERT_TRUE(base::PathExists(crx_path));
}

const Extension* ExtensionServiceTestWithInstall::PackAndInstallCRX(
    const base::FilePath& dir_path,
    const base::FilePath& pem_path,
    InstallState install_state,
    int creation_flags,
    ManifestLocation install_location) {
  base::FilePath crx_path;
  base::ScopedTempDir temp_dir;
  EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
  crx_path = temp_dir.GetPath().AppendASCII("temp.crx");
  PackCRX(dir_path, pem_path, crx_path);

  return InstallCRX(crx_path, install_location, install_state, creation_flags);
}

const Extension* ExtensionServiceTestWithInstall::PackAndInstallCRX(
    const base::FilePath& dir_path,
    const base::FilePath& pem_path,
    InstallState install_state) {
  return PackAndInstallCRX(dir_path, pem_path, install_state,
                           Extension::NO_FLAGS, ManifestLocation::kInternal);
}

const Extension* ExtensionServiceTestWithInstall::PackAndInstallCRX(
    const base::FilePath& dir_path,
    InstallState install_state) {
  return PackAndInstallCRX(dir_path, base::FilePath(), install_state,
                           Extension::NO_FLAGS, ManifestLocation::kInternal);
}

const Extension* ExtensionServiceTestWithInstall::PackAndInstallCRX(
    const base::FilePath& dir_path,
    ManifestLocation install_location,
    InstallState install_state) {
  return PackAndInstallCRX(dir_path, base::FilePath(), install_state,
                           Extension::NO_FLAGS, install_location);
}

// Attempts to install an extension. Use INSTALL_FAILED if the installation
// is expected to fail.
// If |install_state| is INSTALL_UPDATED, and |expected_old_name| is
// non-empty, expects that the existing extension's title was
// |expected_old_name|.
const Extension* ExtensionServiceTestWithInstall::InstallCRX(
    const base::FilePath& path,
    InstallState install_state,
    int creation_flags,
    const std::string& expected_old_name) {
  InstallCRXInternal(path, ManifestLocation::kInternal, install_state,
                     creation_flags);
  return VerifyCrxInstall(path, install_state);
}

const Extension* ExtensionServiceTestWithInstall::InstallCRX(
    const base::FilePath& path,
    ManifestLocation install_location,
    InstallState install_state,
    int creation_flags) {
  InstallCRXInternal(path, install_location, install_state, creation_flags);
  return VerifyCrxInstall(path, install_state);
}

// Attempts to install an extension. Use INSTALL_FAILED if the installation
// is expected to fail.
const Extension* ExtensionServiceTestWithInstall::InstallCRX(
    const base::FilePath& path,
    InstallState install_state,
    int creation_flags) {
  return InstallCRX(path, install_state, creation_flags, std::string());
}

// Attempts to install an extension. Use INSTALL_FAILED if the installation
// is expected to fail.
const Extension* ExtensionServiceTestWithInstall::InstallCRX(
    const base::FilePath& path,
    InstallState install_state) {
  return InstallCRX(path, install_state, Extension::NO_FLAGS);
}

const Extension* ExtensionServiceTestWithInstall::InstallCRXFromWebStore(
    const base::FilePath& path,
    InstallState install_state) {
  InstallCRXInternal(path, ManifestLocation::kInternal, install_state,
                     Extension::FROM_WEBSTORE);
  return VerifyCrxInstall(path, install_state);
}

const Extension* ExtensionServiceTestWithInstall::VerifyCrxInstall(
    const base::FilePath& path,
    InstallState install_state) {
  return VerifyCrxInstall(path, install_state, std::string());
}

const Extension* ExtensionServiceTestWithInstall::VerifyCrxInstall(
    const base::FilePath& path,
    InstallState install_state,
    const std::string& expected_old_name) {
  std::vector<std::u16string> errors = GetErrors();
  const Extension* extension = nullptr;
  if (install_state != INSTALL_FAILED) {
    if (install_state == INSTALL_NEW || install_state == INSTALL_WITHOUT_LOAD)
      ++expected_extensions_count_;

    EXPECT_TRUE(installed_extension_) << path.value();
    // If and only if INSTALL_UPDATED, it should have the is_update flag.
    EXPECT_EQ(install_state == INSTALL_UPDATED, was_update_)
        << path.value();
    // If INSTALL_UPDATED, old_name_ should match the given string.
    if (install_state == INSTALL_UPDATED && !expected_old_name.empty())
      EXPECT_EQ(expected_old_name, old_name_);
    EXPECT_EQ(0u, errors.size()) << path.value();

    if (install_state == INSTALL_WITHOUT_LOAD) {
      EXPECT_EQ(0u, loaded_extensions_.size()) << path.value();
      extension = installed_extension_;
    } else {
      EXPECT_EQ(1u, loaded_extensions_.size()) << path.value();
      size_t actual_extension_count =
          registry()->enabled_extensions().size() +
          registry()->disabled_extensions().size();
      EXPECT_EQ(expected_extensions_count_, actual_extension_count) <<
          path.value();
      extension = loaded_extensions_[0].get();
      EXPECT_TRUE(registry()->enabled_extensions().GetByID(extension->id()))
          << path.value();
    }

    for (auto err = errors.begin(); err != errors.end(); ++err) {
      LOG(ERROR) << *err;
    }
  } else {
    EXPECT_FALSE(installed_extension_) << path.value();
    EXPECT_EQ(0u, loaded_extensions_.size()) << path.value();
    EXPECT_EQ(1u, errors.size()) << path.value();
  }

  installed_extension_ = nullptr;
  was_update_ = false;
  old_name_ = "";
  ClearLoadedExtensions();
  LoadErrorReporter::GetInstance()->ClearErrors();
  return extension;
}

void ExtensionServiceTestWithInstall::PackCRXAndUpdateExtension(
    const std::string& id,
    const base::FilePath& dir_path,
    const base::FilePath& pem_path,
    UpdateState expected_state) {
  base::ScopedTempDir temp_dir;
  EXPECT_TRUE(temp_dir.CreateUniqueTempDir());
  base::FilePath crx_path = temp_dir.GetPath().AppendASCII("temp.crx");

  PackCRX(dir_path, pem_path, crx_path);
  UpdateExtension(id, crx_path, expected_state);
}

void ExtensionServiceTestWithInstall::UpdateExtension(
    const std::string& id,
    const base::FilePath& in_path,
    UpdateState expected_state) {
  ASSERT_TRUE(base::PathExists(in_path));

  // We need to copy this to a temporary location because Update() will delete
  // it.
  base::FilePath path = temp_dir().GetPath();
  path = path.Append(in_path.BaseName());
  ASSERT_TRUE(base::CopyFile(in_path, path));

  int previous_enabled_extension_count =
      registry()->enabled_extensions().size();
  int previous_installed_extension_count =
      previous_enabled_extension_count +
      registry()->disabled_extensions().size();

  CRXFileInfo crx_info(path, GetTestVerifierFormat());
  crx_info.extension_id = id;

  // Create an ExtensionUpdater and use it to create a CrxInstaller.
  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
  ExtensionUpdater updater(profile());
  updater.InitAndEnable(prefs, prefs->pref_service(), base::Minutes(10),
                        /*cache=*/nullptr, ExtensionDownloader::Factory());
  auto installer = updater.CreateUpdateInstaller(crx_info, true);

  if (installer) {
    base::RunLoop run_loop;
    installer->AddInstallerCallback(base::BindLambdaForTesting(
        [&run_loop](const std::optional<CrxInstallError>& error) {
          run_loop.Quit();
        }));
    installer->InstallCrxFile(crx_info);
    run_loop.Run();
  } else {
    content::RunAllTasksUntilIdle();
  }

  std::vector<std::u16string> errors = GetErrors();
  int error_count = errors.size();
  int enabled_extension_count = registry()->enabled_extensions().size();
  int installed_extension_count =
      enabled_extension_count + registry()->disabled_extensions().size();

  int expected_error_count = (expected_state == FAILED) ? 1 : 0;
  EXPECT_EQ(expected_error_count, error_count) << path.value();

  if (expected_state <= FAILED) {
    EXPECT_EQ(previous_enabled_extension_count,
              enabled_extension_count);
    EXPECT_EQ(previous_installed_extension_count,
              installed_extension_count);
  } else {
    int expected_installed_extension_count =
        (expected_state >= INSTALLED) ? 1 : 0;
    int expected_enabled_extension_count =
        (expected_state >= ENABLED) ? 1 : 0;
    EXPECT_EQ(expected_installed_extension_count,
              installed_extension_count);
    EXPECT_EQ(expected_enabled_extension_count,
              enabled_extension_count);
  }

  // Verify that after running all pending tasks, the temporary file has been
  // deleted.
  content::RunAllTasksUntilIdle();
  EXPECT_FALSE(base::PathExists(path));
}

void ExtensionServiceTestWithInstall::UninstallExtension(
    const std::string& id,
    UninstallExtensionFileDeleteType delete_type,
    extensions::UninstallReason reason) {
  // Verify that the extension is installed.
  const Extension* extension =
      registry()->GetExtensionById(id, ExtensionRegistry::EVERYTHING);
  ASSERT_TRUE(extension);
  base::FilePath extension_path = base::FilePath(extension->path());
  EXPECT_TRUE(base::PathExists(extension_path));
  ExtensionPrefs* prefs = ExtensionPrefs::Get(profile());
  EXPECT_TRUE(prefs->GetInstalledExtensionInfo(id));

  // We make a copy of the extension's id since the extension can be deleted
  // once it's uninstalled.
  std::string extension_id = id;
  // Uninstall it.
  EXPECT_TRUE(registrar()->UninstallExtension(id, reason, nullptr));
  --expected_extensions_count_;

  // We should get an unload notification.
  EXPECT_FALSE(unloaded_id_.empty());
  EXPECT_EQ(extension_id, unloaded_id_);

  // The extension should not be in the service anymore.
  EXPECT_FALSE(registry()->GetInstalledExtension(extension_id));
  EXPECT_FALSE(prefs->GetInstalledExtensionInfo(extension_id));
  task_environment()->RunUntilIdle();

  switch (delete_type) {
    case kDeleteAllVersions:
      EXPECT_FALSE(base::PathExists(extension_path.DirName()))
          << extension_path.value();
      break;
    case kDeletePath:
      EXPECT_FALSE(base::PathExists(extension_path)) << extension_path.value();
      break;
    case kDoNotDelete:
      EXPECT_TRUE(base::PathExists(extension_path)) << extension_path.value();
      break;
  }
}

void ExtensionServiceTestWithInstall::TerminateExtension(
    const std::string& id) {
  if (!registry()->GetInstalledExtension(id)) {
    ADD_FAILURE();
    return;
  }
  registrar()->TerminateExtension(id);
}

void ExtensionServiceTestWithInstall::BlockAllExtensions() {
  registrar()->BlockAllExtensions();
}

void ExtensionServiceTestWithInstall::ClearLoadedExtensions() {
  loaded_extensions_.clear();
}

void ExtensionServiceTestWithInstall::OnExtensionLoaded(
    content::BrowserContext* browser_context,
    const Extension* extension) {
  loaded_extensions_.push_back(base::WrapRefCounted(extension));
  // The tests rely on the errors being in a certain order, which can vary
  // depending on how filesystem iteration works.
  std::stable_sort(loaded_extensions_.begin(), loaded_extensions_.end(),
                   ExtensionsOrder());
}

void ExtensionServiceTestWithInstall::OnExtensionUnloaded(
    content::BrowserContext* browser_context,
    const Extension* extension,
    UnloadedExtensionReason reason) {
  unloaded_id_ = extension->id();
  unloaded_reason_ = reason;
  auto i = std::ranges::find(loaded_extensions_, extension);
  // TODO(erikkay) fix so this can be an assert.  Right now the tests
  // are manually calling `ClearLoadedExtensions` since this method is not
  // called by reloads, so this isn't doable.
  if (i == loaded_extensions_.end())
    return;
  loaded_extensions_.erase(i);
}

void ExtensionServiceTestWithInstall::OnExtensionWillBeInstalled(
    content::BrowserContext* browser_context,
    const Extension* extension,
    bool is_update,
    const std::string& old_name) {
  installed_extension_ = extension;
  was_update_ = is_update;
  old_name_ = old_name;
}

// Create a CrxInstaller and install the CRX file.
// Instead of calling this method yourself, use InstallCRX(), which does extra
// error checking.
void ExtensionServiceTestWithInstall::InstallCRXInternal(
    const base::FilePath& crx_path,
    ManifestLocation install_location,
    InstallState install_state,
    int creation_flags) {
  ChromeTestExtensionLoader extension_loader(profile());
  extension_loader.set_location(install_location);
  extension_loader.set_creation_flags(creation_flags);
  extension_loader.set_should_fail(install_state == INSTALL_FAILED);
  // TODO(devlin): We shouldn't be granting permissions based on whether
  // something was installed by default. That's weird.
  extension_loader.set_grant_permissions(
      (creation_flags & Extension::WAS_INSTALLED_BY_DEFAULT) == 0);
  // TODO(devlin): We shouldn't ignore manifest warnings here, but we always
  // did so a bunch of stuff fails. Migrate this over.
  extension_loader.set_ignore_manifest_warnings(true);
  extension_loader.set_wait_for_renderers(false);
  extension_loader.LoadExtension(crx_path);
}

// StatefulChangeProcessor --------------------------------------------

StatefulChangeProcessor::StatefulChangeProcessor(syncer::DataType expected_type)
    : expected_type_(expected_type) {
  DCHECK(expected_type == syncer::DataType::EXTENSIONS ||
         expected_type == syncer::DataType::APPS);
}

StatefulChangeProcessor::~StatefulChangeProcessor() = default;

std::optional<syncer::ModelError> StatefulChangeProcessor::ProcessSyncChanges(
    const base::Location& from_here,
    const syncer::SyncChangeList& change_list) {
  syncer::FakeSyncChangeProcessor::ProcessSyncChanges(from_here, change_list);
  for (const auto& change : change_list) {
    syncer::SyncData sync_data = change.sync_data();
    DCHECK_EQ(expected_type_, sync_data.GetDataType());

    std::unique_ptr<ExtensionSyncData> modified =
        ExtensionSyncData::CreateFromSyncData(sync_data);

    // Start by removing any existing entry for this extension id.
    for (auto iter = data_.begin(); iter != data_.end(); ++iter) {
      std::unique_ptr<ExtensionSyncData> existing =
          ExtensionSyncData::CreateFromSyncData(*iter);
      if (existing->id() == modified->id()) {
        data_.erase(iter);
        break;
      }
    }

    // Now add in the new data for this id, if appropriate.
    if (change.change_type() == syncer::SyncChange::ACTION_ADD ||
        change.change_type() == syncer::SyncChange::ACTION_UPDATE) {
      data_.push_back(sync_data);
    } else if (change.change_type() != syncer::SyncChange::ACTION_DELETE) {
      NOTREACHED() << "Unexpected change type " << change.change_type();
    }
  }
  return std::nullopt;
}

std::unique_ptr<syncer::SyncChangeProcessor>
StatefulChangeProcessor::GetWrapped() {
  return std::make_unique<syncer::SyncChangeProcessorWrapperForTest>(this);
}

}  // namespace extensions
