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

#include "chrome/browser/web_applications/app_service/web_app_publisher_helper.h"

#include <initializer_list>
#include <memory>
#include <optional>
#include <sstream>
#include <utility>

#include "ash/constants/ash_pref_names.h"
#include "ash/constants/web_app_id_constants.h"
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/traits_bag.h"
#include "base/values.h"
#include "base/version.h"
#include "build/buildflag.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/app_service_test.h"
#include "chrome/browser/apps/link_capturing/link_capturing_feature_test_support.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/web_applications/model/isolation_data.h"
#include "chrome/browser/web_applications/scope_extension_info.h"
#include "chrome/browser/web_applications/test/fake_web_app_provider.h"
#include "chrome/browser/web_applications/test/fake_web_app_ui_manager.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/test/web_app_test.h"
#include "chrome/browser/web_applications/test/web_app_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_install_info.h"
#include "chrome/browser/web_applications/web_app_management_type.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/browser/web_applications/web_app_registry_update.h"
#include "chrome/browser/web_applications/web_app_sync_bridge.h"
#include "chrome/browser/web_applications/web_app_ui_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_profile.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/file_handler.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_filter_util.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/webapps/browser/installable/installable_metrics.h"
#include "components/webapps/common/constants.h"
#include "components/webapps/isolated_web_apps/scheme.h"
#include "components/webapps/isolated_web_apps/types/iwa_version.h"
#include "content/public/common/content_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using testing::_;

namespace web_app {

namespace {
class MockWebAppUiManager : public web_app::FakeWebAppUiManager {
 public:
  MOCK_METHOD(void,
              ShowWebAppFileLaunchDialog,
              (const std::vector<base::FilePath>& file_paths,
               const webapps::AppId& app_id,
               WebAppLaunchAcceptanceCallback launch_callback),
              (override));
};

class NoOpWebAppPublisherDelegate : public WebAppPublisherHelper::Delegate {
  // WebAppPublisherHelper::Delegate:
  void PublishWebApps(std::vector<apps::AppPtr> apps) override {}
  void PublishWebApp(apps::AppPtr app) override {}
  void ModifyWebAppCapabilityAccess(
      const std::string& app_id,
      std::optional<bool> accessing_camera,
      std::optional<bool> accessing_microphone) override {}
};

bool HandlesIntent(const apps::AppPtr& app, const apps::IntentPtr& intent) {
  if (!app->intent_filters) {
    return false;
  }
  for (const auto& filter : *app->intent_filters) {
    if (intent->MatchFilter(filter)) {
      return true;
    }
  }
  return false;
}

}  // namespace

class WebAppPublisherHelperTest : public WebAppTest {
 public:
  WebAppPublisherHelperTest() = default;
  WebAppPublisherHelperTest(const WebAppPublisherHelperTest&) = delete;
  WebAppPublisherHelperTest& operator=(const WebAppPublisherHelperTest&) =
      delete;
  ~WebAppPublisherHelperTest() override = default;

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

    apps::WaitForAppServiceProxyReady(
        apps::AppServiceProxyFactory::GetForProfile(profile()));

    publisher_ = std::make_unique<WebAppPublisherHelper>(profile(), &provider(),
                                                         &no_op_delegate_);
    auto ui_manager = std::make_unique<MockWebAppUiManager>();
    ui_manager_ = ui_manager.get();
    web_app::FakeWebAppProvider::Get(profile())->SetWebAppUiManager(
        std::move(ui_manager));
    test::AwaitStartWebAppProviderAndSubsystems(profile());
  }

  void TearDown() override {
    publisher_.reset();
    ui_manager_ = nullptr;
    WebAppTest::TearDown();
  }

  FakeWebAppUiManager& fake_ui_manager() {
    return static_cast<FakeWebAppUiManager&>(provider().ui_manager());
  }

  NoOpWebAppPublisherDelegate no_op_delegate_;
  std::unique_ptr<WebAppPublisherHelper> publisher_;
  raw_ptr<MockWebAppUiManager> ui_manager_ = nullptr;
};

