// Copyright 2022 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/ash/shimless_rma/chrome_shimless_rma_delegate.h"

#include <utility>

#include "ash/constants/ash_features.h"
#include "base/check_deref.h"
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "chrome/browser/ash/shimless_rma/diagnostics_app_profile_helper.h"
#include "chrome/browser/ash/shimless_rma/diagnostics_app_profile_helper_constants.h"
#include "chrome/browser/extensions/extension_garbage_collector_factory.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/external_provider_manager.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/web_applications/isolated_web_apps/commands/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/install/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/iwa_permissions_policy_cache.h"
#include "chrome/browser/web_applications/model/iwa_update_info.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "chromeos/ash/components/browser_context_helper/browser_context_types.h"
#include "components/variations/scoped_variations_ids_provider.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/isolated_web_apps/types/iwa_origin.h"
#include "components/webapps/isolated_web_apps/types/iwa_version.h"
#include "components/webapps/isolated_web_apps/types/storage_location.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/fake_service_worker_context.h"
#include "extensions/common/constants.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash::shimless_rma {
namespace {

const char kTestCrxPath[] = "chrome/test/data/chromeos/3p_diagnostics/diag.crx";
const char kTestWrongIdCrxPath[] =
    "chrome/test/data/chromeos/3p_diagnostics/diag-wrong-id.crx";
// The test wrong id generated by the key signing the above crx.
const char kTestWrongExtId[] = "neacocmolncbbnnameegalgmoedgpfpk";
// The IWA installation is not tested in unit test. So we don't need a real
// IWA.
const char kFakeIwaPath[] = "fake_iwa_path.swbn";
// The IWA ID corresponding to the dev extension, used in development phase.
const char kDevIwaId[] =
    "pt2jysa7yu326m2cbu5mce4rrajvguagronrsqwn5dhbaris6eaaaaic";
}  // namespace

class ChromeShimlessRmaDelegateTest : public testing::Test {
 public:
  ChromeShimlessRmaDelegateTest()
      : chrome_shimless_rma_delegate_(ChromeShimlessRmaDelegate(nullptr)),
        task_environment_(content::BrowserTaskEnvironment::REAL_IO_THREAD) {}
  ~ChromeShimlessRmaDelegateTest() override = default;

 protected:
  ChromeShimlessRmaDelegate chrome_shimless_rma_delegate_;

 private:
  content::BrowserTaskEnvironment task_environment_;
};

// Validates a QrCode Bitmap is correctly converted to a string.
TEST_F(ChromeShimlessRmaDelegateTest, GenerateQrCode) {
  base::RunLoop run_loop;
  chrome_shimless_rma_delegate_.GenerateQrCode(
      "www.sample-url.com",
      base::BindLambdaForTesting([&](const std::string& qr_code_image) {
        EXPECT_FALSE(qr_code_image.empty());
      }));
  run_loop.RunUntilIdle();
}

class FakeServiceWorkerContext : public content::FakeServiceWorkerContext {
 public:
  FakeServiceWorkerContext() = default;
  FakeServiceWorkerContext(const FakeServiceWorkerContext&) = delete;
  ~FakeServiceWorkerContext() override = default;

  void CheckHasServiceWorker(const GURL& url,
                             const blink::StorageKey& key,
                             CheckHasServiceWorkerCallback callback) override {
    content::ServiceWorkerCapability result =
        content::ServiceWorkerCapability::SERVICE_WORKER_NO_FETCH_HANDLER;
    if (service_worker_check_retry_ > 0) {
      --service_worker_check_retry_;
      result = content::ServiceWorkerCapability::NO_SERVICE_WORKER;
    }
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), result));
  }

  void set_service_worker_check_retry(int64_t retry) {
    service_worker_check_retry_ = retry;
  }

 private:
  // The times until the check return service worker found.
  int64_t service_worker_check_retry_ = 2;
};

class FakeWebAppCommandScheduler : public web_app::WebAppCommandScheduler {
 public:
  using web_app::WebAppCommandScheduler::WebAppCommandScheduler;

