// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "ash/app_list/views/app_list_bubble_apps_page.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ash/ash_element_identifiers.h"
#include "ash/constants/web_app_id_constants.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/public/cpp/shelf_item.h"
#include "ash/public/cpp/shelf_model.h"
#include "ash/root_window_controller.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_app_button.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shell.h"
#include "ash/system/status_area_widget.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "ash/webui/settings/public/constants/routes_util.h"
#include "base/base64.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "chrome/browser/ash/app_list/app_list_client_impl.h"
#include "chrome/browser/ash/app_restore/full_restore_app_launch_handler.h"
#include "chrome/browser/ash/login/test/guest_session_mixin.h"
#include "chrome/browser/ash/login/test/logged_in_user_mixin.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/gemini.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/gmail.h"
#include "chrome/browser/web_applications/preinstalled_web_apps/preinstalled_web_apps.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "chromeos/ash/components/file_manager/app_id.h"
#include "chromeos/ash/components/report/utils/time_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/app_constants/constants.h"
#include "components/session_manager/session_manager_types.h"
#include "components/sync/base/command_line_switches.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "ui/events/test/event_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/widget/widget.h"

namespace {

// Aliases.
using ::testing::AllOf;
using ::testing::Bool;
using ::testing::Conditional;
using ::testing::Contains;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Not;
using ::testing::Pointer;
using ::testing::PrintToStringParamName;
using ::testing::Property;
using ::testing::ValuesIn;
using ::testing::WithParamInterface;

// Elements --------------------------------------------------------------------

// Identifiers.
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kBrowserWebContentsElementId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kGeminiAppWebContentsElementId);
DEFINE_LOCAL_ELEMENT_IDENTIFIER_VALUE(kSettingsAppWebContentsElementId);

// Names.
inline char kAppsGridViewElementName[] = "AppsGridView";
inline char kAppListBubbleAppsPageElementName[] = "AppListBubbleAppsPage";
inline char kChromeAppElementName[] = "ChromeApp";
inline char kFilesAppElementName[] = "FilesApp";
inline char kGeminiAppElementName[] = "GeminiApp";
inline char kGmailAppElementName[] = "GmailApp";
inline char kShowAppInfoMenuItemElementName[] = "ShowAppInfoMenuItem";

// Returns all `descendants` of the specified `parent` matching the given class.
template <typename ViewClass>
void FindDescendantsOfClass(views::View* parent,
                            std::vector<raw_ptr<ViewClass>>& descendants) {
  for (views::View* child : parent->children()) {
    if (views::IsViewClass<ViewClass>(child)) {
      descendants.emplace_back(views::AsViewClass<ViewClass>(child));
    }
    FindDescendantsOfClass(child, descendants);
  }
}

// Returns the index of `value` in the specified `range`.
template <typename Range, typename Value>
std::optional<size_t> FindIndex(const Range& range, const Value* value) {
  auto it = std::ranges::find(range, value);
  return it != range.end()
             ? std::make_optional<size_t>(std::distance(range.begin(), it))
             : std::make_optional<size_t>();
}

// Returns the `views::MenuItemView`s for the currently showing menu.
std::vector<raw_ptr<views::MenuItemView>> FindMenuItemViews() {
  if (auto* menu_controller = views::MenuController::GetActiveInstance()) {
    if (auto* menu_item_view = menu_controller->GetSelectedMenuItem()) {
      std::vector<raw_ptr<views::MenuItemView>> items;
      FindDescendantsOfClass(menu_item_view->parent(), items);
      return items;
    }
  }
  return {};
}

// Returns the `views::MenuItemView` for the currently showing menu associated
// with the specified command `id`.
views::MenuItemView* FindMenuItemViewForCommand(int id) {
  std::vector<raw_ptr<views::MenuItemView>> views = FindMenuItemViews();
  auto it = std::ranges::find(views, id, &views::MenuItemView::GetCommand);
  return it != views.end() ? *it : nullptr;
}

// Returns the `ash::ShelfItem` for the given web app `id`.
const ash::ShelfItem* FindShelfItemForWebApp(std::string_view id) {
  const ash::ShelfItems& items = ash::ShelfModel::Get()->items();
  auto it = std::ranges::find_if(
      items, [id](const ash::ShelfItem& item) { return item.id.app_id == id; });
  return it != items.end() ? &*it : nullptr;
}

// Returns if `view` is the `ash::AppListItemView` for the given web app `id`.
bool IsAppListItemViewForWebApp(std::string_view id, const views::View* view) {
  return views::IsViewClass<ash::AppListItemView>(view) &&
         views::AsViewClass<ash::AppListItemView>(view)->item()->id() == id;
}