TEST_F(WebAppPublisherHelperTest, CreateWebApp_Minimal) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_EQ(app->app_id, app_id);
  EXPECT_EQ(app->name, name);
  EXPECT_EQ(app->publisher_id, start_url.spec());
}

TEST_F(WebAppPublisherHelperTest, CreateWebApp_Random) {
  for (int seed = 0; seed < 100; ++seed) {
    test::CreateRandomWebAppParams params;
    params.seed = seed;
    params.parent_manifest_id = std::nullopt;
    std::unique_ptr<WebApp> random_app = test::CreateRandomWebApp(params);

    auto info = std::make_unique<WebAppInstallInfo>(random_app->manifest_id(),
                                                    random_app->start_url());
    info->title = base::UTF8ToUTF16(random_app->untranslated_name());
    info->description =
        base::UTF8ToUTF16(random_app->untranslated_description());
    info->file_handlers = random_app->file_handlers();

    // Unable to install a randomly generated web app struct, so just copy
    // necessary fields into the installation flow.
    webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
    EXPECT_EQ(app_id, random_app->app_id());
    apps::AppPtr app = publisher_->CreateWebApp(random_app.get());

    EXPECT_EQ(app->app_id, random_app->app_id());
    EXPECT_EQ(app->name, random_app->untranslated_name());
    EXPECT_EQ(app->publisher_id, random_app->start_url().spec());
  }
}

// Verifies that the extended_scope matches the specified domain but not
// unrelated domains.
TEST_F(WebAppPublisherHelperTest, CreateWebApp_ScopeExtension) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");
  const GURL extended_scope_url("https://example.org/foo");
  const GURL outside_extended_scope_url("https://nonexample.org/foo");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);
  info->validated_scope_extensions = {
      ScopeExtensionInfo::CreateForScope(extended_scope_url)};

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_TRUE(HandlesIntent(
      app, std::make_unique<apps::Intent>(apps_util::kIntentActionView,
                                          extended_scope_url)));
  EXPECT_FALSE(HandlesIntent(
      app, std::make_unique<apps::Intent>(apps_util::kIntentActionView,
                                          outside_extended_scope_url)));
}

// Verifies that the extended_scope with a registrable_domain wildcard matches
// the domain and its subdomains but not unrelated domains.
TEST_F(WebAppPublisherHelperTest, CreateWebApp_WildcardScopeExtension) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");
  const GURL extended_scope_url("https://example.org/foo");
  const GURL subdomain_extended_scope_url("https://sub.example.org/foo");
  const GURL outside_extended_scope_url("https://nonexample.org/foo");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);
  info->validated_scope_extensions = {
      ScopeExtensionInfo::CreateForScope(extended_scope_url,
                                         /*has_origin_wildcard*/ true)};

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_TRUE(HandlesIntent(
      app, std::make_unique<apps::Intent>(apps_util::kIntentActionView,
                                          extended_scope_url)));
  EXPECT_TRUE(HandlesIntent(
      app, std::make_unique<apps::Intent>(apps_util::kIntentActionView,
                                          subdomain_extended_scope_url)));
  EXPECT_FALSE(HandlesIntent(
      app, std::make_unique<apps::Intent>(apps_util::kIntentActionView,
                                          outside_extended_scope_url)));
}

TEST_F(WebAppPublisherHelperTest, CreateWebApp_NoteTaking) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");
  const GURL new_note_url("https://example.com/new_note");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);
  info->note_taking_new_note_url = new_note_url;

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_TRUE(HandlesIntent(app, apps_util::CreateCreateNoteIntent()));
}

TEST_F(WebAppPublisherHelperTest, CreateWebApp_LockScreen_DisabledByFlag) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");
  const GURL lock_screen_url("https://example.com/lock_screen");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);
  info->lock_screen_start_url = lock_screen_url;

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_FALSE(HandlesIntent(app, apps_util::CreateStartOnLockScreenIntent()));
}

