chromium/chrome/browser/ui/ash/desks/desks_client_browsertest.cc

// Copyright 2021 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/desks/desks_client.h"

#include <cstdint>
#include <cstdlib>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/desk_template.h"
#include "ash/public/cpp/saved_desk_delegate.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/system/toast_manager.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/pill_button.h"
#include "ash/test/ash_test_util.h"
#include "ash/webui/system_apps/public/system_web_app_type.h"
#include "ash/wm/desks/desk.h"
#include "ash/wm/desks/desk_action_context_menu.h"
#include "ash/wm/desks/desks_controller.h"
#include "ash/wm/desks/desks_test_api.h"
#include "ash/wm/desks/desks_test_util.h"
#include "ash/wm/desks/overview_desk_bar_view.h"
#include "ash/wm/desks/templates/admin_template_launch_tracker.h"
#include "ash/wm/desks/templates/saved_desk_controller.h"
#include "ash/wm/desks/templates/saved_desk_metrics_util.h"
#include "ash/wm/desks/templates/saved_desk_presenter.h"
#include "ash/wm/desks/templates/saved_desk_save_desk_button.h"
#include "ash/wm/desks/templates/saved_desk_test_util.h"
#include "ash/wm/desks/templates/saved_desk_util.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_test_util.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_constants.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/containers/contains.h"
#include "base/json/json_writer.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_base.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/spin_wait.h"
#include "base/uuid.h"
#include "base/value_iterators.h"
#include "base/values.h"
#include "build/build_config.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/apps/app_service/launch_result_type.h"
#include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
#include "chrome/browser/ash/app_restore/app_restore_arc_test_helper.h"
#include "chrome/browser/ash/app_restore/app_restore_test_util.h"
#include "chrome/browser/ash/crosapi/browser_manager.h"
#include "chrome/browser/ash/crosapi/browser_util.h"
#include "chrome/browser/ash/login/login_manager_test.h"
#include "chrome/browser/ash/login/test/login_manager_mixin.h"
#include "chrome/browser/ash/login/ui/user_adding_screen.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/ash/system_web_apps/apps/os_url_handler_system_web_app_info.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/ash/desks/chrome_desks_util.h"
#include "chrome/browser/ui/ash/desks/desks_templates_app_launch_handler.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/test/base/chromeos/ash_browser_test_starter.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "chromeos/ui/frame/multitask_menu/multitask_menu_nudge_controller.h"
#include "components/account_id/account_id.h"
#include "components/app_constants/constants.h"
#include "components/app_restore/app_launch_info.h"
#include "components/app_restore/full_restore_save_handler.h"
#include "components/app_restore/full_restore_utils.h"
#include "components/app_restore/restore_data.h"
#include "components/app_restore/window_properties.h"
#include "components/desks_storage/core/admin_template_service.h"
#include "components/desks_storage/core/desk_model.h"
#include "components/desks_storage/core/saved_desk_builder.h"
#include "components/keep_alive_registry/keep_alive_types.h"
#include "components/keep_alive_registry/scoped_keep_alive.h"
#include "components/policy/policy_constants.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_info.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "extensions/common/constants.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/display/test/display_manager_test_api.h"
#include "ui/events/test/event_generator.h"
#include "ui/gfx/range/range.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "url/gurl.h"

using ::testing::_;
using ::testing::ElementsAre;
using ::testing::FloatNear;
using ::testing::Optional;

namespace {

constexpr int32_t kSettingsWindowId = 100;
constexpr int32_t kHelpWindowId = 200;
constexpr int32_t kLaunchedWindowIdBase = -1;
constexpr int32_t kTestWindowId = 300;

constexpr char kExampleUrl1[] = "https://examples1.com";
constexpr char kExampleUrl2[] = "https://examples2.com";
constexpr char kExampleUrl3[] = "https://examples3.com";
constexpr char kAboutBlankUrl[] = "about:blank";
constexpr char kNewTabPageUrl[] = "chrome://newtab/";
constexpr char kYoutubeUrl[] = "https://www.youtube.com/";
constexpr char kTestAdminTemplateUuid[] =
    "1f4ec992-0fa9-415d-a136-4b7c292c39dc";
constexpr char kTestAdminTemplateFormat[] =
    "[{\"version\":1,\"uuid\":\"%s\",\"name\": \"test admin template\","
    "\"created_time_usec\": \"1633535632\",\"updated_time_usec\": "
    "\"1633535632\",\"desk\":{}}]";
constexpr char kTestTabGroupNameFormat[] = "test_tab_group_%u";
constexpr char kTestAppName[] = "test_app_name";
constexpr char kUnknownTestAppName[] = "unknown_test_app_name";
constexpr char kUnknownTestAppId[] = "07eb07d7-f338-48aa-a996-7beb76a5042c";

void WaitForDeskModel() {
  while (
      !(DesksClient::Get() && DesksClient::Get()->GetDeskModel()->IsReady())) {
    base::RunLoop run_loop;
    run_loop.RunUntilIdle();
  }
}

Browser* FindBrowser(int32_t window_id) {
  for (Browser* browser : *BrowserList::GetInstance()) {
    aura::Window* window = browser->window()->GetNativeWindow();
    if (window->GetProperty(app_restore::kRestoreWindowIdKey) == window_id) {
      return browser;
    }
  }
  return nullptr;
}

aura::Window* FindBrowserWindow(int32_t window_id) {
  Browser* browser = FindBrowser(window_id);
  return browser ? browser->window()->GetNativeWindow() : nullptr;
}

std::vector<GURL> GetURLsForBrowserWindow(Browser* browser) {
  TabStripModel* tab_strip_model = browser->tab_strip_model();
  std::vector<GURL> urls;
  for (int i = 0; i < tab_strip_model->count(); ++i) {
    urls.push_back(tab_strip_model->GetWebContentsAt(i)->GetVisibleURL());
  }
  return urls;
}

// Locate all browsers launched from templates whose URLs match `urls`.
std::vector<Browser*> FindLaunchedBrowsersByURLs(
    const std::vector<GURL>& urls) {
  std::vector<Browser*> browsers;
  for (Browser* browser : *BrowserList::GetInstance()) {
    aura::Window* window = browser->window()->GetNativeWindow();
    if (window->GetProperty(app_restore::kRestoreWindowIdKey) <
            kLaunchedWindowIdBase &&
        GetURLsForBrowserWindow(browser) == urls) {
      browsers.push_back(browser);
    }
  }
  return browsers;
}

// Locate a browser launched from a template whose URLs match `urls`.
Browser* FindLaunchedBrowserByURLs(const std::vector<GURL>& urls) {
  auto browsers = FindLaunchedBrowsersByURLs(urls);
  return browsers.empty() ? nullptr : browsers.front();
}

// TODO(crbug.com/1286515): Remove this. Tests should navigate to overview and
// click the button using an event generator.
std::unique_ptr<ash::DeskTemplate> CaptureActiveDeskAndSaveTemplate(
    ash::DeskTemplateType desk_template_type) {
  base::RunLoop run_loop;
  std::unique_ptr<ash::DeskTemplate> desk_template;
  DesksClient::Get()->CaptureActiveDeskAndSaveTemplate(
      base::BindLambdaForTesting(
          [&](std::optional<DesksClient::DeskActionError> error,
              std::unique_ptr<ash::DeskTemplate> captured_desk_template) {
            run_loop.Quit();
            ASSERT_TRUE(captured_desk_template);
            desk_template = std::move(captured_desk_template);
          }),
      desk_template_type);
  run_loop.Run();
  return desk_template;
}

std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>>
GetDeskTemplates() {
  base::RunLoop run_loop;
  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates;

  DesksClient::Get()->GetDeskTemplates(base::BindLambdaForTesting(
      [&](std::optional<DesksClient::DeskActionError> error,
          const std::vector<raw_ptr<const ash::DeskTemplate,
                                    VectorExperimental>>& desk_templates) {
        templates = desk_templates;
        run_loop.Quit();
      }));
  run_loop.Run();

  return templates;
}

// Search `desk_templates` for a template with `uuid` and returns true if found,
// false if not.
bool ContainUuidInTemplates(
    const base::Uuid& uuid,
    const std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>>&
        desk_templates) {
  DCHECK(uuid.is_valid());
  return base::Contains(desk_templates, uuid, &ash::DeskTemplate::uuid);
}

std::string GetTemplateJson(const base::Uuid& uuid, Profile* profile) {
  base::RunLoop run_loop;
  std::string template_json_result;
  DesksClient::Get()->GetTemplateJson(
      uuid, profile,
      base::BindLambdaForTesting(
          [&](std::optional<DesksClient::DeskActionError> error,
              const base::Value& template_json) {
            base::JSONWriter::Write(template_json, &template_json_result);
            run_loop.Quit();
            ASSERT_FALSE(error);
          }));
  run_loop.Run();
  return template_json_result;
}

void DeleteDeskTemplate(const base::Uuid& uuid) {
  base::RunLoop run_loop;
  DesksClient::Get()->DeleteDeskTemplate(
      uuid, base::BindLambdaForTesting(
                [&](std::optional<DesksClient::DeskActionError> error) {
                  run_loop.Quit();
                }));
  run_loop.Run();
}

webapps::AppId CreateSystemWebApp(Profile* profile,
                                  apps::AppLaunchParams params) {
  const webapps::AppId app_id = params.app_id;
  base::RunLoop launch_wait;
  apps::AppServiceProxyFactory::GetForProfile(profile)->LaunchAppWithParams(
      std::move(params),
      base::BindLambdaForTesting(
          [&](apps::LaunchResult&& result) { launch_wait.Quit(); }));
  launch_wait.Run();
  return app_id;
}

// Creates the app launch params for `app_type` for testing.
apps::AppLaunchParams GetAppLaunchParams(Profile* profile,
                                         ash::SystemWebAppType app_type) {
  webapps::AppId app_id = *ash::GetAppIdForSystemWebApp(profile, app_type);
  return apps::AppLaunchParams(
      app_id, apps::LaunchContainer::kLaunchContainerWindow,
      WindowOpenDisposition::NEW_WINDOW, apps::LaunchSource::kFromTest);
}

webapps::AppId CreateFilesSystemWebApp(Profile* profile) {
  apps::AppLaunchParams params =
      GetAppLaunchParams(profile, ash::SystemWebAppType::FILE_MANAGER);
  return CreateSystemWebApp(profile, std::move(params));
}

webapps::AppId CreateSettingsSystemWebApp(Profile* profile) {
  apps::AppLaunchParams params =
      GetAppLaunchParams(profile, ash::SystemWebAppType::SETTINGS);
  params.restore_id = kSettingsWindowId;
  return CreateSystemWebApp(profile, std::move(params));
}

webapps::AppId CreateHelpSystemWebApp(Profile* profile) {
  apps::AppLaunchParams params =
      GetAppLaunchParams(profile, ash::SystemWebAppType::HELP);
  params.restore_id = kHelpWindowId;
  return CreateSystemWebApp(profile, std::move(params));
}

webapps::AppId CreateOsUrlHandlerSystemWebApp(Profile* profile,
                                              const GURL& override_url) {
  apps::AppLaunchParams params =
      GetAppLaunchParams(profile, ash::SystemWebAppType::OS_URL_HANDLER);
  params.restore_id = kTestWindowId;
  params.override_url = override_url;
  return CreateSystemWebApp(profile, std::move(params));
}

void SendKey(ui::KeyboardCode key_code) {
  ui::test::EventGenerator generator(ash::Shell::GetPrimaryRootWindow());
  ash::SendKey(key_code, &generator);
}

// TODO(sammiequon): Use ash::test::ClickOnView().
void ClickView(const views::View* view) {
  DCHECK(view);
  DCHECK(view->GetVisible());
  aura::Window* root_window =
      view->GetWidget()->GetNativeWindow()->GetRootWindow();
  ui::test::EventGenerator event_generator(root_window);
  event_generator.MoveMouseToInHost(view->GetBoundsInScreen().CenterPoint());
  event_generator.ClickLeftButton();
}

// If `wait_for_ui` is true, wait for the callback from the model to update the
// UI.
void ClickSaveDeskAsTemplateButton(bool wait_for_ui) {
  if (ash::features::IsSavedDeskUiRevampEnabled()) {
    // Get the menu option to save the desk as a template and click it.
    views::MenuItemView* menu_item =
        ash::DesksTestApi::OpenDeskContextMenuAndGetMenuItem(
            ash::Shell::GetPrimaryRootWindow(),
            ash::DeskBarViewBase::Type::kOverview, /*index=*/0u,
            ash::DeskActionContextMenu::CommandId::kSaveAsTemplate);
    ASSERT_TRUE(menu_item);
    ClickView(menu_item);
  } else {
    const views::Button* save_desk_as_template_button =
        ash::GetSaveDeskAsTemplateButton();
    DCHECK(save_desk_as_template_button);
    ClickView(save_desk_as_template_button);
  }

  if (wait_for_ui) {
    ash::WaitForSavedDeskUI();
  }

  // Clicking the save template button selects the newly created template's name
  // field. We can press enter or escape or click to select out of it.
  SendKey(ui::VKEY_RETURN);
}

void ClickSaveDeskAsTemplateButton() {
  ClickSaveDeskAsTemplateButton(/*wait_for_ui=*/true);
}

void ClickSaveDeskForLaterButton() {
  const views::Button* save_desk_for_later_button =
      ash::GetSaveDeskForLaterButton();
  DCHECK(save_desk_for_later_button);
  ClickView(save_desk_for_later_button);
}

void ClickLibraryButton() {
  const views::Button* zero_state_templates_button = ash::GetLibraryButton();
  ASSERT_TRUE(zero_state_templates_button);
  ClickView(zero_state_templates_button);
}

void ClickFirstTemplateItem() {
  const views::Button* template_item = ash::GetSavedDeskItemButton(/*index=*/0);
  DCHECK(template_item);
  ClickView(template_item);
}

const std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>>
GetAllEntries() {
  std::vector<const ash::DeskTemplate*> templates;
  auto error = DesksClient::Get()->GetDeskModel()->GetAllEntries();
  DCHECK_EQ(desks_storage::DeskModel::GetAllEntriesStatus::kOk, error.status);
  return error.entries;
}

// Creates a vector of tab groups based on the vector of GURLs passed into it.
// A tab group will be created for each individual tab.
std::vector<tab_groups::TabGroupInfo> MakeExpectedTabGroupsBasedOnTabVector(
    const std::vector<GURL>& urls) {
  std::vector<tab_groups::TabGroupInfo> tab_groups;

  for (uint32_t index = 0; index < urls.size(); ++index) {
    tab_groups.emplace_back(
        gfx::Range(index, index + 1),
        tab_groups::TabGroupVisualData(
            base::UTF8ToUTF16(
                base::StringPrintf(kTestTabGroupNameFormat, index)),
            tab_groups::TabGroupColorId(
                static_cast<tab_groups::TabGroupColorId>(index % 8))));
  }

  return tab_groups;
}

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

  MOCK_METHOD(void,
              LaunchSystemWebAppOrChromeApp,
              (apps::AppType,
               const std::string&,
               const app_restore::RestoreData::LaunchList&),
              (override));
};

class BrowsersAddedObserver : public BrowserListObserver {
 public:
  explicit BrowsersAddedObserver(int num_browser_expected)
      : num_browser_adds_left_(num_browser_expected) {
    BrowserList::AddObserver(this);
  }
  BrowsersAddedObserver(const BrowsersAddedObserver&) = delete;
  BrowsersAddedObserver& operator=(const BrowsersAddedObserver&) = delete;
  ~BrowsersAddedObserver() override { BrowserList::RemoveObserver(this); }

  void Wait() { run_loop_.Run(); }

  // BrowserListObserver:
  void OnBrowserAdded(Browser* browser) override {
    --num_browser_adds_left_;
    if (num_browser_adds_left_ == 0) {
      run_loop_.Quit();
    }
  }

  void OnBrowserRemoved(Browser* browser) override {}

 private:
  int num_browser_adds_left_;
  base::RunLoop run_loop_;
};

class BrowsersRemovedObserver : public BrowserListObserver {
 public:
  explicit BrowsersRemovedObserver(int browser_removes_expected)
      : browser_removes_left_(browser_removes_expected) {
    BrowserList::AddObserver(this);
  }
  BrowsersRemovedObserver(const BrowsersRemovedObserver&) = delete;
  BrowsersRemovedObserver& operator=(const BrowsersRemovedObserver&) = delete;
  ~BrowsersRemovedObserver() override { BrowserList::RemoveObserver(this); }

  void Wait() { run_loop_.Run(); }

  // BrowserListObserver:
  void OnBrowserAdded(Browser* browser) override {}

  void OnBrowserRemoved(Browser* browser) override {
    --browser_removes_left_;
    if (browser_removes_left_ == 0) {
      run_loop_.Quit();
    }
  }

 private:
  int browser_removes_left_;
  base::RunLoop run_loop_;
};

}  // namespace

// Scoped class that temporarily sets a new app launch handler for testing
// purposes.
class ScopedDesksTemplatesAppLaunchHandlerSetter {
 public:
  ScopedDesksTemplatesAppLaunchHandlerSetter(
      std::unique_ptr<DesksTemplatesAppLaunchHandler> launch_handler,
      int launch_id)
      : launch_id_(launch_id) {
    DCHECK(!instance_active_);
    instance_active_ = true;

    DesksClient* desks_client = DesksClient::Get();
    DCHECK(desks_client);
    desks_client->app_launch_handlers_[launch_id_] = std::move(launch_handler);
  }
  ScopedDesksTemplatesAppLaunchHandlerSetter(
      const ScopedDesksTemplatesAppLaunchHandlerSetter&) = delete;
  ScopedDesksTemplatesAppLaunchHandlerSetter& operator=(
      const ScopedDesksTemplatesAppLaunchHandlerSetter&) = delete;
  ~ScopedDesksTemplatesAppLaunchHandlerSetter() {
    DCHECK(instance_active_);
    instance_active_ = false;

    DesksClient* desks_client = DesksClient::Get();
    DCHECK(desks_client);
    desks_client->app_launch_handlers_[launch_id_] = nullptr;
  }

 private:
  // Variable to ensure we never have more than one instance of this object.
  inline static bool instance_active_ = false;

  // The id of the launch used for testing.
  int launch_id_;
};

class DesksClientTest : public extensions::PlatformAppBrowserTest {
 public:
  DesksClientTest() {
    std::vector<base::test::FeatureRef> enabled_features = {
        ash::features::kDesksTemplates};
    std::vector<base::test::FeatureRef> disabled_features = {
        ash::features::kDeskTemplateSync};
    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);