// Returns if `browser` is the `Browser` for the given web app `id`.
bool IsBrowserForWebApp(const webapps::AppId& id,
                        const BrowserWindowInterface* browser) {
  return web_app::AppBrowserController::IsForWebApp(browser, id);
}

// Returns the browser containing `web_contents` as a tab.
BrowserWindowInterface* FindBrowserWithTab(
    const content::WebContents* web_contents) {
  return GlobalBrowserCollection::GetInstance()->FindBrowserWithTab(
      web_contents);
}

// Returns if the menu is currently showing.
bool IsMenuShowing() {
  auto* menu_controller = views::MenuController::GetActiveInstance();
  return menu_controller && menu_controller->GetSelectedMenuItem();
}

// Returns if `view` is the `ash::ShelfAppButton` for the given web app `id`.
bool IsShelfAppButtonForWebApp(
    std::reference_wrapper<const raw_ptr<ash::ShelfView>> shelf,
    std::string_view id,
    const views::View* view) {
  const ash::ShelfItem* const item = FindShelfItemForWebApp(id);
  return views::IsViewClass<ash::ShelfAppButton>(view) && item &&
         shelf.get()->GetShelfAppButton(item->id) == view;
}

// Waiters ---------------------------------------------------------------------

// Class which waits for `BrowserCollectionObserver::OnBrowserActivated()`
// events.
class OnBrowserSetLastActiveWaiter : public BrowserCollectionObserver {
 public:
  void Wait() {
    CHECK(!run_loop_);

    base::ScopedObservation<BrowserCollection, BrowserCollectionObserver>
        observer(this);
    observer.Observe(GlobalBrowserCollection::GetInstance());

    run_loop_ = std::make_unique<base::RunLoop>(
        base::RunLoop::Type::kNestableTasksAllowed);

    run_loop_->Run();
    run_loop_.reset();
  }

 private:
  // BrowserCollectionObserver:
  void OnBrowserActivated(BrowserWindowInterface* browser) override {
    CHECK(run_loop_);
    run_loop_->Quit();
  }

  // Used to wait for `OnBrowserSetLastActive()` events.
  std::unique_ptr<base::RunLoop> run_loop_;
};

}  // namespace

// GeminiAppInteractiveUiTestBase ----------------------------------------------

