// Copyright 2013 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/ui/ash/shelf/chrome_shelf_prefs.h"

#include <algorithm>
#include <map>
#include <memory>
#include <vector>

#include "ash/constants/ash_pref_names.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/webui/mall/app_id.h"
#include "base/containers/to_vector.h"
#include "base/no_destructor.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "build/branding_buildflags.h"
#include "build/buildflag.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service.h"
#include "chrome/browser/ash/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ash/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ash/login/users/fake_chrome_user_manager.h"
#include "chrome/browser/prefs/browser_prefs.h"
#include "chrome/browser/ui/ash/shelf/shelf_controller_helper.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/test/base/testing_profile.h"
#include "chromeos/ash/components/file_manager/app_id.h"
#include "chromeos/ash/experiences/arc/app/arc_app_constants.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/app_constants/constants.h"
#include "components/services/app_service/public/cpp/app.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/package_id.h"
#include "components/sync/model/string_ordinal.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/test_helper.h"
#include "components/user_manager/user_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/common/constants.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

using SyncItem = app_list::AppListSyncableService::SyncItem;

std::unique_ptr<SyncItem> MakeSyncItem(
    const std::string& id,
    const syncer::StringOrdinal& pin_ordinal) {
  auto item = std::make_unique<SyncItem>(
      id, sync_pb::AppListSpecifics::TYPE_APP, /*is_new=*/false);
  item->item_pin_ordinal = pin_ordinal;
  return item;
}

class ShelfControllerHelperFake : public ShelfControllerHelper {
 public:
  explicit ShelfControllerHelperFake(Profile* profile)
      : ShelfControllerHelper(profile) {}
  ~ShelfControllerHelperFake() override = default;
  ShelfControllerHelperFake(const ShelfControllerHelperFake&) = delete;
  ShelfControllerHelperFake& operator=(const ShelfControllerHelperFake&) =
      delete;

  bool IsValidIDForCurrentUser(const std::string& app_id) const override {
    // ash-chrome is never a valid app ids as it is never exposed to the app
    // service.
    return app_id != app_constants::kChromeAppId;
  }
};

// A fake for AppListSyncableService that allows easy modifications.
class AppListSyncableServiceFake : public app_list::AppListSyncableService {
 public:
  explicit AppListSyncableServiceFake(Profile* profile)
      : app_list::AppListSyncableService(profile) {}
  ~AppListSyncableServiceFake() override = default;
  AppListSyncableServiceFake(const AppListSyncableServiceFake&) = delete;
  AppListSyncableServiceFake& operator=(const AppListSyncableServiceFake&) =
      delete;

  syncer::StringOrdinal GetPinPosition(const std::string& app_id) override {
    const SyncItem* item = GetSyncItem(app_id);
    if (!item) {
      return syncer::StringOrdinal();
    }
    return item->item_pin_ordinal;
  }

  // Adds a new pin if it does not already exist.
  void SetPinPosition(const std::string& app_id,
                      const syncer::StringOrdinal& item_pin_ordinal) override {
    auto it = item_map_.find(app_id);
    if (it == item_map_.end()) {
      item_map_[app_id] = MakeSyncItem(app_id, item_pin_ordinal);
      return;
    }
    it->second->item_pin_ordinal = item_pin_ordinal;
  }
  const SyncItemMap& sync_items() const override { return item_map_; }

  const SyncItem* GetSyncItem(const std::string& id) const override {
    auto it = item_map_.find(id);
    if (it == item_map_.end()) {
      return nullptr;
    }
    return it->second.get();
  }

  bool IsInitialized() const override { return true; }

  SyncItemMap item_map_;
};

}  // namespace

// Unit tests for ChromeShelfPrefs
class ChromeShelfPrefsTest : public testing::Test {
 public:
  ChromeShelfPrefsTest() = default;
  ~ChromeShelfPrefsTest() override = default;
  ChromeShelfPrefsTest(const ChromeShelfPrefsTest&) = delete;
  ChromeShelfPrefsTest& operator=(const ChromeShelfPrefsTest&) = delete;

