// Copyright 2018 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/preinstalled_web_app_manager.h"

#include <algorithm>
#include <memory>
#include <set>
#include <string_view>
#include <vector>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_command_line.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_path_override.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_future.h"
#include "base/time/default_clock.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/web_applications/external_install_options.h"
#include "chrome/browser/web_applications/mojom/user_display_mode.mojom.h"
#include "chrome/browser/web_applications/preinstalled_app_install_features.h"
#include "chrome/browser/web_applications/preinstalled_web_app_config_utils.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/browser/web_applications/test/fake_extensions_manager.h"
#include "chrome/browser/web_applications/test/fake_web_app_origin_association_manager.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.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/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_pref_guardrails.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_contents/web_contents_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/testing_profile.h"
#include "components/account_id/account_id.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/webapps/common/manifest_id_constants.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/gaia/gaia_id.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "ui/gfx/test/sk_gmock_support.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_switches.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/policy/profile_policy_connector.h"
#include "chromeos/ash/components/system/fake_statistics_provider.h"
#include "chromeos/ash/components/system/statistics_provider.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user_names.h"
#endif

namespace web_app {

namespace {

constexpr char kUserTypesTestDir[] = "user_types";

#if BUILDFLAG(IS_CHROMEOS)
constexpr char kGoodJsonTestDir[] = "good_json";

constexpr char kAppAllUrl[] = "https://www.google.com/all";
constexpr char kAppGuestUrl[] = "https://www.google.com/guest";
constexpr char kAppManagedUrl[] = "https://www.google.com/managed";
constexpr char kAppUnmanagedUrl[] = "https://www.google.com/unmanaged";
constexpr char kAppChildUrl[] = "https://www.google.com/child";
#endif

}  // namespace

class PreinstalledWebAppManagerTest : public testing::Test {
 public:
  PreinstalledWebAppManagerTest() = default;
  PreinstalledWebAppManagerTest(const PreinstalledWebAppManagerTest&) = delete;
  PreinstalledWebAppManagerTest& operator=(
      const PreinstalledWebAppManagerTest&) = delete;
  ~PreinstalledWebAppManagerTest() override = default;

  // testing::Test:
  void SetUp() override {
    testing::Test::SetUp();
#if BUILDFLAG(IS_CHROMEOS)
    user_manager_enabler_ = std::make_unique<user_manager::ScopedUserManager>(
        std::make_unique<ash::FakeChromeUserManager>());
    // Mocking the StatisticsProvider for testing.
    ash::system::StatisticsProvider::SetTestProvider(&statistics_provider);
    statistics_provider.SetMachineStatistic(ash::system::kActivateDateKey,
                                            "2023-18");
#endif
  }

  void TearDown() override {
    // Set `provider_` to nullptr before `profile_` is reset to avoid a dangling
    // pointer.
    provider_ = nullptr;
    profile_.reset();
#if BUILDFLAG(IS_CHROMEOS)
    ash::system::StatisticsProvider::SetTestProvider(nullptr);
    user_manager_enabler_.reset();
#endif
    testing::Test::TearDown();
  }

 protected:
  void set_profile(std::unique_ptr<Profile> profile) {
    profile_ = std::move(profile);
  }

  // Use the primary OTR profile of `profile_` when loading apps.
  void UseOtrProfile() {
    DCHECK(profile_);
    Profile* otr_profile =
        profile_->GetPrimaryOTRProfile(/*create_if_needed=*/true);
    provider_ = FakeWebAppProvider::Get(otr_profile);
    test::AwaitStartWebAppProviderAndSubsystems(otr_profile);
  }

  std::vector<ExternalInstallOptions> LoadApps(
      std::string_view test_dir,
      bool disable_default_apps = false) {
    DCHECK(profile_);

    // Set the `FakeWebAppProvider` if it hasn't been set yet.
    if (!provider_) {
      provider_ = FakeWebAppProvider::Get(profile_.get());
      test::AwaitStartWebAppProviderAndSubsystems(profile_.get());
    }

    base::FilePath config_dir = GetConfigDir(test_dir);
    test::ConfigDirAutoReset config_reset =
        test::SetPreinstalledWebAppConfigDirForTesting(config_dir);

    if (!disable_default_apps) {
      base::CommandLine::ForCurrentProcess()->RemoveSwitch(
          switches::kDisableDefaultApps);
    }

    std::vector<ExternalInstallOptions> result;
    base::RunLoop run_loop;
    provider_->preinstalled_web_app_manager().LoadForTesting(
        base::BindLambdaForTesting(
            [&](std::vector<ExternalInstallOptions> install_options_list) {
              result = std::move(install_options_list);
              run_loop.Quit();
            }));
    run_loop.Run();

    return result;
  }