// Base class for interactive UI tests of the Gemini app.
class GeminiAppInteractiveUiTestBase
    : public InteractiveBrowserTestMixin<MixinBasedInProcessBrowserTest> {
 public:
  GeminiAppInteractiveUiTestBase(
      std::optional<ash::LoggedInUserMixin::LogInType> login_type)
      : user_session_mixin_(CreateUserSessionMixin(login_type)),
        scoped_preinstall_url_allow_list_(
            web_app::SetPreinstallUrlAllowListForTesting(
                {{web_app::GetConfigForGemini(/*device_info=*/std::nullopt)
                      .install_url,
                  web_app::GetConfigForGmail().install_url}})) {
    // Enable Gemini app preinstallation.
    scoped_feature_list_.InitAndEnableFeature(
        chromeos::features::kGeminiAppPreinstall);
  }

  // Returns a builder for a step which assigns the view associated with the
  // given `element_specifier` to the given `ptr`.
  template <typename ViewClass>
  [[nodiscard]] static auto AssignView(
      ElementSpecifier element_specifier,
      std::reference_wrapper<raw_ptr<ViewClass>> ptr) {
    return WithView(element_specifier,
                    [ptr](ViewClass* view) { ptr.get() = view; });
  }

  // Returns the expected launch URL for the Gemini app.
  GURL GetGeminiAppLaunchUrl() const {
    GURL::Replacements components;
    components.SetQueryStr(*gemini_app_install_info_->launch_query_params);
    return gemini_app_install_info_->start_url().ReplaceComponents(components);
  }

  // Returns the expected title for the Gemini app.
  const std::u16string& GetGeminiAppTitle() const {
    return gemini_app_install_info_->title.value();
  }

  // Returns a builder for a step which presses and releases the given `key`.
  [[nodiscard]] static auto PressAndReleaseKey(ui::KeyboardCode key) {
    return Do([key]() {
      ui::test::EventGenerator(ash::Shell::GetPrimaryRootWindow())
          .PressAndReleaseKeyAndModifierKeys(key, ui::EF_NONE);
    });
  }

  // Returns a builder for a step which resets the specified `ptr`.
  template <typename T>
  [[nodiscard]] static auto Reset(std::reference_wrapper<raw_ptr<T>> ptr) {
    return Do([ptr]() { ptr.get() = nullptr; });
  }

  // Returns a builder for a step which waits for a
  // `BrowserCollectionObserver::OnBrowserActivated()` event.
  [[nodiscard]] auto WaitForOnBrowserSetLastActive() {
    return Do([]() { OnBrowserSetLastActiveWaiter().Wait(); });
  }

 protected:
  // InteractiveBrowserTestMixin<MixinBasedInProcessBrowserTest>:
  void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
    InteractiveBrowserTestMixin<
        MixinBasedInProcessBrowserTest>::SetUpDefaultCommandLine(command_line);

    // Disable sync as it would otherwise block updating of shelf pins.
    command_line->AppendSwitch(syncer::kDisableSync);
  }

  void SetUpOnMainThread() override {
    // There's nothing to do if not logging in the user.
    if (!ShouldLogInUser()) {
      InteractiveBrowserTestMixin<
          MixinBasedInProcessBrowserTest>::SetUpOnMainThread();
      return;
    }

    // For logged-in user sessions, perform login prior to
    // `InteractiveBrowserTestMixin<>::SetUpOnMainThread()` so that the
    // interactive browser test base class will successfully set the context
    // widget for the test sequence. The context widget will be associated with
    // the browser.
    if (std::holds_alternative<ash::LoggedInUserMixin>(user_session_mixin_)) {
      std::get<ash::LoggedInUserMixin>(user_session_mixin_).LogInUser();
    }

    InteractiveBrowserTestMixin<
        MixinBasedInProcessBrowserTest>::SetUpOnMainThread();

    // Wait for installation of both system and external web apps. The Gemini
    // app is an external app and this test suite will verify its adjacency to
    // system web apps.
    Profile* const profile = browser()->GetProfile();
    ash::SystemWebAppManager::GetForTest(profile)
        ->InstallSystemAppsForTesting();
    web_app::test::WaitUntilWebAppProviderAndSubsystemsReady(
        web_app::WebAppProvider::GetForTest(profile));
    AppListClientImpl::GetInstance()->UpdateProfile();

    // Fetch `device_info` from echo.
    std::optional<base::Time> oobe_timestamp =
        ash::report::utils::GetFirstActiveWeek();
    ASSERT_TRUE(oobe_timestamp.has_value());
    web_app::DeviceInfo device_info;
    device_info.oobe_timestamp = oobe_timestamp.value();

    // Cache install info for the Gemini app.
    gemini_app_install_info_ =
        web_app::GetConfigForGemini(device_info).app_info_factory.Run();
  }

 private:
  // Creates the appropriate guest or logged-in user session mixin based on
  // the presence of `login_type`.
  std::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>
  CreateUserSessionMixin(
      std::optional<ash::LoggedInUserMixin::LogInType> login_type) {
    if (!login_type) {
      return std::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>(
          std::in_place_type_t<ash::GuestSessionMixin>(), &mixin_host_);
    }

    return std::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>(
        std::in_place_type_t<ash::LoggedInUserMixin>(), &mixin_host_,
        /*test_base=*/this, embedded_test_server(), login_type.value());
  }

  // Returns whether the user should be logged in as part of test setup.
  virtual bool ShouldLogInUser() const { return true; }

  // Used to manage either a guest or logged-in user session based on test
  // parameterization.
  std::variant<ash::GuestSessionMixin, ash::LoggedInUserMixin>
      user_session_mixin_;

  // Used to enable the Gemini app preinstallation.
  base::test::ScopedFeatureList scoped_feature_list_;

  // Used to retrieve expected title/URL for the Gemini app.
  std::unique_ptr<web_app::WebAppInstallInfo> gemini_app_install_info_;

  // Allowlists the specific apps to be preinstalled.
  web_app::ScopedPreinstallUrlAllowList scoped_preinstall_url_allow_list_;
};

// GeminiAppInteractiveUiTest --------------------------------------------------