TEST_F(WebAppPublisherHelperTest,
       CreateIntentFiltersForWebApp_WebApp_HasUrlFilter) {
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(
      GURL("https://example.com/path"));
  info->scope = GURL("https://example.com/path");
  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);

  apps::IntentFilters filters =
      WebAppPublisherHelper::CreateIntentFiltersForWebApp(provider(), *app);

  ASSERT_EQ(filters.size(), 1u);
  apps::IntentFilterPtr& filter = filters[0];
  EXPECT_FALSE(filter->activity_name.has_value());
  EXPECT_FALSE(filter->activity_label.has_value());
  ASSERT_EQ(filter->conditions.size(), 4U);

  {
    const apps::Condition& condition = *filter->conditions[0];
    EXPECT_EQ(condition.condition_type, apps::ConditionType::kAction);
    ASSERT_EQ(condition.condition_values.size(), 1U);
    EXPECT_EQ(condition.condition_values[0]->match_type,
              apps::PatternMatchType::kLiteral);
    EXPECT_EQ(condition.condition_values[0]->value,
              apps_util::kIntentActionView);
  }

  {
    const apps::Condition& condition = *filter->conditions[1];
    EXPECT_EQ(condition.condition_type, apps::ConditionType::kScheme);
    ASSERT_EQ(condition.condition_values.size(), 1U);
    EXPECT_EQ(condition.condition_values[0]->match_type,
              apps::PatternMatchType::kLiteral);
    EXPECT_EQ(condition.condition_values[0]->value, app->scope().GetScheme());
  }

  {
    const apps::Condition& condition = *filter->conditions[2];
    EXPECT_EQ(condition.condition_type, apps::ConditionType::kAuthority);
    ASSERT_EQ(condition.condition_values.size(), 1U);
    EXPECT_EQ(condition.condition_values[0]->match_type,
              apps::PatternMatchType::kLiteral);
    EXPECT_EQ(condition.condition_values[0]->value,
              apps_util::AuthorityView::Encode(app->scope()));
  }

  {
    const apps::Condition& condition = *filter->conditions[3];
    EXPECT_EQ(condition.condition_type, apps::ConditionType::kPath);
    ASSERT_EQ(condition.condition_values.size(), 1U);
    EXPECT_EQ(condition.condition_values[0]->match_type,
              apps::PatternMatchType::kPrefix);
    EXPECT_EQ(condition.condition_values[0]->value, app->scope().GetPath());
  }
}

// TODO(crbug.com/327431493): Use a more holistic approach than adding apps to
// the registry.
TEST_F(WebAppPublisherHelperTest, CreateIntentFiltersForWebApp_FileHandlers) {
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(
      GURL("https://example.com/path"));
  info->scope = GURL("https://example.com/path");

  apps::FileHandler::AcceptEntry accept_entry;
  accept_entry.mime_type = "text/plain";
  accept_entry.file_extensions.insert(".txt");

  apps::FileHandler file_handler;
  file_handler.action = GURL("https://example.com/path/handler.html");
  file_handler.accept.push_back(std::move(accept_entry));

  info->file_handlers.push_back(std::move(file_handler));

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);

  apps::IntentFilters filters =
      WebAppPublisherHelper::CreateIntentFiltersForWebApp(provider(), *app);

  ASSERT_EQ(filters.size(), 2u);
  // 1st filter is URL filter.

  // File filter - View action
  const apps::IntentFilterPtr& file_filter = filters[1];
  ASSERT_EQ(file_filter->conditions.size(), 2u);
  const apps::Condition& view_cond = *file_filter->conditions[0];
  EXPECT_EQ(view_cond.condition_type, apps::ConditionType::kAction);
  ASSERT_EQ(view_cond.condition_values.size(), 1u);
  EXPECT_EQ(view_cond.condition_values[0]->value, apps_util::kIntentActionView);

  // File filter - mime & file extension match
  const apps::Condition& file_cond = *file_filter->conditions[1];
  EXPECT_EQ(file_cond.condition_type, apps::ConditionType::kFile);
  ASSERT_EQ(file_cond.condition_values.size(), 2u);
  EXPECT_EQ(file_cond.condition_values[0]->match_type,
            apps::PatternMatchType::kMimeType);
  EXPECT_EQ(file_cond.condition_values[0]->value, "text/plain");
  EXPECT_EQ(file_cond.condition_values[1]->match_type,
            apps::PatternMatchType::kFileExtension);
  EXPECT_EQ(file_cond.condition_values[1]->value, ".txt");
}

