chromium/chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager_unittest.cc

// 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/web_applications/isolated_web_apps/policy/isolated_web_app_policy_manager.h"

#include <memory>
#include <string>
#include <string_view>
#include <vector>

#include "base/containers/contains.h"
#include "base/containers/flat_set.h"
#include "base/containers/to_vector.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/functional/callback_helpers.h"
#include "base/run_loop.h"
#include "base/strings/to_string.h"
#include "base/test/bind.h"
#include "base/test/repeating_test_future.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "base/version.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/install_isolated_web_app_command.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_install_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_source.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_storage_location.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_discovery_task.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_update_manager.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_external_install_options.h"
#include "chrome/browser/web_applications/isolated_web_apps/policy/isolated_web_app_policy_constants.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/iwa_test_server_configurator.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/mock_isolated_web_app_install_command_wrapper.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/policy_generator.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/test_iwa_installer_factory.h"
#include "chrome/browser/web_applications/isolated_web_apps/update_manifest/update_manifest.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/fake_web_contents_manager.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/test/web_app_test_observers.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chromeos/ash/components/login/login_state/login_state.h"
#include "components/nacl/common/buildflags.h"
#include "components/user_manager/user.h"
#include "components/web_package/signed_web_bundles/signed_web_bundle_id.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/url_response_head.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(ENABLE_NACL)
#include "chrome/browser/nacl_host/nacl_browser_delegate_impl.h"
#include "components/nacl/browser/nacl_browser.h"
#endif  // BUILDFLAG(ENABLE_NACL)

namespace web_app {

namespace {

using base::test::DictionaryHasValue;
using testing::_;
using ::testing::AllOf;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::IsEmpty;
using ::testing::IsNull;
using ::testing::Not;
using ::testing::NotNull;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::Property;
using ::testing::UnorderedElementsAre;

constexpr char kUpdateManifestUrl1[] =
    "https://example.com/1/update-manifest-1.json";
constexpr char kUpdateManifestUrl2[] =
    "https://example.com/2/update-manifest-2.json";
constexpr char kUpdateManifestUrl3[] =
    "https://example.com/3/update-manifest-3.json";
constexpr char kUpdateManifestUrl4[] =
    "https://example.com/4/update-manifest-4.json";
constexpr char kUpdateManifestUrl5[] =
    "https://example.com/5/update-manifest-5.json";
constexpr char kUpdateManifestUrl6[] =
    "https://example.com/6/update-manifest-6.json";
constexpr char kUpdateManifestUrl7[] =
    "https://example.com/7/update-manifest-7.json";

constexpr char kUpdateManifestValue1[] = R"(
    {"versions":[
      {"version": "1.0.0", "src": "https://example.com/not-used.swbn"},
      {"version": "7.0.6", "src": "https://example.com/app1.swbn"}]
    })";
constexpr char kUpdateManifestValue2[] = R"(
    {"versions":
    [{"version": "3.0.0","src": "https://example.com/app2.swbn"}]})";
constexpr char kUpdateManifestValue3[] =
    "This update manifest should return error 404";
constexpr char kUpdateManifestValue4[] = R"(This is not JSON)";
// This manifest contains an invalid `src` URL.
constexpr char kUpdateManifestValue5[] = R"(
    {"versions":
    [{"version": "1.0.0", "src": "chrome-extension://app5.wbn"}]})";
constexpr char kUpdateManifestValue6[] = R"(
    {"versions":
    [{"version": "1.0.0","src": "https://example.com/app6.swbn"}]})";
constexpr char kUpdateManifestValue7[] = R"(
    {"versions":
    [{"version": "1.0.0", "src": "https://example.com/app7.swbn"}]})";

constexpr char kWebBundleId1[] =
    "aerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId2[] =
    "berugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId3[] =
    "cerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId4[] =
    "derugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId5[] =
    "eerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId6[] =
    "herugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";
constexpr char kWebBundleId7[] =
    "gerugqztij5biqquuk3mfwpsaibuegaqcitgfchwuosuofdjabzqaaic";

base::Value CreatePolicyEntry(std::string_view web_bundle_id,
                              std::string_view update_manifest_url) {
  base::Value::Dict policy_entry =
      base::Value::Dict()
          .Set(web_app::kPolicyWebBundleIdKey, web_bundle_id)
          .Set(web_app::kPolicyUpdateManifestUrlKey, update_manifest_url);
  return base::Value(std::move(policy_entry));
}

class MockIwaInstallCommandWrapper
    : public internal::IwaInstaller::IwaInstallCommandWrapper {
 public:
  MockIwaInstallCommandWrapper() = default;
  ~MockIwaInstallCommandWrapper() override = default;

  MOCK_METHOD(void,
              Install,
              (const IsolatedWebAppInstallSource& install_source,
               const IsolatedWebAppUrlInfo& url_info,
               const base::Version& expected_version,
               WebAppCommandScheduler::InstallIsolatedWebAppCallback callback),
              (override));
};

class TestOrphanedCleanupWebAppCommandScheduler
    : public WebAppCommandScheduler {
 public:
  explicit TestOrphanedCleanupWebAppCommandScheduler(Profile& profile)
      : WebAppCommandScheduler(profile) {}

  void CleanupOrphanedIsolatedApps(
      CleanupOrphanedIsolatedWebAppsCallback callback,
      const base::Location& call_location) override {
    ++number_of_calls_;
    std::move(callback).Run(CleanupOrphanedIsolatedWebAppsCommandSuccess(0u));
    command_done_closure_.Run();
  }

  size_t GetNumberOfCalls() { return number_of_calls_; }

  void SetCommandDoneClosure(base::RepeatingClosure closure) {
    command_done_closure_ = std::move(closure);
  }

 private:
  base::RepeatingClosure command_done_closure_;
  size_t number_of_calls_ = 0;
};

#if BUILDFLAG(ENABLE_NACL)
class ScopedNaClBrowserDelegate {
 public:
  explicit ScopedNaClBrowserDelegate(ProfileManager* profile_manager) {
    nacl::NaClBrowser::SetDelegate(
        std::make_unique<NaClBrowserDelegateImpl>(profile_manager));
  }

  ~ScopedNaClBrowserDelegate() { nacl::NaClBrowser::ClearAndDeleteDelegate(); }
};
#endif  // BUILDFLAG(ENABLE_NACL)