// Base class for interactive UI tests of the Gemini app, parameterized by
// whether the logged-in user is new or existing. Tests include a PRE_ session,
// where user state is initialized, followed by a subsequent session containing
// test logic. Chrome is restarted between sessions.
class GeminiAppInteractiveUiTest
    : public GeminiAppInteractiveUiTestBase,
      public WithParamInterface</*existing_user=*/bool> {
 public:
  GeminiAppInteractiveUiTest()
      : GeminiAppInteractiveUiTestBase(
            ash::LoggedInUserMixin::LogInType::kConsumer) {
    // Disable the Gemini app during the PRE_ session so that the subsequent
    // session containing test logic is when the app preinstallation occurs.
    if (IsPreSession()) {
      scoped_feature_list_.InitAndDisableFeature(
          chromeos::features::kGeminiAppPreinstall);
    }
  }

 protected:
  // GeminiAppInteractiveUiTestBase:
  void SetUpOnMainThread() override {
    GeminiAppInteractiveUiTestBase::SetUpOnMainThread();

    // Check that session state is as expected.
    const auto* session_controller = ash::Shell::Get()->session_controller();
    EXPECT_THAT(session_controller->GetSessionState(),
                Conditional(ShouldLogInUser(),
                            Eq(session_manager::SessionState::ACTIVE),
                            Eq(session_manager::SessionState::LOGIN_PRIMARY)));

    // Check that login state is as expected.
    EXPECT_THAT(
        session_controller->IsUserFirstLogin(),
        Conditional(IsPreSession(), IsExistingUser(), Not(IsExistingUser())));
  }

  bool ShouldLogInUser() const override {
    // Existing users should be logged in for both the PRE_ session and the
    // subsequent session containing test logic. New users should only be logged
    // in for the subsequent session.
    return IsExistingUser() || !IsPreSession();
  }

  // Returns whether the logged-in user is existing given test parameterization.
  bool IsExistingUser() const { return GetParam(); }

  // Returns whether the current session is the PRE_ session. The PRE_ session
  // is the session before the subsequent session containing test logic.
  bool IsPreSession() const {
    return base::StartsWith(
        testing::UnitTest::GetInstance()->current_test_info()->name(), "PRE_");
  }

 private:
  // Used to disable Gemini app preinstallation for the PRE_ session.
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    GeminiAppInteractiveUiTest,
    /*existing_user=*/Bool(),
    [](const testing::TestParamInfo</*existing_user=*/bool>& info) {
      return info.param ? "ExistingUser" : "NewUser";
    });

// Tests -----------------------------------------------------------------------

// Initializes user state and restarts Chrome before `LaunchFromAppList`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, PRE_LaunchFromAppList) {}

// Verifies that the Gemini app can be launched from the app list.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, LaunchFromAppList) {
  // Views.
  raw_ptr<ash::AppsGridView> apps_grid_view = nullptr;
  raw_ptr<ash::AppListItemView> files_app = nullptr;
  raw_ptr<ash::AppListItemView> gemini_app = nullptr;
  raw_ptr<ash::AppListItemView> gmail_app = nullptr;

  // Test.
  RunTestSequence(InAnyContext(
      // Launch app list.
      DoDefaultAction(ash::kHomeButtonElementId),
      WaitForShow(ash::kAppListBubbleViewElementId),

      // Find apps page.
      NameDescendantViewByType<ash::AppListBubbleAppsPage>(
          ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),

      // Find apps grid.
      NameDescendantViewByType<ash::AppsGridView>(
          kAppListBubbleAppsPageElementName, kAppsGridViewElementName),

      // Cache apps grid.
      AssignView(kAppsGridViewElementName, std::ref(apps_grid_view)),

      // Find Gemini app.
      NameDescendantView(
          kAppsGridViewElementName, kGeminiAppElementName,
          base::BindRepeating(&IsAppListItemViewForWebApp, ash::kGeminiAppId)),

      // Cache Gemini app.
      AssignView(kGeminiAppElementName, std::ref(gemini_app)),

      // Find Files app.
      NameDescendantView(
          kAppsGridViewElementName, kFilesAppElementName,
          base::BindRepeating(&IsAppListItemViewForWebApp,
                              file_manager::kFileManagerSwaAppId)),

      // Cache Files app.
      AssignView(kFilesAppElementName, std::ref(files_app)),

      // Find Gmail app.
      NameDescendantView(
          kAppsGridViewElementName, kGmailAppElementName,
          base::BindRepeating(&IsAppListItemViewForWebApp, ash::kGmailAppId)),

      // Cache Gmail app.
      AssignView(kGmailAppElementName, std::ref(gmail_app)),

      // Check Gemini app title.
      CheckView(
          kGeminiAppElementName,
          [](ash::AppListItemView* item) { return item->title(); },
          Property(&views::Label::GetText, Eq(GetGeminiAppTitle()))),

      // Check Gemini app position.
      Check([&]() {
        std::vector<raw_ptr<ash::AppListItemView>> apps;
        FindDescendantsOfClass(apps_grid_view, apps);
        const auto gemini_app_index = FindIndex(apps, gemini_app.get());
        if (IsExistingUser()) {
          return gemini_app_index == 0u;
        }
        const auto files_app_index = FindIndex(apps, files_app.get());
        const auto gmail_app_index = FindIndex(apps, gmail_app.get());
        return (files_app_index == gemini_app_index.value() - 1u) &&
               (gmail_app_index == gemini_app_index.value() + 1u);
      }),

      // Reset cached pointers which might dangle when the app list closes.
      Reset(std::ref(apps_grid_view)), Reset(std::ref(gemini_app)),
      Reset(std::ref(files_app)), Reset(std::ref(gmail_app)),

      // Launch Gemini app.
      InstrumentNextTab(kGeminiAppWebContentsElementId, AnyBrowser()),
      DoDefaultAction(kGeminiAppElementName),

      // Check Gemini app browser.
      CheckElement(
          kGeminiAppWebContentsElementId,
          base::BindOnce(&AsInstrumentedWebContents)
              .Then(
                  base::BindOnce(&WebContentsInteractionTestUtil::web_contents))
              .Then(base::BindOnce(&FindBrowserWithTab))
              .Then(base::BindOnce(&IsBrowserForWebApp, ash::kGeminiAppId))),

      // Check Gemini app launch URL.
      WaitForWebContentsReady(kGeminiAppWebContentsElementId,
                              GetGeminiAppLaunchUrl())));
}