  void InstallIsolatedWebApp(
      const web_app::IsolatedWebAppUrlInfo& url_info,
      const web_app::IsolatedWebAppInstallSource& install_source,
      const std::optional<web_app::IwaVersion>& expected_version,
      std::unique_ptr<ScopedKeepAlive> keep_alive,
      std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive,
      web_app::WebAppCommandScheduler::InstallIsolatedWebAppCallback callback,
      std::optional<web_app::IwaUpdateInfo> optional_update_info,
      const base::Location& call_location) override {
    EXPECT_EQ(install_source.install_surface(),
              webapps::WebappInstallSource::IWA_SHIMLESS_RMA);
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(std::move(callback),
                       web_app::InstallIsolatedWebAppCommandSuccess(
                           url_info, *web_app::IwaVersion::Create("0"),
                           web_app::IwaStorageOwnedBundle{
                               "random_folder", /*dev_mode=*/false})));
  }

  void RemoveInstallManagementMaybeUninstall(
      const webapps::AppId& app_id,
      web_app::WebAppManagement::Type install_management,
      webapps::WebappUninstallSource uninstall_source,
      UninstallCallback callback,
      const base::Location& location) override {
    EXPECT_EQ(install_management,
              web_app::WebAppManagement::Type::kIwaShimlessRma);
    EXPECT_EQ(uninstall_source, webapps::WebappUninstallSource::kUnknown);

    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback),
                                  webapps::UninstallResultCode::kAppRemoved));
  }
};

class FakeDiagnosticsAppProfileHelperDelegate
    : public DiagnosticsAppProfileHelperDelegate {
 public:
  explicit FakeDiagnosticsAppProfileHelperDelegate(Profile* profile)
      : web_app_command_scheduler_(*profile) {
    web_app_ = web_app::test::CreateWebApp(
        GURL(base::StrCat({"isolated-app://", kDevIwaId})));
    web_app_->SetName("App Name");
  }
  FakeDiagnosticsAppProfileHelperDelegate(
      const DiagnosticsAppProfileHelperDelegate&) = delete;
  ~FakeDiagnosticsAppProfileHelperDelegate() override = default;

  content::ServiceWorkerContext* GetServiceWorkerContextForExtensionId(
      const extensions::ExtensionId& extension_id,
      content::BrowserContext* browser_context) override {
    return &fake_service_worker_context_;
  }

  web_app::WebAppCommandScheduler* GetWebAppCommandScheduler(
      content::BrowserContext* browser_context) override {
    return &web_app_command_scheduler_;
  }

  const web_app::WebApp* GetWebAppByIdUnsafe(
      const webapps::AppId& app_id,
      content::BrowserContext* browser_context) override {
    return web_app_.get();
  }

  FakeServiceWorkerContext& fake_service_worker_context() {
    return fake_service_worker_context_;
  }

  web_app::WebApp& web_app() { return CHECK_DEREF(web_app_); }

 protected:
  FakeServiceWorkerContext fake_service_worker_context_;
  FakeWebAppCommandScheduler web_app_command_scheduler_;
  std::unique_ptr<web_app::WebApp> web_app_;
};

class ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest
    : public extensions::ExtensionServiceTestBase {
 public:
  ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest()
      : extensions::ExtensionServiceTestBase(
            std::make_unique<content::BrowserTaskEnvironment>(
                base::test::TaskEnvironment::TimeSource::MOCK_TIME)) {}

  void SetUp() override {
    extensions::ExtensionServiceTestBase::SetUp();

    feature_list_.InitWithFeatures(
        {
            ash::features::kShimlessRMA3pDiagnosticsDevMode,
            ash::features::kShimlessRMA3pDiagnosticsAllowPermissionPolicy,
        },
        {});
    profile_ = testing_profile_manager()->CreateTestingProfile(
        kShimlessRmaAppBrowserContextBaseName);

    InitializeExtensionSystem(profile_);
    ::web_app::test::AwaitStartWebAppProviderAndSubsystems(profile_);

    auto fake_diagnostics_app_profile_helper_delegate =
        std::make_unique<FakeDiagnosticsAppProfileHelperDelegate>(profile_);
    fake_diagnostics_app_profile_helper_delegate_ =
        fake_diagnostics_app_profile_helper_delegate.get();
    chrome_shimless_rma_delegate_ = std::make_unique<ChromeShimlessRmaDelegate>(
        std::move(fake_diagnostics_app_profile_helper_delegate));
  }

  void InitializeExtensionSystem(Profile* profile) {
    auto extensions_install_dir =
        profile->GetPath().AppendASCII(extensions::kInstallDirectoryName);
    auto unpacked_install_dir = profile->GetPath().AppendASCII(
        extensions::kUnpackedInstallDirectoryName);

    extensions::TestExtensionSystem* system =
        static_cast<extensions::TestExtensionSystem*>(
            extensions::ExtensionSystem::Get(profile));
    auto* service = system->CreateExtensionService(
        base::CommandLine::ForCurrentProcess(), extensions_install_dir,
        unpacked_install_dir, true, true);

    // When we start up, we want to make sure there is no external provider,
    // since the ExtensionService on Windows will use the Registry as a default
    // provider and if there is something already registered there then it will
    // interfere with the tests. Those tests that need an external provider
    // will register one specifically.
    extensions::ExternalProviderManager::Get(profile)
        ->ClearProvidersForTesting();

    service->Init();

    // Garbage collector is typically NULL during tests, so give it a build.
    extensions::ExtensionGarbageCollectorFactory::GetInstance()
        ->SetTestingFactoryAndUse(
            profile,
            base::BindRepeating(&extensions::ExtensionGarbageCollectorFactory::
                                    BuildInstanceFor));
  }

  void TearDown() override {
    fake_diagnostics_app_profile_helper_delegate_ = nullptr;
    chrome_shimless_rma_delegate_.reset();
    profile_ = nullptr;
    extensions::ExtensionServiceTestBase::TearDown();
  }

  using PrepareResult = base::expected<
      ChromeShimlessRmaDelegate::PrepareDiagnosticsAppBrowserContextResult,
      std::string>;

  PrepareResult PrepareDiagnosticsAppBrowserContext(
      const base::FilePath& crx_path) {
    base::test::TestFuture<PrepareResult> future;
    chrome_shimless_rma_delegate_->PrepareDiagnosticsAppBrowserContext(
        crx_path, base::FilePath{kFakeIwaPath}, future.GetCallback());
    return future.Get();
  }

 protected:
  raw_ptr<TestingProfile> profile_;
  base::test::ScopedFeatureList feature_list_;
  variations::test::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};
  raw_ptr<FakeDiagnosticsAppProfileHelperDelegate>
      fake_diagnostics_app_profile_helper_delegate_;
  std::unique_ptr<ChromeShimlessRmaDelegate> chrome_shimless_rma_delegate_;
};

// Verify the whole flow of `PrepareDiagnosticsAppProfile`.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest, Success) {
  // Call this twice to verify that even if the profile has already been loaded
  // it still works.
  for (int i = 0; i < 2; ++i) {
    auto result = PrepareDiagnosticsAppBrowserContext(
        base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
            .Append(kTestCrxPath));

    EXPECT_TRUE(result.has_value());
    EXPECT_EQ(result->extension_id, "jmalcmbicpnakfkncbgbcmlmgpfkhdca");
    EXPECT_EQ(result->iwa_id.id(), kDevIwaId);
    EXPECT_EQ(result->name, "App Name");
    EXPECT_EQ(result->permission_message,
              "Run ChromeOS diagnostic tests\nRead ChromeOS device information "
              "and data\nRead ChromeOS device and component serial numbers\n");

    content::BrowserContext* context = result->context;
    EXPECT_FALSE(context->IsOffTheRecord());
    EXPECT_TRUE(Profile::FromBrowserContext(context)->GetPrefs()->GetBoolean(
        prefs::kForceEphemeralProfiles));
    EXPECT_TRUE(
        DiagnosticsAppProfileHelperDelegate::GetInstalledDiagnosticsAppOrigin()
            .has_value());
  }
}