    // Suppress the multitask menu nudge as we'll be checking the stacking order
    // and the count of the active desk children.
    chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(true);

    OsUrlHandlerSystemWebAppDelegate::EnableDelegateForTesting(true);
  }
  DesksClientTest(const DesksClientTest&) = delete;
  DesksClientTest& operator=(const DesksClientTest&) = delete;
  ~DesksClientTest() override {
    OsUrlHandlerSystemWebAppDelegate::EnableDelegateForTesting(false);
  }

  // TODO(crbug.com/1286515): These functions will be removed with the
  // extension. Avoid further uses of this method and create or launch templates
  // by mocking clicks on the system UI.
  void SetTemplate(std::unique_ptr<ash::DeskTemplate> launch_template) {
    DesksClient::Get()->launch_template_for_test_ = std::move(launch_template);
  }

  void LaunchTemplate(const base::Uuid& uuid) {
    base::RunLoop waiter;
    DesksClient::Get()->LaunchDeskTemplate(
        uuid, base::BindLambdaForTesting(
                  [&](std::optional<DesksClient::DeskActionError> error,
                      const base::Uuid& desk_uuid) { waiter.Quit(); }));
    waiter.Run();
  }

  void SetAndLaunchTemplate(std::unique_ptr<ash::DeskTemplate> desk_template) {
    ash::DeskTemplate* desk_template_ptr = desk_template.get();
    SetTemplate(std::move(desk_template));
    LaunchTemplate(desk_template_ptr->uuid());
  }

  Browser* CreateBrowser(
      const std::vector<GURL>& urls,
      std::optional<size_t> active_url_index = std::nullopt) {
    Browser* browser = CreateBrowserImpl(urls, active_url_index);
    browser->window()->Show();
    return browser;
  }

  Browser* CreateBrowserWithPinnedTabs(const std::vector<GURL>& urls,
                                       int first_non_pinned_tab_index) {
    Browser* browser = CreateBrowserImpl(urls, std::nullopt);

    chrome_desks_util::SetBrowserPinnedTabs(first_non_pinned_tab_index,
                                            browser);
    browser->window()->Show();
    return browser;
  }

  Browser* CreateBrowserWithTabGroups(
      const std::vector<GURL>& urls,
      const std::vector<tab_groups::TabGroupInfo>& tab_groups) {
    Browser* browser = CreateBrowserImpl(urls, std::nullopt);

    chrome_desks_util::AttachTabGroupsToBrowserInstance(tab_groups, browser);
    browser->window()->Show();
    return browser;
  }

  Browser* InstallAndLaunchPWA(const GURL& start_url, bool launch_in_browser) {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
    web_app_info->scope = start_url.GetWithoutFilename();
    if (!launch_in_browser) {
      web_app_info->user_display_mode =
          web_app::mojom::UserDisplayMode::kStandalone;
    }
    web_app_info->title = u"A Web App";
    const webapps::AppId app_id =
        web_app::test::InstallWebApp(profile(), std::move(web_app_info));

    return launch_in_browser
               ? web_app::LaunchBrowserForWebAppInTab(profile(), app_id)
               : web_app::LaunchWebAppBrowserAndWait(profile(), app_id);
  }

  // This navigates the browser to a page that will show a close confirmation
  // dialog when closed.
  void SetupBrowserToConfirmClose(Browser* browser) {
    std::string page_that_requires_close_confirmation =
        "<html><head>"
        "<script>window.onbeforeunload = function() { return \"x\"; };</script>"
        "</head><body></body></html>";

    ASSERT_TRUE(ui_test_utils::NavigateToURL(
        browser,
        GURL("data:text/html, " + page_that_requires_close_confirmation)));

    // Note that the `onbeforeunload` handler will not run for a page that
    // hasn't been interacted with. To meet that requirement, we'll click on the
    // page.
    aura::Window* window = browser->window()->GetNativeWindow();
    ui::test::EventGenerator event_generator(window->GetRootWindow());
    event_generator.MoveMouseToInHost(
        window->GetBoundsInScreen().CenterPoint());
    event_generator.ClickLeftButton();
  }

  // extensions::PlatformAppBrowserTest:
  void SetUpOnMainThread() override {
    ::full_restore::SetActiveProfilePath(profile()->GetPath());
    ash::SystemWebAppManager::GetForTest(profile())
        ->InstallSystemAppsForTesting();
    extensions::PlatformAppBrowserTest::SetUpOnMainThread();
  }

 private:
  Browser* CreateBrowserImpl(const std::vector<GURL>& urls,
                             std::optional<size_t> active_url_index) {
    Browser::CreateParams params(Browser::TYPE_NORMAL, profile(),
                                 /*user_gesture=*/false);
    Browser* browser = Browser::Create(params);
    // Create a new tab and make sure the urls have loaded.
    for (size_t i = 0; i < urls.size(); i++) {
      content::TestNavigationObserver navigation_observer(urls[i]);
      navigation_observer.StartWatchingNewWebContents();
      chrome::AddTabAt(
          browser, urls[i], /*index=*/-1,
          /*foreground=*/!active_url_index || active_url_index.value() == i);
      navigation_observer.Wait();
    }
    return browser;
  }

  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that a browser's urls can be captured correctly in the desk template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, CaptureBrowserUrlsTest) {
  // Create a new browser and add a few tabs to it.
  Browser* browser = CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Check the urls are captured correctly in the |desk_template|.
  EXPECT_EQ(data->browser_extra_info.urls, urls);
}

// Tests that a browser's tab groups can be captured correctly in a saved desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, CaptureBrowserTabGroupsTest) {
  std::vector<GURL> tabs = {GURL(kExampleUrl1), GURL(kExampleUrl2)};
  std::vector<tab_groups::TabGroupInfo> expected_tab_groups =
      MakeExpectedTabGroupsBasedOnTabVector(tabs);

  // Create a new browser and add a few tabs to it.
  Browser* browser = CreateBrowserWithTabGroups(tabs, expected_tab_groups);
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *templates.front(), app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Check the urls are captured correctly in the `desk_template`.
  EXPECT_EQ(urls, data->browser_extra_info.urls);

  // We don't care about the order of the tab groups.
  EXPECT_THAT(data->browser_extra_info.tab_group_infos,
              testing::UnorderedElementsAreArray(expected_tab_groups));
}

// Tests that a browser's pinned tabs can be captured correctly in a saved desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, CaptureBrowserWithPinnedTabs) {
  std::vector<GURL> tabs = {GURL(kExampleUrl1), GURL(kExampleUrl2)};
  int expected_number_of_pinned_tabs = 1;

  // Create a new browser and add a few tabs to it.
  Browser* browser =
      CreateBrowserWithPinnedTabs(tabs, expected_number_of_pinned_tabs);
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *templates.front(), app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Check the urls are captured correctly in the `desk_template`.
  EXPECT_EQ(urls, data->browser_extra_info.urls);

  // Check that the number of pinned tabs is correct.
  EXPECT_THAT(data->browser_extra_info.first_non_pinned_tab_index,
              Optional(expected_number_of_pinned_tabs));
}

// Tests that incognito browser windows will NOT be captured in the desk
// template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, CaptureIncognitoBrowserTest) {
  Browser* incognito_browser = CreateIncognitoBrowser();
  chrome::AddTabAt(incognito_browser, GURL(kExampleUrl1), /*index=*/-1,
                   /*foreground=*/true);
  chrome::AddTabAt(incognito_browser, GURL(kExampleUrl2), /*index=*/-1,
                   /*foreground=*/true);
  incognito_browser->window()->Show();
  aura::Window* window = incognito_browser->window()->GetNativeWindow();

  const int32_t incognito_browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  const int32_t browser_window_id =
      browser()->window()->GetNativeWindow()->GetProperty(
          app_restore::kWindowIdKey);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  ASSERT_TRUE(desk_template);

  // We expect to find the non-incognito window in the capture.
  EXPECT_TRUE(ash::QueryRestoreData(*desk_template, app_constants::kChromeAppId,
                                    browser_window_id));
  // The incognito window should not be there.
  EXPECT_FALSE(ash::QueryRestoreData(*desk_template,
                                     app_constants::kChromeAppId,
                                     incognito_browser_window_id));
}

// Tests that browsers and chrome apps can be captured correctly in the desk
// template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, CaptureActiveDeskAsTemplateTest) {
  // Test that Singleton was properly initialized.
  ASSERT_TRUE(DesksClient::Get());

  // Change |browser|'s bounds.
  const gfx::Rect browser_bounds = gfx::Rect(0, 0, 800, 200);
  aura::Window* window = browser()->window()->GetNativeWindow();
  window->SetBounds(browser_bounds);
  // Make window visible on all desks.
  window->SetProperty(aura::client::kWindowWorkspaceKey,
                      aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);
  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);

  // Create the settings app, which is a system web app.
  webapps::AppId settings_app_id =
      CreateSettingsSystemWebApp(browser()->profile());

  // Change the Settings app's bounds too.
  const gfx::Rect settings_app_bounds = gfx::Rect(100, 100, 800, 300);
  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  const int32_t settings_window_id =
      settings_window->GetProperty(app_restore::kWindowIdKey);
  ASSERT_TRUE(settings_window);
  settings_window->SetBounds(settings_app_bounds);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  // Test the default template's name is the current desk's name.
  auto* desks_controller = ash::DesksController::Get();
  EXPECT_EQ(
      desk_template->template_name(),
      desks_controller->GetDeskName(desks_controller->GetActiveDeskIndex()));

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Verify window info are correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(browser_bounds));
  // `visible_on_all_workspaces` should have been reset even though
  // the captured window is visible on all workspaces.
  EXPECT_FALSE(data->window_info.desk_id.has_value());
  auto* screen = display::Screen::GetScreen();
  EXPECT_EQ(screen->GetDisplayNearestWindow(window).id(),
            data->display_id.value());
  EXPECT_EQ(
      window->GetProperty(aura::client::kShowStateKey),
      chromeos::ToWindowShowState(data->window_info.window_state_type.value()));
  // We don't capture the window's desk_id as a template will always
  // create in a new desk.
  EXPECT_FALSE(data->window_info.desk_id.has_value());

  // Find Setting app's app restore data.
  const app_restore::AppRestoreData* data2 = ash::QueryRestoreData(
      *desk_template, settings_app_id, settings_window_id);
  ASSERT_TRUE(data2);

  EXPECT_EQ(static_cast<int>(apps::LaunchContainer::kLaunchContainerWindow),
            data2->container.value());
  EXPECT_EQ(static_cast<int>(WindowOpenDisposition::NEW_WINDOW),
            data2->disposition.value());
  // Verify window info are correctly captured.
  EXPECT_THAT(data2->window_info.current_bounds, Optional(settings_app_bounds));
  EXPECT_FALSE(data2->window_info.desk_id.has_value());
  EXPECT_EQ(screen->GetDisplayNearestWindow(window).id(),
            data->display_id.value());
  EXPECT_EQ(
      window->GetProperty(aura::client::kShowStateKey),
      chromeos::ToWindowShowState(data->window_info.window_state_type.value()));
}

// Tests that launching the same desk template multiple times creates desks with
// different/incremented names.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchMultipleDeskTemplates) {
  const base::Uuid kDeskUuid = base::Uuid::GenerateRandomV4();
  const std::u16string kDeskName(u"Test Desk Name");

  auto* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // TODO(crbug.com/40206393): Note that `SetTemplate` allows setting an empty
  // desk template which shouldn't be possible in a real workflow. Make sure a
  // non empty desks are launched when this test is updated to use the real
  // workflow.
  auto desk_template = std::make_unique<ash::DeskTemplate>(
      kDeskUuid, ash::DeskTemplateSource::kUser, base::UTF16ToUTF8(kDeskName),
      base::Time::Now(), ash::DeskTemplateType::kTemplate);
  SetTemplate(std::move(desk_template));

  auto check_launch_template_desk_name =
      [kDeskUuid, desks_controller, this](const std::u16string& desk_name) {
        LaunchTemplate(kDeskUuid);

        EXPECT_EQ(desk_name, desks_controller->GetDeskName(
                                 desks_controller->GetActiveDeskIndex()));
      };

  // Launching a desk from the template creates a desk with the same name as the
  // template.
  check_launch_template_desk_name(kDeskName);

  // Launch more desks from the template and verify that the newly created desks
  // have unique names.
  check_launch_template_desk_name(std::u16string(kDeskName).append(u" (1)"));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u" (2)"));

  // Remove "Test Desk Name (1)", which means the next created desk from
  // template will have that name. Then it will skip (2) since it already
  // exists, and create the next desk with (3).
  RemoveDesk(desks_controller->GetDeskAtIndex(2));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u" (1)"));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u" (3)"));

  // Same as above, but make sure that deleting the desk with the exact template
  // name still functions the same by only filling in whatever name is
  // available.
  RemoveDesk(desks_controller->GetDeskAtIndex(1));
  check_launch_template_desk_name(kDeskName);
  check_launch_template_desk_name(std::u16string(kDeskName).append(u" (4)"));
}

// Tests that launching a template that contains a system web app works as
// expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithSystemApp) {
  ASSERT_TRUE(DesksClient::Get());

  // Create the settings app, which is a system web app.
  CreateSettingsSystemWebApp(browser()->profile());

  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  ASSERT_TRUE(settings_window);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  // Close the settings window. We'll need to verify if it reopens later.
  views::Widget* settings_widget =
      views::Widget::GetWidgetForNativeWindow(settings_window);
  settings_widget->CloseNow();
  ASSERT_FALSE(FindBrowserWindow(kSettingsWindowId));

  auto* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // Set the template we created as the template we want to launch.
  BrowsersAddedObserver browsers_added(/*num_browser_expected=*/2);
  SetAndLaunchTemplate(std::move(desk_template));
  browsers_added.Wait();

  // Verify that the settings window has been launched on the new desk (desk B).
  EXPECT_EQ(1, desks_controller->GetActiveDeskIndex());
  auto it =
      base::ranges::find_if(*BrowserList::GetInstance(), [](Browser* browser) {
        return ash::IsBrowserForSystemWebApp(browser,
                                             ash::SystemWebAppType::SETTINGS);
      });
  ASSERT_NE(it, BrowserList::GetInstance()->end());
  aura::Window* new_settings_window = (*it)->window()->GetNativeWindow();
  EXPECT_EQ(ash::Shell::GetContainer(new_settings_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            new_settings_window->parent());
}

// Tests that launching a template that contains a system web app will move the
// existing instance of the system web app to the current desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithSystemAppExisting) {
  ASSERT_TRUE(DesksClient::Get());
  Profile* profile = browser()->profile();

  // Create the settings app, which is a system web app.
  CreateSettingsSystemWebApp(profile);

  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  ASSERT_TRUE(settings_window);
  EXPECT_EQ(2u, BrowserList::GetInstance()->size());

  // Give the settings app a known position.
  const gfx::Rect settings_bounds(100, 100, 600, 400);
  settings_window->SetBounds(settings_bounds);
  // Focus the browser so that the settings window is stacked at the bottom.
  browser()->window()->GetNativeWindow()->Focus();
  ASSERT_THAT(settings_window->parent()->children(),
              ElementsAre(settings_window, _));

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Move the settings window to a new place and stack it on top so that we can
  // later verify that it has been placed and stacked correctly.
  settings_window->SetBounds(gfx::Rect(150, 150, 650, 500));
  settings_window->Focus();

  ash::DesksController* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // Set the template we created as the template we want to launch.
  BrowsersAddedObserver browsers_added(/*num_browser_expected=*/1);
  SetAndLaunchTemplate(std::move(desk_template));
  browsers_added.Wait();

  // We launch a new browser window, but not a new settings app. Verify that the
  // window has been moved to the right place and stacked at the bottom.
  EXPECT_EQ(3u, BrowserList::GetInstance()->size());
  EXPECT_TRUE(desks_controller->BelongsToActiveDesk(settings_window));
  EXPECT_EQ(settings_bounds, settings_window->bounds());
  ASSERT_THAT(settings_window->parent()->children(),
              ElementsAre(settings_window, _));
}

// Tests that launching a template that contains a chrome app works as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithChromeApp) {
  DesksClient* desks_client = DesksClient::Get();
  ASSERT_TRUE(desks_client);

  // Create a chrome app.
  const extensions::Extension* extension =
      LoadAndLaunchPlatformApp("launch", "Launched");
  ASSERT_TRUE(extension);

  const std::string extension_id = extension->id();
  ::full_restore::SaveAppLaunchInfo(
      profile()->GetPath(),
      std::make_unique<app_restore::AppLaunchInfo>(
          extension_id, apps::LaunchContainer::kLaunchContainerWindow,
          WindowOpenDisposition::NEW_WINDOW, display::kDefaultDisplayId,
          std::vector<base::FilePath>{}, nullptr));

  extensions::AppWindow* app_window = CreateAppWindow(profile(), extension);
  ASSERT_TRUE(app_window);
  ASSERT_TRUE(GetFirstAppWindowForApp(extension_id));

  // Capture the active desk, which contains the chrome app.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  ASSERT_TRUE(desk_template);

  // Close the chrome app window. We'll need to verify if it reopens later.
  views::Widget* app_widget =
      views::Widget::GetWidgetForNativeWindow(app_window->GetNativeWindow());
  app_widget->CloseNow();
  ASSERT_FALSE(GetFirstAppWindowForApp(extension_id));

  ash::DesksController* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // `BrowserAppLauncher::LaunchAppWithParams()` does not launch the chrome app
  // in tests, so here we set up a mock app launch handler and just verify a
  // `LaunchSystemWebAppOrChromeApp()` call with the associated extension is
  // seen.
  auto mock_app_launch_handler =
      std::make_unique<MockDesksTemplatesAppLaunchHandler>(profile());
  MockDesksTemplatesAppLaunchHandler* mock_app_launch_handler_ptr =
      mock_app_launch_handler.get();
  ScopedDesksTemplatesAppLaunchHandlerSetter scoped_launch_handler(
      std::move(mock_app_launch_handler), /*launch_id=*/1);

  EXPECT_CALL(*mock_app_launch_handler_ptr,
              LaunchSystemWebAppOrChromeApp(_, extension_id, _));

  // Set the template we created as the template we want to launch.
  SetAndLaunchTemplate(std::move(desk_template));
}