// Initializes user state and restarts Chrome before `LaunchFromShelf`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, PRE_LaunchFromShelf) {}

// Verifies that the Gemini app can be launched from the shelf.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, LaunchFromShelf) {
  // Views.
  raw_ptr<ash::ShelfAppButton> chrome_app = nullptr;
  raw_ptr<ash::ShelfAppButton> gemini_app = nullptr;
  raw_ptr<ash::ShelfView> shelf = nullptr;

  // Test.
  RunTestSequence(InAnyContext(
      // Cache shelf.
      AssignView(ash::kShelfViewElementId, std::ref(shelf)),

      // Find Gemini app.
      NameDescendantView(
          ash::kShelfViewElementId, kGeminiAppElementName,
          base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
                              ash::kGeminiAppId)),

      // Cache Gemini app.
      AssignView(kGeminiAppElementName, std::ref(gemini_app)),

      // Find Chrome app.
      NameDescendantView(
          ash::kShelfViewElementId, kChromeAppElementName,
          base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
                              app_constants::kChromeAppId)),

      // Cache Chrome app.
      AssignView(kChromeAppElementName, std::ref(chrome_app)),

      // Check Gemini app position.
      Check(
          [&]() {
            std::vector<raw_ptr<ash::ShelfAppButton>> apps;
            FindDescendantsOfClass(shelf, apps);
            const auto gemini_app_index = FindIndex(apps, gemini_app.get());
            if (IsExistingUser()) {
              return gemini_app_index == 0u;
            }
            const auto chrome_app_index = FindIndex(apps, chrome_app.get());
            return chrome_app_index == 0u && gemini_app_index == 1u;
          },
          "Gemini app is positioned correctly"),

      // Launch Gemini app.
      InstrumentNextTab(kGeminiAppWebContentsElementId, AnyBrowser()),
      DoDefaultAction(kGeminiAppElementName),

      // Check Gemini app browser.
      CheckElement(
          kGeminiAppWebContentsElementId,
          base::BindOnce(&AsInstrumentedWebContents)
              .Then(
                  base::BindOnce(&WebContentsInteractionTestUtil::web_contents))
              .Then(base::BindOnce(&FindBrowserWithTab))
              .Then(base::BindOnce(&IsBrowserForWebApp, ash::kGeminiAppId))),

      // Check Gemini app launch URL.
      WaitForWebContentsReady(kGeminiAppWebContentsElementId,
                              GetGeminiAppLaunchUrl())));
}

// Initializes user state and restarts Chrome before
// `PreferredAppForSupportedLinks`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest,
                       PRE_PreferredAppForSupportedLinks) {}