void HandleInstallBasedOnId(
    const IsolatedWebAppInstallSource& install_source,
    const IsolatedWebAppUrlInfo& url_info,
    const base::Version& expected_version,
    WebAppCommandScheduler::InstallIsolatedWebAppCallback callback) {
  if (url_info.web_bundle_id().id() == kWebBundleId1 ||
      url_info.web_bundle_id().id() == kWebBundleId2) {
    if (url_info.web_bundle_id().id() == kWebBundleId1) {
      EXPECT_EQ(expected_version, base::Version("7.0.6"));
    } else if (url_info.web_bundle_id().id() == kWebBundleId2) {
      EXPECT_EQ(expected_version, base::Version("3.0.0"));
    }

    std::move(callback).Run(InstallIsolatedWebAppCommandSuccess(
        expected_version,
        IwaStorageOwnedBundle{"random_folder", /*dev_mode=*/false}));
    return;
  }

  std::move(callback).Run(base::unexpected{InstallIsolatedWebAppCommandError{
      .message = std::string{"Install error message"}}});
}

}  // namespace

namespace internal {

struct IwaInstallerTestParam {
  bool is_mgs_install_enabled;
  bool is_user_session;
  std::string bundle_id;
  std::string manifest_url;
  internal::IwaInstallerResult::Type result_type;
};

class IwaInstallerTest
    : public ::testing::TestWithParam<IwaInstallerTestParam> {
 public:
  IwaInstallerTest()
      : shared_url_loader_factory_(
            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
                &test_factory_)) {
    std::vector<base::test::FeatureRef> enabled_features = {
        features::kIsolatedWebApps};
    if (GetParam().is_mgs_install_enabled) {
      enabled_features.push_back(
          features::kIsolatedWebAppManagedGuestSessionInstall);
    }
    scoped_feature_list_.InitWithFeatures(std::move(enabled_features),
                                          /*disabled_features=*/{});
  }

 protected:
  using InstallResult = internal::IwaInstallerResult;

  void SetUp() override {
    ASSERT_TRUE(dir_.CreateUniqueTempDir());
    AddJsonResponse(kUpdateManifestUrl1, kUpdateManifestValue1);
    AddJsonResponse(kUpdateManifestUrl2, kUpdateManifestValue2);
    test_factory_.AddResponse(kUpdateManifestUrl3, kUpdateManifestValue3,
                              net::HttpStatusCode::HTTP_NOT_FOUND);
    AddJsonResponse(kUpdateManifestUrl4, kUpdateManifestValue4);
    AddJsonResponse(kUpdateManifestUrl5, kUpdateManifestValue5);
    AddJsonResponse(kUpdateManifestUrl6, kUpdateManifestValue6);
    AddJsonResponse(kUpdateManifestUrl7, kUpdateManifestValue7);
    test_factory_.AddResponse("https://example.com/app1.swbn",
                              "Content of app1");
    test_factory_.AddResponse("https://example.com/app2.swbn",
                              "Content of app2");
    test_factory_.AddResponse("https://example.com/app6.swbn",
                              "Content of app6");
    test_factory_.AddResponse("https://example.com/app7.swbn", "",
                              net::HttpStatusCode::HTTP_NOT_FOUND);

    if (!GetParam().is_user_session) {
      test_managed_guest_session_ =
          std::make_unique<profiles::testing::ScopedTestManagedGuestSession>();
    }
  }

  void TearDown() override { test_factory_.ClearResponses(); }

  void AddJsonResponse(std::string_view url, std::string_view content) {
    network::mojom::URLResponseHeadPtr head =
        network::CreateURLResponseHead(net::HttpStatusCode::HTTP_OK);
    head->mime_type = "application/json";
    network::URLLoaderCompletionStatus status;
    test_factory_.AddResponse(GURL(url), std::move(head), std::string(content),
                              status);
  }

  base::test::TaskEnvironment task_environment_;
  base::ScopedTempDir dir_;
  network::TestURLLoaderFactory test_factory_;
  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory_;
  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
  std::unique_ptr<profiles::testing::ScopedTestManagedGuestSession>
      test_managed_guest_session_;
  base::test::ScopedFeatureList scoped_feature_list_;

  IsolatedWebAppExternalInstallOptions install_options_ =
      IsolatedWebAppExternalInstallOptions::FromPolicyPrefValue(
          CreatePolicyEntry(/*web_bundle_id=*/GetParam().bundle_id,
                            /*update_manifest_url=*/GetParam().manifest_url))
          .value();
};

// This test case represents the regular flow of force installing IWA for
// ephemeral session. The install options will cover cases of success for
// both, managed guest sessions and managed user sessions.
TEST_P(IwaInstallerTest, MgsRegularFlow) {
  base::test::TestFuture<InstallResult> future;
  base::Value::List log;

  auto install_command = std::make_unique<MockIwaInstallCommandWrapper>();
  EXPECT_CALL(*install_command, Install(_, _, _, _))
      .WillRepeatedly(Invoke(
          [](const IsolatedWebAppInstallSource& install_source,
             const IsolatedWebAppUrlInfo& url_info,
             const base::Version& expected_version,
             WebAppCommandScheduler::InstallIsolatedWebAppCallback callback) {
            HandleInstallBasedOnId(install_source, url_info, expected_version,
                                   std::move(callback));
          }));
  IwaInstaller installer(install_options_, shared_url_loader_factory_,
                         std::move(install_command), log, future.GetCallback());
  installer.Start();

  EXPECT_THAT(
      future.Get(),
      Property(
          "type", &InstallResult::type,
          Eq(!GetParam().is_user_session && !GetParam().is_mgs_install_enabled
                 ? InstallResult::Type::kErrorManagedGuestSessionInstallDisabled
                 : GetParam().result_type)));
}