// Tests that launching a template that contains a browser window works as
// expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithBrowserWindow) {
  ASSERT_TRUE(DesksClient::Get());

  // Create a new browser and add a few tabs to it, and specify the active tab
  // index.
  const size_t browser_active_index = 1;
  Browser* browser = CreateBrowser(
      {GURL(kExampleUrl1), GURL(kExampleUrl2), GURL(kExampleUrl3)},
      /*active_url_index=*/browser_active_index);

  // Verify that the active tab is correct.
  EXPECT_EQ(static_cast<int>(browser_active_index),
            browser->tab_strip_model()->active_index());

  // Get current tabs from browser.
  const std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  ASSERT_TRUE(desk_template);

  ash::DesksController* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // Set the template we created as the template we want to launch.
  SetAndLaunchTemplate(std::move(desk_template));

  // Wait for the tabs to load.
  content::RunAllTasksUntilIdle();

  // Verify that the browser was launched with the correct urls and active tab.
  Browser* new_browser = FindLaunchedBrowserByURLs(urls);
  ASSERT_TRUE(new_browser);
  EXPECT_EQ(static_cast<int>(browser_active_index),
            new_browser->tab_strip_model()->active_index());

  // Verify that the browser window has been launched on the new desk (desk B).
  EXPECT_EQ(1, desks_controller->GetActiveDeskIndex());
  aura::Window* browser_window = new_browser->window()->GetNativeWindow();
  ASSERT_TRUE(browser_window);
  EXPECT_EQ(ash::Shell::GetContainer(browser_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            browser_window->parent());
}

// Tests that launching a template that contains a floated browser window works
// as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithFloatedWindow) {
  // Test that Singleton was properly initialized.
  ASSERT_TRUE(DesksClient::Get());

  // Float browser window and move out from default location.
  const gfx::Rect browser_bounds = gfx::Rect(0, 0, 800, 200);
  aura::Window* window = browser()->window()->GetNativeWindow();
  ui::test::EventGenerator event_generator(window->GetRootWindow());
  event_generator.PressAndReleaseKeyAndModifierKeys(
      ui::VKEY_F, ui::EF_ALT_DOWN | ui::EF_COMMAND_DOWN);
  ASSERT_TRUE(ash::WindowState::Get(window)->IsFloated());
  window->SetBounds(browser_bounds);
  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Test the default template's name is the current desk's name.
  auto* desks_controller = ash::DesksController::Get();
  EXPECT_EQ(
      desk_template->template_name(),
      desks_controller->GetDeskName(desks_controller->GetActiveDeskIndex()));

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Verify floated window bounds and state is correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(browser_bounds));
  // Verify window float state is correctly captured.
  EXPECT_THAT(data->window_info.window_state_type,
              Optional(chromeos::WindowStateType::kFloated));

  // Launch saved template and test floated window is restored correctly.
  SetAndLaunchTemplate(std::move(desk_template));
  EXPECT_EQ(1, desks_controller->GetActiveDeskIndex());

  // Get the floated window from newly created desk.
  auto* floated_window = ash::window_util::GetFloatedWindowForActiveDesk();
  ASSERT_TRUE(floated_window);
  ASSERT_TRUE(ash::WindowState::Get(floated_window)->IsFloated());
  // Restored floated window to the saved bounds instead of default bounds.
  EXPECT_EQ(floated_window->bounds(), browser_bounds);
}

// Tests that launching a template that contains a browser window with tab
// groups works as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       LaunchTemplateWithBrowserWindowTabGroups) {
  ASSERT_TRUE(DesksClient::Get());

  // Create a new browser and add a few tabs to it, and specify the active tab
  // index.
  std::vector<GURL> creation_urls = {GURL(kExampleUrl1), GURL(kExampleUrl2),
                                     GURL(kExampleUrl3)};
  std::vector<tab_groups::TabGroupInfo> expected_tab_groups =
      MakeExpectedTabGroupsBasedOnTabVector(creation_urls);

  Browser* browser =
      CreateBrowserWithTabGroups(creation_urls, expected_tab_groups);

  // Get current tabs from browser.
  const std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  ClickFirstTemplateItem();

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Wait for the tabs to load.
  content::RunAllTasksUntilIdle();

  // Verify that the browser was launched with the correct urls and active tab.
  Browser* new_browser = FindLaunchedBrowserByURLs(urls);
  ASSERT_TRUE(new_browser);

  std::vector<tab_groups::TabGroupInfo> got_tab_groups =
      chrome_desks_util::ConvertTabGroupsToTabGroupInfos(
          new_browser->tab_strip_model()->group_model());

  EXPECT_FALSE(got_tab_groups.empty());

  // We don't care about the order of the tab groups.
  EXPECT_THAT(expected_tab_groups,
              testing::UnorderedElementsAreArray(got_tab_groups));
}

// Tests that a browser's pinned tabs can be launched correctly in a saved desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchBrowserWithPinnedTabs) {
  ASSERT_TRUE(DesksClient::Get());

  // create expected values.
  std::vector<GURL> tabs = {GURL(kExampleUrl1), GURL(kExampleUrl2)};
  int expected_number_of_pinned_tabs = 1;

  // Create a new browser and add a few tabs to it.
  Browser* browser =
      CreateBrowserWithPinnedTabs(tabs, expected_number_of_pinned_tabs);

  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  ClickFirstTemplateItem();

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Wait for tabs to load.
  content::RunAllTasksUntilIdle();

  // Verify that the browser was launched with the correct urls and active tab.
  Browser* new_browser = FindLaunchedBrowserByURLs(urls);
  ASSERT_TRUE(new_browser);

  // Assert the number of pinned tabs is correct.
  ASSERT_EQ(expected_number_of_pinned_tabs,
            new_browser->tab_strip_model()->IndexOfFirstNonPinnedTab());
}

// Tests that browser session restore isn't triggered when we launch a template
// that contains a browser window.
IN_PROC_BROWSER_TEST_F(DesksClientTest, PreventBrowserSessionRestoreTest) {
  ASSERT_TRUE(DesksClient::Get());

  // Do not exit from test or delete the Profile* when last browser is closed.
  ScopedKeepAlive keep_alive(KeepAliveOrigin::BROWSER,
                             KeepAliveRestartOption::DISABLED);
  ScopedProfileKeepAlive profile_keep_alive(
      browser()->profile(), ProfileKeepAliveOrigin::kBrowserWindow);

  // Enable session service.
  SessionStartupPref pref(SessionStartupPref::LAST);
  Profile* profile = browser()->profile();
  SessionStartupPref::SetStartupPref(profile, pref);

  const int expected_tab_count = 2;
  chrome::AddTabAt(browser(), GURL(kExampleUrl2), /*index=*/-1,
                   /*foreground=*/true);
  EXPECT_EQ(expected_tab_count, browser()->tab_strip_model()->count());

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  ASSERT_TRUE(desk_template);

  // Close the browser and verify that all browser windows are closed.
  CloseBrowserSynchronously(browser());
  EXPECT_EQ(0u, chrome::GetTotalBrowserCount());

  // Set the template we created and launch the template.
  SetAndLaunchTemplate(std::move(desk_template));

  // Verify that the browser was launched with the correct number of tabs, and
  // that browser session restore did not restore any windows/tabs.
  Browser* new_browser =
      FindLaunchedBrowserByURLs({GURL(kAboutBlankUrl), GURL(kNewTabPageUrl)});
  ASSERT_TRUE(new_browser);
  EXPECT_EQ(1u, chrome::GetTotalBrowserCount());
}

// Tests that browser windows created from a template have the correct bounds
// and window state.
IN_PROC_BROWSER_TEST_F(DesksClientTest, BrowserWindowRestorationTest) {
  ASSERT_TRUE(DesksClient::Get());

  // Create a new browser and set its bounds.
  std::vector<GURL> browser_urls_1 = {GURL(kExampleUrl1), GURL(kExampleUrl2)};
  Browser* browser_1 = CreateBrowser(browser_urls_1);
  const gfx::Rect browser_bounds_1 = gfx::Rect(100, 100, 600, 200);
  aura::Window* window_1 = browser_1->window()->GetNativeWindow();
  window_1->SetBounds(browser_bounds_1);

  // Create a new minimized browser.
  std::vector<GURL> browser_urls_2 = {GURL(kExampleUrl1)};
  Browser* browser_2 = CreateBrowser(browser_urls_2);
  const gfx::Rect browser_bounds_2 = gfx::Rect(150, 150, 500, 300);
  aura::Window* window_2 = browser_2->window()->GetNativeWindow();
  window_2->SetBounds(browser_bounds_2);
  EXPECT_EQ(browser_bounds_2, window_2->bounds());
  browser_2->window()->Minimize();

  // Create a new maximized browser.
  std::vector<GURL> browser_urls_3 = {GURL(kExampleUrl2)};
  Browser* browser_3 = CreateBrowser(browser_urls_3);
  browser_3->window()->Maximize();

  EXPECT_EQ(browser_bounds_1, window_1->bounds());
  EXPECT_EQ(browser_bounds_2, window_2->bounds());
  ASSERT_TRUE(browser_2->window()->IsMinimized());
  ASSERT_TRUE(browser_3->window()->IsMaximized());

  // Capture the active desk, which contains the two browser windows.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Set the template and launch it.
  SetAndLaunchTemplate(std::move(desk_template));

  // Verify that the browser was launched with the correct bounds.
  Browser* new_browser_1 = FindLaunchedBrowserByURLs(browser_urls_1);
  ASSERT_TRUE(new_browser_1);
  EXPECT_EQ(browser_bounds_1,
            new_browser_1->window()->GetNativeWindow()->bounds());

  // Verify that the browser was launched and minimized.
  Browser* new_browser_2 = FindLaunchedBrowserByURLs(browser_urls_2);
  ASSERT_TRUE(new_browser_2);
  ASSERT_TRUE(new_browser_2->window()->IsMinimized());
  EXPECT_EQ(browser_bounds_2,
            new_browser_2->window()->GetNativeWindow()->bounds());

  // Verify that the browser was launched and maximized.
  Browser* new_browser_3 = FindLaunchedBrowserByURLs(browser_urls_3);
  ASSERT_TRUE(new_browser_3);
  ASSERT_TRUE(new_browser_3->window()->IsMaximized());
}

// Tests that saving and launching a template that contains a PWA works as
// expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithPWA) {
  ASSERT_TRUE(DesksClient::Get());

  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kExampleUrl1), /*launch_in_browser=*/false);
  ASSERT_TRUE(pwa_browser->is_type_app());
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const gfx::Rect pwa_bounds(50, 50, 500, 500);
  pwa_window->SetBounds(pwa_bounds);
  const int32_t pwa_window_id =
      pwa_window->GetProperty(app_restore::kWindowIdKey);
  const std::string* app_name =
      pwa_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(app_name);

  // Capture the active desk, which contains the PWA.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Find |pwa_browser| window's app restore data.
  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, pwa_window_id);
  ASSERT_TRUE(data);

  // Verify window info are correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(pwa_bounds));
  EXPECT_THAT(data->browser_extra_info.app_type_browser, Optional(true));
  EXPECT_THAT(data->browser_extra_info.app_name, Optional(*app_name));

  // Set the template and launch it.
  SetAndLaunchTemplate(std::move(desk_template));

  // Verify that the PWA was launched correctly.
  Browser* new_pwa_browser = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  ASSERT_TRUE(new_pwa_browser);
  ASSERT_TRUE(new_pwa_browser->is_type_app());
  aura::Window* new_browser_window =
      new_pwa_browser->window()->GetNativeWindow();
  EXPECT_NE(new_browser_window, pwa_window);
  EXPECT_EQ(pwa_bounds, new_browser_window->bounds());
  const std::string* new_app_name =
      new_browser_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(new_app_name);
  EXPECT_EQ(*app_name, *new_app_name);
}

// Tests that launching a template that contains a PWA on a device that does not
// contain the PWA does not open the PWA.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithMissingPWA) {
  ASSERT_TRUE(DesksClient::Get());

  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kExampleUrl1), /*launch_in_browser=*/false);
  ASSERT_TRUE(pwa_browser->is_type_app());
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const gfx::Rect pwa_bounds(50, 50, 500, 500);
  pwa_window->SetBounds(pwa_bounds);
  const int32_t pwa_window_id =
      pwa_window->GetProperty(app_restore::kWindowIdKey);
  const std::string* app_name =
      pwa_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(app_name);

  // Capture the active desk, which contains the PWA.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Find |pwa_browser| window's app restore data.
  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, pwa_window_id);
  ASSERT_TRUE(data);

  // Verify window info are correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(pwa_bounds));
  EXPECT_THAT(data->browser_extra_info.app_type_browser, Optional(true));
  EXPECT_THAT(data->browser_extra_info.app_name, Optional(*app_name));
  const std::vector<GURL> urls = GetURLsForBrowserWindow(pwa_browser);

  // Set the template and launch it.
  base::Uuid uuid = desk_template->uuid();
  SetTemplate(std::move(desk_template));

  views::Widget::GetWidgetForNativeWindow(pwa_window)->CloseNow();
  ASSERT_FALSE(FindLaunchedBrowserByURLs(urls));
  web_app::test::UninstallAllWebApps(profile());

  LaunchTemplate(uuid);
  // Verify that the PWA was not launched.
  Browser* new_pwa_browser = FindLaunchedBrowserByURLs(urls);
  EXPECT_FALSE(new_pwa_browser);
}

// Tests that PWAs with out of scope urls are saved and launched correctly.
// Regression test for b/248645623.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithOutOfScopeURL) {
  ASSERT_TRUE(DesksClient::Get());

  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kYoutubeUrl), /*launch_in_browser=*/false);
  ASSERT_TRUE(pwa_browser->is_type_app());
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const std::string* app_name =
      pwa_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(app_name);

  const GURL out_of_scope_url(kExampleUrl1);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(pwa_browser, out_of_scope_url));
  // Verify that the PWA has navigated to the out of scope url.
  const std::vector<GURL> urls = GetURLsForBrowserWindow(pwa_browser);
  ASSERT_THAT(urls, ElementsAre(out_of_scope_url));

  // Set the template and launch it.
  SetAndLaunchTemplate(
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate));

  // Verify that the PWA was launched correctly, and that the out of scope url
  // was successfully opened in the PWA (and not in a normal browser window).
  Browser* new_pwa_browser = FindLaunchedBrowserByURLs(urls);
  ASSERT_TRUE(new_pwa_browser);
  ASSERT_TRUE(new_pwa_browser->is_type_app());
  aura::Window* new_browser_window =
      new_pwa_browser->window()->GetNativeWindow();
  EXPECT_NE(new_browser_window, pwa_window);
  const std::string* new_app_name =
      new_browser_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(new_app_name);
  EXPECT_EQ(*app_name, *new_app_name);
}

// Tests that saving and launching a template that contains a PWA in a browser
// window works as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateWithPWAInBrowser) {
  ASSERT_TRUE(DesksClient::Get());

  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kYoutubeUrl), /*launch_in_browser=*/true);
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const int32_t pwa_window_id =
      pwa_window->GetProperty(app_restore::kWindowIdKey);

  // Capture the active desk, which contains the PWA.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Test that |pwa_browser|'s restore data is saved under the Chrome browser
  // app id app_constants::kChromeAppId, not Youtube app id
  // extension_misc::kYoutubeAppId.
  ASSERT_TRUE(ash::QueryRestoreData(*desk_template, app_constants::kChromeAppId,
                                    pwa_window_id));
  ASSERT_FALSE(
      ash::QueryRestoreData(*desk_template, extension_misc::kYoutubeAppId));
}

// Tests that captured desk templates can be recalled as a JSON string.
IN_PROC_BROWSER_TEST_F(DesksClientTest, GetDeskTemplateJson) {
  // Test that Singleton was properly initialized.
  ASSERT_TRUE(DesksClient::Get());

  // Change |browser|'s bounds.
  const gfx::Rect browser_bounds = gfx::Rect(0, 0, 800, 200);
  aura::Window* window = browser()->window()->GetNativeWindow();
  window->SetBounds(browser_bounds);
  // Make window visible on all desks.
  window->SetProperty(aura::client::kWindowWorkspaceKey,
                      aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);

  // Create the settings app, which is a system web app.
  webapps::AppId settings_app_id =
      CreateSettingsSystemWebApp(browser()->profile());

  // Change the Settings app's bounds too.
  const gfx::Rect settings_app_bounds = gfx::Rect(100, 100, 800, 300);
  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  ASSERT_TRUE(settings_window);
  settings_window->SetBounds(settings_app_bounds);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  std::string template_json =
      GetTemplateJson(desk_template->uuid(), browser()->profile());

  // content of the conversion is tested in:
  // components/desks_storage/core/desk_template_conversion_unittests.cc in this
  // case we're simply interested in whether or not we got content back.
  ASSERT_TRUE(!template_json.empty());
}

// Tests that basic operations using the native UI work as expected.
// TODO(crbug.com/1286515): Remove the SystemUI prefix from these tests. Remove
// the tests that do not have the SystemUI prefix other than GetDeskTemplateJson
// once the extension is deprecated.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUIBasic) {
  auto* desk_model = DesksClient::Get()->GetDeskModel();
  ASSERT_EQ(0u, desk_model->GetEntryCount());

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  // Tests that since we have no saved desk right now, so the library button is
  // hidden.
  const views::Button* library_button = ash::GetLibraryButton();
  ASSERT_TRUE(library_button);
  EXPECT_FALSE(library_button->GetVisible());

  // Note that this button needs at least one window to show up. Browser tests
  // have an existing browser window, so no new window needs to be created.
  // TODO(http://b/350771229): Remove `if` when Forest is enabled.
  if (ash::features::IsSavedDeskUiRevampEnabled()) {
    ClickSaveDeskAsTemplateButton();
  } else {
    const views::Button* save_desk_as_template_button =
        ash::GetSaveDeskAsTemplateButton();
    ASSERT_TRUE(save_desk_as_template_button);
    ClickView(save_desk_as_template_button);

    ash::WaitForSavedDeskUI();
  }

  EXPECT_EQ(1u, desk_model->GetEntryCount());

  // Tests that since we have one template right now, so that the expanded state
  // library button is shown, and the saved desk grid has one item.
  ASSERT_TRUE(library_button);
  EXPECT_TRUE(library_button->GetVisible());

  const views::Button* template_item = ash::GetSavedDeskItemButton(/*index=*/0);
  EXPECT_TRUE(template_item);
}