  // Helper that creates simple test profile.
  std::unique_ptr<TestingProfile> CreateProfile(bool is_guest = false) {
    TestingProfile::Builder profile_builder;
    if (is_guest) {
      profile_builder.SetGuestSession();
    }

    return profile_builder.Build();
  }

#if BUILDFLAG(IS_CHROMEOS)
  // Helper that creates simple test guest profile.
  std::unique_ptr<TestingProfile> CreateGuestProfile() {
    return CreateProfile(/*is_guest=*/true);
  }

  // Helper that creates simple test profile and logs it into user manager.
  // This makes profile appears as a primary profile in ChromeOS.
  std::unique_ptr<TestingProfile> CreateProfileAndLogin() {
    std::unique_ptr<TestingProfile> profile = CreateProfile();
    const AccountId account_id(AccountId::FromUserEmailGaiaId(
        profile->GetProfileUserName(), GaiaId("1234567890")));
    user_manager()->AddUser(account_id);
    user_manager()->LoginUser(account_id);
    return profile;
  }

  // Helper that creates simple test guest profile and logs it into user
  // manager. This makes profile appears as a primary profile in ChromeOS.
  std::unique_ptr<TestingProfile> CreateGuestProfileAndLogin() {
    std::unique_ptr<TestingProfile> profile = CreateGuestProfile();
    user_manager()->AddGuestUser();
    user_manager()->LoginUser(user_manager::GuestAccountId());
    return profile;
  }

  void SetExtraWebAppsDir(std::string_view test_dir,
                          std::string_view extra_web_apps_dir) {
    command_line_.GetProcessCommandLine()->AppendSwitchASCII(
        ash::switches::kExtraWebAppsDir, extra_web_apps_dir);
  }