TEST_P(IwaInstallerTest, NotMgs) {
  test_managed_guest_session_.reset();

  base::test::TestFuture<InstallResult> future;
  base::Value::List log;

  auto install_command = std::make_unique<MockIwaInstallCommandWrapper>();
  EXPECT_CALL(*install_command, Install(_, _, _, _))
      .WillRepeatedly(Invoke(
          [](const IsolatedWebAppInstallSource& install_source,
             const IsolatedWebAppUrlInfo& url_info,
             const base::Version& expected_version,
             WebAppCommandScheduler::InstallIsolatedWebAppCallback callback) {
            HandleInstallBasedOnId(install_source, url_info, expected_version,
                                   std::move(callback));
          }));
  IwaInstaller installer(install_options_, shared_url_loader_factory_,
                         std::move(install_command), log, future.GetCallback());
  installer.Start();

  EXPECT_THAT(future.Get(), Property("type", &InstallResult::type,
                                     Eq(GetParam().result_type)));
}

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    IwaInstallerTest,
    ::testing::ValuesIn(std::vector<IwaInstallerTestParam>{
        // App 1 represents the most general case: the Update Manifest has
        // several records. We should determine the latest version, download
        // the appropriate file and install the app. It is successful case.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId1,
         .manifest_url = kUpdateManifestUrl1,
         .result_type = internal::IwaInstallerResult::Type::kSuccess},
        // Same as the previous test case, but inside a managed guest session.
        {.is_mgs_install_enabled = true,
         .is_user_session = false,
         .bundle_id = kWebBundleId1,
         .manifest_url = kUpdateManifestUrl1,
         .result_type = internal::IwaInstallerResult::Type::kSuccess},
        // App 2 is similar to App 1 but has only one record in the Update
        // Manifest.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId2,
         .manifest_url = kUpdateManifestUrl2,
         .result_type = internal::IwaInstallerResult::Type::kSuccess},
        // We can't download Update Manifest for the app 3.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId3,
         .manifest_url = kUpdateManifestUrl3,
         .result_type = internal::IwaInstallerResult::Type::
             kErrorUpdateManifestDownloadFailed},
        // App 4 represents the case where the Update Manifest if not parsable.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId4,
         .manifest_url = kUpdateManifestUrl4,
         .result_type = internal::IwaInstallerResult::Type::
             kErrorUpdateManifestParsingFailed},
        // The Web Bundle URL of the App 5 is not valid.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId5,
         .manifest_url = kUpdateManifestUrl5,
         .result_type = internal::IwaInstallerResult::Type::
             kErrorWebBundleUrlCantBeDetermined},
        // The Web Bundle of the App 6 can't be installed.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId6,
         .manifest_url = kUpdateManifestUrl6,
         .result_type = internal::IwaInstallerResult::Type::
             kErrorCantInstallFromWebBundle},
        // The Web Bundle file of the App 7 can't be downloaded.
        {.is_mgs_install_enabled = true,
         .is_user_session = true,
         .bundle_id = kWebBundleId7,
         .manifest_url = kUpdateManifestUrl7,
         .result_type =
             internal::IwaInstallerResult::Type::kErrorCantDownloadWebBundle},
        {.is_mgs_install_enabled = false,
         .is_user_session = false,
         .bundle_id = kWebBundleId1,
         .manifest_url = kUpdateManifestUrl1,
         .result_type = internal::IwaInstallerResult::Type::kSuccess}}));

}  // namespace internal

constexpr char kUpdateManifestUrlApp1[] =
    "https://example.com/manifest_app1.json";
constexpr char kUpdateManifestUrlApp2[] =
    "https://example.com/manifest_app2.json";

constexpr char kUpdateManifestValueApp1[] = R"(
    {"versions":
    [{"version": "1.0.0","src": "https://example.com/web_bundle_app1.swbn"}]})";
constexpr char kUpdateManifestValueApp2[] = R"(
    {"versions":
    [{"version": "1.0.0","src": "https://example.com/web_bundle_app2.swbn"}]})";

class IsolatedWebAppPolicyManagerTestBase : public WebAppTest {
 public:
  explicit IsolatedWebAppPolicyManagerTestBase(
      bool is_mgs_session_install_enabled,
      bool is_user_session,
      base::test::TaskEnvironment::TimeSource time_source =
          base::test::TaskEnvironment::TimeSource::DEFAULT)
      : WebAppTest(WebAppTest::WithTestUrlLoaderFactory(), time_source),
        is_mgs_session_install_enabled_(is_mgs_session_install_enabled),
        is_user_session_(is_user_session) {
    std::vector<base::test::FeatureRef> enabled_features = {
        features::kIsolatedWebApps};
    if (is_mgs_session_install_enabled_) {
      enabled_features.push_back(
          features::kIsolatedWebAppManagedGuestSessionInstall);
    }

    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/
        std::move(enabled_features),
        /*disabled_features=*/{});
  }

  void SetUpServedIwas() {
    web_app::TestSignedWebBundle swbn_app1 =
        web_app::TestSignedWebBundleBuilder::BuildDefault();
    web_app::TestSignedWebBundle swbn_app2 =
        web_app::TestSignedWebBundleBuilder::BuildDefault(
            TestSignedWebBundleBuilder::BuildOptions().AddKeyPair(
                web_package::test::Ed25519KeyPair::CreateRandom()));

    lazy_app1_id_ = swbn_app1.id;
    lazy_app2_id_ = swbn_app2.id;

    IwaTestServerConfigurator configurator;
    configurator.AddUpdateManifest("manifest_app1.json",
                                   kUpdateManifestValueApp1);
    configurator.AddSignedWebBundle("web_bundle_app1.swbn",
                                    std::move(swbn_app1));
    configurator.AddUpdateManifest("manifest_app2.json",
                                   kUpdateManifestValueApp2);
    configurator.AddSignedWebBundle("web_bundle_app2.swbn",
                                    std::move(swbn_app2));
    configurator.ConfigureURLLoader(GURL("https://example.com/"),
                                    profile_url_loader_factory(),
                                    fake_web_contents_manager());
  }

  void SetUp() override {
    WebAppTest::SetUp();
    SetCommandScheduler();
    test::AwaitStartWebAppProviderAndSubsystems(profile());
    SetUpServedIwas();

    if (!is_user_session_) {
      test_managed_guest_session_ =
          std::make_unique<profiles::testing::ScopedTestManagedGuestSession>();
    }

#if BUILDFLAG(ENABLE_NACL)
    // Uninstalling an IWA will clear PNACL cache, which needs this delegate
    // set.
    nacl_browser_delegate_ = std::make_unique<ScopedNaClBrowserDelegate>(
        profile_manager().profile_manager());
#endif  // BUILDFLAG(ENABLE_NACL)
  }

  virtual void SetCommandScheduler() = 0;

  sync_preferences::TestingPrefServiceSyncable* pref_service() {
    return profile()->GetTestingPrefService();
  }

  WebAppProvider& provider() { return *WebAppProvider::GetForTest(profile()); }
  FakeWebContentsManager& fake_web_contents_manager() {
    return static_cast<FakeWebContentsManager&>(
        provider().web_contents_manager());
  }

  void AssertAppInstalled(const web_package::SignedWebBundleId& swbn_id) {
    const WebApp* web_app = fake_provider().registrar_unsafe().GetAppById(
        IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(swbn_id).app_id());
    ASSERT_THAT(web_app, testing::NotNull()) << "The app in not installed :(";
  }

  bool IsManagedGuestSessionInstallEnabled() {
    return is_mgs_session_install_enabled_;
  }

  const web_package::SignedWebBundleId& get_app1_id() { return *lazy_app1_id_; }
  const web_package::SignedWebBundleId& get_app2_id() { return *lazy_app2_id_; }

 private:
  const bool is_mgs_session_install_enabled_;
  const bool is_user_session_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<profiles::testing::ScopedTestManagedGuestSession>
      test_managed_guest_session_;
  data_decoder::test::InProcessDataDecoder data_decoder_;