// Tests launching a template with a browser window.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchBrowser) {
  // Create a new browser and add a few tabs to it, and specify the active tab
  // index.
  const size_t browser_active_index = 1;
  Browser* browser = CreateBrowser(
      {GURL(kExampleUrl1), GURL(kExampleUrl2), GURL(kExampleUrl3)},
      /*active_url_index=*/browser_active_index);

  // Verify that the active tab is correct.
  EXPECT_EQ(static_cast<int>(browser_active_index),
            browser->tab_strip_model()->active_index());

  // Get current tabs from browser.
  const std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  // There are two browser windows currently, the default one and the one we
  // just created.
  ASSERT_EQ(2u, BrowserList::GetInstance()->size());

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  ClickFirstTemplateItem();

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Wait for the tabs to load.
  content::RunAllTasksUntilIdle();

  // There are a total of four browser windows now. The two initial ones and the
  // two created from our template.
  EXPECT_EQ(4u, BrowserList::GetInstance()->size());

  // Test that the created browser has the same tabs and the same active tab.
  Browser* new_browser = FindLaunchedBrowserByURLs(urls);
  ASSERT_TRUE(new_browser);
  EXPECT_EQ(static_cast<int>(browser_active_index),
            new_browser->tab_strip_model()->active_index());

  // Verify that the browser window has been launched on the new desk (desk B).
  EXPECT_EQ(1, ash::DesksController::Get()->GetActiveDeskIndex());
  aura::Window* browser_window = new_browser->window()->GetNativeWindow();
  ASSERT_TRUE(browser_window);
  EXPECT_EQ(ash::Shell::GetContainer(browser_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            browser_window->parent());
}

// Tests that a browser's urls can be captured correctly in the desk template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUICaptureBrowserUrlsTest) {
  // Create a new browser and add a few tabs to it.
  Browser* browser = CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *templates.front(), app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);
  // Check the urls are captured correctly in the `desk_template`.
  EXPECT_EQ(data->browser_extra_info.urls, urls);
}

// Tests that snapped window's snap ratio/percentage is maintained when
// launching a desk template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchSnappedWindow) {
  const int shelf_height = ash::ShelfConfig::Get()->shelf_size();
  display::test::DisplayManagerTestApi display_manager_test_api(
      ash::Shell::Get()->display_manager());
  display_manager_test_api.UpdateDisplay(
      "2000x" + base::NumberToString(1000 + shelf_height));

  aura::Window* window = browser()->window()->GetNativeWindow();

  // Snap the window to the left.
  const ash::WindowSnapWMEvent left_snap_event(ash::WM_EVENT_SNAP_PRIMARY);
  ash::WindowState::Get(window)->OnWMEvent(&left_snap_event);
  ASSERT_EQ(gfx::Rect(1000, 1000), window->GetBoundsInScreen());

  // Drag the window so it is now 60% of the work area.
  ui::test::EventGenerator event_generator(window->GetRootWindow());
  event_generator.set_current_screen_location(gfx::Point(1000, 500));
  event_generator.DragMouseBy(200, 0);
  ASSERT_EQ(gfx::Rect(1200, 1000), window->GetBoundsInScreen());
  auto* window_state = ash::WindowState::Get(window);
  EXPECT_EQ(0.6f, *window_state->snap_ratio());

  // Enter overview and save our snapped window as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  auto* split_view_controller =
      ash::SplitViewController::Get(window->GetRootWindow());
  ASSERT_FALSE(split_view_controller->IsWindowInSplitView(window));
  ClickSaveDeskAsTemplateButton();

  // Launch our template and then exit overview.
  ClickFirstTemplateItem();
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  ASSERT_FALSE(split_view_controller->IsWindowInSplitView(window));

  // Our snapped window should have the similar bounds as it did when it was
  // saved. We may lose some precision when saving a float as a percentage.
  ASSERT_EQ(2u, BrowserList::GetInstance()->size());
  aura::Window* new_browser_window =
      BrowserList::GetInstance()->get(1)->window()->GetNativeWindow();
  gfx::Rect new_bounds = new_browser_window->GetBoundsInScreen();
  EXPECT_EQ(0, new_bounds.x());
  EXPECT_EQ(0, new_bounds.y());
  EXPECT_NEAR(1200, new_bounds.width(), 5);
  EXPECT_EQ(1000, new_bounds.height());
  EXPECT_EQ(0.6f, *window_state->snap_ratio());

  // Launches the first template on the template grid.
  auto launch_first_template = []() {
    // Remove a desk first, otherwise we will run into an accessibility error
    // with `DeskPreviewView` upon entering overview.
    auto* desks_controller = ash::DesksController::Get();
    RemoveDesk(desks_controller->GetDeskAtIndex(1));

    // Enter overview and launch the same template.
    ash::ToggleOverview();
    ash::WaitForOverviewEnterAnimation();
    ClickLibraryButton();
    ClickFirstTemplateItem();
    ash::ToggleOverview();
    ash::WaitForOverviewExitAnimation();
  };

  // Tests the bounds of the window if we launch it while the display is upside
  // down.
  display_manager_test_api.UpdateDisplay(
      "2000x" + base::NumberToString(1000 + shelf_height) + "/u");
  launch_first_template();

  // The window is physically on the left, but the coordinates system is as if
  // it were on the right.
  new_browser_window =
      BrowserList::GetInstance()->get(2)->window()->GetNativeWindow();
  new_bounds = new_browser_window->GetBoundsInScreen();
  EXPECT_EQ(800, new_bounds.x());
  EXPECT_EQ(0, new_bounds.y());
  EXPECT_NEAR(1200, new_bounds.width(), 5);
  EXPECT_EQ(1000, new_bounds.height());
  EXPECT_EQ(0.6f, *window_state->snap_ratio());

  // Change to portrait mode, work area is 1000x2000.
  display_manager_test_api.UpdateDisplay(
      "1000x" + base::NumberToString(2000 + shelf_height));
  launch_first_template();

  // The window is at the top of the screen since we are in portrait
  // orientation, and its height is 60% of the work area height.
  new_browser_window =
      BrowserList::GetInstance()->get(3)->window()->GetNativeWindow();
  new_bounds = new_browser_window->GetBoundsInScreen();
  EXPECT_EQ(0, new_bounds.x());
  EXPECT_EQ(0, new_bounds.y());
  EXPECT_EQ(1000, new_bounds.width());
  EXPECT_NEAR(1200, new_bounds.height(), 5);
  EXPECT_EQ(0.6f, *window_state->snap_ratio());

  // Launch the window in upside down portrait mode. The height is 60% of the
  // work area height and the window is physically on the top, but the
  // coordinate system is as if it were on the bottom.
  display_manager_test_api.UpdateDisplay(
      "1000x" + base::NumberToString(2000 + shelf_height) + "/u");
  launch_first_template();

  new_browser_window =
      BrowserList::GetInstance()->get(4)->window()->GetNativeWindow();
  new_bounds = new_browser_window->GetBoundsInScreen();
  EXPECT_EQ(0, new_bounds.x());
  EXPECT_EQ(800, new_bounds.y());
  EXPECT_EQ(1000, new_bounds.width());
  EXPECT_NEAR(1200, new_bounds.height(), 5);
  EXPECT_EQ(0.6f, *window_state->snap_ratio());
}

// Tests that incognito browser windows will NOT be captured in the desk
// template.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUICaptureIncognitoBrowserTest) {
  Browser* incognito_browser = CreateIncognitoBrowser();
  chrome::AddTabAt(incognito_browser, GURL(kExampleUrl1), /*index=*/-1,
                   /*foreground=*/true);
  chrome::AddTabAt(incognito_browser, GURL(kExampleUrl2), /*index=*/-1,
                   /*foreground=*/true);
  incognito_browser->window()->Show();
  aura::Window* window = incognito_browser->window()->GetNativeWindow();

  const int32_t incognito_browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  const int32_t browser_window_id =
      browser()->window()->GetNativeWindow()->GetProperty(
          app_restore::kWindowIdKey);

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  // Incognito browsers are unsupported so a dialog will popup asking users if
  // they are sure.
  ClickSaveDeskAsTemplateButton(/*wait_for_ui=*/false);
  const views::Button* dialog_accept_button =
      ash::GetSavedDeskDialogAcceptButton();
  ASSERT_TRUE(dialog_accept_button);
  // MaterialNext uses PillButton instead of dialog buttons.
  if (std::string_view(dialog_accept_button->GetClassName()) ==
      std::string_view(ash::PillButton::kViewClassName)) {
    ClickView(dialog_accept_button);
  } else {
    // Use a key press to accept the dialog instead of a click as
    // dialog buttons think a click generated by the event generator is an
    // accidentally click and therefore ignores it.
    aura::Window* root_window =
        dialog_accept_button->GetWidget()->GetNativeWindow()->GetRootWindow();
    ui::test::EventGenerator event_generator(root_window);
    event_generator.PressAndReleaseKey(ui::VKEY_RETURN);
  }

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  const ash::DeskTemplate* desk_template = templates.front();

  // We expect to find the non-incognito window in the capture.
  EXPECT_TRUE(ash::QueryRestoreData(*desk_template, app_constants::kChromeAppId,
                                    browser_window_id));
  // The incognito window should not be there.
  EXPECT_FALSE(ash::QueryRestoreData(*desk_template,
                                     app_constants::kChromeAppId,
                                     incognito_browser_window_id));
}

// Tests that launching a template that contains a system web app works as
// expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUILaunchTemplateWithSystemWebApp) {
  // Create the settings and help apps, which are system web apps.
  CreateSettingsSystemWebApp(browser()->profile());
  CreateHelpSystemWebApp(browser()->profile());

  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  aura::Window* help_window = FindBrowserWindow(kHelpWindowId);
  ASSERT_TRUE(settings_window);
  ASSERT_TRUE(help_window);
  const std::u16string settings_title = settings_window->GetTitle();
  const std::u16string help_title = help_window->GetTitle();

  // Maximize the help app.
  ash::WindowState::Get(help_window)->Maximize();

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Exit overview and close the settings window. We'll need to verify if it
  // reopens later.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Close both apps.
  views::Widget::GetWidgetForNativeWindow(settings_window)->CloseNow();
  views::Widget::GetWidgetForNativeWindow(help_window)->CloseNow();
  ASSERT_FALSE(FindBrowserWindow(kSettingsWindowId));
  ASSERT_FALSE(FindBrowserWindow(kHelpWindowId));

  // Enter overview, head over to the desks templates grid and launch the
  // template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickLibraryButton();

  BrowsersAddedObserver browsers_added(/*num_browser_expected=*/3);
  ClickFirstTemplateItem();
  browsers_added.Wait();

  settings_window = nullptr;
  help_window = nullptr;
  for (Browser* browser : *BrowserList::GetInstance()) {
    aura::Window* window = browser->window()->GetNativeWindow();
    const std::u16string title = window->GetTitle();
    if (title == settings_title) {
      settings_window = window;
    }
    if (title == help_title) {
      help_window = window;
    }
  }
  ASSERT_TRUE(settings_window);
  ASSERT_TRUE(help_window);
  EXPECT_EQ(ash::Shell::GetContainer(settings_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            settings_window->parent());
  EXPECT_EQ(ash::Shell::GetContainer(help_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            help_window->parent());
  EXPECT_TRUE(ash::WindowState::Get(help_window)->IsMaximized());
}

// Tests that launching a template that contains a system web app will move the
// existing instance of the system web app to the current desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchTemplateWithSWAExisting) {
  Profile* profile = browser()->profile();

  // Create the settings and help apps, which are system web apps.
  CreateSettingsSystemWebApp(profile);
  CreateHelpSystemWebApp(profile);

  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  aura::Window* help_window = FindBrowserWindow(kHelpWindowId);
  aura::Window* browser_window = browser()->window()->GetNativeWindow();
  aura::Window* parent = settings_window->parent();
  ASSERT_TRUE(settings_window);
  ASSERT_TRUE(help_window);
  EXPECT_EQ(3u, BrowserList::GetInstance()->size());

  // Give the settings app a known position, and maximize the help app.
  const gfx::Rect settings_bounds(100, 100, 600, 400);
  settings_window->SetBounds(settings_bounds);
  ash::WindowState::Get(help_window)->Maximize();

  // Focus the settings window and then the help window. The MRU order should be
  // [`browser_window`, `settings_window`, `help_window`].
  settings_window->Focus();
  help_window->Focus();
  ASSERT_THAT(parent->children(),
              ElementsAre(browser_window, settings_window, help_window));

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Exit overview and move the settings window to a new place and stack it on
  // top so that we can later verify that it has been placed.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  settings_window->SetBounds(gfx::Rect(150, 150, 650, 500));

  // Restore the help window so we can later verify that it remaximizes.
  ash::WindowState::Get(help_window)->Restore();

  // Focus the settings window so it is now on top. We will verify that it gets
  // restacked later.
  settings_window->Focus();
  ASSERT_THAT(parent->children(),
              ElementsAre(browser_window, help_window, settings_window));

  // Enter overview, head over to the desks templates grid and launch the
  // template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickLibraryButton();
  ClickFirstTemplateItem();

  // Wait for the tabs to load.
  content::RunAllTasksUntilIdle();

  EXPECT_EQ(4u, BrowserList::GetInstance()->size());
  aura::Window* new_browser_window =
      BrowserList::GetInstance()->get(3)->window()->GetNativeWindow();

  // Tests that the stacking is correct while in overview. The parent has other
  // children for overview mode windows, but the first three app windows should
  // be [`new_browser_window`, `settings_window`, `help_window`].
  parent = settings_window->parent();
  aura::Window::Windows app_windows = parent->children();
  app_windows.erase(
      base::ranges::remove_if(app_windows,
                              [](aura::Window* w) {
                                return w->GetProperty(chromeos::kAppTypeKey) ==
                                       chromeos::AppType::NON_APP;
                              }),
      app_windows.end());
  ASSERT_THAT(app_windows,
              ElementsAre(new_browser_window, settings_window, help_window));

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  ash::DesksController* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(1, desks_controller->GetActiveDeskIndex());

  // Verify
  // that the settings window has been moved to the right place and stacked at
  // the bottom. Verify that the help window is maximized.
  EXPECT_TRUE(desks_controller->BelongsToActiveDesk(new_browser_window));
  EXPECT_TRUE(desks_controller->BelongsToActiveDesk(settings_window));
  EXPECT_TRUE(desks_controller->BelongsToActiveDesk(help_window));
  EXPECT_EQ(settings_bounds, settings_window->bounds());
  EXPECT_TRUE(ash::WindowState::Get(help_window)->IsMaximized());

  // Tests that the stacking is correct after exiting overview.
  EXPECT_THAT(parent->children(),
              ElementsAre(new_browser_window, settings_window, help_window));

  // Tests that there is no clipping on either window.
  EXPECT_EQ(gfx::Rect(), settings_window->layer()->clip_rect());
  EXPECT_EQ(gfx::Rect(), help_window->layer()->clip_rect());
}

// Tests that when restoring the OsUrlHandler SWA, the override URL is restored
// as expected. Regression test for crbug.com/1466634.
IN_PROC_BROWSER_TEST_F(DesksClientTest, OsUrlHandlerSWARestoreTest) {
  // Do not exit from test or delete the Profile* when last browser is closed.
  ScopedKeepAlive keep_alive(KeepAliveOrigin::BROWSER,
                             KeepAliveRestartOption::DISABLED);
  Profile* profile = browser()->profile();
  ScopedProfileKeepAlive profile_keep_alive(
      profile, ProfileKeepAliveOrigin::kBrowserWindow);

  // Create the OsUrlHandler SWA.
  constexpr char kOverrideUrl[] = "chrome://version";
  CreateOsUrlHandlerSystemWebApp(browser()->profile(), GURL(kOverrideUrl));

  aura::Window* url_handler_window = FindBrowserWindow(kTestWindowId);
  ASSERT_TRUE(url_handler_window);
  const std::u16string url_handler_title = url_handler_window->GetTitle();

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Exit overview and close the settings window. We'll need to verify if it
  // reopens later.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Close both apps.
  views::Widget::GetWidgetForNativeWindow(url_handler_window)->CloseNow();
  ASSERT_FALSE(FindBrowserWindow(kTestWindowId));

  // Enter overview, head over to the desks templates grid and launch the
  // template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickLibraryButton();

  BrowsersAddedObserver browsers_added(/*num_browser_expected=*/2);
  ClickFirstTemplateItem();
  browsers_added.Wait();

  url_handler_window = nullptr;
  for (Browser* browser : *BrowserList::GetInstance()) {
    aura::Window* window = browser->window()->GetNativeWindow();
    const std::u16string title = window->GetTitle();
    if (title == url_handler_title) {
      url_handler_window = window;

      // Ensure the kOverrideUrl is honored by the SWA after it is restored.
      content::WebContents* active_contents =
          browser->tab_strip_model()->GetActiveWebContents();
      content::TestNavigationObserver navigation_observer(active_contents);
      navigation_observer.Wait();
      EXPECT_EQ(GURL(kOverrideUrl), active_contents->GetLastCommittedURL());
    }
  }
  ASSERT_TRUE(url_handler_window);
  EXPECT_EQ(ash::Shell::GetContainer(url_handler_window->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            url_handler_window->parent());
}

// Tests that browser windows created from a template have the correct bounds
// and window state.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUIBrowserWindowRestorationTest) {
  // Create a new browser and set its bounds.
  std::vector<GURL> browser_urls_1 = {GURL(kExampleUrl1), GURL(kExampleUrl2)};
  Browser* browser_1 = CreateBrowser(browser_urls_1);
  const gfx::Rect browser_bounds_1 = gfx::Rect(100, 100, 600, 200);
  aura::Window* window_1 = browser_1->window()->GetNativeWindow();
  window_1->SetBounds(browser_bounds_1);

  // Create a new minimized browser.
  std::vector<GURL> browser_urls_2 = {GURL(kExampleUrl1)};
  Browser* browser_2 = CreateBrowser(browser_urls_2);
  const gfx::Rect browser_bounds_2 = gfx::Rect(150, 150, 500, 300);
  aura::Window* window_2 = browser_2->window()->GetNativeWindow();
  window_2->SetBounds(browser_bounds_2);
  EXPECT_EQ(browser_bounds_2, window_2->bounds());
  browser_2->window()->Minimize();

  // Create a new maximized browser.
  std::vector<GURL> browser_urls_3 = {GURL(kExampleUrl2)};
  Browser* browser_3 = CreateBrowser(browser_urls_3);
  browser_3->window()->Maximize();

  EXPECT_EQ(browser_bounds_1, window_1->bounds());
  EXPECT_EQ(browser_bounds_2, window_2->bounds());
  ASSERT_TRUE(browser_2->window()->IsMinimized());
  ASSERT_TRUE(browser_3->window()->IsMaximized());

  // Capture the active desk, which contains the three browser windows.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  ClickFirstTemplateItem();

  // Wait for the tabs to load.
  content::RunAllTasksUntilIdle();

  // Verify that the browser was launched with the correct bounds.
  Browser* new_browser_1 = FindLaunchedBrowserByURLs(browser_urls_1);
  ASSERT_TRUE(new_browser_1);
  EXPECT_EQ(browser_bounds_1,
            new_browser_1->window()->GetNativeWindow()->bounds());

  // Verify that the browser was launched and minimized.
  Browser* new_browser_2 = FindLaunchedBrowserByURLs(browser_urls_2);
  ASSERT_TRUE(new_browser_2);
  ASSERT_TRUE(new_browser_2->window()->IsMinimized());
  EXPECT_EQ(browser_bounds_2,
            new_browser_2->window()->GetNativeWindow()->bounds());

  // Verify that the browser was launched and maximized.
  Browser* new_browser_3 = FindLaunchedBrowserByURLs(browser_urls_3);
  ASSERT_TRUE(new_browser_3);
  ASSERT_TRUE(new_browser_3->window()->IsMaximized());
}