#if (BUILDFLAG(IS_CHROMEOS))
TEST_F(WebAppPublisherHelperTest, LaunchWithFiles_AllowWithNoPrompt) {
  const GURL app_url("https://example.com/path/index.html");

  base::DictValue pref_value;
  pref_value.Set(".txt", app_url.spec());
  profile()->GetPrefs()->SetDict(ash::prefs::kDefaultHandlersForFileExtensions,
                                 std::move(pref_value));

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(app_url);
  info->scope = app_url;

  apps::FileHandler::AcceptEntry accept_entry;
  accept_entry.mime_type = "text/plain";
  accept_entry.file_extensions.insert(".txt");
  apps::FileHandler file_handler;
  file_handler.action = GURL("https://example.com/path/handler.html");
  file_handler.accept.push_back(std::move(accept_entry));
  info->file_handlers.push_back(std::move(file_handler));

  info->install_url = app_url;
  webapps::AppId app_id =
      test::InstallWebApp(profile(), std::move(info), false,
                          webapps::WebappInstallSource::EXTERNAL_POLICY);
  const WebApp* app = provider().registrar_unsafe().GetAppById(app_id);

  MockWebAppUiManager& ui_manager =
      static_cast<MockWebAppUiManager&>(fake_ui_manager());

  EXPECT_EQ(provider().registrar_unsafe().GetAppFileHandlerApprovalState(
                app->app_id(), ".txt"),
            ApiApprovalState::kAllowed);

  // Default handlers pref setting does not influence user choice.
  EXPECT_EQ(provider().registrar_unsafe().GetAppFileHandlerUserApprovalState(
                app->app_id()),
            ApiApprovalState::kRequiresPrompt);

  // Open a file using app as a handler.
  std::vector<base::FilePath> test_files = {
      base::FilePath::FromUTF8Unsafe("file.txt")};
  apps::AppLaunchParams params(
      app->app_id(), apps::LaunchContainer::kLaunchContainerWindow,
      WindowOpenDisposition::NEW_WINDOW, apps::LaunchSource::kFromFileManager);
  params.launch_files = test_files;

  base::test::TestFuture<std::vector<content::WebContents*>> launch_future;
  publisher_->LaunchAppWithFilesCheckingUserPermission(
      app->app_id(), std::move(params), launch_future.GetCallback());

  // Check if the app was launched without showing file handler dialog.
  EXPECT_TRUE(launch_future.Wait());
  EXPECT_CALL(ui_manager, ShowWebAppFileLaunchDialog(_, _, _)).Times(0);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

class WebAppPublisherHelperTest_WebLockScreenApi
    : public WebAppPublisherHelperTest {
  base::test::ScopedFeatureList features{features::kWebLockScreenApi};
};

TEST_F(WebAppPublisherHelperTest_WebLockScreenApi, CreateWebApp_LockScreen) {
  const std::string name = "some app name";
  const GURL start_url("https://example.com/start_url");
  const GURL lock_screen_url("https://example.com/lock_screen");

  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = base::UTF8ToUTF16(name);
  info->lock_screen_start_url = lock_screen_url;

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));
  const WebApp* web_app = provider().registrar_unsafe().GetAppById(app_id);
  apps::AppPtr app = publisher_->CreateWebApp(web_app);

  EXPECT_TRUE(HandlesIntent(app, apps_util::CreateStartOnLockScreenIntent()));
}

#if BUILDFLAG(IS_CHROMEOS)
class WebAppPublisherHelperNavigationCapturingTest
    : public WebAppPublisherHelperTest {
 public:
  WebAppPublisherHelperNavigationCapturingTest() = default;

  void SetUpFeature(apps::test::LinkCapturingFeatureVersion version) {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        apps::test::GetFeaturesToEnableLinkCapturingUX(version), {});
  }

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

TEST_F(WebAppPublisherHelperNavigationCapturingTest,
       LinkCapturingDefaultOn_WebAppIsPreferred) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOn);

  const GURL start_url("https://example.com/start");
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = u"PWA Test App";
  info->scope = start_url.GetWithoutFilename();

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id));
}