  void VerifySetOfApps(const std::set<GURL>& expectations) {
    const auto install_options_list = LoadApps(kUserTypesTestDir);
    ASSERT_EQ(expectations.size(), install_options_list.size());
    for (const auto& install_options : install_options_list)
      ASSERT_EQ(1u, expectations.count(install_options.install_url));
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  void ExpectHistograms(int enabled, int disabled, int errors) {
    histograms_.ExpectUniqueSample(
        PreinstalledWebAppManager::kHistogramEnabledCount, enabled, 1);
    histograms_.ExpectUniqueSample(
        PreinstalledWebAppManager::kHistogramDisabledCount, disabled, 1);
    histograms_.ExpectUniqueSample(
        PreinstalledWebAppManager::kHistogramConfigErrorCount, errors, 1);
  }

  base::HistogramTester histograms_;

  ScopedTestingPreinstalledAppData preinstalled_web_app_override_;

 private:
  base::FilePath GetConfigDir(std::string_view test_dir) {
    // Uses the chrome/test/data/web_app_default_apps/test_dir directory
    // that holds the *.json data files from which tests should parse as app
    // configs.
    base::FilePath config_dir;
    if (!base::PathService::Get(chrome::DIR_TEST_DATA, &config_dir)) {
      ADD_FAILURE()
          << "base::PathService::Get could not resolve chrome::DIR_TEST_DATA";
    }
    return config_dir.AppendASCII("web_app_default_apps").AppendASCII(test_dir);
  }

#if BUILDFLAG(IS_CHROMEOS)
  ash::FakeChromeUserManager* user_manager() {
    return static_cast<ash::FakeChromeUserManager*>(
        user_manager::UserManager::Get());
  }

  // To support primary/non-primary users.
  std::unique_ptr<user_manager::ScopedUserManager> user_manager_enabler_;
  ash::system::FakeStatisticsProvider statistics_provider;

  base::test::ScopedCommandLine command_line_;
#endif
 protected:
  raw_ptr<FakeWebAppProvider> provider_ = nullptr;
  std::unique_ptr<Profile> profile_;

 private:
  // To support context of browser threads.
  content::BrowserTaskEnvironment task_environment_;
};

TEST_F(PreinstalledWebAppManagerTest, ReplacementExtensionBlockedByPolicy) {
  auto test_profile = CreateProfile();
  set_profile(std::move(test_profile));

  provider_ = FakeWebAppProvider::Get(profile_.get());
  test::AwaitStartWebAppProviderAndSubsystems(profile_.get());

  auto& extensions_manager =
      static_cast<FakeExtensionsManager&>(provider_->extensions_manager());

  GURL install_url("https://test.app");
  constexpr char kExtensionId[] = "abcdefghijklmnopabcdefghijklmnop";
  ExternalInstallOptions options(install_url, mojom::UserDisplayMode::kBrowser,
                                 ExternalInstallSource::kExternalDefault);
  options.user_type_allowlist = {"unmanaged"};
  options.uninstall_and_replace = {kExtensionId};
  options.only_use_app_info_factory = true;
  options.app_info_factory = base::BindRepeating(
      WebAppInstallInfo::CreateWithStartUrlForTesting, install_url);
  preinstalled_web_app_override_.apps.push_back(std::move(options));

  auto expect_present = [&]() {
    std::vector<ExternalInstallOptions> options_list =
        LoadApps(/*test_dir=*/"");
    ASSERT_EQ(options_list.size(), 1u);
    EXPECT_EQ(options_list[0].install_url, install_url);
  };

  auto expect_not_present = [&]() {
    std::vector<ExternalInstallOptions> options_list =
        LoadApps(/*test_dir=*/"");
    ASSERT_EQ(options_list.size(), 0u);
  };

  // By default, not blocked.
  expect_present();

  // Block it.
  extensions_manager.SetExtensionBlockedByPolicy(kExtensionId, true);
  expect_not_present();

  // Unblock it.
  extensions_manager.SetExtensionBlockedByPolicy(kExtensionId, false);
  expect_present();

  // Force installing the replaced extension also blocks the replacement.
  extensions_manager.SetExtensionForceInstalled(kExtensionId, true);
  expect_not_present();

  extensions_manager.SetExtensionForceInstalled(kExtensionId, false);
  expect_present();
}

// Only Chrome OS parses config files.
#if BUILDFLAG(IS_CHROMEOS)
TEST_F(PreinstalledWebAppManagerTest, GoodJson) {
  set_profile(CreateProfileAndLogin());
  const auto install_options_list = LoadApps(kGoodJsonTestDir);

  // The good_json directory contains two good JSON files:
  // chrome_platform_status.json and google_io_2016.json.
  // google_io_2016.json is missing a "create_shortcuts" field, so the default
  // value of false should be used.
  std::vector<ExternalInstallOptions> test_install_options_list;
  {
    ExternalInstallOptions install_options(
        GURL("https://www.chromestatus.com/features"),
        mojom::UserDisplayMode::kBrowser,
        ExternalInstallSource::kExternalDefault);
    install_options.user_type_allowlist = {"unmanaged"};
    install_options.add_to_applications_menu = true;
    install_options.add_to_search = true;
    install_options.add_to_management = true;
    install_options.add_to_desktop = true;
    install_options.add_to_quick_launch_bar = false;
    install_options.require_manifest = true;
    install_options.disable_if_touchscreen_with_stylus_not_supported = false;
    test_install_options_list.push_back(std::move(install_options));
  }
  {
    ExternalInstallOptions install_options(
        GURL("https://events.google.com/io2016/?utm_source=web_app_manifest"),
        mojom::UserDisplayMode::kStandalone,
        ExternalInstallSource::kExternalDefault);
    install_options.user_type_allowlist = {"unmanaged"};
    install_options.add_to_applications_menu = true;
    install_options.add_to_search = true;
    install_options.add_to_management = true;
    install_options.add_to_desktop = false;
    install_options.add_to_quick_launch_bar = false;
    install_options.require_manifest = true;
    install_options.disable_if_touchscreen_with_stylus_not_supported = false;
    install_options.uninstall_and_replace.push_back("migrationsourceappid");
    test_install_options_list.push_back(std::move(install_options));
  }

  EXPECT_EQ(test_install_options_list.size(), install_options_list.size());
  for (const auto& install_option : test_install_options_list) {
    EXPECT_TRUE(std::ranges::contains(install_options_list, install_option));
  }
  ExpectHistograms(/*enabled=*/2, /*disabled=*/0, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, BadJson) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("bad_json");

  // The bad_json directory contains one (malformed) JSON file.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, TxtButNoJson) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("txt_but_no_json");

  // The txt_but_no_json directory contains one file, and the contents of that
  // file is valid JSON, but that file's name does not end with ".json".
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, MixedJson) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("mixed_json");