// Tests that saving and launching a template that contains a PWA works as
// expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchTemplateWithPWA) {
  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kExampleUrl1), /*launch_in_browser=*/false);
  ASSERT_TRUE(pwa_browser->is_type_app());
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const gfx::Rect pwa_bounds(50, 50, 500, 500);
  pwa_window->SetBounds(pwa_bounds);
  const int32_t pwa_window_id =
      pwa_window->GetProperty(app_restore::kWindowIdKey);
  const std::string* app_name =
      pwa_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(app_name);

  // Capture the active desk, which contains the PWA.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  // Find `pwa_browser` window's app restore data.
  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *templates.front(), app_constants::kChromeAppId, pwa_window_id);
  ASSERT_TRUE(data);

  // Verify window info are correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(pwa_bounds));
  EXPECT_THAT(data->browser_extra_info.app_type_browser, Optional(true));
  EXPECT_THAT(data->browser_extra_info.app_name, Optional(*app_name));

  // Launch the template.
  ClickFirstTemplateItem();

  // Verify that the PWA was launched correctly.
  Browser* new_pwa_browser = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  ASSERT_TRUE(new_pwa_browser);
  ASSERT_TRUE(new_pwa_browser->is_type_app());
  aura::Window* new_browser_window =
      new_pwa_browser->window()->GetNativeWindow();
  EXPECT_NE(new_browser_window, pwa_window);
  EXPECT_EQ(pwa_bounds, new_browser_window->bounds());
  const std::string* new_app_name =
      new_browser_window->GetProperty(app_restore::kBrowserAppNameKey);
  ASSERT_TRUE(new_app_name);
  EXPECT_EQ(*app_name, *new_app_name);
}

// Tests that saving and launching a template that contains a PWA in a browser
// window works as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUILaunchTemplateWithPWAInBrowser) {
  Browser* pwa_browser =
      InstallAndLaunchPWA(GURL(kYoutubeUrl), /*launch_in_browser=*/true);
  aura::Window* pwa_window = pwa_browser->window()->GetNativeWindow();
  const int32_t pwa_window_id =
      pwa_window->GetProperty(app_restore::kWindowIdKey);

  // Capture the active desk, which contains the PWA.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  // Test that `pwa_browser` restore data can be found.
  const ash::DeskTemplate* desk_template = templates.front();
  // Test that `pwa_browser`'s restore data is saved under the Chrome browser
  // app id app_constants::kChromeAppId, not Youtube app id
  // extension_misc::kYoutubeAppId.
  ASSERT_TRUE(ash::QueryRestoreData(*desk_template, app_constants::kChromeAppId,
                                    pwa_window_id));
  ASSERT_FALSE(
      ash::QueryRestoreData(*desk_template, extension_misc::kYoutubeAppId));
}

// Tests that browsers and SWAs can be captured correctly in the desk template.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUICaptureActiveDeskAsTemplateTest) {
  // Change `browser`'s bounds.
  const gfx::Rect browser_bounds(800, 200);
  aura::Window* window = browser()->window()->GetNativeWindow();
  window->SetBounds(browser_bounds);
  // Make the window visible on all desks.
  window->SetProperty(aura::client::kWindowWorkspaceKey,
                      aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);
  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);

  // Create the settings app, which is a system web app.
  webapps::AppId settings_app_id =
      CreateSettingsSystemWebApp(browser()->profile());

  // Change the Settings app's bounds too.
  const gfx::Rect settings_app_bounds(100, 100, 800, 300);
  aura::Window* settings_window = FindBrowserWindow(kSettingsWindowId);
  const int32_t settings_window_id =
      settings_window->GetProperty(app_restore::kWindowIdKey);
  ASSERT_TRUE(settings_window);
  settings_window->SetBounds(settings_app_bounds);

  auto* desks_controller = ash::DesksController::Get();
  const std::u16string desk_name =
      desks_controller->GetDeskName(desks_controller->GetActiveDeskIndex());

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  ASSERT_EQ(1u, templates.size());

  const ash::DeskTemplate* desk_template = templates.front();

  // Test the default template's name is the desk's name it was created from.
  EXPECT_EQ(desk_name, desk_template->template_name());

  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);
  // Verify window info are correctly captured.
  EXPECT_THAT(data->window_info.current_bounds, Optional(browser_bounds));
  // `visible_on_all_workspaces` should have been reset even though
  // the captured window is visible on all workspaces.
  EXPECT_FALSE(data->window_info.desk_id.has_value());
  auto* screen = display::Screen::GetScreen();
  EXPECT_EQ(screen->GetDisplayNearestWindow(window).id(),
            data->display_id.value());
  auto normalize_state = [](ui::WindowShowState state) {
    return state == ui::SHOW_STATE_DEFAULT ? ui::SHOW_STATE_NORMAL : state;
  };
  EXPECT_EQ(
      normalize_state(window->GetProperty(aura::client::kShowStateKey)),
      chromeos::ToWindowShowState(data->window_info.window_state_type.value()));
  // We don't capture the window's desk_id as a template will always
  // create in a new desk.
  EXPECT_FALSE(data->window_info.desk_id.has_value());

  // Find Setting app's app restore data.
  const app_restore::AppRestoreData* data2 = ash::QueryRestoreData(
      *desk_template, settings_app_id, settings_window_id);
  ASSERT_TRUE(data2);

  EXPECT_EQ(static_cast<int>(apps::LaunchContainer::kLaunchContainerWindow),
            data2->container.value());
  EXPECT_EQ(static_cast<int>(WindowOpenDisposition::NEW_WINDOW),
            data2->disposition.value());
  // Verify window info are correctly captured.
  EXPECT_THAT(data2->window_info.current_bounds, Optional(settings_app_bounds));
  EXPECT_FALSE(data2->window_info.desk_id.has_value());
  EXPECT_EQ(screen->GetDisplayNearestWindow(window).id(),
            data->display_id.value());
  EXPECT_EQ(
      normalize_state(window->GetProperty(aura::client::kShowStateKey)),
      chromeos::ToWindowShowState(data->window_info.window_state_type.value()));
}

// Tests that launching a template that contains a chrome app works as expected.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchTemplateWithChromeApp) {
  // Create a chrome app.
  const extensions::Extension* extension =
      LoadAndLaunchPlatformApp("launch", "Launched");
  ASSERT_TRUE(extension);

  const std::string extension_id = extension->id();
  ::full_restore::SaveAppLaunchInfo(
      profile()->GetPath(),
      std::make_unique<app_restore::AppLaunchInfo>(
          extension_id, apps::LaunchContainer::kLaunchContainerWindow,
          WindowOpenDisposition::NEW_WINDOW, display::kDefaultDisplayId,
          std::vector<base::FilePath>{}, nullptr));

  extensions::AppWindow* app_window = CreateAppWindow(profile(), extension);
  ASSERT_TRUE(app_window);
  ASSERT_TRUE(GetFirstAppWindowForApp(extension_id));

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Close the chrome app window. We'll need to verify if it reopens later.
  views::Widget* app_widget =
      views::Widget::GetWidgetForNativeWindow(app_window->GetNativeWindow());
  app_widget->CloseNow();
  ASSERT_FALSE(GetFirstAppWindowForApp(extension_id));

  ash::DesksController* desks_controller = ash::DesksController::Get();
  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());

  // `BrowserAppLauncher::LaunchAppWithParams()` does not launch the chrome app
  // in tests, so here we set up a mock app launch handler and just verify a
  // `LaunchSystemWebAppOrChromeApp()` call with the associated extension is
  // seen.
  auto mock_app_launch_handler =
      std::make_unique<MockDesksTemplatesAppLaunchHandler>(profile());
  MockDesksTemplatesAppLaunchHandler* mock_app_launch_handler_ptr =
      mock_app_launch_handler.get();
  ScopedDesksTemplatesAppLaunchHandlerSetter scoped_launch_handler(
      std::move(mock_app_launch_handler), /*launch_id=*/1);

  EXPECT_CALL(*mock_app_launch_handler_ptr,
              LaunchSystemWebAppOrChromeApp(_, extension_id, _));

  // Launch the template we saved.
  ClickFirstTemplateItem();
}

// Tests that the windows and tabs count histogram is recorded properly.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUIDeskTemplateWindowAndTabCountHistogram) {
  base::HistogramTester histogram_tester;

  // Create the two file manager (system web app) windows.
  CreateFilesSystemWebApp(browser()->profile());
  CreateFilesSystemWebApp(browser()->profile());

  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2), GURL(kExampleUrl3)});

  // Save a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  // NOTE: there is an existing browser with 1 tab created by BrowserMain().
  // Window count: 2 files app windows + 2 created browsers + 1 existing browser
  //               = 5.
  // Tab count: 5 tabs on the created browsers + 1 tab on the existing browser
  //            = 6.
  // Total count: 2 files app windows + 6 tabs = 8.
  histogram_tester.ExpectBucketCount(ash::kTemplateWindowCountHistogramName, 5,
                                     1);
  histogram_tester.ExpectBucketCount(ash::kTemplateTabCountHistogramName, 6, 1);
  histogram_tester.ExpectBucketCount(
      ash::kTemplateWindowAndTabCountHistogramName, 8, 1);
}

// Tests that the template count histogram is recorded properly.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUIDeskTemplateUserTemplateCountHistogram) {
  base::HistogramTester histogram_tester;

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  auto* desks_controller = ash::DesksController::Get();
  auto active_desk_index = desks_controller->GetActiveDeskIndex();

  // Save 3 templates.
  const int saves = 3;
  for (int i = 0; i < saves; i++) {
    ClickSaveDeskAsTemplateButton();

    // Change desk name to avoid duplication on template name. Having duplicate
    // names invokes a workflow that involves showing and accepting the replace
    // dialog, which is unnecessary for this test.
    desks_controller->GetDeskAtIndex(active_desk_index)
        ->SetName(base::UTF8ToUTF16(base::NumberToString(i)), true);

    // Exit and renenter overview to save the next template. Once we are viewing
    // the grid we can't go back to regular overview unless we exit overview or
    // delete all the templates.
    if (i != saves - 1) {
      ash::ToggleOverview();
      ash::WaitForOverviewExitAnimation();
      ash::ToggleOverview();
      ash::WaitForOverviewEnterAnimation();
    }
  }

  const views::Button* delete_button =
      ash::GetSavedDeskItemDeleteButton(/*index=*/0);
  ClickView(delete_button);

  // Confirm deleting a template. Use a key press to accept the dialog instead
  // of a click as dialog buttons think a click generated by the event generator
  // is an accidentally click and therefore ignores it.
  const views::Button* dialog_accept_button =
      ash::GetSavedDeskDialogAcceptButton();
  ASSERT_TRUE(dialog_accept_button);
  aura::Window* root_window =
      dialog_accept_button->GetWidget()->GetNativeWindow()->GetRootWindow();
  ui::test::EventGenerator event_generator(root_window);
  event_generator.PressAndReleaseKey(ui::VKEY_RETURN);

  // Wait for the model to update.
  ash::WaitForSavedDeskUI();

  // Save one more template.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  // Verify that all template saves and deletes are captured by the histogram.
  histogram_tester.ExpectBucketCount(ash::kUserTemplateCountHistogramName, 1,
                                     1);
  histogram_tester.ExpectBucketCount(ash::kUserTemplateCountHistogramName, 2,
                                     2);
  histogram_tester.ExpectBucketCount(ash::kUserTemplateCountHistogramName, 3,
                                     2);
}

// Tests that browser session restore isn't triggered when we launch a template
// that contains a browser window.

IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUIPreventBrowserSessionRestoreTest) {
  // Do not exit from test or delete the Profile* when last browser is closed.
  ScopedKeepAlive keep_alive(KeepAliveOrigin::BROWSER,
                             KeepAliveRestartOption::DISABLED);
  Profile* profile = browser()->profile();
  ScopedProfileKeepAlive profile_keep_alive(
      profile, ProfileKeepAliveOrigin::kBrowserWindow);

  // Enable session service.
  SessionStartupPref pref(SessionStartupPref::LAST);
  SessionStartupPref::SetStartupPref(profile, pref);

  const int expected_tab_count = 2;
  GURL example2 = GURL(kExampleUrl2);
  content::TestNavigationObserver navigation_observer(example2);
  navigation_observer.StartWatchingNewWebContents();
  chrome::AddTabAt(browser(), example2, /*index=*/-1,
                   /*foreground=*/true);
  navigation_observer.Wait();
  EXPECT_EQ(expected_tab_count, browser()->tab_strip_model()->count());

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Exit overview, close the browser and verify that all browser windows are
  // closed.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  CloseBrowserSynchronously(browser());
  EXPECT_EQ(0u, chrome::GetTotalBrowserCount());

  // Reenter overview and launch the template we saved.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickLibraryButton();
  ClickFirstTemplateItem();
  content::RunAllTasksUntilIdle();

  // Verify that the browser was launched with the correct number of tabs, and
  // that browser session restore did not restore any windows/tabs.
  EXPECT_EQ(1u, chrome::GetTotalBrowserCount());
  Browser* new_browser = BrowserList::GetInstance()->get(0);
  ASSERT_TRUE(new_browser);
  EXPECT_EQ(expected_tab_count, new_browser->tab_strip_model()->count());
}

// Tests that launching the same desk template multiple times creates desks with
// different/incremented names.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SystemUILaunchMultipleDeskTemplates) {
  const base::Uuid kDeskUuid = base::Uuid::GenerateRandomV4();
  const std::u16string kDeskName(u"Test Desk Name");

  auto* desks_controller = ash::DesksController::Get();

  ASSERT_EQ(0, desks_controller->GetActiveDeskIndex());
  desks_controller->GetDeskAtIndex(0)->SetName(kDeskName, true);

  // Save a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // `ClickSaveDeskAsTemplateButton` will take us to the templates grid. For all
  // subsuquent runs, we enter the templates grid by click the templates button
  // on the desks bar.
  bool first_run = true;
  auto check_launch_template_desk_name =
      [kDeskUuid, &first_run](const std::u16string& desk_name) {
        SCOPED_TRACE(desk_name);

        if (!first_run) {
          ClickLibraryButton();
        }

        ClickFirstTemplateItem();
        content::RunAllTasksUntilIdle();

        first_run = false;
      };

  // Launching a desk from the template creates a desk with the same name as
  // the template.
  desks_controller->GetDeskAtIndex(0)->SetName(u"Desk", true);
  check_launch_template_desk_name(kDeskName);

  // Launch more desks from the template and verify that the newly create desks
  // have unique names.
  check_launch_template_desk_name(std::u16string(kDeskName).append(u"(1)"));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u"(2)"));

  // Remove "Test Desk Name (1)", which means the next created desk from
  // template will have that name. Then it will skip (2) since it already
  // exists, and create the next desk with (3).
  RemoveDesk(desks_controller->GetDeskAtIndex(2));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u"(1)"));
  check_launch_template_desk_name(std::u16string(kDeskName).append(u"(3)"));

  // Same as above, but make sure that deleting the desk with the exact template
  // name still functions the same by only filling in whatever name is
  // available.
  RemoveDesk(desks_controller->GetDeskAtIndex(1));
  check_launch_template_desk_name(kDeskName);
  check_launch_template_desk_name(std::u16string(kDeskName).append(u"(4)"));
}

// Tests that the launch from template histogram is recorded properly.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       SystemUIDeskTemplateLaunchFromTemplateHistogram) {
  base::HistogramTester histogram_tester;

  // Create a new browser.
  CreateBrowser({});

  // Save a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  const int launches = 5;
  for (int i = 0; i < launches; i++) {
    ClickFirstTemplateItem();
    ClickLibraryButton();
  }

  histogram_tester.ExpectTotalCount(ash::kLaunchTemplateHistogramName,
                                    launches);
}

// Tests that launching a desk template records the appropriate performance
// metric.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateRecordsLoadTimeMetric) {
  base::HistogramTester histogram_tester;

  // Create the settings app, which is a system web app.
  CreateSettingsSystemWebApp(browser()->profile());

  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2), GURL(kExampleUrl3)});

  // Save and launch a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();
  ClickFirstTemplateItem();

  // Verify that the metric was recorded.
  histogram_tester.ExpectTotalCount("Ash.DeskTemplate.TimeToLoadTemplate", 1ul);
}

// Tests launch a template to a new desk and clean up desk. Number of windows
// closed should be properly recorded.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchTemplateAndCleanUpDesk) {
  auto* desks_controller = ash::DesksController::Get();
  // Should have 1 default active desk.
  EXPECT_EQ(1u, desks_controller->desks().size());

  // Capture and create a desk template.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);

  // Set template for testing.
  SetTemplate(std::move(desk_template));

  // Retrieve the `desk_uuid` from `LaunchDeskTemplate` operation and
  // `closeAll` on it.
  base::RunLoop loop;
  base::Uuid desk_id;
  // Launch one template, desk size should increase by 1.
  DesksClient::Get()->LaunchDeskTemplate(
      base::Uuid(), base::BindLambdaForTesting(
                        [&](std::optional<DesksClient::DeskActionError> error,
                            const base::Uuid& desk_uuid) {
                          EXPECT_EQ(2u, desks_controller->desks().size());
                          desk_id = desk_uuid;
                          loop.Quit();
                          ASSERT_FALSE(error);
                        }));
  loop.Run();

  ash::DeskSwitchAnimationWaiter waiter;
  // Creates a new window.
  CreateBrowser({});
  base::HistogramTester histogram_tester;
  ASSERT_FALSE(DesksClient::Get()->RemoveDesk(
      desk_id, ash::DeskCloseType::kCloseAllWindows));
  waiter.Wait();
  // Record number of windows being closed per source.
  // NOTE: The template contains an existing browser with 1 tab created by
  // `BrowserMain()`.
  histogram_tester.ExpectUniqueSample("Ash.Desks.NumberOfWindowsClosed2.Api", 2,
                                      1);
  EXPECT_EQ(1u, desks_controller->desks().size());
}