TEST_F(WebAppPublisherHelperNavigationCapturingTest,
       LinkCapturingDefaultOff_WebAppIsNotPreferred) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOff);

  const GURL start_url("https://example.com/start");
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = u"PWA Test App";
  info->scope = start_url.GetWithoutFilename();

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  EXPECT_FALSE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id));
}

TEST_F(WebAppPublisherHelperNavigationCapturingTest, LinkCapturingClientMode) {
  SetUpFeature(
      apps::test::LinkCapturingFeatureVersion::kV2DefaultOnViaClientMode);

  // App 1: Client mode NOT specified in launch handler.
  const GURL start_url1("https://example1.com/start");
  auto info1 = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url1);
  info1->title = u"PWA Test App 1";
  info1->scope = start_url1.GetWithoutFilename();

  webapps::AppId app_id1 = test::InstallWebApp(profile(), std::move(info1));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  EXPECT_FALSE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id1));

  // App 2: Client mode specified in launch handler.
  const GURL start_url2("https://example2.com/start");
  auto info2 = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url2);
  info2->title = u"PWA Test App 2";
  info2->scope = start_url2.GetWithoutFilename();
  info2->launch_handler = LaunchHandler{LaunchHandler::ClientMode::kAuto};

  webapps::AppId app_id2 = test::InstallWebApp(profile(), std::move(info2));

  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id2));
}

TEST_F(WebAppPublisherHelperNavigationCapturingTest,
       LinkCapturingDefaultOn_ArcPrecedence) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOn);

  const GURL start_url("https://example.com/start");

  // Set non-web ARC/Android app as preferred first.
  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  const std::string arc_app_id = "arc_app_id";

  // Set up ARC app in registry.
  auto arc_app = std::make_unique<apps::App>(apps::AppType::kArc, arc_app_id);
  arc_app->readiness = apps::Readiness::kReady;
  std::vector<apps::AppPtr> apps;
  apps.push_back(std::move(arc_app));
  proxy->OnApps(std::move(apps), apps::AppType::kArc,
                /*should_notify_initialized=*/true);

  // Set the ARC app as preferred for the intent filter.
  apps::IntentFilters filters;
  filters.push_back(
      apps_util::MakeIntentFilterForUrlScope(start_url.GetWithoutFilename()));
  proxy->SetSupportedLinksPreference(arc_app_id, std::move(filters));
  ASSERT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(arc_app_id));

  // Install a web app with the same scope.
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = u"PWA Test App";
  info->scope = start_url.GetWithoutFilename();

  webapps::AppId web_app_id = test::InstallWebApp(profile(), std::move(info));

  // The ARC app should stay as the preferred app.
  EXPECT_FALSE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(web_app_id));
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(arc_app_id));
}

TEST_F(WebAppPublisherHelperNavigationCapturingTest,
       EffectiveScopeChanged_UpdateScopeExtensions) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOn);

  const GURL start_url_a("https://example.com/start");
  auto info_a = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url_a);
  info_a->title = u"App A";
  info_a->scope = start_url_a.GetWithoutFilename();
  webapps::AppId app_id_a = test::InstallWebApp(profile(), std::move(info_a));

  const GURL start_url_b("https://example.com/inner/start");
  auto info_b = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url_b);
  info_b->title = u"App B";
  info_b->scope = start_url_b.GetWithoutFilename();
  webapps::AppId app_id_b = test::InstallWebApp(profile(), std::move(info_b));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id_a));
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id_b));

  const GURL extended_url("https://example.org/");
  {
    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
    WebApp* web_app_a = update->UpdateApp(app_id_a);
    ASSERT_TRUE(web_app_a);
    web_app_a->SetValidatedScopeExtensions({ScopeExtensionInfo::CreateForOrigin(
        url::Origin::Create(extended_url))});
  }

  provider().registrar_unsafe().NotifyWebAppEffectiveScopeChanged(app_id_a);

  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id_a));
  EXPECT_EQ(proxy->PreferredAppsList().FindPreferredAppForUrl(extended_url),
            app_id_a);
}