  // The mixed_json directory contains one empty JSON file, one malformed JSON
  // file and one good JSON file. ScanDirForExternalWebAppsForTesting should
  // still pick up that one good JSON file: polytimer.json.
  EXPECT_EQ(1u, app_infos.size());
  if (app_infos.size() == 1) {
    EXPECT_EQ(app_infos[0].install_url.spec(),
              std::string("https://polytimer.rocks/?homescreen=1"));
  }
  ExpectHistograms(/*enabled=*/1, /*disabled=*/0, /*errors=*/2);
}

TEST_F(PreinstalledWebAppManagerTest, MissingAppUrl) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("missing_app_url");

  // The missing_app_url directory contains one JSON file which is correct
  // except for a missing "app_url" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, EmptyAppUrl) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("empty_app_url");

  // The empty_app_url directory contains one JSON file which is correct
  // except for an empty "app_url" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, InvalidAppUrl) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("invalid_app_url");

  // The invalid_app_url directory contains one JSON file which is correct
  // except for an invalid "app_url" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, TrueHideFromUser) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("true_hide_from_user");

  EXPECT_EQ(1u, app_infos.size());
  const auto& app = app_infos[0];
  EXPECT_FALSE(app.add_to_applications_menu);
  EXPECT_FALSE(app.add_to_search);
  EXPECT_FALSE(app.add_to_management);
  ExpectHistograms(/*enabled=*/1, /*disabled=*/0, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, InvalidHideFromUser) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("invalid_hide_from_user");

  // The invalid_hide_from_user directory contains on JSON file which is correct
  // except for an invalid "hide_from_user" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, InvalidCreateShortcuts) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("invalid_create_shortcuts");

  // The invalid_create_shortcuts directory contains one JSON file which is
  // correct except for an invalid "create_shortcuts" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, MissingLaunchContainer) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("missing_launch_container");

  // The missing_launch_container directory contains one JSON file which is
  // correct except for a missing "launch_container" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, InvalidLaunchContainer) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("invalid_launch_container");

  // The invalid_launch_container directory contains one JSON file which is
  // correct except for an invalid "launch_container" field.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/1);
}

TEST_F(PreinstalledWebAppManagerTest, InvalidUninstallAndReplace) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("invalid_uninstall_and_replace");

  // The invalid_uninstall_and_replace directory contains 2 JSON files which are
  // correct except for invalid "uninstall_and_replace" fields.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/2);
}

TEST_F(PreinstalledWebAppManagerTest, PreinstalledWebAppInstallDisabled) {
  set_profile(CreateProfileAndLogin());
  base::test::ScopedFeatureList scoped_feature_list;
  scoped_feature_list.InitAndDisableFeature(
      features::kPreinstalledWebAppInstallation);
  const auto app_infos = LoadApps(kGoodJsonTestDir);

  EXPECT_EQ(0u, app_infos.size());
  histograms_.ExpectTotalCount(
      PreinstalledWebAppManager::kHistogramConfigErrorCount, 0);
  histograms_.ExpectTotalCount(
      PreinstalledWebAppManager::kHistogramEnabledCount, 0);
  histograms_.ExpectTotalCount(
      PreinstalledWebAppManager::kHistogramDisabledCount, 0);
}