// Tests that if we've been in the library, then switched to a different desk,
// and then save the desk, that the desk is closed. Regression test for
// https://crbug.com/1329350.
IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       // TODO(crbug.com/40240645): Re-enable this test
                       DISABLED_SystemUIReEnterLibraryAndSaveDesk) {
  // Create a template that has a window because the "Save desk for later"
  // button is not enabled on empty desks.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  ash::DesksController* desks_controller = ash::DesksController::Get();
  const auto& desks = desks_controller->desks();
  ASSERT_EQ(1u, desks.size());
  ASSERT_TRUE(desks[0]->ContainsAppWindows());

  // Click on the "Use template" button to launch the template.
  ClickFirstTemplateItem();

  // Verify that a new desk has been created and activated, and that it has app
  // windows.
  ASSERT_EQ(2ul, desks.size());
  ASSERT_EQ(1, desks_controller->GetActiveDeskIndex());
  ASSERT_TRUE(desks_controller->active_desk()->ContainsAppWindows());

  // Now save the desk. This should close the desk.
  auto* overview_grid = ash::GetOverviewSession()->GetGridWithRootWindow(
      ash::Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  auto* save_desk_button = overview_grid->GetSaveDeskForLaterButton();
  ASSERT_TRUE(save_desk_button);

  // Wait for the bounds to finish animating.
  ash::ShellTestApi().WaitForWindowFinishAnimating(
      save_desk_button->GetWidget()->GetNativeWindow());
  ClickView(save_desk_button);
  ash::WaitForSavedDeskUI();

  // Wait for the browser to close.
  ui_test_utils::WaitForBrowserToClose();

  // Verify that we're back to one desk.
  EXPECT_EQ(1u, desks.size());
}

// Tests trying to remove an invalid desk Id should return error.
IN_PROC_BROWSER_TEST_F(DesksClientTest, RemoveWithInvalidDeskId) {
  auto* desks_controller = ash::DesksController::Get();
  // Should have 1 default desk.
  EXPECT_EQ(1u, desks_controller->desks().size());
  // Construct an empty invalid desk_id.
  base::Uuid desk_id;
  EXPECT_THAT(DesksClient::Get()->RemoveDesk(
                  desk_id, ash::DeskCloseType::kCloseAllWindows),
              testing::Optional(DesksClient::DeskActionError::kInvalidIdError));
  EXPECT_EQ(1u, desks_controller->desks().size());
}

// Tests list all available desks. Remove desk should fail when there is only
// one desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, GetAllDesksAndRemove) {
  auto* desks_controller = ash::DesksController::Get();
  // Should have 1 default active desk.
  EXPECT_EQ(1u, desks_controller->desks().size());

  base::RunLoop loop;
  // Retrieve desk id;
  base::Uuid desk_id;
  auto desks = DesksClient::Get()->GetAllDesks();
  ASSERT_TRUE(desks.has_value());
  ASSERT_EQ(1u, desks.value().size());
  desk_id = desks.value().at(0)->uuid();

  EXPECT_THAT(DesksClient::Get()->RemoveDesk(
                  desk_id, ash::DeskCloseType::kCloseAllWindows),
              testing::Optional(
                  DesksClient::DeskActionError::kDesksCountCheckFailedError));
}

// Tests launch an empty desk with `desk_name` provided.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchEmptyDeskWithProvidedName) {
  auto* desks_controller = ash::DesksController::Get();
  // Should have 1 default active desk.
  EXPECT_EQ(1u, desks_controller->desks().size());

  base::RunLoop loop;
  std::u16string desk_name(u"test");
  ash::DeskSwitchAnimationWaiter waiter;
  auto result = DesksClient::Get()->LaunchEmptyDesk(desk_name);

  ASSERT_TRUE(result.has_value());
  // Launch one template, desk size should increase by 1.
  ASSERT_EQ(2u, desks_controller->desks().size());
  // `desk_name` should be set as provided
  EXPECT_EQ(desk_name, desks_controller->desks().back()->name());
  // `desk_uuid` should be returned.
  EXPECT_GT(result.value().AsLowercaseString().size(), 0u);
  waiter.Wait();
}

// Tests launch an empty desk with default name.
IN_PROC_BROWSER_TEST_F(DesksClientTest, LaunchEmptyDeskWithDefaultName) {
  auto* desks_controller = ash::DesksController::Get();
  // Should have 1 default active desk.
  EXPECT_EQ(1u, desks_controller->desks().size());

  base::RunLoop loop;
  ash::DeskSwitchAnimationWaiter waiter;
  auto result = DesksClient::Get()->LaunchEmptyDesk();
  ASSERT_TRUE(result.has_value());
  // Launch one template, desk size should increase by 1.
  ASSERT_EQ(2u, desks_controller->desks().size());
  // `desk_name` should be set as default desk name
  EXPECT_EQ(u"Desk 2", desks_controller->desks().back()->name());
  // `desk_uuid` should be returned.
  EXPECT_GT(result.value().AsLowercaseString().size(), 0u);
  waiter.Wait();
}

IN_PROC_BROWSER_TEST_F(DesksClientTest, UndoLaunchedDesk) {
  auto* desks_controller = ash::DesksController::Get();

  // Launch a new desk
  ash::NewDesk();
  EXPECT_EQ(2, desks_controller->GetNumberOfDesks());

  // Remove the desk with option to undo
  ash::Desk* testDesk = desks_controller->GetDeskAtIndex(1);
  DesksClient::Get()->RemoveDesk(testDesk->uuid(),
                                 ash::DeskCloseType::kCloseAllWindowsAndWait);
  EXPECT_EQ(1, desks_controller->GetNumberOfDesks());

  // Undo the removal
  desks_controller->MaybeCancelDeskRemoval();
  EXPECT_EQ(2, desks_controller->GetNumberOfDesks());
}

// Tests setting first window to show on all desk and then unset it.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SetWindowProperties) {
  // Create a new browser window.
  CreateBrowser({});

  auto* desks_controller = ash::DesksController::Get();

  // Start with no all-desk window.
  EXPECT_EQ(0u, desks_controller->visible_on_all_desks_windows().size());

  // Get the first browser window.
  SessionID browser_session_id =
      BrowserList::GetInstance()->get(0)->session_id();

  // Set to all-desk window.
  // Assert no error.
  ASSERT_FALSE(DesksClient::Get()->SetAllDeskPropertyByBrowserSessionId(
      browser_session_id, true));
  // Should have 1 all-desk window now.
  EXPECT_EQ(1u, desks_controller->visible_on_all_desks_windows().size());

  // Unset all-desk window.
  // Assert no error.
  ASSERT_FALSE(DesksClient::Get()->SetAllDeskPropertyByBrowserSessionId(
      browser_session_id, false));
  // Should have no all-desk window now.
  EXPECT_EQ(0u, desks_controller->visible_on_all_desks_windows().size());
}

// Tests immediate desk action should be throttled.
IN_PROC_BROWSER_TEST_F(DesksClientTest, ThrottleImmediateDeskAction) {
  base::Uuid new_desk_id;
  ash::DeskSwitchAnimationWaiter waiter;
  auto result = DesksClient::Get()->LaunchEmptyDesk();
  ASSERT_TRUE(result.has_value());
  new_desk_id = result.value();

  EXPECT_THAT(DesksClient::Get()->RemoveDesk(
                  new_desk_id, ash::DeskCloseType::kCloseAllWindows),
              testing::Optional(
                  DesksClient::DeskActionError::kDesksBeingModifiedError));

  auto result1 = DesksClient::Get()->LaunchEmptyDesk();
  EXPECT_EQ(DesksClient::DeskActionError::kDesksBeingModifiedError,
            result1.error());
  EXPECT_THAT(DesksClient::Get()->SwitchDesk(new_desk_id),
              testing::Optional(
                  DesksClient::DeskActionError::kDesksBeingModifiedError));
  waiter.Wait();
}

// Tests save an empty desk should fail.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SaveEmptyDesk) {
  // Create a new browser and add a few tabs to it.
  Browser* browser = CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kSaveAndRecall);
  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Check the urls are captured correctly in the `saved_desk`.
  EXPECT_EQ(data->browser_extra_info.urls, urls);

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  // An empty desk should be created.
  EXPECT_EQ(ash::DesksController::Get()->GetNumberOfDesks(), 1);
  EXPECT_EQ(ash::DesksController::Get()->active_desk()->windows().size(), 0u);
}

// Tests save an active desk to library and remove it from desk list.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SaveActiveDesk) {
  // Create a new browser and add a few tabs to it.
  Browser* browser = CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  aura::Window* window = browser->window()->GetNativeWindow();

  const int32_t browser_window_id =
      window->GetProperty(app_restore::kWindowIdKey);
  // Get current tabs from browser.
  std::vector<GURL> urls = GetURLsForBrowserWindow(browser);

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kSaveAndRecall);
  const app_restore::AppRestoreData* data = ash::QueryRestoreData(
      *desk_template, app_constants::kChromeAppId, browser_window_id);
  ASSERT_TRUE(data);

  // Check the urls are captured correctly in the `saved_desk`.
  EXPECT_EQ(data->browser_extra_info.urls, urls);

  // Exit overview.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  // An empty desk should be created.
  EXPECT_EQ(ash::DesksController::Get()->GetNumberOfDesks(), 1);
  EXPECT_EQ(ash::DesksController::Get()->active_desk()->windows().size(), 0u);
}

// Tests delete a saved desk from library.
IN_PROC_BROWSER_TEST_F(DesksClientTest, DeleteSavedDesk) {
  auto* desk_model = DesksClient::Get()->GetDeskModel();

  // Create a new browser and add a few tabs to it.
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kSaveAndRecall);
  EXPECT_EQ(1u, desk_model->GetEntryCount());

  DeleteDeskTemplate(desk_template->uuid());

  EXPECT_EQ(0u, desk_model->GetEntryCount());
}

// Tests recall a saved desk from library.
IN_PROC_BROWSER_TEST_F(DesksClientTest, RecallSavedDesk) {
  auto* desk_model = DesksClient::Get()->GetDeskModel();
  EXPECT_EQ(ash::DesksController::Get()->GetNumberOfDesks(), 1);

  // Create a new browser and add a few tabs to it.
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});

  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kSaveAndRecall);

  EXPECT_EQ(1u, desk_model->GetEntryCount());
  EXPECT_EQ(ash::DesksController::Get()->GetNumberOfDesks(), 1);

  // Restart browser process.
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  base::RunLoop loop;
  DesksClient::Get()->LaunchDeskTemplate(
      desk_template->uuid(),
      base::BindLambdaForTesting(
          [desk_model, &loop](std::optional<DesksClient::DeskActionError> error,
                              const base::Uuid& desk_uuid) {
            EXPECT_EQ(ash::DesksController::Get()->GetNumberOfDesks(), 2);
            EXPECT_EQ(0u, desk_model->GetEntryCount());
            loop.Quit();
          }));
  loop.Run();
}

// Tests switch to current desk should be no ops.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SwitchToCurrentDesk) {
  base::Uuid current_desk_uuid;
  current_desk_uuid = DesksClient::Get()->GetActiveDesk();

  // Expect no error message.
  EXPECT_FALSE(DesksClient::Get()->SwitchDesk(current_desk_uuid));

  base::Uuid desk_uuid = DesksClient::Get()->GetActiveDesk();
  EXPECT_EQ(current_desk_uuid, desk_uuid);
}

// Tests switch to invalid desk should return error.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SwitchToInvalidDesk) {
  EXPECT_THAT(
      DesksClient::Get()->SwitchDesk({}),
      testing::Optional(DesksClient::DeskActionError::kResourceNotFoundError));
}

// Tests switch to different desk should be trigger desk animation.
IN_PROC_BROWSER_TEST_F(DesksClientTest, SwitchToDifferentDesk) {
  base::Uuid desk_uuid = DesksClient::Get()->GetActiveDesk();

  // Launches a new desk.
  auto result = DesksClient::Get()->LaunchEmptyDesk();
  ASSERT_TRUE(result.has_value());

  // Wait for launch desk animation to settle.
  ash::DeskSwitchAnimationWaiter waiter;
  waiter.Wait();

  // Switches to previous desk. Expect no error message.
  EXPECT_FALSE(DesksClient::Get()->SwitchDesk(desk_uuid));

  // Wait for desk switch animation.
  ash::DeskSwitchAnimationWaiter waiter_;
  waiter_.Wait();
  base::Uuid desk_uuid_ = DesksClient::Get()->GetActiveDesk();
  EXPECT_EQ(desk_uuid_, desk_uuid);
}

// Tests retrieve an existing desk.
IN_PROC_BROWSER_TEST_F(DesksClientTest, GetDeskByValidDeskId) {
  base::Uuid desk_uuid = DesksClient::Get()->GetActiveDesk();
  auto result = DesksClient::Get()->GetDeskByID(desk_uuid);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(result.value()->uuid(), desk_uuid);
}

// Tests retrieve an non-exist desk should return error.
IN_PROC_BROWSER_TEST_F(DesksClientTest, GetDeskByInvalidDeskId) {
  auto result = DesksClient::Get()->GetDeskByID(base::Uuid::GenerateRandomV4());

  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(result.error(),
            DesksClient::DeskActionError::kResourceNotFoundError);
}

// Tests that floating workspace templates do not count towards template counts
// for saved desks functionality.
IN_PROC_BROWSER_TEST_F(DesksClientTest, FloatingWorkspaceOnSavedDesksUI) {
  // Create a new browser and add a few tabs to it.
  CreateBrowser({GURL(kExampleUrl1), GURL(kExampleUrl2)});
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(
          ash::DeskTemplateType::kFloatingWorkspace);
  EXPECT_TRUE(desk_template->uuid().is_valid());
  EXPECT_EQ(desk_template->type(), ash::DeskTemplateType::kFloatingWorkspace);

  auto* desk_model = DesksClient::Get()->GetDeskModel();
  ASSERT_EQ(0u, desk_model->GetEntryCount());

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  // Tests that since we have no saved desk right now, so the library button is
  // hidden.
  const views::Button* library_button = ash::GetLibraryButton();
  ASSERT_TRUE(library_button);
  EXPECT_FALSE(library_button->GetVisible());
}

IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       DisplaysAppUnavailableToastForUnavailableBrowserApp) {
  // Build a saved desk with an unsupoorted browser app.
  std::unique_ptr<ash::DeskTemplate> unsupported_template =
      desks_storage::SavedDeskBuilder()
          .AddAppWindow(
              desks_storage::SavedDeskBrowserBuilder()
                  .SetIsApp(true)
                  .SetIsLacros(false)
                  .SetUrls({GURL(kExampleUrl1)})
                  .SetGenericBuilder(desks_storage::SavedDeskGenericAppBuilder()
                                         .SetWindowId(kTestWindowId)
                                         .SetName(kUnknownTestAppName))
                  .Build())
          .Build();

  // Programmatically add to storage, we do this because you cannot capture
  // an unsupported app.
  base::RunLoop loop;
  DesksClient::Get()->GetDeskModel()->AddOrUpdateEntry(
      std::move(unsupported_template),
      base::BindLambdaForTesting(
          [&](desks_storage::DeskModel::AddOrUpdateEntryStatus status,
              std::unique_ptr<ash::DeskTemplate> new_entry) {
            EXPECT_EQ(desks_storage::DeskModel::AddOrUpdateEntryStatus::kOk,
                      status);
            loop.Quit();
          }));
  loop.Run();

  // Enter overview and launch template
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickLibraryButton();
  ClickFirstTemplateItem();

  // Spin in case we need to wait for the toast to appear.
  SPIN_FOR_TIMEDELTA_OR_UNTIL_TRUE(
      base::Seconds(45),
      ash::ToastManager::Get()->IsToastShown(
          chrome_desks_util::kAppNotAvailableTemplateToastName));
}

IN_PROC_BROWSER_TEST_F(DesksClientTest,
                       DisplaysAppUnavailableToastForUnavailableGenericApp) {
  // Build a saved desk with an unsupoorted generic app.
  std::unique_ptr<ash::DeskTemplate> unsupported_template =
      desks_storage::SavedDeskBuilder()
          .AddAppWindow(desks_storage::SavedDeskGenericAppBuilder()
                            .SetWindowId(kTestWindowId)
                            .SetAppId(kUnknownTestAppId)
                            .Build())
          .Build();

  // We will need to add `kUnknownAppId` to the app registry cache so that
  // it will be stored properly.  We will make sure that its readiness will
  // trigger the toast on launch. We use kArc so that it will be a supported
  // type for a template, however its readiness will remain kUnknown which
  // should trigger the toast.
  std::vector<apps::AppPtr> deltas;
  deltas.push_back(
      std::make_unique<apps::App>(apps::AppType::kArc, kUnknownTestAppId));
  apps::AppServiceProxyFactory::GetForProfile(browser()->profile())
      ->OnApps(std::move(deltas), apps::AppType::kArc,
               /*should_notify_initialized=*/false);

  // Programmatically add to storage, we do this because you cannot capture
  // an unsupported app.
  base::RunLoop loop;
  DesksClient::Get()->GetDeskModel()->AddOrUpdateEntry(
      std::move(unsupported_template),
      base::BindLambdaForTesting(
          [&](desks_storage::DeskModel::AddOrUpdateEntryStatus status,
              std::unique_ptr<ash::DeskTemplate> new_entry) {
            EXPECT_EQ(desks_storage::DeskModel::AddOrUpdateEntryStatus::kOk,
                      status);
            loop.Quit();
          }));
  loop.Run();

  // Enter overview and launch template
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickLibraryButton();
  ClickFirstTemplateItem();

  // Spin in case we need to wait for the toast to appear.
  SPIN_FOR_TIMEDELTA_OR_UNTIL_TRUE(
      base::Seconds(45),
      ash::ToastManager::Get()->IsToastShown(
          chrome_desks_util::kAppNotAvailableTemplateToastName));
}