// Verifies that when an app expands its scope extensions, it does not
// override an existing preferred app selection for any of those origins.
TEST_F(
    WebAppPublisherHelperNavigationCapturingTest,
    EffectiveScopeChanged_UpdateScopeExtensionsPreservesExistingPreferredApp) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOn);

  const GURL start_url_a("https://example.org/start");
  auto info_a = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url_a);
  info_a->title = u"App A";
  info_a->scope = start_url_a.GetWithoutFilename();
  info_a->user_display_mode = mojom::UserDisplayMode::kStandalone;
  webapps::AppId app_id_a = test::InstallWebApp(profile(), std::move(info_a));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  proxy->SetSupportedLinksPreference(app_id_a);
  EXPECT_EQ(proxy->PreferredAppsList().FindPreferredAppForUrl(start_url_a),
            app_id_a);

  const GURL start_url_b(base::StrCat(
      {webapps::kIsolatedAppScheme, url::kStandardSchemeSeparator,
       "ber4vf2vfeuma4g7khmguj22nifwjcvo2vhicxbgbioqioicaozaaaaa", "/start"}));
  auto info_b = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url_b);
  info_b->title = u"App B";
  info_b->scope = start_url_b.GetWithoutFilename();
  info_b->user_display_mode = mojom::UserDisplayMode::kStandalone;
  webapps::AppId app_id_b = test::InstallWebApp(profile(), std::move(info_b));

  const GURL attacker_url("https://attacker.example/");
  {
    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
    WebApp* web_app_b = update->UpdateApp(app_id_b);
    ASSERT_TRUE(web_app_b);
    web_app_b->SetIsolationData(
        IsolationData::Builder(
            IwaStorageOwnedBundle{"random_name", /*dev_mode=*/false},
            *IwaVersion::Create("1.0.0"))
            .Build());
    web_app_b->SetValidatedScopeExtensions({ScopeExtensionInfo::CreateForOrigin(
        url::Origin::Create(attacker_url))});
  }

  provider().registrar_unsafe().NotifyWebAppEffectiveScopeChanged(app_id_b);
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id_b));

  {
    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
    WebApp* web_app_b = update->UpdateApp(app_id_b);
    ASSERT_TRUE(web_app_b);
    web_app_b->SetValidatedScopeExtensions(
        {ScopeExtensionInfo::CreateForOrigin(url::Origin::Create(attacker_url)),
         ScopeExtensionInfo::CreateForOrigin(
             url::Origin::Create(start_url_a))});
  }

  provider().registrar_unsafe().NotifyWebAppEffectiveScopeChanged(app_id_b);

  EXPECT_EQ(proxy->PreferredAppsList().FindPreferredAppForUrl(start_url_a),
            app_id_a);
}

TEST_F(WebAppPublisherHelperNavigationCapturingTest,
       AppReinstallShowsLinkCapturingPreference) {
  SetUpFeature(apps::test::LinkCapturingFeatureVersion::kV2DefaultOn);

  const GURL start_url("https://example.com/start");
  auto info = WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  info->title = u"Reinstall Test App";
  info->scope = start_url.GetWithoutFilename();

  webapps::AppId app_id = test::InstallWebApp(profile(), std::move(info));

  auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile());
  EXPECT_TRUE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id));

  // Uninstall the app.
  test::UninstallWebApp(profile(), app_id);

  EXPECT_FALSE(
      proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(app_id));

  // Reinstall the same app.
  auto reinstall_info =
      WebAppInstallInfo::CreateWithStartUrlForTesting(start_url);
  reinstall_info->title = u"Reinstall Test App";
  reinstall_info->scope = start_url.GetWithoutFilename();

  webapps::AppId reinstalled_app_id =
      test::InstallWebApp(profile(), std::move(reinstall_info));
  EXPECT_EQ(reinstalled_app_id, app_id);

  // The reinstalled app should be set to preferred for supported links.
  EXPECT_TRUE(proxy->PreferredAppsList().IsPreferredAppForSupportedLinks(
      reinstalled_app_id));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace web_app