#if BUILDFLAG(ENABLE_NACL)
  std::unique_ptr<ScopedNaClBrowserDelegate> nacl_browser_delegate_;
#endif  // BUILDFLAG(ENABLE_NACL)

  std::optional<web_package::SignedWebBundleId> lazy_app1_id_;
  std::optional<web_package::SignedWebBundleId> lazy_app2_id_;
};

class IsolatedWebAppPolicyManagerTest
    : public IsolatedWebAppPolicyManagerTestBase {
 public:
  IsolatedWebAppPolicyManagerTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/false,
            /*is_user_session=*/true) {}

  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    // For these tests we are fine with regular command scheduler.
  }
};

TEST_F(IsolatedWebAppPolicyManagerTest, AppInstalled) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());

  WebAppTestInstallObserver install_observer(profile());
  install_observer.BeginListening({url_info.app_id()});

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  EXPECT_EQ(install_observer.Wait(), url_info.app_id());
  task_environment()->RunUntilIdle();

  const WebApp* web_app =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  ASSERT_THAT(web_app, NotNull());
  EXPECT_THAT(web_app->GetSources(),
              Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
}

TEST_F(IsolatedWebAppPolicyManagerTest,
       AppSourceAddedWhenPreviouslyUserInstalled) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  AddDummyIsolatedAppToRegistry(
      profile(), url_info.origin().GetURL(), "iwa",
      WebApp::IsolationData(
          IwaStorageOwnedBundle("some_folder", /*dev_mode=*/false),
          base::Version("1.0.0")),
      webapps::WebappInstallSource::IWA_GRAPHICAL_INSTALLER);
  {
    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaUserInstalled})));
  }

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  task_environment()->RunUntilIdle();
  {
    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaUserInstalled,
                                  WebAppManagement::Type::kIwaPolicy})));
  }

  auto debug_log = provider().iwa_update_manager().AsDebugValue();
  EXPECT_THAT(
      debug_log.GetDict()
          .FindDict("task_queue")
          ->FindList("update_discovery_log"),
      Pointee(UnorderedElementsAre(Property(
          "GetDict", &base::Value::GetDict,
          AllOf(DictionaryHasValue("app_id", base::Value(url_info.app_id())),
                DictionaryHasValue(
                    "result", base::Value(base::ToString(
                                  IsolatedWebAppUpdateDiscoveryTask::Success::
                                      kNoUpdateFound))))))))
      << debug_log;
}

TEST_F(IsolatedWebAppPolicyManagerTest,
       AppInstalledWhenPreviouslyDevInstalled) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  AddDummyIsolatedAppToRegistry(
      profile(), url_info.origin().GetURL(), "iwa",
      WebApp::IsolationData(
          IwaStorageOwnedBundle("some_folder", /*dev_mode=*/true),
          base::Version("1.0.0")),
      webapps::WebappInstallSource::IWA_DEV_UI);

  WebAppTestUninstallObserver uninstall_observer(profile());
  uninstall_observer.BeginListening({url_info.app_id()});
  WebAppTestInstallObserver install_observer(profile());
  install_observer.BeginListening({url_info.app_id()});

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  // Dev-mode apps should be fully uninstalled before they can be
  // force-installed.
  EXPECT_EQ(uninstall_observer.Wait(), url_info.app_id());
  EXPECT_EQ(install_observer.Wait(), url_info.app_id());
  task_environment()->RunUntilIdle();

  const WebApp* web_app =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  ASSERT_THAT(web_app, NotNull());
  EXPECT_THAT(web_app->GetSources(),
              Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
}

class ManagedGuestSessionInstallFlagTest
    : public IsolatedWebAppPolicyManagerTestBase,
      public testing::WithParamInterface<bool> {
 public:
  ManagedGuestSessionInstallFlagTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/GetParam(),
            /*is_user_session=*/false) {}

  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    // For these tests we are fine with regular command scheduler.
  }
};

TEST_P(ManagedGuestSessionInstallFlagTest, AppInstalledIfFlagEnabled) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  task_environment()->RunUntilIdle();

  const WebApp* web_app =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  if (IsManagedGuestSessionInstallEnabled()) {
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
  } else {
    ASSERT_THAT(web_app, IsNull());
  }
}

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    ManagedGuestSessionInstallFlagTest,
    // Determines whether managed guest session install is enabled.
    testing::Bool());

// This implementation of the command scheduler can't install an IWA. Instead
// it hangs and waits for the signal to signalize the
// invoker that the install failed.
class TestWebAppCommandScheduler : public WebAppCommandScheduler {
 public:
  using WebAppCommandScheduler::WebAppCommandScheduler;

  void InstallIsolatedWebApp(
      const IsolatedWebAppUrlInfo& url_info,
      const IsolatedWebAppInstallSource& install_source,
      const std::optional<base::Version>& expected_version,
      std::unique_ptr<ScopedKeepAlive> keep_alive,
      std::unique_ptr<ScopedProfileKeepAlive> profile_keep_alive,
      InstallIsolatedWebAppCallback callback,
      const base::Location& call_location) override {
    EXPECT_TRUE(stashed_callback_.is_null());
    EXPECT_EQ(install_source.install_surface(),
              webapps::WebappInstallSource::IWA_EXTERNAL_POLICY);
    id_ = url_info.web_bundle_id();
    stashed_callback_ = std::move(callback);
  }

  web_package::SignedWebBundleId FinishWithError() {
    std::move(stashed_callback_)
        .Run(base::unexpected<InstallIsolatedWebAppCommandError>(
            InstallIsolatedWebAppCommandError{
                .message = "Just test error. We even didn't try..."}));
    return id_.value();
  }

 private:
  InstallIsolatedWebAppCallback stashed_callback_;
  std::optional<web_package::SignedWebBundleId> id_;
};

template <typename T>
class IsolatedWebAppPolicyManagerCustomSchedulerTest
    : public IsolatedWebAppPolicyManagerTestBase {
 public:
  IsolatedWebAppPolicyManagerCustomSchedulerTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/false,
            /*is_user_session=*/true) {}

  T* get_command_scheduler() { return scheduler_; }
  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    std::unique_ptr<T> scheduler = std::make_unique<T>(*profile());
    scheduler_ = scheduler.get();
    fake_provider().SetScheduler(std::move(scheduler));
  }

  void TearDown() override {
    scheduler_ = nullptr;
    IsolatedWebAppPolicyManagerTestBase::TearDown();
  }

 private:
  raw_ptr<T> scheduler_;
};