class DesksTemplatesClientLacrosTest : public InProcessBrowserTest {
 public:
  DesksTemplatesClientLacrosTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{ash::features::kDesksTemplates},
        /*disabled_features=*/{ash::features::kDeskTemplateSync});
  }
  DesksTemplatesClientLacrosTest(const DesksTemplatesClientLacrosTest&) =
      delete;
  DesksTemplatesClientLacrosTest& operator=(
      const DesksTemplatesClientLacrosTest&) = delete;
  ~DesksTemplatesClientLacrosTest() override = default;

  // InProcessBrowserTest:
  void SetUpInProcessBrowserTestFixture() override {
    if (!ash_starter_.HasLacrosArgument()) {
      return;
    }

    ASSERT_TRUE(ash_starter_.PrepareEnvironmentForLacros());
  }

  void SetUpOnMainThread() override {
    if (!ash_starter_.HasLacrosArgument()) {
      return;
    }

    // `StartLacros()` will bring up one lacros browser. There will also be one
    // classic browser from `InProcessBrowserTest` that can be accessed with
    // `browser()`.
    LacrosWindowWaiter waiter;
    ash_starter_.StartLacros(this);
    std::ignore = waiter.Wait(/*expected_count=*/1u);
  }

 protected:
  // Helper class which waits for lacros windows to become visible.
  class LacrosWindowWaiter : public aura::WindowObserver {
   public:
    LacrosWindowWaiter() {
      window_observation_.Observe(ash::Shell::GetPrimaryRootWindow());
    }
    LacrosWindowWaiter(const LacrosWindowWaiter&) = delete;
    LacrosWindowWaiter& operator=(const LacrosWindowWaiter&) = delete;
    ~LacrosWindowWaiter() override = default;

    // Spins the loop and waits for `expected_count` number of lacros windows to
    // become visible.
    aura::Window::Windows Wait(size_t expected_count) {
      DCHECK(windows_.empty());
      DCHECK_GT(expected_count, 0u);

      expected_count_ = expected_count;
      run_loop_.Run();
      return windows_;
    }

    // aura::WindowObserver::
    void OnWindowVisibilityChanged(aura::Window* window,
                                   bool visible) override {
      if (!visible || !crosapi::browser_util::IsLacrosWindow(window)) {
        return;
      }

      windows_.push_back(window);
      if (windows_.size() < expected_count_) {
        return;
      }

      run_loop_.Quit();
    }

   private:
    size_t expected_count_ = 0u;

    // The vector of lacros windows that where shown while waiting.
    aura::Window::Windows windows_;

    base::RunLoop run_loop_;

    base::ScopedObservation<aura::Window, aura::WindowObserver>
        window_observation_{this};
  };

  base::test::ScopedFeatureList scoped_feature_list_;
  test::AshBrowserTestStarter ash_starter_;
};

// Tests launching a template with a browser window.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientLacrosTest, SystemUILaunchBrowser) {
  if (!ash_starter_.HasLacrosArgument()) {
    return;
  }

  ASSERT_TRUE(crosapi::BrowserManager::Get()->IsRunning());

  // Enter overview and save the current desk as a template. The current desk
  // has one lacros browser.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  // Launch the saved desk template. We expect one launched lacros windows.
  // Check the launched windows will have data in `app_restore::kWindowInfoKey`,
  // otherwise ash does not know that they are launched from desk templates. See
  // https://crbug.com/1333965 for more details.
  LacrosWindowWaiter waiter;
  ClickFirstTemplateItem();
  aura::Window::Windows launched_windows = waiter.Wait(/*expected_count=*/1u);
  ASSERT_EQ(1u, launched_windows.size());
  for (aura::Window* window : launched_windows) {
    EXPECT_TRUE(window->GetProperty(app_restore::kWindowInfoKey));
  }

  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
}

// Tests that app browsers are captured correctly, coverage for launching
// lacros browsers can be found in
// c/b/lacros/desk_template_client_browsertest.cc This simply confirms that the
// chrome desk client handles apps correctly when converting the returned mojom
// from crosapi to app_launch_info.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientLacrosTest,
                       DISABLED_CapturesLacrosAppCorrectly) {
  // Prevents test from running when running in a build without lacros.
  if (!ash_starter_.HasLacrosArgument()) {
    return;
  }

  ASSERT_TRUE(crosapi::BrowserManager::Get()->IsRunning());

  // Add our browser under test, this is the only way to launch an app
  // via the BrowserManager.
  crosapi::BrowserManager::Get()->CreateBrowserWithRestoredData(
      {GURL(kExampleUrl1)}, {0, 0, 256, 256}, {},
      ui::WindowShowState::SHOW_STATE_DEFAULT,
      /*active_tab_index=*/0, /*first_non_pinned_tab_index=*/0, kTestAppName,
      kTestWindowId, /*lacros_profile_id=*/0);
  LacrosWindowWaiter waiter;
  aura::Window::Windows launched_windows = waiter.Wait(/*expected_count=*/1u);
  ASSERT_EQ(1u, launched_windows.size());

  // Enter overview and save the current desk as a template. The current desk
  // has one lacros app and the default browser.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskAsTemplateButton();

  // Grab all entries to assert.
  const std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>>
      all_entries = GetAllEntries();
  ASSERT_EQ(all_entries.size(), 1u);

  // Since we only have one template grab the first one.
  const app_restore::RestoreData* desk_restore_data =
      all_entries[0]->desk_restore_data();
  ASSERT_NE(desk_restore_data, nullptr);

  // Through an exhaustive process of trial and error we cannot retrieve our
  // desired window through any other means than running a search for the app
  // name in the mapping of window ID's to app launch info.  There isn't a way,
  // currently at least, within this test class or through the crosapi
  // to close the default browser.  We can't use the test's class' close browser
  // method either as both have been tried and failed to successfully close the
  // window.  Furthermore obscuring the window from the capture logic doesn't
  // seem to work in the testing logic either.  Simply using the window ID the
  // browser was launched with doesn't work either because the window ID is
  // changed on capture.
  const auto& app_id_to_launch_list =
      desk_restore_data->app_id_to_launch_list();
  const auto& launch_list =
      app_id_to_launch_list.at(app_constants::kLacrosAppId);
  const app_restore::AppRestoreData* actual_app_data = nullptr;

  for (const auto& it : launch_list) {
    if (it.second->browser_extra_info.app_name.value_or("") == kTestAppName) {
      actual_app_data = it.second.get();
      break;
    }
  }
  ASSERT_TRUE(actual_app_data);

  // Finally assert we set the relevant fields properly.
  EXPECT_THAT(actual_app_data->browser_extra_info.app_type_browser,
              testing::Optional((true)));
}

using SaveAndRecallBrowserTest = DesksClientTest;

IN_PROC_BROWSER_TEST_F(SaveAndRecallBrowserTest,
                       SystemUIBlockingDialogAccepted) {
  // TODO(http://b/350771229): This test tests clicking the "Save desk for
  // later" button that will not be shown if the feature is enabled. This test
  // will be fixed before the button change is no longer hidden behind the flag.
  if (ash::features::IsSavedDeskUiRevampEnabled()) {
    GTEST_SKIP() << "Skipping test body for saved desk revamp feature.";
  }

  SetupBrowserToConfirmClose(browser());

  // We'll now save the desk as Save & Recall. After saving desks, this
  // operation will try to automatically close windows.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskForLaterButton();
  ash::WaitForSavedDeskUI();

  ash::SavedDeskPresenterTestApi::WaitForSaveAndRecallBlockingDialog();
  EXPECT_EQ(1u, chrome::GetTotalBrowserCount());

  // Send a key to OK the close dialog.
  BrowsersRemovedObserver browsers_removed(/*browser_removes_expected=*/1);
  SendKey(ui::VKEY_RETURN);
  browsers_removed.Wait();

  EXPECT_EQ(0u, chrome::GetTotalBrowserCount());

  // Verify that we are in the library and that there's one saved desk.
  auto* overview_grid = ash::GetOverviewSession()->GetGridWithRootWindow(
      ash::Shell::GetPrimaryRootWindow());
  ASSERT_TRUE(overview_grid);
  EXPECT_TRUE(overview_grid->IsShowingSavedDeskLibrary());

  std::vector<raw_ptr<const ash::DeskTemplate, VectorExperimental>> templates =
      GetAllEntries();
  EXPECT_EQ(1u, templates.size());
}

IN_PROC_BROWSER_TEST_F(SaveAndRecallBrowserTest,
                       SystemUIBlockingDialogRejected) {
  // TODO(http://b/350771229): This test tests clicking the "Save desk for
  // later" button that will not be shown if the feature is enabled. This test
  // will be fixed before the button change is no longer hidden behind the flag.
  if (ash::features::IsSavedDeskUiRevampEnabled()) {
    GTEST_SKIP() << "Skipping test body for saved desk revamp feature.";
  }

  SetupBrowserToConfirmClose(browser());

  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();
  ClickSaveDeskForLaterButton();
  ash::WaitForSavedDeskUI();

  ash::SavedDeskPresenterTestApi::WaitForSaveAndRecallBlockingDialog();
  EXPECT_EQ(1u, chrome::GetTotalBrowserCount());

  // Send escape to cancel the dialog (keep the browser running).
  SendKey(ui::VKEY_ESCAPE);
  content::RunAllTasksUntilIdle();

  ash::SavedDeskPresenterTestApi::FireWindowWatcherTimer();

  EXPECT_EQ(1u, chrome::GetTotalBrowserCount());

  // We should be in overview mode.
  ASSERT_TRUE(ash::Shell::Get()->overview_controller()->overview_session());
}
// TODO(crbug.com/40228006): Add some tests to launch LaCros browser.

class SnapGroupDesksClientTest : public DesksClientTest {
 public:
  SnapGroupDesksClientTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{ash::features::kSnapGroup},
        /*disabled_features=*/{});
  }
  SnapGroupDesksClientTest(const SnapGroupDesksClientTest&) = delete;
  SnapGroupDesksClientTest& operator=(const SnapGroupDesksClientTest&) = delete;
  ~SnapGroupDesksClientTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_F(SnapGroupDesksClientTest, DesksTemplates) {
  // Create 1 other window to create a snap group.
  Browser* browser2 = CreateBrowser({GURL(kExampleUrl2)});
  aura::Window* w1 = browser()->window()->GetNativeWindow();
  aura::Window* w2 = browser2->window()->GetNativeWindow();

  auto* snap_group_controller = ash::SnapGroupController::Get();
  ASSERT_TRUE(snap_group_controller);
  const ash::WindowSnapWMEvent snap_primary(
      ash::WM_EVENT_SNAP_PRIMARY, chromeos::kTwoThirdSnapRatio,
      ash::WindowSnapActionSource::kDragWindowToEdgeToSnap);
  auto* window_state1 = ash::WindowState::Get(w1);
  window_state1->OnWMEvent(&snap_primary);
  const ash::WindowSnapWMEvent snap_secondary(
      ash::WM_EVENT_SNAP_SECONDARY, chromeos::kOneThirdSnapRatio,
      ash::WindowSnapActionSource::kDragWindowToEdgeToSnap);
  auto* window_state2 = ash::WindowState::Get(w2);
  window_state2->OnWMEvent(&snap_secondary);
  EXPECT_TRUE(snap_group_controller->AreWindowsInSnapGroup(w1, w2));
  const gfx::Rect w1_bounds_before_restore = w1->GetBoundsInScreen();
  const gfx::Rect w2_bounds_before_restore = w2->GetBoundsInScreen();
  const float snap_ratio1 = window_state1->snap_ratio().value();
  const float snap_ratio2 = window_state2->snap_ratio().value();
  ASSERT_EQ(2u, BrowserList::GetInstance()->size());

  // Open overview and save the desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();

  // Launch the template.
  ClickFirstTemplateItem();
  content::RunAllTasksUntilIdle();

  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  BrowserList* browser_list = BrowserList::GetInstance();
  ASSERT_EQ(4u, browser_list->size());
  aura::Window* new_w1 = browser_list->get(3)->window()->GetNativeWindow();
  aura::Window* new_w2 = browser_list->get(2)->window()->GetNativeWindow();

  // The new windows will preserve the snap ratio but not added to a snap
  // group so no divider will be shown.
  // TODO(b/335294166): Implement SnapGroups for desks templates.
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(new_w1));
  EXPECT_FALSE(snap_group_controller->GetSnapGroupForGivenWindow(new_w2));
  EXPECT_THAT(ash::WindowState::Get(new_w1)->snap_ratio(),
              Optional(FloatNear(snap_ratio1, /*max_abs_error=*/0.01)));
  EXPECT_TRUE(new_w1->GetBoundsInScreen().ApproximatelyEqual(
      w1_bounds_before_restore,
      /*tolerance=*/ash::kSplitviewDividerShortSideLength));
  EXPECT_THAT(ash::WindowState::Get(new_w2)->snap_ratio(),
              Optional(FloatNear(snap_ratio2, /*max_abs_error=*/0.01)));
  EXPECT_TRUE(new_w2->GetBoundsInScreen().ApproximatelyEqual(
      w2_bounds_before_restore,
      /*tolerance=*/ash::kSplitviewDividerShortSideLength));
}

class DesksTemplatesClientArcTest : public InProcessBrowserTest {
 public:
  DesksTemplatesClientArcTest() {
    std::vector<base::test::FeatureRef> enabled_features = {
        ash::features::kDesksTemplates};
    std::vector<base::test::FeatureRef> disabled_features = {
        ash::features::kDeskTemplateSync};
    scoped_feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }
  DesksTemplatesClientArcTest(const DesksTemplatesClientArcTest&) = delete;
  DesksTemplatesClientArcTest& operator=(const DesksTemplatesClientArcTest&) =
      delete;
  ~DesksTemplatesClientArcTest() override = default;

  ash::AppRestoreArcTestHelper* arc_helper() { return &arc_helper_; }

  // InProcessBrowserTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    arc_helper_.SetUpCommandLine(command_line);
    InProcessBrowserTest::SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    arc_helper_.SetUpInProcessBrowserTestFixture();
    InProcessBrowserTest::SetUpInProcessBrowserTestFixture();
  }

  void SetUpOnMainThread() override {
    arc_helper_.SetUpOnMainThread(browser()->profile());
    InProcessBrowserTest::SetUpOnMainThread();
  }

 private:
  ash::AppRestoreArcTestHelper arc_helper_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that launching a template that contains an ARC app works as expected.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientArcTest,
                       SystemUILaunchTemplateWithArcApp) {
  auto* desk_model = DesksClient::Get()->GetDeskModel();
  ASSERT_EQ(0u, desk_model->GetEntryCount());

  constexpr char kTestAppPackage[] = "test.arc.app.package";
  arc_helper()->InstallTestApps(kTestAppPackage, /*multi_app=*/false);
  const std::string app_id = ash::GetTestApp1Id(kTestAppPackage);

  int32_t session_id1 =
      full_restore::FullRestoreSaveHandler::GetInstance()->GetArcSessionId();

  // Create the window for app1. The task id needs to match the
  // `window_app_id` arg of `CreateExoWindow`.
  const int32_t kTaskId1 = 100;
  views::Widget* widget = ash::CreateExoWindow("org.chromium.arc.100");
  widget->SetBounds(gfx::Rect(500, 500));
  full_restore::SaveAppLaunchInfo(
      browser()->profile()->GetPath(),
      std::make_unique<app_restore::AppLaunchInfo>(
          app_id, ui::EF_NONE, session_id1, display::kDefaultDisplayId));

  // Simulate creating the task.
  arc_helper()->CreateTask(app_id, kTaskId1, session_id1);

  // Enter overview and save the current desk as a template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickSaveDeskAsTemplateButton();
  ASSERT_EQ(1u, desk_model->GetEntryCount());

  // Exit overview and close the Arc window. We'll need to verify if it
  // reopens later.
  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();
  widget->CloseNow();
  arc_helper()->GetAppHost()->OnTaskDestroyed(kTaskId1);

  // Enter overview, head over to the desks templates grid and launch the
  // template.
  ash::ToggleOverview();
  ash::WaitForOverviewEnterAnimation();

  ClickLibraryButton();
  ClickFirstTemplateItem();

  ash::ToggleOverview();
  ash::WaitForOverviewExitAnimation();

  // Create the window to simulate launching the ARC app.
  const int32_t kTaskId2 = 200;
  auto* widget1 = ash::CreateExoWindow("org.chromium.arc.200");
  auto* window1 = widget1->GetNativeWindow();
  arc_helper()->CreateTask(app_id, kTaskId2, session_id1);

  // Tests that the ARC app is launched on desk 2.
  EXPECT_EQ(ash::Shell::GetContainer(window1->GetRootWindow(),
                                     ash::kShellWindowId_DeskContainerB),
            window1->parent());

  widget1->CloseNow();
  arc_helper()->GetAppHost()->OnTaskDestroyed(kTaskId2);
  arc_helper()->StopInstance();
}

class DesksTemplatesClientPolicyTest : public policy::PolicyTest {
 public:
  void SetDeskTemplateEnabledPolicy(bool policy_value) {
    policy::PolicyMap policies;
    policies.Set(policy::key::kDeskTemplatesEnabled,
                 policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
                 policy::POLICY_SOURCE_CLOUD, base::Value(policy_value),
                 nullptr);
    UpdateProviderPolicy(policies);
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
};

class DesksTemplatesClientPolicyWithFeatureEnabledTest
    : public DesksTemplatesClientPolicyTest {
 public:
  DesksTemplatesClientPolicyWithFeatureEnabledTest() {
    scoped_feature_list_.Reset();
    scoped_feature_list_.InitAndEnableFeature(ash::features::kDesksTemplates);
  }
};

// Tests that the desks templates feature should be controlled by policy.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientPolicyWithFeatureEnabledTest,
                       CanBeControlledByPolicy) {
  const PrefService* prefs = chrome_test_utils::GetProfile(this)->GetPrefs();

  EXPECT_FALSE(
      prefs->FindPreference(ash::prefs::kDeskTemplatesEnabled)->IsManaged());

  // Without setting up the enterprise policy, desk templates feature is
  // controlled by feature flag, which is enabled in this test.
  EXPECT_TRUE(ash::saved_desk_util::AreDesksTemplatesEnabled());

  // Disable desk templates through policy.
  SetDeskTemplateEnabledPolicy(false);
  // Desk templates feature should be disabled, despite feature flag is set to
  // enabled.
  EXPECT_FALSE(ash::saved_desk_util::AreDesksTemplatesEnabled());

  // Enable desk templates through policy.
  SetDeskTemplateEnabledPolicy(true);
  // Desk templates feature should be enabled.
  EXPECT_TRUE(ash::saved_desk_util::AreDesksTemplatesEnabled());
}