  void SetUp() override {
    auto prefs =
        std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
    auto* registry = prefs->registry();
    RegisterUserProfilePrefs(registry);
    prefs->SetBoolean(ash::prefs::kFilesAppUIPrefsMigrated, true);
    prefs->SetBoolean(ash::prefs::kProjectorSWAUIPrefsMigrated, true);
    profile_ =
        TestingProfile::Builder()
            .SetProfileName("Test")
            .SetPrefService(std::move(prefs))
            .AddTestingFactory(
                app_list::AppListSyncableServiceFactory::GetInstance(),
                base::BindRepeating([](content::BrowserContext* browser_context)
                                        -> std::unique_ptr<KeyedService> {
                  return std::make_unique<AppListSyncableServiceFake>(
                      Profile::FromBrowserContext(browser_context));
                }))
            .Build();
    ChromeShelfPrefs::SetShouldAddDefaultAppsForTest(true);
    shelf_prefs_ = std::make_unique<ChromeShelfPrefs>(profile_.get());
    scoped_user_manager_ = std::make_unique<user_manager::ScopedUserManager>(
        std::make_unique<ash::FakeChromeUserManager>());
    helper_ = std::make_unique<ShelfControllerHelperFake>(profile_.get());
  }

  void TearDown() override {
    shelf_prefs_.reset();
    scoped_user_manager_.reset();
    ChromeShelfPrefs::SetShouldAddDefaultAppsForTest(false);
    profile_.reset();
  }

  void AddRegularUser(const std::string& email) {
    AccountId account_id = AccountId::FromUserEmail(email);
    auto* fake_user_manager = static_cast<ash::FakeChromeUserManager*>(
        user_manager::UserManager::Get());
    fake_user_manager->AddUser(account_id);
    fake_user_manager->UserLoggedIn(
        account_id, user_manager::TestHelper::GetFakeUsernameHash(account_id));
  }

  void InstallApp(apps::AppPtr app) {
    app->readiness = apps::Readiness::kReady;

    std::vector<apps::AppPtr> deltas;
    deltas.push_back(std::move(app));
    apps::AppServiceProxyFactory::GetForProfile(profile_.get())
        ->OnApps(std::move(deltas), apps::AppType::kUnknown,
                 /*should_notify_initialized=*/false);
  }

  void InstallApp(const apps::PackageId& package_id) {
    auto app_type = apps::AppType::kChromeApp;
    auto app_id = package_id.identifier();
    if (package_id.package_type() == apps::PackageType::kWeb) {
      app_type = apps::AppType::kWeb;
      app_id =
          web_app::GenerateAppId(std::nullopt, GURL(package_id.identifier()));
    }
    apps::AppPtr app = std::make_unique<apps::App>(app_type, app_id);
    app->name = package_id.identifier();
    app->installer_package_id = package_id;

    InstallApp(std::move(app));
  }

  void InstallMallApp() {
    apps::AppPtr app = std::make_unique<apps::App>(apps::AppType::kSystemWeb,
                                                   ash::kMallSystemAppId);
    app->install_reason = apps::InstallReason::kSystem;
    InstallApp(std::move(app));
  }

  void InstallNotebookLmApp() {
    apps::AppPtr app =
        std::make_unique<apps::App>(apps::AppType::kWeb, ash::kNotebookLmAppId);
    app->install_reason = apps::InstallReason::kDefault;
    InstallApp(std::move(app));
  }

  AppListSyncableServiceFake& syncable_service() {
    return *static_cast<AppListSyncableServiceFake*>(
        app_list::AppListSyncableServiceFactory::GetForProfile(profile_.get()));
  }

  std::vector<std::string> GetPinnedAppIds() const {
    return base::ToVector(shelf_prefs_->GetPinnedAppsFromSync(helper_.get()),
                          &ash::ShelfID::app_id);
  }

  std::string GetPinned() {
    static const base::NoDestructor<std::map<std::string, std::string>> kAppMap(
        {
            {app_constants::kChromeAppId, "chrome"},
            {ash::kGeminiAppId, "gemini"},
            {ash::kNotebookLmAppId, "notebook_lm"},
            {ash::kMallSystemAppId, "mall"},
            {ash::kGmailAppId, "gmail"},
            {ash::kGoogleCalendarAppId, "cal"},
            {file_manager::kFileManagerSwaAppId, "files"},
            {ash::kMessagesAppId, "messages"},
            {ash::kGoogleMeetAppId, "meet"},
            {arc::kPlayStoreAppId, "play"},
            {ash::kYoutubeAppId, "youtube"},
            {arc::kGooglePhotosAppId, "photos"},
        });
    std::vector<std::string> apps;
    for (const auto& app_id : GetPinnedAppIds()) {
      auto it = kAppMap->find(app_id);
      apps.push_back(it != kAppMap->end() ? it->second : app_id);
    }
    return base::JoinString(apps, ", ");
  }