// Verifies that the Gemini app is the preferred app for supported links.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest,
                       PreferredAppForSupportedLinks) {
  const std::string url_with_contents =
      base::StrCat({"data:text/html;base64,",
                    base::Base64Encode(base::ReplaceStringPlaceholders(
                        R"(<DOCTYPE html>
                             <html>
                               <head>
                                 <style>
                                   html, body, a {
                                     display: block;
                                     height: 100%;
                                     width: 100%;
                                   }
                                 </style>
                               </head>
                               <body>
                                 <a href="$1" target="_blank"></a>
                               </body>
                             </html>)",
                        /*subst=*/{GetGeminiAppLaunchUrl().spec()},
                        /*offsets=*/nullptr))});

  // Test.
  RunTestSequence(
      // Navigate browser to page with supported link.
      AddInstrumentedTab(kBrowserWebContentsElementId, GURL(url_with_contents)),

      // Launch Gemini app via supported link.
      MoveMouseTo(kBrowserViewElementId), ClickMouse(),
      InAnyContext(
          WaitForShow(kBrowserViewElementId).SetTransitionOnlyOnEvent(true)),
      InSameContext(
          InstrumentTab(kGeminiAppWebContentsElementId),

          // Check Gemini app browser.
          CheckElement(kGeminiAppWebContentsElementId,
                       [](ui::TrackedElement* el) {
                         auto* const browser = FindBrowserWithTab(
                             AsInstrumentedWebContents(el)->web_contents());
                         return IsBrowserForWebApp(ash::kGeminiAppId, browser);
                       }),

          // Check Gemini app launch URL.
          WaitForWebContentsReady(kGeminiAppWebContentsElementId,
                                  GetGeminiAppLaunchUrl())));
}

// Initializes user state and restarts Chrome before `UninstallFromAppList`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, PRE_UninstallFromAppList) {}

// Verifies that the Gemini app cannot be uninstalled from the app list.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, UninstallFromAppList) {
  RunTestSequence(InAnyContext(
      // Launch app list.
      DoDefaultAction(ash::kHomeButtonElementId),
      WaitForShow(ash::kAppListBubbleViewElementId),

      // Find apps page.
      NameDescendantViewByType<ash::AppListBubbleAppsPage>(
          ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),

      // Find apps grid.
      NameDescendantViewByType<ash::AppsGridView>(
          kAppListBubbleAppsPageElementName, kAppsGridViewElementName),

      // Find Gemini app.
      NameDescendantView(
          kAppsGridViewElementName, kGeminiAppElementName,
          base::BindRepeating(&IsAppListItemViewForWebApp, ash::kGeminiAppId)),

      // Open menu.
      MoveMouseTo(kGeminiAppElementName), ClickMouse(ui_controls::RIGHT),
      Check(&IsMenuShowing),

      // Activate menu.
      PressAndReleaseKey(ui::VKEY_DOWN),

      // Check Gemini app cannot be uninstalled.
      CheckResult(&FindMenuItemViews,
                  AllOf(Not(IsEmpty()), Not(Contains(Pointer(Property(
                                            &views::MenuItemView::GetCommand,
                                            Eq(ash::UNINSTALL)))))))));
}

// Initializes user state and restarts Chrome before `UninstallFromSettings`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, PRE_UninstallFromSettings) {}