TEST_F(PreinstalledWebAppManagerTest, EnabledByFinch) {
  set_profile(CreateProfileAndLogin());
  base::AutoReset<bool> testing_scope =
      SetPreinstalledAppInstallFeatureAlwaysEnabledForTesting();

  const auto app_infos = LoadApps("enabled_by_finch");

  // The enabled_by_finch directory contains two JSON file containing apps
  // that have field trials. As the matching feature is enabled, they should be
  // in our list of apps to install.
  EXPECT_EQ(2u, app_infos.size());
  ExpectHistograms(/*enabled=*/2, /*disabled=*/0, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, NotEnabledByFinch) {
  set_profile(CreateProfileAndLogin());
  const auto app_infos = LoadApps("enabled_by_finch");

  // The enabled_by_finch directory contains two JSON file containing apps
  // that have field trials. As the matching feature isn't enabled, they should
  // not be in our list of apps to install.
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/2, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, GuestUser) {
  // App service is available for OTR profile in Guest mode.
  set_profile(CreateGuestProfileAndLogin());
  UseOtrProfile();
  VerifySetOfApps({GURL(kAppAllUrl), GURL(kAppGuestUrl)});
}

TEST_F(PreinstalledWebAppManagerTest, UnmanagedUser) {
  set_profile(CreateProfileAndLogin());
  VerifySetOfApps({GURL(kAppAllUrl), GURL(kAppUnmanagedUrl)});
}

TEST_F(PreinstalledWebAppManagerTest, ManagedUser) {
  auto profile = CreateProfileAndLogin();
  profile->GetProfilePolicyConnector()->OverrideIsManagedForTesting(true);
  set_profile(std::move(profile));
  VerifySetOfApps({GURL(kAppAllUrl), GURL(kAppManagedUrl)});
}

TEST_F(PreinstalledWebAppManagerTest, ManagedGuestUser) {
  profiles::testing::ScopedTestManagedGuestSession test_managed_guest_session;
  auto profile = CreateProfileAndLogin();
  profile->GetProfilePolicyConnector()->OverrideIsManagedForTesting(true);
  set_profile(std::move(profile));
  VerifySetOfApps({});
}

TEST_F(PreinstalledWebAppManagerTest, ChildUser) {
  auto profile = CreateProfileAndLogin();
  profile->SetIsSupervisedProfile();
  EXPECT_TRUE(profile->IsChild());
  set_profile(std::move(profile));
  VerifySetOfApps({GURL(kAppAllUrl), GURL(kAppChildUrl)});
}

TEST_F(PreinstalledWebAppManagerTest, NonPrimaryProfile) {
  set_profile(CreateProfile());
  VerifySetOfApps({GURL(kAppAllUrl), GURL(kAppUnmanagedUrl)});
}

TEST_F(PreinstalledWebAppManagerTest, ExtraWebApps) {
  set_profile(CreateProfileAndLogin());
  // The extra_web_apps directory contains two JSON files in different named
  // subdirectories. The --extra-web-apps-dir switch should control which
  // directory apps are loaded from.
  SetExtraWebAppsDir("extra_web_apps", "model1");

  const auto app_infos = LoadApps("extra_web_apps");
  EXPECT_EQ(1u, app_infos.size());
  ExpectHistograms(/*enabled=*/1, /*disabled=*/0, /*errors=*/0);
}

TEST_F(PreinstalledWebAppManagerTest, ExtraWebAppsNoMatchingDirectory) {
  set_profile(CreateProfileAndLogin());
  SetExtraWebAppsDir("extra_web_apps", "model3");

  const auto app_infos = LoadApps("extra_web_apps");
  EXPECT_EQ(0u, app_infos.size());
  ExpectHistograms(/*enabled=*/0, /*disabled=*/0, /*errors=*/0);
}
#else
// No app is expected for non-ChromeOS builds.
TEST_F(PreinstalledWebAppManagerTest, NoApp) {
  set_profile(CreateProfile());
  EXPECT_TRUE(LoadApps(kUserTypesTestDir).empty());
}
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
class DisabledPreinstalledWebAppManagerTest
    : public PreinstalledWebAppManagerTest {
 public:
  DisabledPreinstalledWebAppManagerTest() {
    base::CommandLine::ForCurrentProcess()->AppendSwitch(
        switches::kDisableDefaultApps);
  }
};

TEST_F(DisabledPreinstalledWebAppManagerTest, LoadConfigsWhileDisabled) {
  set_profile(CreateProfileAndLogin());
  EXPECT_EQ(LoadApps(kGoodJsonTestDir,
                     /*disable_default_apps=*/true)
                .size(),
            0u);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

// This test does not 'start' the web app provider in the setup, so each test
// can override the exact preinstall config they want, then start the provider.
class PreinstalledWebAppManagerBasicTest : public WebAppTest {
 public:
  static constexpr std::string_view kInstallUrl =
      "https://www.example.com/install_url.html";
  static constexpr std::string_view kManifestUrl =
      "https://www.example.com/manifest_url.json";
  static constexpr std::string_view kManifestId = "https://www.example.com/id";
  static constexpr std::string_view kStartUrl =
      "https://www.example.com/index.html";
  static constexpr std::string_view kScope = "https://www.example.com/";
  static constexpr std::u16string_view kAppName = u"Example App";

  static ExternalInstallOptions GetInstallOptionsWithFactory(
      webapps::ManifestId manifest_id = webapps::ManifestId(GURL(kManifestId)),
      GURL install_url = GURL(kInstallUrl),
      GURL start_url = GURL(kStartUrl),
      GURL manifest_url = GURL(kManifestUrl),
      GURL scope = GURL(kScope)) {
    ExternalInstallOptions options(
        /*install_url=*/install_url,
        /*user_display_mode=*/
        mojom::UserDisplayMode::kBrowser,
        /*install_source=*/ExternalInstallSource::kExternalDefault);

    options.user_type_allowlist = {"unmanaged", "managed", "child"};
    options.expected_app_id = GenerateAppIdFromManifestId(manifest_id);
    options.app_info_factory = base::BindRepeating(
        [](webapps::ManifestId manifest_id, GURL start_url, GURL scope,
           GURL install_url) {
          auto info =
              std::make_unique<WebAppInstallInfo>(manifest_id, start_url);
          info->title = kAppName;
          info->scope = scope;
          info->display_mode = DisplayMode::kStandalone;
          info->install_url = install_url;
          info->icon_bitmaps.any = {
              {144, ::gfx::test::CreateBitmap(
                        FakeWebContentsManager::kBasicInstallIconSize,
                        SK_ColorGREEN)}};
          return info;
        },
        manifest_id, start_url, scope, install_url);

    return options;
  }

  PreinstalledWebAppManagerBasicTest()
      : app_id_(GenerateAppIdFromManifestId(
            webapps::ManifestId(GURL(kManifestId)))) {}
  ~PreinstalledWebAppManagerBasicTest() override = default;

  void SetUp() override {
    WebAppTest::SetUp();

#if BUILDFLAG(IS_CHROMEOS)
    ash::system::StatisticsProvider::SetTestProvider(&statistics_provider_);
    statistics_provider_.SetMachineStatistic(ash::system::kActivateDateKey,
                                             "2023-18");
#endif

    preinstalled_app_override_ =
        std::make_unique<ScopedTestingPreinstalledAppData>();
    fake_provider().SetSynchronizePreinstalledAppsOnStartup(true);
    fake_provider()
        .preinstalled_web_app_manager()
        .SetPreinstalledAppForUpdatingForTesting(PreinstalledAppForUpdating{
            webapps::ManifestId(GURL(kManifestId)),
            GURL(kInstallUrl)});
    auto fake_extensions_manager = std::make_unique<FakeExtensionsManager>();
    fake_extensions_manager->SetExtensionsSystemReady(true);
    fake_provider().SetExtensionsManager(std::move(fake_extensions_manager));

    SetupPageState();
  }

  void SetupPageState(
      webapps::ManifestId manifest_id = webapps::ManifestId(GURL(kManifestId)),
      GURL install_url = GURL(kInstallUrl),
      GURL start_url = GURL(kStartUrl),
      GURL manifest_url = GURL(kManifestUrl)) {
    // Make sure the 'manifest' preinstall state matches the app factory
    // preinstall state
    fake_web_contents_manager().CreateBasicInstallPageState(
        install_url, manifest_url, start_url);

    // Make the manifest state match GetInstallOptionsWithFactory().
    auto& page_state =
        fake_web_contents_manager().GetOrCreatePageState(install_url);
    page_state.manifest_before_default_processing->id = manifest_id.value();
    page_state.manifest_before_default_processing->name = kAppName;

    auto& icon_state = fake_web_contents_manager().GetOrCreateIconState(
        GURL(FakeWebContentsManager::kBasicInstallIconUrl));
    icon_state.bitmaps = {::gfx::test::CreateBitmap(144, SK_ColorGREEN)};
  }

  void TearDown() override {
#if BUILDFLAG(IS_CHROMEOS)
    ash::system::StatisticsProvider::SetTestProvider(nullptr);
#endif
    WebAppTest::TearDown();
  }

 protected:
  std::unique_ptr<ScopedTestingPreinstalledAppData> preinstalled_app_override_;

// This might be good to move to the WebAppTest base class.
#if BUILDFLAG(IS_CHROMEOS)
  ash::system::FakeStatisticsProvider statistics_provider_;
#endif

  const webapps::AppId app_id_;
  base::AutoReset<bool> bypass_awaiting_dependencies_{
      PreinstalledWebAppManager::BypassAwaitingDependenciesForTesting()};
};

TEST_F(PreinstalledWebAppManagerBasicTest, PreinstallWorks) {
  preinstalled_app_override_->apps = {GetInstallOptionsWithFactory()};
  test::AwaitStartWebAppProviderAndSubsystems(profile());

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      GenerateAppIdFromManifestId(webapps::ManifestId(GURL(kManifestId))),
      WebAppFilter::InstalledInChrome()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      GenerateAppIdFromManifestId(webapps::ManifestId(GURL(kManifestId))),
      WebAppFilter::OpensInBrowserTab()));

  // State matches.
  EXPECT_EQ(provider().registrar_unsafe().GetAppShortName(app_id_),
            base::UTF16ToUTF8(kAppName));
  WebAppIconManager::WebAppBitmaps bitmaps;
  base::test::TestFuture<WebAppIconManager::WebAppBitmaps> icons;
  provider().icon_manager().ReadAllIcons(app_id_, icons.GetCallback());
  ASSERT_TRUE(icons.Wait());
  ASSERT_TRUE(icons.Get().trusted_icons.any.contains(144));
  EXPECT_THAT(
      icons.Get().trusted_icons.any.at(144),
      gfx::test::EqualsBitmap(gfx::test::CreateBitmap(144, SK_ColorGREEN)));
}