using IsolatedWebAppPolicyManagerPolicyRaceTest =
    IsolatedWebAppPolicyManagerCustomSchedulerTest<TestWebAppCommandScheduler>;

// Verifies that the updating of policy during previous policy processing
// is handled correctly.
TEST_F(IsolatedWebAppPolicyManagerPolicyRaceTest,
       PolicyUpdateWhileInstallInProgress) {
  {
    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(get_app1_id(),
                                          GURL(kUpdateManifestUrlApp1));
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());
  }

  task_environment()->RunUntilIdle();

  // Update the policy at the moment when first policy update is being
  // processed. We set the policy to force install not existing app.
  // This policy variant will not be processed because it will be replaced
  // by the third policy update.
  {
    PolicyGenerator policy_generator;
    const web_package::SignedWebBundleId id =
        web_package::SignedWebBundleId::Create(
            "xyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzxyzaaaic")
            .value();
    policy_generator.AddForceInstalledIwa(
        id, GURL("https://update/manifest/does/not/exist"));
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());
  }

  task_environment()->RunUntilIdle();

  // The third policy update. This one must be processed.
  {
    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(get_app1_id(),
                                          GURL(kUpdateManifestUrlApp1));
    policy_generator.AddForceInstalledIwa(get_app2_id(),
                                          GURL(kUpdateManifestUrlApp2));
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());
  }

  // Finish the installation of the app1 from the first policy update.
  EXPECT_THAT(get_command_scheduler()->FinishWithError(), Eq(get_app1_id()));
  task_environment()->RunUntilIdle();

  // The second policy update is ignored as it was replaced by the third one.

  // Processing the third policy update.
  std::vector<web_package::SignedWebBundleId> ids;

  // Finish app1 from the third policy update.
  ids.push_back(get_command_scheduler()->FinishWithError());
  task_environment()->RunUntilIdle();

  // Finish app2 from the third policy update.
  ids.push_back(get_command_scheduler()->FinishWithError());
  task_environment()->RunUntilIdle();

  EXPECT_THAT(ids, UnorderedElementsAre(get_app1_id(), get_app2_id()));
}

// This scheduler is intercepting scheduling of the uninstall command,
// verifying if the App ID is expected for removal.
class UninstallWebAppCommandScheduler : public WebAppCommandScheduler {
 public:
  using WebAppCommandScheduler::WebAppCommandScheduler;

  void RemoveInstallManagementMaybeUninstall(
      const webapps::AppId& app_id,
      WebAppManagement::Type management_type,
      webapps::WebappUninstallSource uninstall_source,
      UninstallJob::Callback callback,
      const base::Location& location) override {
    tried_to_uninstall_ = true;
    EXPECT_TRUE(base::Contains(expected_apps_to_remove_, app_id));
    EXPECT_EQ(management_type, WebAppManagement::Type::kIwaPolicy);
    EXPECT_EQ(uninstall_source,
              webapps::WebappUninstallSource::kIwaEnterprisePolicy);
    auto app = expected_apps_to_remove_.find(app_id);
    expected_apps_to_remove_.erase(app);

    WebAppCommandScheduler::RemoveInstallManagementMaybeUninstall(
        app_id, management_type, uninstall_source, std::move(callback),
        location);
  }

  void AddExpectedToUninstallApp(const webapps::AppId& app_id) {
    expected_apps_to_remove_.insert(app_id);
  }

  size_t GetNumberOfAppsRemainingToUninstall() const {
    return expected_apps_to_remove_.size();
  }

  bool TriedToUninstall() { return tried_to_uninstall_; }

 private:
  base::flat_set<webapps::AppId> expected_apps_to_remove_;
  bool tried_to_uninstall_ = false;
};

using IsolatedWebAppPolicyManagerUninstallTest =
    IsolatedWebAppPolicyManagerCustomSchedulerTest<
        UninstallWebAppCommandScheduler>;

// Remove the app from policy and check if there will be attempt to uninstall
// that app.
TEST_F(IsolatedWebAppPolicyManagerUninstallTest, OneAppUninstalled) {
  // Force install 2 apps.
  {
    PolicyGenerator policy_generator_2_apps;
    policy_generator_2_apps.AddForceInstalledIwa(get_app1_id(),
                                                 GURL(kUpdateManifestUrlApp1));
    policy_generator_2_apps.AddForceInstalledIwa(get_app2_id(),
                                                 GURL(kUpdateManifestUrlApp2));

    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator_2_apps.Generate());

    task_environment()->RunUntilIdle();

    AssertAppInstalled(get_app1_id());
    AssertAppInstalled(get_app2_id());
  }

  // Now generate a policy with 1 app and expect an attempt to
  // remove the other app.
  {
    PolicyGenerator policy_generator_1_app;
    policy_generator_1_app.AddForceInstalledIwa(get_app1_id(),
                                                GURL(kUpdateManifestUrlApp1));

    const webapps::AppId app2_id =
        IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app2_id())
            .app_id();
    get_command_scheduler()->AddExpectedToUninstallApp(app2_id);
    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              1U);

    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator_1_app.Generate());

    task_environment()->RunUntilIdle();

    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              0U);
  }
}

TEST_F(IsolatedWebAppPolicyManagerUninstallTest, BothAppUninstalled) {
  // Force install 2 apps.
  {
    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(get_app1_id(),
                                          GURL(kUpdateManifestUrlApp1));
    policy_generator.AddForceInstalledIwa(get_app2_id(),
                                          GURL(kUpdateManifestUrlApp2));

    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());

    task_environment()->RunUntilIdle();

    AssertAppInstalled(get_app1_id());
    AssertAppInstalled(get_app2_id());
  }

  // Set the policy without any app and expect an attempt to uninstall
  // both previously installed apps.
  {
    const webapps::AppId app1_id =
        IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id())
            .app_id();
    const webapps::AppId app2_id =
        IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app2_id())
            .app_id();

    WebAppTestUninstallObserver uninstall_observer(profile());
    uninstall_observer.BeginListening({app1_id, app2_id});

    get_command_scheduler()->AddExpectedToUninstallApp(app1_id);
    get_command_scheduler()->AddExpectedToUninstallApp(app2_id);
    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              2U);

    PolicyGenerator empty_policy;
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               empty_policy.Generate());

    uninstall_observer.Wait();

    // WebAppTestUninstallObserver already triggers when the app is not fully
    // uninstalled. This causes issues with references to destroyed profiles
    // (see https://crbug.com/41484323#comment7). Wait until the app is actually
    // uninstalled here.
    task_environment()->RunUntilIdle();

    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              0U);
  }
}