// Verifies that the Gemini app cannot be uninstalled from the Settings app.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, UninstallFromSettings) {
  // Views.
  raw_ptr<ash::ShelfView> shelf = nullptr;

  // Queries.
  auto get_settings_app_subpage_query = [](std::string_view query,
                                           bool shadow_dom = false) {
    constexpr char kOsSettingsAppsPage[] = "os-settings-apps-page";
    constexpr char kOsSettingsMain[] = "os-settings-main";
    constexpr char kOsSettingsMainPageContainer[] = "#mainPageContainer";
    constexpr char kOsSettingsSubpage[] = "os-settings-subpage";
    constexpr char kOsSettingsUi[] = "os-settings-ui";

    const DeepQuery deep_query({kOsSettingsUi, kOsSettingsMain,
                                kOsSettingsMainPageContainer,
                                kOsSettingsAppsPage});

    return shadow_dom
               ? (deep_query + kOsSettingsSubpage) + std::string(query)
               : (deep_query + base::StrCat({kOsSettingsSubpage, " ", query}));
  };

  // Test.
  RunTestSequence(InAnyContext(
      // Cache shelf.
      AssignView(ash::kShelfViewElementId, std::ref(shelf)),

      // Find Gemini app.
      NameDescendantView(
          ash::kShelfViewElementId, kGeminiAppElementName,
          base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
                              ash::kGeminiAppId)),

      // Open menu.
      MoveMouseTo(kGeminiAppElementName), ClickMouse(ui_controls::RIGHT),
      Check(&IsMenuShowing),

      // Activate menu.
      PressAndReleaseKey(ui::VKEY_DOWN),

      // Find menu item.
      NameView(kShowAppInfoMenuItemElementName,
               base::BindOnce(FindMenuItemViewForCommand, ash::SHOW_APP_INFO)
                   .Then(base::BindOnce<views::View*(views::View*)>(
                       views::AsViewClass<views::View>))),

      // Launch Settings app.
      InstrumentNextTab(kSettingsAppWebContentsElementId, AnyBrowser()),
      DoDefaultAction(kShowAppInfoMenuItemElementName),

      // Check Settings app browser.
      CheckElement(kSettingsAppWebContentsElementId,
                   base::BindOnce(&AsInstrumentedWebContents)
                       .Then(base::BindOnce(
                           &WebContentsInteractionTestUtil::web_contents))
                       .Then(base::BindOnce(&FindBrowserWithTab))
                       .Then(base::BindOnce(&IsBrowserForWebApp,
                                            ash::kOsSettingsAppId))),

      // Check Settings app launch URL.
      WaitForWebContentsReady(
          kSettingsAppWebContentsElementId,
          chromeos::settings::GetOSSettingsUrl(
              base::StrCat({chromeos::settings::mojom::kAppDetailsSubpagePath,
                            "?id=", ash::kGeminiAppId}))),

      // Check Gemini app title.
      CheckJsResultAt(
          kSettingsAppWebContentsElementId,
          get_settings_app_subpage_query("#subpageTitle", /*shadow_dom=*/true),
          base::StrCat({"subpageTitle => subpageTitle.innerText === '",
                        base::UTF16ToUTF8(GetGeminiAppTitle()), "'"})),

      // Check Gemini app cannot be uninstalled.
      CheckJsResultAt(
          kSettingsAppWebContentsElementId,
          get_settings_app_subpage_query("app-management-uninstall-button"),
          "appManagementUninstallButton => "
          "!appManagementUninstallButton.shadowRoot.querySelector('*[role="
          "button]')")));
}

// Initializes user state and restarts Chrome before `UninstallFromShelf`.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, PRE_UninstallFromShelf) {}

// Verifies that the Gemini app cannot be uninstalled from the shelf.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiTest, UninstallFromShelf) {
  // Views.
  raw_ptr<ash::ShelfView> shelf = nullptr;

  // Test.
  RunTestSequence(InAnyContext(
      // Cache shelf.
      AssignView(ash::kShelfViewElementId, std::ref(shelf)),

      // Find Gemini app.
      NameDescendantView(
          ash::kShelfViewElementId, kGeminiAppElementName,
          base::BindRepeating(&IsShelfAppButtonForWebApp, std::cref(shelf),
                              ash::kGeminiAppId)),

      // Open menu.
      MoveMouseTo(kGeminiAppElementName), ClickMouse(ui_controls::RIGHT),
      Check(&IsMenuShowing),

      // Activate menu.
      PressAndReleaseKey(ui::VKEY_DOWN),

      // Check Gemini app cannot be uninstalled.
      CheckResult(&FindMenuItemViews,
                  AllOf(Not(IsEmpty()), Not(Contains(Pointer(Property(
                                            &views::MenuItemView::GetCommand,
                                            Eq(ash::UNINSTALL)))))))));
}

// GeminiAppInteractiveUiIneligibilityTest -------------------------------------

// Reasons why the user may be ineligible for Gemini app preinstallation.
enum class IneligibilityReason {
  kMinValue = 0,
  kFeatureFlagDisabled = kMinValue,
  kUserManaged,
  kUserTypeChild,
  kUserTypeGuest,
  kMaxValue = kUserTypeGuest,
};