class DesksTemplatesClientPolicyWithFeatureDisabledTest
    : public DesksTemplatesClientPolicyTest {
 public:
  DesksTemplatesClientPolicyWithFeatureDisabledTest() {
    scoped_feature_list_.Reset();
    scoped_feature_list_.Init();
  }
};

// Tests that the desks templates feature should be controlled by policy.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientPolicyWithFeatureDisabledTest,
                       CanBeControlledByPolicy) {
  const PrefService* prefs = chrome_test_utils::GetProfile(this)->GetPrefs();

  EXPECT_FALSE(
      prefs->FindPreference(ash::prefs::kDeskTemplatesEnabled)->IsManaged());

  // Without setting up the enterprise policy, desk templates feature is
  // controlled by feature flag, which is disabled in this test.
  EXPECT_FALSE(ash::saved_desk_util::AreDesksTemplatesEnabled());

  // Disable desk templates through policy.
  SetDeskTemplateEnabledPolicy(false);
  // Desk templates feature should be disabled.
  EXPECT_FALSE(ash::saved_desk_util::AreDesksTemplatesEnabled());

  // Enable desk templates through policy.
  SetDeskTemplateEnabledPolicy(true);
  // Desk templates feature should be enabled, despite feature flag is set to
  // disabled.
  EXPECT_TRUE(ash::saved_desk_util::AreDesksTemplatesEnabled());
}

class DesksTemplatesClientMultiProfileTest : public ash::LoginManagerTest {
 public:
  DesksTemplatesClientMultiProfileTest() {
    login_mixin_.AppendRegularUsers(2);
    account_id1_ = login_mixin_.users()[0].account_id;
    account_id2_ = login_mixin_.users()[1].account_id;

    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{},
        /*disabled_features=*/{ash::features::kDeskTemplateSync});
  }
  ~DesksTemplatesClientMultiProfileTest() override = default;

  void SetUpOnMainThread() override {
    ash::LoginManagerTest::SetUpOnMainThread();

    LoginUser(account_id1_);
    ::full_restore::SetActiveProfilePath(
        ash::ProfileHelper::Get()
            ->GetProfileByAccountId(account_id1_)
            ->GetPath());

    WaitForDeskModel();
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
  ash::LoginManagerMixin login_mixin_{&mixin_host_};
  AccountId account_id1_;
  AccountId account_id2_;
};

IN_PROC_BROWSER_TEST_F(DesksTemplatesClientMultiProfileTest, MultiProfileTest) {
  CreateBrowser(ash::ProfileHelper::Get()->GetProfileByAccountId(account_id1_));
  // Capture the active desk, which contains the browser windows.
  std::unique_ptr<ash::DeskTemplate> desk_template =
      CaptureActiveDeskAndSaveTemplate(ash::DeskTemplateType::kTemplate);
  const app_restore::RestoreData* restore_data =
      desk_template->desk_restore_data();
  const auto& app_id_to_launch_list = restore_data->app_id_to_launch_list();
  EXPECT_EQ(1u, app_id_to_launch_list.size());

  EXPECT_EQ(1u, GetDeskTemplates().size());

  // Now switch to |account_id2_|. Test that the captured desk template can't
  // be accessed from |account_id2_|.
  ash::UserAddingScreen::Get()->Start();
  AddUser(account_id2_);

  // Make sure desk template storage async load completes after user switches.
  WaitForDeskModel();
  EXPECT_EQ(0u, GetDeskTemplates().size());
}

// Flakily failing https://crbug.com/1447440
// Tests that admin templates policy can be set.
IN_PROC_BROWSER_TEST_F(DesksTemplatesClientMultiProfileTest,
                       SetAndClearAdminTemplates) {
  EXPECT_TRUE(DesksClient::Get());

  base::Uuid admin_template_uuid =
      base::Uuid::ParseCaseInsensitive(kTestAdminTemplateUuid);

  // Set an admin template policy.
  DesksClient::Get()->SetPolicyPreconfiguredTemplate(
      account_id1_, std::make_unique<std::string>(base::StringPrintf(
                        kTestAdminTemplateFormat, kTestAdminTemplateUuid)));

  // Verify that the admin templates is present.
  EXPECT_TRUE(ContainUuidInTemplates(admin_template_uuid, GetDeskTemplates()));

  // Clear admin templates.
  DesksClient::Get()->RemovePolicyPreconfiguredTemplate(account_id1_);

  // Verify that the admin templates is removed.
  EXPECT_FALSE(ContainUuidInTemplates(admin_template_uuid, GetDeskTemplates()));
}

class AdminTemplateTest : public extensions::PlatformAppBrowserTest {
 public:
  AdminTemplateTest() {
    // Suppress the multitask menu nudge as we'll be checking the stacking order
    // and the count of the active desk children.
    chromeos::MultitaskMenuNudgeController::SetSuppressNudgeForTesting(true);
  }
  AdminTemplateTest(const AdminTemplateTest&) = delete;
  AdminTemplateTest& operator=(const AdminTemplateTest&) = delete;
  ~AdminTemplateTest() override = default;

  // The definition of an admin template for a test.
  struct AdminTemplateDefinition {
    struct WindowDefinition {
      std::vector<std::string> urls;
      std::optional<gfx::Rect> bounds;
      std::optional<int32_t> activation_index;
    };

    std::vector<WindowDefinition> windows;

    bool should_launch_on_startup = false;
  };

  // Converts a `gfx::Rect` to a list, as expected by `RestoreData`.
  base::Value::List CreateBounds(const gfx::Rect& bounds) {
    base::Value::List list;
    list.Append(bounds.x());
    list.Append(bounds.y());
    list.Append(bounds.width());
    list.Append(bounds.height());
    return list;
  }

  // Creates an admin template with the windows and URLs given by `definition`.
  std::unique_ptr<ash::DeskTemplate> CreateAdminTemplate(
      const AdminTemplateDefinition& definition) {
    base::Value::Dict windows;
    for (size_t i = 0; i != definition.windows.size(); ++i) {
      base::Value::Dict window;
      window.Set("title", "Chrome");
      window.Set("window_state_type", 0);

      if (definition.windows[i].bounds) {
        window.Set("current_bounds",
                   CreateBounds(*definition.windows[i].bounds));
      }

      if (definition.windows[i].activation_index) {
        window.Set("index", *definition.windows[i].activation_index);
      }

      base::Value::List urls;
      for (const std::string& url : definition.windows[i].urls) {
        urls.Append(url);
      }
      window.Set("urls", std::move(urls));

      windows.Set(base::NumberToString(i + 1), std::move(window));
    }

    base::Value::Dict root;
    root.Set(app_constants::kChromeAppId, std::move(windows));

    // Policy for the admin template. The contents doesn't matter for the test
    // as long as the root is a dict.
    base::Value policy(base::Value::Dict{});

    auto admin_template = std::make_unique<ash::DeskTemplate>(
        base::Uuid::GenerateRandomV4(), ash::DeskTemplateSource::kPolicy,
        "Admin template", base::Time::Now(), ash::DeskTemplateType::kTemplate,
        definition.should_launch_on_startup, std::move(policy));

    admin_template->set_desk_restore_data(
        std::make_unique<app_restore::RestoreData>(
            base::Value(std::move(root))));

    return admin_template;
  }

  void WaitForAdminTemplateService() {
    auto* admin_template_service =
        ash::Shell::Get()->saved_desk_delegate()->GetAdminTemplateService();
    if (!admin_template_service) {
      return;
    }
    while (!admin_template_service->IsReady()) {
      base::RunLoop run_loop;
      run_loop.RunUntilIdle();
    }
  }

  void AddAdminTemplateToModel(
      std::unique_ptr<ash::DeskTemplate> admin_template) {
    WaitForAdminTemplateService();
    ash::AddSavedDeskEntry(ash::Shell::Get()
                               ->saved_desk_delegate()
                               ->GetAdminTemplateService()
                               ->GetFullDeskModel(),
                           std::move(admin_template));
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// TODO(b/273803538): Add tests for lacros.
IN_PROC_BROWSER_TEST_F(AdminTemplateTest, LaunchAdminTemplate) {
  // Launch an admin template with two browsers. Verifies that the browsers were
  // actually launched.  One browser will have an activation index, but it
  // it should be ignored.
  auto admin_template =
      CreateAdminTemplate({.windows = {{.urls = {kExampleUrl1},
                                        .bounds = gfx::Rect(100, 50, 400, 300)},
                                       {.urls = {kExampleUrl2},
                                        .bounds = gfx::Rect(100, 50, 400, 300),
                                        .activation_index = -100}}});
  ASSERT_NE(admin_template, nullptr);

  base::Uuid template_uuid = admin_template->uuid();
  AddAdminTemplateToModel(std::move(admin_template));

  auto* saved_desk_controller = ash::Shell::Get()->saved_desk_controller();

  saved_desk_controller->LaunchAdminTemplate(
      template_uuid, display::Screen::GetScreen()->GetPrimaryDisplay().id());

  // Verify that there are three browsers (two from the suite and one from the
  // test), and verify that our launched browsers are stacked on top.
  Browser* new_browser_one = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  Browser* new_browser_two = FindLaunchedBrowserByURLs({GURL(kExampleUrl2)});
  ASSERT_TRUE(new_browser_one);
  ASSERT_TRUE(new_browser_two);

  aura::Window* old_browser_window = browser()->window()->GetNativeWindow();
  aura::Window* new_browser_window_one =
      new_browser_one->window()->GetNativeWindow();
  aura::Window* new_browser_window_two =
      new_browser_two->window()->GetNativeWindow();

  // All browsers should be on the same desk.
  ASSERT_EQ(old_browser_window->parent(), new_browser_window_one->parent());
  ASSERT_EQ(old_browser_window->parent(), new_browser_window_two->parent());
  ASSERT_EQ(new_browser_window_one->parent(), new_browser_window_two->parent());

  // Verify that the new browser windows are stacked in front of the
  // existing window and that they are ordered relative to the order in their
  // template. Children are ordered from bottommost to topmost. We therefore
  // expect the new window to have an index that is higher than the old.
  const auto& container = new_browser_window_one->parent()->children();
  size_t new_index_one =
      base::ranges::find(container, new_browser_window_one) - container.begin();
  size_t new_index_two =
      base::ranges::find(container, new_browser_window_two) - container.begin();
  size_t old_index =
      base::ranges::find(container, old_browser_window) - container.begin();

  EXPECT_GT(new_index_one, new_index_two);
  EXPECT_GT(new_index_two, old_index);
}

IN_PROC_BROWSER_TEST_F(AdminTemplateTest, AdminTemplateWindowOffset) {
  // Launch an admin template with a single browser. Verifies that a browser was
  // actually launched.
  auto admin_template = CreateAdminTemplate(
      {.windows = {
           {.urls = {kExampleUrl1}, .bounds = gfx::Rect(50, 50, 640, 480)}}});
  ASSERT_NE(admin_template, nullptr);

  base::Uuid template_uuid = admin_template->uuid();
  AddAdminTemplateToModel(std::move(admin_template));

  auto* saved_desk_controller = ash::Shell::Get()->saved_desk_controller();
  // Launch the template twice.
  for (int i = 0; i != 2; ++i) {
    saved_desk_controller->LaunchAdminTemplate(
        template_uuid, display::Screen::GetScreen()->GetPrimaryDisplay().id());
  }

  auto browsers = FindLaunchedBrowsersByURLs({GURL(kExampleUrl1)});
  ASSERT_EQ(browsers.size(), 2u);

  // Verify that the two windows are not in the same position.
  aura::Window* window1 = browsers[0]->window()->GetNativeWindow();
  aura::Window* window2 = browsers[1]->window()->GetNativeWindow();
  EXPECT_NE(window1->bounds(), window2->bounds());
}

IN_PROC_BROWSER_TEST_F(AdminTemplateTest, AdminTemplateWindowUpdate) {
  // Launch an admin template with a browser. Move the browser window and verify
  // that the update callback is invoked.

  // Set up the display with a known size.
  display::test::DisplayManagerTestApi display_manager_test_api(
      ash::Shell::Get()->display_manager());
  display_manager_test_api.UpdateDisplay("1920x1080");

  // Create an admin template without bounds. The admin template will then be
  // launched with generated window bounds.
  auto admin_template = CreateAdminTemplate(
      {.windows = {{.urls = {kExampleUrl1}}, {.urls = {kExampleUrl2}}}});
  ASSERT_NE(admin_template, nullptr);

  // A template that will receive updates.
  std::unique_ptr<ash::DeskTemplate> updated_template;

  // Create a launch tracker with a zero update delay. This ensures that the
  // update callback is called synchronously.
  ash::AdminTemplateLaunchTracker launch_tracker(
      std::move(admin_template),
      base::BindLambdaForTesting([&](const ash::DeskTemplate& update) {
        updated_template = update.Clone();
      }),
      /*update_delay=*/base::TimeDelta());

  launch_tracker.LaunchTemplate(
      ash::Shell::Get()->saved_desk_delegate(),
      display::Screen::GetScreen()->GetPrimaryDisplay().id());

  Browser* browser1 = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  ASSERT_TRUE(browser1);
  Browser* browser2 = FindLaunchedBrowserByURLs({GURL(kExampleUrl2)});
  ASSERT_TRUE(browser2);

  // The update callback should have been invoked already and the template
  // should have bounds for both windows.
  ASSERT_TRUE(updated_template);

  // Verify that both windows have bounds and display set.
  const app_restore::AppRestoreData* data1 =
      ash::QueryRestoreData(*updated_template, {}, /*window_id=*/1);
  ASSERT_TRUE(data1);
  EXPECT_TRUE(data1->display_id.has_value());
  EXPECT_TRUE(data1->window_info.current_bounds.has_value());

  const app_restore::AppRestoreData* data2 =
      ash::QueryRestoreData(*updated_template, {}, /*window_id=*/2);
  ASSERT_TRUE(data2);
  EXPECT_TRUE(data2->display_id.has_value());
  EXPECT_TRUE(data2->window_info.current_bounds.has_value());

  // Clear the stored template. It will be populated again when the tracked
  // windows are updated.
  updated_template = nullptr;

  aura::Window* browser2_window = browser2->window()->GetNativeWindow();

  // Set the bounds of the two windows. This should result in the template
  // getting updated.
  const gfx::Rect window1_set_bounds(50, 50, 640, 480);
  browser1->window()->GetNativeWindow()->SetBounds(window1_set_bounds);
  const gfx::Rect window2_set_bounds(100, 100, 800, 600);
  browser2->window()->GetNativeWindow()->SetBounds(window2_set_bounds);

  // Reverse the Z order, we will use this to verify that the Z order is
  // persisted.
  browser2_window->parent()->StackChildAtTop(browser2_window);

  ASSERT_TRUE(updated_template);

  // Verify that both windows have had their bounds updated in the template.
  data1 = ash::QueryRestoreData(*updated_template, {}, /*window_id=*/1);
  ASSERT_TRUE(data1);
  EXPECT_THAT(data1->window_info.current_bounds, Optional(window1_set_bounds));

  data2 = ash::QueryRestoreData(*updated_template, {}, /*window_id=*/2);
  ASSERT_TRUE(data2);
  EXPECT_THAT(data2->window_info.current_bounds, Optional(window2_set_bounds));

  // Verify that Z ordering was preserved
  EXPECT_EQ(data1->window_info.activation_index, 0);
  EXPECT_EQ(data2->window_info.activation_index, -1);
}

IN_PROC_BROWSER_TEST_F(AdminTemplateTest, AdminTemplateHistograms) {
  base::HistogramTester histogram_tester;

  // Create an admin template two windows, which will have one tab or two tabs
  // respectively.
  auto admin_template = CreateAdminTemplate(
      {.windows = {{.urls = {kExampleUrl1}},
                   {.urls = {kExampleUrl2, kExampleUrl3}}}});
  ASSERT_NE(admin_template, nullptr);

  base::Uuid template_uuid = admin_template->uuid();

  auto* saved_desk_controller = ash::Shell::Get()->saved_desk_controller();
  ash::SavedDeskControllerTestApi(saved_desk_controller)
      .SetAdminTemplate(std::move(admin_template));

  saved_desk_controller->LaunchAdminTemplate(
      template_uuid, display::Screen::GetScreen()->GetPrimaryDisplay().id());

  histogram_tester.ExpectBucketCount(
      ash::kAdminTemplateWindowCountHistogramName, 2, 1);
  histogram_tester.ExpectBucketCount(ash::kAdminTemplateTabCountHistogramName,
                                     3, 1);
  histogram_tester.ExpectTotalCount(ash::kLaunchAdminTemplateHistogramName, 1);
}

IN_PROC_BROWSER_TEST_F(AdminTemplateTest, AdminTemplateAutoLaunch) {
  auto* saved_desk_controller = ash::Shell::Get()->saved_desk_controller();
  ash::SavedDeskControllerTestApi(saved_desk_controller).ResetAutoLaunch();

  // Add an auto-launch entry to the model.
  auto admin_template = CreateAdminTemplate(
      {.windows = {{.urls = {kExampleUrl1},
                    .bounds = gfx::Rect(100, 50, 400, 300)}},
       .should_launch_on_startup = true});
  ASSERT_NE(admin_template, nullptr);
  AddAdminTemplateToModel(std::move(admin_template));

  base::RunLoop run_loop;
  saved_desk_controller->InitiateAdminTemplateAutoLaunch(
      run_loop.QuitClosure());
  run_loop.Run();

  // Verify that a new browser has been launched.
  Browser* new_browser = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  ASSERT_TRUE(new_browser);
}

IN_PROC_BROWSER_TEST_F(AdminTemplateTest, AdminTemplateNoAutoLaunch) {
  auto* saved_desk_controller = ash::Shell::Get()->saved_desk_controller();
  ash::SavedDeskControllerTestApi(saved_desk_controller).ResetAutoLaunch();

  // Add an entry to the model that is not marked for auto-launch.
  auto admin_template = CreateAdminTemplate(
      {.windows = {{.urls = {kExampleUrl1},
                    .bounds = gfx::Rect(100, 50, 400, 300)}},
       .should_launch_on_startup = false});
  ASSERT_NE(admin_template, nullptr);
  AddAdminTemplateToModel(std::move(admin_template));

  base::RunLoop run_loop;
  saved_desk_controller->InitiateAdminTemplateAutoLaunch(
      run_loop.QuitClosure());
  run_loop.Run();

  // Verify that a new browser has *not* been launched.
  Browser* new_browser = FindLaunchedBrowserByURLs({GURL(kExampleUrl1)});
  ASSERT_FALSE(new_browser);
}