TEST_F(IsolatedWebAppPolicyManagerUninstallTest,
       UserInstalledAppNotUninstalled) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());

  // User-install the app.
  {
    AddDummyIsolatedAppToRegistry(
        profile(), url_info.origin().GetURL(), "iwa",
        WebApp::IsolationData(
            IwaStorageOwnedBundle("some_folder", /*dev_mode=*/false),
            base::Version("1.0.0")),
        webapps::WebappInstallSource::IWA_GRAPHICAL_INSTALLER);

    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaUserInstalled})));
  }

  // Force install the app via policy.
  {
    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                          GURL(kUpdateManifestUrlApp1));
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());

    task_environment()->RunUntilIdle();

    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaUserInstalled,
                                  WebAppManagement::Type::kIwaPolicy})));
  }

  // Set the policy without any app and expect an attempt to remove the policy
  // install source.
  {
    get_command_scheduler()->AddExpectedToUninstallApp(url_info.app_id());
    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              1U);
    WebAppInstallManagerObserverAdapter observer(profile());
    base::test::RepeatingTestFuture<const webapps::AppId&>
        source_removed_future;
    observer.SetWebAppSourceRemovedDelegate(
        source_removed_future.GetCallback());

    PolicyGenerator empty_policy;
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               empty_policy.Generate());

    EXPECT_EQ(source_removed_future.Take(), url_info.app_id());
    EXPECT_EQ(get_command_scheduler()->GetNumberOfAppsRemainingToUninstall(),
              0U);

    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaUserInstalled})));
  }
}

// There should not be any attempt to uninstall an app if no apps have been
// removed from the apps.
TEST_F(IsolatedWebAppPolicyManagerUninstallTest, NoAppsUninstalled) {
  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(get_app1_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());
  task_environment()->RunUntilIdle();

  AssertAppInstalled(get_app1_id());

  policy_generator.AddForceInstalledIwa(get_app2_id(),
                                        GURL(kUpdateManifestUrlApp2));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());
  task_environment()->RunUntilIdle();

  AssertAppInstalled(get_app1_id());
  AssertAppInstalled(get_app2_id());
  EXPECT_FALSE(get_command_scheduler()->TriedToUninstall());
}

class IsolatedWebAppRetryTest : public IsolatedWebAppPolicyManagerTestBase {
 public:
  IsolatedWebAppRetryTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/false,
            /*is_user_session=*/true,
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

 protected:
  TestIwaInstallerFactory iwa_installer_factory_;

 private:
  void SetUp() override {
    IsolatedWebAppPolicyManagerTestBase::SetUp();
    iwa_installer_factory_.SetUp(profile());
  }

  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    // For these tests we are fine with the regular command scheduler.
  }
};

TEST_F(IsolatedWebAppRetryTest, FirstInstallFailsRetrySucceeds) {
  auto url_info =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  iwa_installer_factory_.SetCommandBehavior(
      url_info.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kSimulateFailure,
      /*execute_immediately=*/true);

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  // Run the first attempt to install the isolated web app (which should fail).
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));

  ASSERT_EQ(1u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());
  const WebApp* web_app_t0 =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  ASSERT_THAT(web_app_t0, IsNull());

  // Fast forward right before the retry should happen --> retry to process the
  // policy is still scheduled, but the isolated web app is not yet installed.
  iwa_installer_factory_.SetCommandBehavior(
      url_info.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/true);
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(58)));

  const WebApp* web_app_t1 =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  ASSERT_THAT(web_app_t1, IsNull());

  WebAppTestInstallObserver install_observer(profile());
  install_observer.BeginListening({url_info.app_id()});

  // Fast forward another second and the app should be installed.
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));

  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());

  // Make sure that even if there are further install tasks scheduled, they are
  // failing and therefore do not accidentally make this test pass.
  iwa_installer_factory_.SetCommandBehavior(
      url_info.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kSimulateFailure,
      /*execute_immediately=*/true);

  EXPECT_EQ(install_observer.Wait(), url_info.app_id());

  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());
  const WebApp* web_app_t2 =
      fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
  ASSERT_THAT(web_app_t2, NotNull());
  EXPECT_THAT(web_app_t2->GetSources(),
              Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
}

TEST_F(IsolatedWebAppRetryTest, RetryTimeStepsCorrect) {
  const std::vector<std::pair<web_package::SignedWebBundleId, GURL>> apps = {
      {get_app1_id(), GURL(kUpdateManifestUrlApp1)},
      {get_app2_id(), GURL(kUpdateManifestUrlApp2)}};
  const std::vector<int> desired_retry_time_steps_in_seconds = {
      // Continuously increasing delay by i * 60.
      0,
      60,
      180,
      420,
      900,
      1860,
      3780,
      7620,
      15300,
      30660,
      // From here on the delay saturates at 5 hours.
      48660,
      66660,
      84660,
  };

  // Try multiple apps to make sure that the delay gets reset after a successful
  // installation.
  PolicyGenerator policy_generator;
  unsigned int expected_number_install_tasks = 1u;
  for (const auto& [app_id, update_manifest_url] : apps) {
    auto url_info = IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(app_id);
    iwa_installer_factory_.SetCommandBehavior(
        url_info.web_bundle_id().id(),
        /*execution_mode=*/
        MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::
            kSimulateFailure,
        /*execute_immediately=*/true);

    policy_generator.AddForceInstalledIwa(url_info.web_bundle_id(),
                                          update_manifest_url);
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());

    for (size_t i = 0; i < desired_retry_time_steps_in_seconds.size() - 1;
         ++i) {
      const int& current_time_step = desired_retry_time_steps_in_seconds[i];
      const int& next_time_step = desired_retry_time_steps_in_seconds[i + 1];

      // Another (failed) attempt to install the isolated web app
      task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));

      ASSERT_EQ(expected_number_install_tasks,
                iwa_installer_factory_.GetNumberOfCreatedInstallTasks());
      const WebApp* web_app_t0 =
          fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
      ASSERT_THAT(web_app_t0, IsNull());

      // Fast forward right before the retry should happen --> retry to process
      // the policy is still scheduled, but the install task is not yet created.
      task_environment()->FastForwardBy(base::TimeDelta(
          base::Seconds(next_time_step - current_time_step - 2)));

      const WebApp* web_app_t1 =
          fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
      ASSERT_THAT(web_app_t1, IsNull());

      // Fast forward another second and the next retry should happen.
      task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));
      ASSERT_EQ(++expected_number_install_tasks,
                iwa_installer_factory_.GetNumberOfCreatedInstallTasks());
    }

    WebAppTestInstallObserver install_observer(profile());
    install_observer.BeginListening({url_info.app_id()});

    // Finally make the installation work. This should reset the delay for the
    // next install.
    iwa_installer_factory_.SetCommandBehavior(
        url_info.web_bundle_id().id(),
        /*execution_mode=*/
        MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
        /*execute_immediately=*/true);
    task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(18000)));

    EXPECT_EQ(install_observer.Wait(), url_info.app_id());

    const WebApp* web_app =
        fake_provider().registrar_unsafe().GetAppById(url_info.app_id());
    ASSERT_THAT(web_app, NotNull());
    EXPECT_THAT(
        web_app->GetSources(),
        Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
    expected_number_install_tasks += 2;
  }
}