  bool IsGoogleChromeBranded() const {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
    return true;
#else
    return false;
#endif
  }

  const char* MaybeGeminiItem() {
    return IsGoogleChromeBranded() ? "gemini, " : "";
  }

  const char* MaybeNotebookLmItem() {
    return IsGoogleChromeBranded() ? "notebook_lm, " : "";
  }

  void ResetShelfToOrder(std::vector<std::string> pin_order) {
    // Reset the shelf to empty so that we can reset the order without
    // interference from existing ordinals.
    for (const auto& app_id : GetPinnedAppIds()) {
      shelf_prefs_->RemovePinPosition(ash::ShelfID(app_id));
    }

    syncer::StringOrdinal position =
        syncer::StringOrdinal::CreateInitialOrdinal();

    for (const auto& pin_id : pin_order) {
      syncable_service().SetPinPosition(pin_id, position);
      position = position.CreateAfter();
    }

    ASSERT_THAT(GetPinnedAppIds(), testing::ContainerEq(pin_order));
  }

 protected:
  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<user_manager::ScopedUserManager> scoped_user_manager_;
  std::unique_ptr<ChromeShelfPrefs> shelf_prefs_;
  std::unique_ptr<ShelfControllerHelperFake> helper_;

  std::unique_ptr<Profile> profile_;
};

TEST_F(ChromeShelfPrefsTest, AddChromePinNoExistingOrdinal) {
  shelf_prefs_->EnsureChromePinned();

  // Check that chrome now has a valid ordinal.
  EXPECT_TRUE(syncable_service()
                  .item_map_[app_constants::kChromeAppId]
                  ->item_pin_ordinal.IsValid());
}

TEST_F(ChromeShelfPrefsTest, AddChromePinExistingOrdinal) {
  // Set up the initial ordinals.
  syncer::StringOrdinal initial_ordinal =
      syncer::StringOrdinal::CreateInitialOrdinal();
  syncable_service().item_map_[app_constants::kChromeAppId] =
      MakeSyncItem(app_constants::kChromeAppId, initial_ordinal);

  shelf_prefs_->EnsureChromePinned();

  // Check that the chrome ordinal did not change.
  ASSERT_TRUE(syncable_service()
                  .item_map_[app_constants::kChromeAppId]
                  ->item_pin_ordinal.IsValid());
  auto& pin_ordinal = syncable_service()
                          .item_map_[app_constants::kChromeAppId]
                          ->item_pin_ordinal;
  EXPECT_TRUE(pin_ordinal.Equals(initial_ordinal));
}

TEST_F(ChromeShelfPrefsTest, AddDefaultApps) {
  shelf_prefs_->EnsureChromePinned();
  shelf_prefs_->AddDefaultApps();

  ASSERT_TRUE(syncable_service()
                  .item_map_[app_constants::kChromeAppId]
                  ->item_pin_ordinal.IsValid());

  // Check that a pin was added for the gmail app.
  ASSERT_TRUE(syncable_service()
                  .item_map_[ash::kGmailAppId]
                  ->item_pin_ordinal.IsValid());
}

// If the profile changes, then migrations should be run again.
TEST_F(ChromeShelfPrefsTest, ProfileChanged) {
  // Migration is necessary to begin with.
  ASSERT_TRUE(shelf_prefs_->ShouldPerformConsistencyMigrations());
  std::vector<std::string> pinned_apps_strs = GetPinnedAppIds();

  // Pinned apps should have the chrome app as the first item.
  ASSERT_GE(pinned_apps_strs.size(), 1u);
  EXPECT_EQ(pinned_apps_strs[0], app_constants::kChromeAppId);

  // Pinned apps should have the gmail app.
  EXPECT_TRUE(std::ranges::contains(pinned_apps_strs, ash::kGmailAppId));

  // Migration is no longer necessary.
  ASSERT_FALSE(shelf_prefs_->ShouldPerformConsistencyMigrations());

  // Change the profile. Migration is necessary again!
  shelf_prefs_->AttachProfile(nullptr);
  ASSERT_TRUE(shelf_prefs_->ShouldPerformConsistencyMigrations());
}