class PreinstalledWebAppManagerChatUpdate
    : public PreinstalledWebAppManagerBasicTest {
 public:
  void SetUp() override {
    PreinstalledWebAppManagerBasicTest::SetUp();
    fake_provider()
        .preinstalled_web_app_manager()
        .SetPreinstalledAppForUpdatingForTesting(
            PreinstalledAppForUpdating{GetChatManifestId(),
                                       GetChatInstallUrl()});
  }

  GURL GetChatInstallUrl() const {
    return GURL(webapps::kMailGoogleChatInstallUrl);
  }

  GURL GetChatInstallUrlFetchedForUpdate() const {
    GURL::Replacements update_url_query_adder;
    update_url_query_adder.SetQueryStr("usp=chrome_preinstall_update");
    return GetChatInstallUrl().ReplaceComponents(update_url_query_adder);
  }

  webapps::ManifestId GetChatManifestId() const {
    return webapps::ManifestId(GURL(webapps::kMailGoogleChatManifestId));
  }

  GURL GetChatStartUrl() const {
    return GURL(webapps::kMailGoogleChatManifestId);
  }

  GURL GetChatManifestUrl() const {
    return GURL(
        base::StrCat({webapps::kMailGoogleChatManifestId, "manifest.json"}));
  }

  webapps::AppId GetChatAppId() const {
    return GenerateAppIdFromManifestId(
        webapps::ManifestId(GURL(webapps::kMailGoogleChatManifestId)));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_{
      features::kWebAppPeriodicPreinstallUpdate};
};

TEST_F(PreinstalledWebAppManagerChatUpdate, PRE_UpdateOccursForChat) {
  // The PRE test should install the chat app where the configuration should
  // match the one that is attempted to be updated by the
  // `WebAppProvider::DoDelayedPostStartupWork`.
  preinstalled_app_override_->apps = {GetInstallOptionsWithFactory(
      GetChatManifestId(), GetChatInstallUrl(), GetChatStartUrl(),
      GetChatManifestUrl(), GetChatStartUrl().GetWithoutFilename())};

  // This should install the chat app with the configuration of SetupPageState
  test::AwaitStartWebAppProviderAndSubsystems(profile());

  // Expect no scope extensions.
  ASSERT_TRUE(provider().registrar_unsafe().AppMatches(
      GetChatAppId(), WebAppFilter::InstalledInChrome()));
  EXPECT_THAT(provider()
                  .registrar_unsafe()
                  .GetAppById(GetChatAppId())
                  ->validated_scope_extensions(),
              testing::IsEmpty());
}

TEST_F(PreinstalledWebAppManagerChatUpdate, UpdateOccursForChat) {
  const url::Origin kOtherOrigin =
      url::Origin::Create(GURL("https://www.example.com"));
  // This shouldn't result in any app changes, it's the same configuration.
  preinstalled_app_override_->apps = {GetInstallOptionsWithFactory(
      GetChatManifestId(), GetChatInstallUrl(), GetChatStartUrl(),
      GetChatManifestUrl(), GetChatStartUrl().GetWithoutFilename())};

  // This should NOT install the chat app with scope extensions, instead the
  // state should stay the same.
  base::OnceClosure post_startup_tasks =
      provider().DisableDelayedPostStartupWorkForTesting();
  // Fake out the association fetcher, so we don't have to handle those
  // requests.
  auto fake_association_manager =
      std::make_unique<FakeWebAppOriginAssociationManager>(*profile());
  fake_association_manager->set_pass_through(true);
  fake_provider().SetOriginAssociationManager(
      std::move(fake_association_manager));
  test::AwaitStartWebAppProviderAndSubsystems(profile());

  // Expect no scope extensions.
  ASSERT_TRUE(provider().registrar_unsafe().AppMatches(
      GetChatAppId(), WebAppFilter::InstalledInChrome()));
  EXPECT_THAT(provider()
                  .registrar_unsafe()
                  .GetAppById(GetChatAppId())
                  ->validated_scope_extensions(),
              testing::IsEmpty());

  // Set up the manifest state to have scope extensions, and trigger the
  // post-startup task to update.
  SetupPageState(GetChatManifestId(), GetChatInstallUrlFetchedForUpdate(),
                 GetChatStartUrl(), GetChatManifestUrl());
  auto& page_state = fake_web_contents_manager().GetOrCreatePageState(
      GetChatInstallUrlFetchedForUpdate());
  page_state.manifest_before_default_processing->scope_extensions.push_back(
      blink::mojom::ManifestScopeExtension::New(kOtherOrigin,
                                                /*has_origin_wildcard=*/false));
  std::move(post_startup_tasks).Run();

  provider().command_manager().AwaitAllCommandsCompleteForTesting();

  EXPECT_THAT(provider()
                  .registrar_unsafe()
                  .GetAppById(GetChatAppId())
                  ->validated_scope_extensions(),
              testing::ElementsAre(ScopeExtensionInfo::CreateForOrigin(
                  kOtherOrigin, /*has_origin_wildcard=*/false)));
}

TEST_F(PreinstalledWebAppManagerChatUpdate, UpdateIsThrottled) {
  const url::Origin kOtherOrigin =
      url::Origin::Create(GURL("https://www.example.com"));
  const std::u16string kNewAppName1 = u"New App Name 1";
  const std::u16string kNewAppName2 = u"New App Name 2";

  base::SimpleTestClock clock;
  clock.SetNow(base::Time::Now());
  provider().SetClockForTesting(&clock);
  auto clock_reset = WebAppPrefGuardrails::SetClockForTesting(&clock);

  // Start up, and ensure the app is installed by default.
  preinstalled_app_override_->apps = {GetInstallOptionsWithFactory(
      GetChatManifestId(), GetChatInstallUrl(), GetChatStartUrl(),
      GetChatManifestUrl(), GetChatStartUrl().GetWithoutFilename())};
  base::RepeatingClosure post_startup_tasks =
      provider().DisableDelayedPostStartupWorkForTesting();
  test::AwaitStartWebAppProviderAndSubsystems(profile());
  ASSERT_TRUE(provider().registrar_unsafe().AppMatches(
      GetChatAppId(), WebAppFilter::InstalledInChrome()));

  // Set up the manifest state to have a new name for first update.
  SetupPageState(GetChatManifestId(), GetChatInstallUrlFetchedForUpdate(),
                 GetChatStartUrl(), GetChatManifestUrl());
  auto& page_state = fake_web_contents_manager().GetOrCreatePageState(
      GetChatInstallUrlFetchedForUpdate());
  page_state.manifest_before_default_processing->name = kNewAppName1;
  // Trigger the first update via startup task.
  post_startup_tasks.Run();
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_EQ(base::UTF16ToUTF8(kNewAppName1),
            provider().registrar_unsafe().GetAppShortName(GetChatAppId()));

  // Modify the manifest again to have a different name, and trigger update
  // again, verify it doesn't work.
  page_state.manifest_before_default_processing->name = kNewAppName2;
  post_startup_tasks.Run();
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_EQ(base::UTF16ToUTF8(kNewAppName1),
            provider().registrar_unsafe().GetAppShortName(GetChatAppId()));

  // Advance the clock more than 7 days, trigger update again, and it should
  // work.
  clock.Advance(base::Days(8));
  post_startup_tasks.Run();
  provider().command_manager().AwaitAllCommandsCompleteForTesting();
  EXPECT_EQ(base::UTF16ToUTF8(kNewAppName2),
            provider().registrar_unsafe().GetAppShortName(GetChatAppId()));

  // Reset the clock.
  provider().SetClockForTesting(base::DefaultClock::GetInstance());
}

}  // namespace web_app