#define INELIGIBILITY_REASON_CASE(reason) \
  case IneligibilityReason::reason:       \
    return os << std::string(#reason)

inline std::ostream& operator<<(std::ostream& os, IneligibilityReason reason) {
  switch (reason) {
    INELIGIBILITY_REASON_CASE(kFeatureFlagDisabled);
    INELIGIBILITY_REASON_CASE(kUserManaged);
    INELIGIBILITY_REASON_CASE(kUserTypeChild);
    INELIGIBILITY_REASON_CASE(kUserTypeGuest);
  }
}

// Base class for interactive UI tests of Gemini app ineligibility.
class GeminiAppInteractiveUiIneligibilityTest
    : public GeminiAppInteractiveUiTestBase,
      public WithParamInterface<IneligibilityReason> {
 public:
  GeminiAppInteractiveUiIneligibilityTest()
      : GeminiAppInteractiveUiTestBase(GetLoginType()) {
    scoped_feature_list_.InitWithFeatureState(
        chromeos::features::kGeminiAppPreinstall, IsFeatureFlagEnabled());
  }

 private:
  // GeminiAppInteractiveUiTestBase:
  void SetUpOnMainThread() override {
    // Web app preinstallation times out for child user types due to failure to
    // install some default web apps. Since this test suite only cares about the
    // preinstallation of the Gemini app, circumvent timeouts by disabling other
    // default web apps.
    std::unique_ptr<web_app::ScopedTestingPreinstalledAppData> app_data;
    if (GetLoginType() == ash::LoggedInUserMixin::LogInType::kChild) {
      app_data = std::make_unique<web_app::ScopedTestingPreinstalledAppData>();
      app_data->apps.emplace_back(
          web_app::GetConfigForGemini(/*device_info=*/std::nullopt));
    }

    GeminiAppInteractiveUiTestBase::SetUpOnMainThread();
  }

  // Returns the login type for the user given test parameterization.
  std::optional<ash::LoggedInUserMixin::LogInType> GetLoginType() const {
    switch (GetParam()) {
      case IneligibilityReason::kUserTypeChild:
        return ash::LoggedInUserMixin::LogInType::kChild;
      case IneligibilityReason::kUserTypeGuest:
        return std::nullopt;
      case IneligibilityReason::kUserManaged:
        return ash::LoggedInUserMixin::LogInType::kManaged;
      default:
        return ash::LoggedInUserMixin::LogInType::kConsumer;
    }
  }

  // Returns whether the feature flag is enabled given test parameterization.
  bool IsFeatureFlagEnabled() const {
    return GetParam() != IneligibilityReason::kFeatureFlagDisabled;
  }

  // Used to enable/disable the Gemini app preinstallation based on test
  // parameterization.
  base::test::ScopedFeatureList scoped_feature_list_;
};

INSTANTIATE_TEST_SUITE_P(
    All,
    GeminiAppInteractiveUiIneligibilityTest,
    ValuesIn(([]() {
      std::vector<IneligibilityReason> reasons(
          static_cast<int>(IneligibilityReason::kMaxValue) -
          static_cast<int>(IneligibilityReason::kMinValue) + 1);
      std::ranges::generate(
          reasons.begin(), reasons.end(),
          [i = static_cast<int>(IneligibilityReason::kMinValue)]() mutable {
            return static_cast<IneligibilityReason>(i++);
          });
      return reasons;
    })()),
    PrintToStringParamName());

// Tests -----------------------------------------------------------------------

// Verifies the Gemini app is absent from the app list for ineligible users.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiIneligibilityTest,
                       AbsentFromAppList) {
  RunTestSequence(InAnyContext(
      // Launch app list.
      DoDefaultAction(ash::kHomeButtonElementId),
      WaitForShow(ash::kAppListBubbleViewElementId),

      // Find apps page.
      NameDescendantViewByType<ash::AppListBubbleAppsPage>(
          ash::kAppListBubbleViewElementId, kAppListBubbleAppsPageElementName),

      // Find apps grid.
      NameDescendantViewByType<ash::AppsGridView>(
          kAppListBubbleAppsPageElementName, kAppsGridViewElementName),

      // Check Gemini app absent.
      CheckView(
          kAppsGridViewElementName, [](ash::AppsGridView* apps_grid_view) {
            std::vector<raw_ptr<ash::AppListItemView>> apps;
            FindDescendantsOfClass(apps_grid_view, apps);
            return apps.size() &&
                   std::ranges::none_of(apps, [&](ash::AppListItemView* app) {
                     return IsAppListItemViewForWebApp(ash::kGeminiAppId, app);
                   });
          })));
}

// Verifies the Gemini app is absent from the shelf for ineligible users.
IN_PROC_BROWSER_TEST_P(GeminiAppInteractiveUiIneligibilityTest,
                       AbsentFromShelf) {
  RunTestSequence(InAnyContext(
      // Check Gemini app absent.
      CheckView(ash::kShelfViewElementId, [](ash::ShelfView* shelf) {
        std::vector<raw_ptr<ash::ShelfAppButton>> apps;
        FindDescendantsOfClass(shelf, apps);
        return apps.size() &&
               std::ranges::none_of(
                   apps, [&, shelf = raw_ptr(shelf)](ash::ShelfAppButton* app) {
                     return IsShelfAppButtonForWebApp(std::cref(shelf),
                                                      ash::kGeminiAppId, app);
                   });
      })));
}