// NotebookLM is only pinned in branded versions of Chrome.
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
TEST_F(ChromeShelfPrefsTest, PinNotebookLmAppWhenInstalled) {
  InstallNotebookLmApp();

  std::vector<std::string> expected_order = {
      ash::kGeminiAppId,     app_constants::kChromeAppId,
      ash::kNotebookLmAppId, ash::kMallSystemAppId,
      ash::kGmailAppId,
  };
  EXPECT_THAT(GetPinnedAppIds(), testing::IsSupersetOf(expected_order));
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmAppOnceOnly) {
  EXPECT_THAT(GetPinnedAppIds(), testing::Contains(ash::kNotebookLmAppId));

  shelf_prefs_->RemovePinPosition(ash::ShelfID(ash::kNotebookLmAppId));

  // The NotebookLM app must not reappear in the pinned apps list.
  EXPECT_THAT(GetPinnedAppIds(),
              testing::Not(testing::Contains(ash::kNotebookLmAppId)));
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_ChromeOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGmailAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "chrome, notebook_lm, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_ChromeGeminiOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
      ash::kGmailAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "chrome, gemini, notebook_lm, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_GeminiChromeOther) {
  ResetShelfToOrder({
      ash::kGeminiAppId,
      app_constants::kChromeAppId,
      ash::kGmailAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "gemini, chrome, notebook_lm, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_Chrome) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "chrome, notebook_lm");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_ChromeGemini) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "chrome, gemini, notebook_lm");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_ChromeOtherGemini) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGmailAppId,
      ash::kGeminiAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "chrome, notebook_lm, gmail, gemini");
}

TEST_F(ChromeShelfPrefsTest, PinNotebookLmMigration_OtherChromeGemini) {
  ResetShelfToOrder({
      ash::kGmailAppId,
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
  });

  InstallNotebookLmApp();

  EXPECT_EQ(GetPinned(), "gmail, chrome, gemini, notebook_lm");
}
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)

TEST_F(ChromeShelfPrefsTest, PinMallSystemAppWhenInstalled) {
  InstallMallApp();

  std::vector<std::string> expected_order = {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
      ash::kGeminiAppId,
#endif
      app_constants::kChromeAppId,
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
      ash::kNotebookLmAppId,
#endif
      ash::kMallSystemAppId,       ash::kGmailAppId,
  };
  EXPECT_THAT(GetPinnedAppIds(), testing::IsSupersetOf(expected_order));
}