// Verify that we denied extensions which is not in the ChromeOS system
// extension allowlist.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       NotChromeOSSystemExtension) {
  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestWrongIdCrxPath));

  EXPECT_FALSE(result.has_value());
  EXPECT_EQ(result.error(),
            base::StringPrintf(k3pDiagErrorNotChromeOSSystemExtension,
                               kTestWrongExtId));
}

// Verify the service worker polling logic break after reaching the timeout.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       ServiceWorkerTimeout) {
  // To hit the timeout, we need to retry more than ((timeout / polling
  // interval) + 1) times.
  int64_t retry_times = k3pDiagExtensionReadyPollingTimeout.IntDiv(
                            k3pDiagExtensionReadyPollingInterval) +
                        1;
  fake_diagnostics_app_profile_helper_delegate_->fake_service_worker_context()
      .set_service_worker_check_retry(retry_times);

  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestCrxPath));

  EXPECT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), k3pDiagErrorCannotActivateExtension);
}

// Verify that IWA with allowlisted permission policy will be installed.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       IWACanHaveAllowlistedPermissionsPolicy) {
  web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile_)
      ->SetPolicyForTesting(
          web_app::IwaOrigin::Create(
              GURL(base::StrCat({"isolated-app://", kDevIwaId})))
              .value(),
          {{"camera", {}},
           {"fullscreen", {}},
           {"microphone", {}},
           {"hid", {}}});

  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestCrxPath));

  EXPECT_TRUE(result.has_value());
}

// Verify that IWA with not-allowlisted permission policy will be blocked.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       IWACannotHavePermissionsPolicyOutsideAllowlist) {
  web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile_)
      ->SetPolicyForTesting(
          web_app::IwaOrigin::Create(
              GURL(base::StrCat({"isolated-app://", kDevIwaId})))
              .value(),
          {{"camera", {}}, {"unknown-feature", {}}});

  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestCrxPath));

  EXPECT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), k3pDiagErrorIWACannotHasPermissionPolicy);
}

// Verify that IWA with allowlisted permission policy but without feature flag
// will be blocked.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       IWACannotHavePermissionsPolicyWithoutFeatureFlag) {
  base::test::ScopedFeatureList scoped_list;
  scoped_list.InitAndDisableFeature(
      ash::features::kShimlessRMA3pDiagnosticsAllowPermissionPolicy);

  web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile_)
      ->SetPolicyForTesting(
          web_app::IwaOrigin::Create(
              GURL(base::StrCat({"isolated-app://", kDevIwaId})))
              .value(),
          {{"camera", {}}});

  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestCrxPath));

  EXPECT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), k3pDiagErrorIWACannotHasPermissionPolicy);
}

// Verify that if IWA is not installed successfully, the Delegate will not
// return the installed app origin.
TEST_F(ChromeShimlessRmaDelegatePrepareDiagnosticsAppProfileTest,
       InstalledAppOriginNotSetAfterIwaInstallFailure) {
  web_app::IwaPermissionsPolicyCacheFactory::GetForProfile(profile_)
      ->SetPolicyForTesting(
          web_app::IwaOrigin::Create(
              GURL(base::StrCat({"isolated-app://", kDevIwaId})))
              .value(),
          {{"unknown-feature", {}}});

  auto result = PrepareDiagnosticsAppBrowserContext(
      base::PathService::CheckedGet(base::DIR_SRC_TEST_DATA_ROOT)
          .Append(kTestCrxPath));

  EXPECT_FALSE(result.has_value());
  EXPECT_EQ(result.error(), k3pDiagErrorIWACannotHasPermissionPolicy);
  EXPECT_FALSE(
      DiagnosticsAppProfileHelperDelegate::GetInstalledDiagnosticsAppOrigin()
          .has_value());
}

}  // namespace ash::shimless_rma