// This test checks that retries are only scheduled once all install tasks are
// done. It does so by installing two isolated web apps. The first app install
// finishes immediately (but fails), while the second app does not finish for 60
// seconds. In these 60 seconds, no retry should be scheduled. The test then
// manually triggers the completion of the second install task (which succeeds).
// From that point in time, a retry should be scheduled with a delay of another
// 60 seconds.
TEST_F(IsolatedWebAppRetryTest, RetryTriggeredWhenAllTasksDone) {
  auto url_info_1 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  auto url_info_2 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app2_id());

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info_1.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  policy_generator.AddForceInstalledIwa(url_info_2.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp2));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  // The first app installation finishes immediately, but fails. The second app
  // installation does not finish immediately and completion has to be triggered
  // later by the test (this simulates a completion delay), but will succeed.
  iwa_installer_factory_.SetCommandBehavior(
      url_info_1.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kSimulateFailure,
      /*execute_immediately=*/true);
  iwa_installer_factory_.SetCommandBehavior(
      url_info_2.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/false);

  // Run the first attempt to install the isolated apps (the first one fails
  // immediately, the second one is still busy).
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));

  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());
  const WebApp* web_app1_t0 =
      fake_provider().registrar_unsafe().GetAppById(url_info_1.app_id());
  ASSERT_THAT(web_app1_t0, IsNull());
  const WebApp* web_app2_t0 =
      fake_provider().registrar_unsafe().GetAppById(url_info_2.app_id());
  ASSERT_THAT(web_app2_t0, IsNull());

  // Forward by 60 seconds. Because the second app was not completed yet, still
  // no retry should be scheduled.
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(60)));
  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());

  ASSERT_TRUE(iwa_installer_factory_.GetLatestCommandWrapper(
      url_info_2.web_bundle_id().id()));
  ASSERT_FALSE(iwa_installer_factory_
                   .GetLatestCommandWrapper(url_info_2.web_bundle_id().id())
                   ->CommandWasScheduled());

  // Complete install task for the second app (which succeeds).
  WebAppTestInstallObserver app2_install_observer(profile());
  app2_install_observer.BeginListening({url_info_2.app_id()});

  task_environment()->GetMainThreadTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(
          &MockIsolatedWebAppInstallCommandWrapper::ScheduleCommand,
          base::Unretained(iwa_installer_factory_.GetLatestCommandWrapper(
              url_info_2.web_bundle_id().id()))));
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));

  EXPECT_EQ(app2_install_observer.Wait(), url_info_2.app_id());

  // The retry command for the first app should be successful. The second app
  // doesn't need a retry.
  iwa_installer_factory_.SetCommandBehavior(
      url_info_1.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/true);
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));
  // The retry is scheduled, but the install task for the remaining app is not
  // yet created.
  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());

  // Forward to right before an additional install task for the first app is
  // scheduled.
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(57)));
  ASSERT_EQ(2u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());

  WebAppTestInstallObserver app1_install_observer(profile());
  app1_install_observer.BeginListening({url_info_1.app_id()});

  // Moving the clock forward will finally install the second app.
  task_environment()->FastForwardBy(base::TimeDelta(base::Seconds(1)));
  ASSERT_EQ(3u, iwa_installer_factory_.GetNumberOfCreatedInstallTasks());

  EXPECT_EQ(app1_install_observer.Wait(), url_info_1.app_id());

  const WebApp* web_app1_t2 =
      fake_provider().registrar_unsafe().GetAppById(url_info_1.app_id());
  ASSERT_THAT(web_app1_t2, NotNull());
  EXPECT_THAT(web_app1_t2->GetSources(),
              Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
  const WebApp* web_app2_t2 =
      fake_provider().registrar_unsafe().GetAppById(url_info_2.app_id());
  ASSERT_THAT(web_app2_t2, NotNull());
  EXPECT_THAT(web_app2_t2->GetSources(),
              Eq(WebAppManagementTypes({WebAppManagement::Type::kIwaPolicy})));
}

class CleanupOrphanedBundlesTest : public IsolatedWebAppPolicyManagerTestBase {
 public:
  CleanupOrphanedBundlesTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/false,
            /*is_user_session=*/true) {}

  void SetUp() override {
    IsolatedWebAppPolicyManagerTestBase::SetUp();
    iwa_installer_factory_.SetUp(profile());
  }

  void TearDown() override {
    command_scheduler_ = nullptr;
    IsolatedWebAppPolicyManagerTestBase::TearDown();
  }

 protected:
  TestIwaInstallerFactory iwa_installer_factory_;
  raw_ptr<TestOrphanedCleanupWebAppCommandScheduler> command_scheduler_ =
      nullptr;
  base::test::TestFuture<void> command_done_future_;

 private:
  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    auto command_scheduler =
        std::make_unique<TestOrphanedCleanupWebAppCommandScheduler>(*profile());
    command_scheduler_ = command_scheduler.get();
    command_scheduler_->SetCommandDoneClosure(
        command_done_future_.GetRepeatingCallback());
    fake_provider().SetScheduler(std::move(command_scheduler));
  }
};

TEST_F(CleanupOrphanedBundlesTest, CleanUpCalledOnSessionStart) {
  // Nothing to do here. The session start is automatically performed and the
  // expectations are therefore set early in
  // `CleanupOrphanedBundlesTest::SetCommandScheduler`.
  command_scheduler_->SetCommandDoneClosure(
      command_done_future_.GetRepeatingCallback());
  ASSERT_TRUE(command_done_future_.Wait());
  EXPECT_EQ(1u, command_scheduler_->GetNumberOfCalls());
}