TEST_F(ChromeShelfPrefsTest, PinMallSystemAppOnceOnly) {
  InstallMallApp();

  EXPECT_THAT(GetPinnedAppIds(), testing::Contains(ash::kMallSystemAppId));

  shelf_prefs_->RemovePinPosition(ash::ShelfID(ash::kMallSystemAppId));

  // The Mall app must not reappear in the pinned apps list.
  EXPECT_THAT(GetPinnedAppIds(),
              testing::Not(testing::Contains(ash::kMallSystemAppId)));
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeGeminiOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, gemini, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeNotebookLmOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kNotebookLmAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, notebook_lm, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeGeminiNotebookLmOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
      ash::kNotebookLmAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, gemini, notebook_lm, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeNotebookLmGeminiOther) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kNotebookLmAppId,
      ash::kGeminiAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, notebook_lm, gemini, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_GeminiChromeOther) {
  ResetShelfToOrder({
      ash::kGeminiAppId,
      app_constants::kChromeAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "gemini, chrome, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_GeminiNotebookLmChromeOther) {
  ResetShelfToOrder({
      ash::kGeminiAppId,
      ash::kNotebookLmAppId,
      app_constants::kChromeAppId,
      ash::kGmailAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "gemini, notebook_lm, chrome, mall, gmail");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_Chrome) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, mall");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeGemini) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, gemini, mall");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_ChromeOtherGemini) {
  ResetShelfToOrder({
      app_constants::kChromeAppId,
      ash::kGmailAppId,
      ash::kGeminiAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "chrome, mall, gmail, gemini");
}

TEST_F(ChromeShelfPrefsTest, PinMallMigration_OtherChromeGemini) {
  ResetShelfToOrder({
      ash::kGmailAppId,
      app_constants::kChromeAppId,
      ash::kGeminiAppId,
  });

  InstallMallApp();

  EXPECT_EQ(GetPinned(), "gmail, chrome, gemini, mall");
}

TEST_F(ChromeShelfPrefsTest, PinPreloadApps) {
  apps::PackageId chrome = *apps::PackageId::FromString(
      "chromeapp:" + std::string(app_constants::kChromeAppId));
  apps::PackageId gmail = *apps::PackageId::FromString(
      "web:https://mail.google.com/mail/?usp=installed_webapp");
  apps::PackageId youtube =
      *apps::PackageId::FromString("web:https://www.youtube.com/?feature=ytca");
  apps::PackageId app1 = *apps::PackageId::FromString("chromeapp:app1");
  apps::PackageId app2 = *apps::PackageId::FromString("chromeapp:app2");
  apps::PackageId app3 = *apps::PackageId::FromString("chromeapp:app3");
  // App4 is not listed in ShelfConfig and should be added to the end.
  apps::PackageId app4 = *apps::PackageId::FromString("chromeapp:app4");
  // App5 should not get pinned.
  apps::PackageId app5 = *apps::PackageId::FromString("chromeapp:app5");

  std::vector<apps::PackageId> apps_to_pin({app1, app2, app3, app4});
  // Intentionally switch order of gmail and youtube.
  std::vector<apps::PackageId> pin_order(
      {app4, chrome, app1, app2, youtube, gmail, app3});

  // Register chrome app and some other default pins as installed
  InstallApp(chrome);
  InstallApp(gmail);
  InstallApp(youtube);

  EXPECT_EQ(
      GetPinned(),
      base::StrCat(
          {"chrome, ", MaybeGeminiItem(), MaybeNotebookLmItem(),
           "mall, gmail, cal, files, messages, meet, play, youtube, photos"}));

  // Simulate installation finishing in unpredictable order.
  // Install app2, comes after chrome since app1 is not installed yet.
  InstallApp(app2);

  // Install app4 which will go before chrome.
  InstallApp(app4);

  // Installed apps (app2 and app4) should pin immediately.
  shelf_prefs_->OnGetPinPreloadApps(apps_to_pin, pin_order);
  EXPECT_EQ(
      GetPinned(),
      base::StrCat(
          {"app4, chrome, app2, ", MaybeGeminiItem(), MaybeNotebookLmItem(),
           "mall, gmail, cal, files, messages, meet, play, youtube, photos"}));

  // Install app3, comes after gmail.
  InstallApp(app3);
  EXPECT_EQ(GetPinned(),
            base::StrCat({"app4, chrome, app2, ", MaybeGeminiItem(),
                          MaybeNotebookLmItem(),
                          "mall, gmail, app3, cal, files, messages, meet, "
                          "play, youtube, photos"}));

  // Install app5, which should not get pinned since it is not in first list.
  InstallApp(app5);
  EXPECT_EQ(GetPinned(),
            base::StrCat({"app4, chrome, app2, ", MaybeGeminiItem(),
                          MaybeNotebookLmItem(),
                          "mall, gmail, app3, cal, files, messages, meet, "
                          "play, youtube, photos"}));

  // Install app1, comes after chrome.
  InstallApp(app1);
  EXPECT_EQ(GetPinned(),
            base::StrCat({"app4, chrome, app1, app2, ", MaybeGeminiItem(),
                          MaybeNotebookLmItem(),
                          "mall, gmail, app3, cal, files, messages, meet, "
                          "play, youtube, photos"}));
}

TEST_F(ChromeShelfPrefsTest, PinPreloadRepeats) {
  apps::PackageId chrome = *apps::PackageId::FromString(
      "chromeapp:" + std::string(app_constants::kChromeAppId));
  apps::PackageId app1 = *apps::PackageId::FromString("chromeapp:app1");
  apps::PackageId app2 = *apps::PackageId::FromString("chromeapp:app2");
  apps::PackageId app3 = *apps::PackageId::FromString("chromeapp:app3");
  InstallApp(chrome);

  std::vector<apps::PackageId> pin_order({app1, app2, app3, chrome});
  std::string default_apps = base::StrCat(
      {"chrome, ", MaybeGeminiItem(), MaybeNotebookLmItem(),
       "mall, gmail, cal, files, messages, meet, play, youtube, photos"});

  // Request to pin app1, and app2, but only install app1.
  shelf_prefs_->OnGetPinPreloadApps({app1, app2}, pin_order);
  InstallApp(app1);
  EXPECT_EQ(GetPinned(), "app1, " + default_apps);

  // Pin should continue if it is called again before it is complete.
  shelf_prefs_->OnGetPinPreloadApps({app2}, pin_order);
  InstallApp(app2);
  EXPECT_EQ(GetPinned(), "app1, app2, " + default_apps);

  // Pin should only run once per user once it completes, app3 should not pin.
  shelf_prefs_->OnGetPinPreloadApps({app3}, pin_order);
  InstallApp(app3);
  EXPECT_EQ(GetPinned(), "app1, app2, " + default_apps);
}

TEST_F(ChromeShelfPrefsTest, PinPreloadEmpty) {
  apps::PackageId chrome = *apps::PackageId::FromString(
      "chromeapp:" + std::string(app_constants::kChromeAppId));
  apps::PackageId app1 = *apps::PackageId::FromString("chromeapp:app1");
  InstallApp(chrome);
  EXPECT_EQ(
      GetPinned(),
      base::StrCat(
          {"chrome, ", MaybeGeminiItem(), MaybeNotebookLmItem(),
           "mall, gmail, cal, files, messages, meet, play, youtube, photos"}));
  auto get_prefs = [&]() {
    return profile_->GetPrefs()
        ->GetList(ash::prefs::kShelfDefaultPinLayoutRolls)
        .DebugString();
  };

  std::vector<apps::PackageId> pin_order({app1, chrome});

  // Pin should be considered complete if it is requested to pin no apps.
  EXPECT_FALSE(shelf_prefs_->DidAddPreloadApps());
  EXPECT_EQ(get_prefs(), "[ \"default\" ]\n");
  shelf_prefs_->OnGetPinPreloadApps({}, pin_order);
  EXPECT_TRUE(shelf_prefs_->DidAddPreloadApps());
  EXPECT_EQ(get_prefs(), "[ \"default\", \"preload\" ]\n");
  shelf_prefs_->OnGetPinPreloadApps({app1}, pin_order);
  InstallApp(app1);
  EXPECT_EQ(
      GetPinned(),
      base::StrCat(
          {"chrome, ", MaybeGeminiItem(), MaybeNotebookLmItem(),
           "mall, gmail, cal, files, messages, meet, play, youtube, photos"}));

  // Further calls to OnGetPinPreloadApps() should not write additional values
  // of 'preload' to prefs (crbug.com/350769496).
  shelf_prefs_->OnGetPinPreloadApps({}, pin_order);
  EXPECT_TRUE(shelf_prefs_->DidAddPreloadApps());
  EXPECT_EQ(get_prefs(), "[ \"default\", \"preload\" ]\n");
}

// Cleanup duplicate values of 'preload' in prefs (crbug.com/350769496).
TEST_F(ChromeShelfPrefsTest, CleanupPreloadPrefs) {
  PrefService* prefs = profile_->GetPrefs();
  std::vector<std::string> pref_names = {
      ash::prefs::kShelfDefaultPinLayoutRolls,
      ash::prefs::kShelfDefaultPinLayoutRollsForTabletFormFactor};

  const struct {
    std::vector<std::string> pref_list;
    std::string expected;
  } tests[] = {
      {{}, R"([  ])"},
      {{"default"}, R"([ "default" ])"},
      {{"default", "preload"}, R"([ "default", "preload" ])"},
      {{"default", "preload", "preload"}, R"([ "default", "preload" ])"},
      {{"preload", "default", "preload"}, R"([ "default", "preload" ])"},
      {{"preload"}, R"([ "preload" ])"},
      {{"preload", "preload"}, R"([ "preload" ])"},
  };

  for (const auto& test : tests) {
    for (const auto& pref_name : pref_names) {
      base::ListValue list;
      for (const auto& item : test.pref_list) {
        list.Append(item);
      }
      prefs->SetList(pref_name, std::move(list));
      ChromeShelfPrefs::CleanupPreloadPrefs(prefs);
      EXPECT_EQ(test.expected + "\n", prefs->GetList(pref_name).DebugString());
    }
  }
}