// Install two isolated web apps. One of them succeeds, the other one fails and
// therefore the cleanup command should be scheduled.
TEST_F(CleanupOrphanedBundlesTest, CleanUpCalledOnTaskFailure) {
  ASSERT_TRUE(command_done_future_.Wait());
  EXPECT_EQ(1u, command_scheduler_->GetNumberOfCalls());
  command_done_future_.Clear();

  auto url_info_1 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  auto url_info_2 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app2_id());
  iwa_installer_factory_.SetCommandBehavior(
      url_info_1.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/true);
  iwa_installer_factory_.SetCommandBehavior(
      url_info_2.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kSimulateFailure,
      /*execute_immediately=*/true);

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info_1.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  policy_generator.AddForceInstalledIwa(url_info_2.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp2));
  WebAppTestInstallObserver install_observer(profile());
  install_observer.BeginListening({url_info_1.app_id()});
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());
  EXPECT_EQ(install_observer.Wait(), url_info_1.app_id());

  ASSERT_TRUE(command_done_future_.Wait());
  EXPECT_EQ(2u, command_scheduler_->GetNumberOfCalls());
}

TEST_F(CleanupOrphanedBundlesTest, CleanUpNotCalledOnAllTasksSuccess) {
  ASSERT_TRUE(command_done_future_.Wait());
  EXPECT_EQ(1u, command_scheduler_->GetNumberOfCalls());
  command_done_future_.Clear();

  auto url_info_1 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app1_id());
  auto url_info_2 =
      IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(get_app2_id());
  iwa_installer_factory_.SetCommandBehavior(
      url_info_1.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/true);
  iwa_installer_factory_.SetCommandBehavior(
      url_info_2.web_bundle_id().id(),
      /*execution_mode=*/
      MockIsolatedWebAppInstallCommandWrapper::ExecutionMode::kRunCommand,
      /*execute_immediately=*/true);

  // Wait until the initial commands were executed (among of which one is a
  // cleanup command).
  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  // We do not expect the cleanup command to be called.
  command_scheduler_->SetCommandDoneClosure(base::NullCallback());

  WebAppTestInstallObserver install_observer(profile());
  install_observer.BeginListening({url_info_1.app_id(), url_info_2.app_id()});

  PolicyGenerator policy_generator;
  policy_generator.AddForceInstalledIwa(url_info_1.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp1));
  policy_generator.AddForceInstalledIwa(url_info_2.web_bundle_id(),
                                        GURL(kUpdateManifestUrlApp2));
  profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                             policy_generator.Generate());

  const webapps::AppId last_installed_app_id = install_observer.Wait();
  task_environment()->RunUntilIdle();

  EXPECT_TRUE(last_installed_app_id == url_info_1.app_id() ||
              last_installed_app_id == url_info_2.app_id());
}

class IsolatedWebAppInstallEmergencyMechanismTest
    : public IsolatedWebAppPolicyManagerTestBase,
      public testing::WithParamInterface<int> {
 public:
  IsolatedWebAppInstallEmergencyMechanismTest()
      : IsolatedWebAppPolicyManagerTestBase(
            /*is_mgs_session_install_enabled=*/false,
            /*is_user_session=*/true,
            base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

 protected:
  int GetSimulatedPendingInstallCount() { return GetParam(); }

  webapps::AppId app_id_;

 private:
  // `IsolatedWebAppPolicyManagerTestBase`:
  void SetCommandScheduler() override {
    // For these tests we are fine with the regular command scheduler.
    const auto url_info_1 = IsolatedWebAppUrlInfo::CreateFromSignedWebBundleId(
        web_app::TestSignedWebBundleBuilder::BuildDefault().id);
    app_id_ = url_info_1.app_id();

    PolicyGenerator policy_generator;
    policy_generator.AddForceInstalledIwa(url_info_1.web_bundle_id(),
                                          GURL(kUpdateManifestUrlApp1));
    profile()->GetPrefs()->Set(prefs::kIsolatedWebAppInstallForceList,
                               policy_generator.Generate());

    // Set the number of previous crashes on profile creation to simulate a
    // previously crashing device.
    profile()->GetPrefs()->SetInteger(
        prefs::kIsolatedWebAppPendingInitializationCount,
        GetSimulatedPendingInstallCount());
  }
};

TEST_P(IsolatedWebAppInstallEmergencyMechanismTest,
       EmergencyMechanismOnStartup) {
  // If the emergency mechanism is triggered, the install count is increaded by
  // one. If not, the startup is successful and the pending install count is
  // reset to 0.
  if (GetSimulatedPendingInstallCount() > 2) {
    EXPECT_EQ(GetSimulatedPendingInstallCount() + 1,
              profile()->GetPrefs()->GetInteger(
                  prefs::kIsolatedWebAppPendingInitializationCount));
  } else {
    EXPECT_EQ(0, profile()->GetPrefs()->GetInteger(
                     prefs::kIsolatedWebAppPendingInitializationCount));
  }

  // Process all the pending immediate tasks (not the delayed emergency task).
  task_environment()->FastForwardBy(base::Seconds(1));

  // If we already tried twice, we delay the execution to allow for updates.
  if (GetSimulatedPendingInstallCount() > 2) {
    EXPECT_EQ(GetSimulatedPendingInstallCount() + 1,
              profile()->GetPrefs()->GetInteger(
                  prefs::kIsolatedWebAppPendingInitializationCount));
    EXPECT_EQ(0u, provider().registrar_unsafe().GetAppIds().size());

    // Forward until one second before the retry. The pending installation count
    // is still not reset.
    task_environment()->FastForwardBy(base::Hours(4) + base::Minutes(59) +
                                      base::Seconds(58));
    EXPECT_EQ(GetSimulatedPendingInstallCount() + 1,
              profile()->GetPrefs()->GetInteger(
                  prefs::kIsolatedWebAppPendingInitializationCount));
    EXPECT_EQ(0u, provider().registrar_unsafe().GetAppIds().size());

    // Forward by another second, which triggers the retry.
    task_environment()->FastForwardBy(base::Seconds(1));
  }

  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_EQ(1u, provider().registrar_unsafe().GetAppIds().size());
  EXPECT_EQ(0, profile()->GetPrefs()->GetInteger(
                   prefs::kIsolatedWebAppPendingInitializationCount));
}

INSTANTIATE_TEST_SUITE_P(
    /***/,
    IsolatedWebAppInstallEmergencyMechanismTest,
    // Simulates the number of failed attempts before the current session start.
    testing::ValuesIn({0, 1, 2, 3}));

}  // namespace web_app