// 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/web_applications/sub_apps/sub_apps_service_impl.h"

#include <memory>
#include <string>
#include <utility>
#include <vector>

#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_future.h"
#include "base/types/expected.h"
#include "base/values.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/permissions/permission_decision_auto_blocker_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/web_apps/sub_apps/sub_apps_install_dialog_controller.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/ui/web_applications/web_app_browsertest_base.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_command_scheduler.h"
#include "chrome/browser/web_applications/web_app_constants.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_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/test/base/ui_test_utils.h"
#include "components/permissions/permission_decision_auto_blocker.h"
#include "components/webapps/browser/uninstall_result_code.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/subapps/sub_apps_service.mojom-shared.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#include "chrome/browser/web_applications/web_app_utils.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/policy_constants.h"
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

using blink::mojom::SubAppsService;
using blink::mojom::SubAppsServiceAddResult;
using blink::mojom::SubAppsServiceAddResultPtr;
using blink::mojom::SubAppsServiceAddResultType;
using blink::mojom::SubAppsServiceListResultEntry;
using blink::mojom::SubAppsServiceListResultEntryPtr;
using blink::mojom::SubAppsServiceRemoveResult;
using blink::mojom::SubAppsServiceRemoveResultPtr;
using blink::mojom::SubAppsServiceRemoveResultType;
using blink::mojom::SubAppsServiceResultCode;

namespace web_app {

namespace {

// `kDomain` must be just a hostname, not a full URL.
constexpr const char kDomain[] = "www.foo.bar";
constexpr const char kDifferentDomain[] = "https://www.different-domain.com/";
constexpr const char kParentAppPath[] = "/web_apps/basic.html";
constexpr const char kSubAppPath[] = "/sub1/page.html";
constexpr const char kSubAppName[] = "sub1";
constexpr const char kSubAppPathMinimalUi[] = "/sub1/page.html?minimal-ui";
constexpr const char kSubAppPath2[] = "/sub2/page.html";
constexpr const char kSubAppName2[] = "sub2";
constexpr const char kSubAppPath3[] = "/sub3/page.html";
constexpr const char kSubAppName3[] = "sub3";
constexpr const char kSubAppPathInvalid[] = "/invalid/sub/app/path.html";
constexpr const char kSubAppPathSameScope[] = "/sub1/page_same_scope.html";

constexpr const char kSub1[] = "/sub1/page.html";
constexpr const char kSub2[] = "/sub2/page.html";

}  // namespace

using RemoveResultsMojo =
    std::vector<blink::mojom::SubAppsServiceRemoveResultPtr>;

using AddResults = std::vector<
    std::pair<std::string, blink::mojom::SubAppsServiceAddResultType>>;

// There's one simple end-to-end test that actually calls the JS API interface,
// the rest test the mojo interface (since the first layer listening to the API
// calls is almost a direct passthrough to the mojo service).
//
// JS API interface tests are in
// third_party/blink/web_tests/external/wpt/subapps/.

class SubAppsServiceImplBrowserTest : public IsolatedWebAppBrowserTestHarness {
 public:
  SubAppsServiceImplBrowserTest()
      : dialog_override_(
            SubAppsInstallDialogController::SetAutomaticActionForTesting(
                SubAppsInstallDialogController::DialogActionForTesting::
                    kAccept)) {}

  void SetUpOnMainThread() override {
    IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
    notification_display_service_ =
        std::make_unique<NotificationDisplayServiceTester>(profile());
  }

  void TearDownOnMainThread() override {
    notification_display_service_.reset();
    IsolatedWebAppBrowserTestHarness::TearDownOnMainThread();
  }

  content::RenderFrameHost* render_frame_host(
      content::WebContents* web_contents = nullptr) {
    if (!web_contents) {
      web_contents = browser()->tab_strip_model()->GetActiveWebContents();
    }
    return web_contents->GetPrimaryMainFrame();
  }

  GURL GetURLFromPath(const std::string& path) {
    return embedded_https_test_server().GetURL(kDomain, path);
  }

  GURL GetURLFromPath(const std::string& path,
                      const content::RenderFrameHost* parent_frame) {
    return GURL(parent_frame->GetLastCommittedOrigin().GetURL().Resolve(path));
  }

  webapps::AppId GenerateSubAppIdFromPath(
      const std::string& path,
      const content::RenderFrameHost* parent_frame) {
    return GenerateAppId(/*manifest_id_path=*/std::nullopt,
                         GetURLFromPath(path, parent_frame));
  }

  webapps::AppId InstallPwaFromPath(const std::string& path) {
    return InstallPWA(GetURLFromPath(path));
  }

  content::RenderFrameHost* InstallAndOpenParentIwaApp() {
    IsolatedWebAppUrlInfo parent_app = InstallIwaParentApp();
    return OpenApp(parent_app.app_id());
  }

  IsolatedWebAppUrlInfo InstallIwaParentApp() {
    std::unique_ptr<ScopedBundledIsolatedWebApp> app =
        IsolatedWebAppBuilder(
            ManifestBuilder().AddPermissionsPolicy(
                network::mojom::PermissionsPolicyFeature::kSubApps,
                /*self=*/true,
                /*origins=*/{}))
            .AddFolderFromDisk("/", "web_apps/subapps_isolated_app")
            .BuildBundle();
    app->TrustSigningKey();
    IsolatedWebAppUrlInfo parent_app = app->InstallChecked(profile());
    parent_app_id_ = parent_app.app_id();

    EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
        parent_app_id_, WebAppFilter::InstalledInOperatingSystemForTesting()));
    EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
        parent_app_id_, WebAppFilter::IsIsolatedApp()));
    EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

    return parent_app;
  }

  // sub_app_paths should contain paths, not full URLs.
  content::EvalJsResult AddSubAppsJS(
      content::RenderFrameHost* frame,
      const std::vector<std::string>& sub_app_paths) const {
    std::string script = "window.subApps.add([";
    for (const std::string& path : sub_app_paths) {
      base::StringAppendF(&script, R"("%s",)", path.c_str());
    }
    script += "])";

    return content::EvalJs(frame, script);
  }

  content::EvalJsResult ListSubAppsJS(content::RenderFrameHost* frame) const {
    return content::EvalJs(frame, "window.subApps.list()");
  }

  void UninstallParentApp() { UninstallWebApp(parent_app_id_); }

  void UninstallParentAppBySource(WebAppManagement::Type source) {
    base::test::TestFuture<void> uninstall_future;
    provider().scheduler().RemoveInstallManagementMaybeUninstall(
        parent_app_id_, source, webapps::WebappUninstallSource::kAppsPage,
        base::BindLambdaForTesting([&](webapps::UninstallResultCode code) {
          EXPECT_EQ(code, webapps::UninstallResultCode::kInstallSourceRemoved);
          uninstall_future.SetValue();
        }));
    ASSERT_TRUE(uninstall_future.Wait())
        << "UninstallExternalWebApp did not trigger the callback.";
  }

  std::vector<webapps::AppId> GetAllSubAppIds(
      const webapps::AppId& parent_app_id) {
    return provider().registrar_unsafe().GetAllSubAppIds(parent_app_id);
  }

  void BindRemote(content::RenderFrameHost* frame) {
    // Any navigation causes the remote to be destroyed (since the
    // render_frame_host that owns it gets destroyed.)
    SubAppsServiceImpl::CreateIfAllowed(frame,
                                        remote_.BindNewPipeAndPassReceiver());
  }

  // Calls the Add() method on the mojo interface which is async, and waits for
  // it to finish. Argument should contain paths, not full URLs.
  base::expected<AddResults, SubAppsServiceResultCode> CallAdd(
      std::vector<std::string> sub_app_paths) {
    base::test::TestFuture<
        base::expected<std::vector<blink::mojom::SubAppsServiceAddResultPtr>,
                       SubAppsServiceResultCode>>
        future;
    remote_->Add(sub_app_paths, future.GetCallback());
    EXPECT_TRUE(future.Wait()) << "Add did not trigger the callback.";

    auto result = future.Take();
    if (!result.has_value()) {
      return base::unexpected(result.error());
    }

    AddResults add_results;
    for (const auto& add_result : result.value()) {
      add_results.emplace_back(add_result->install_path,
                               add_result->result_type);
    }
    return add_results;
  }

  void ExpectCallAdd(
      base::flat_set<std::pair<std::string, SubAppsServiceAddResultType>>
          expected,
      std::vector<std::string> subapps) {
    auto actual = CallAdd(subapps);
    ASSERT_TRUE(actual.has_value());
    EXPECT_THAT(actual.value(), testing::UnorderedElementsAreArray(expected));
  }

  // Calls the List() method on the mojo interface which is async, and waits for
  // it to finish.
  base::expected<std::vector<blink::mojom::SubAppsServiceListResultEntryPtr>,
                 SubAppsServiceResultCode>
  CallList() {
    base::test::TestFuture<base::expected<
        std::vector<blink::mojom::SubAppsServiceListResultEntryPtr>,
        SubAppsServiceResultCode>>
        future;
    remote_->List(future.GetCallback());
    EXPECT_TRUE(future.Wait()) << "List did not trigger the callback.";
    return future.Take();
  }

  // Calls the Remove() method on the mojo interface which is async, and waits
  // for it to finish.
  base::expected<RemoveResultsMojo, SubAppsServiceResultCode> CallRemove(
      const std::vector<std::string>& manifest_ids) {
    base::test::TestFuture<
        base::expected<std::vector<blink::mojom::SubAppsServiceRemoveResultPtr>,
                       SubAppsServiceResultCode>>
        future;
    remote_->Remove(manifest_ids, future.GetCallback());
    EXPECT_TRUE(future.Wait()) << "Remove did not trigger the callback.";
    return future.Take();
  }

  RemoveResultsMojo SingleRemoveResultMojo(
      const std::string& manifest_id,
      SubAppsServiceRemoveResultType result_type) {
    std::vector<blink::mojom::SubAppsServiceRemoveResultPtr> result;
    result.emplace_back(
        SubAppsServiceRemoveResult::New(manifest_id, result_type));
    return result;
  }

  std::vector<std::pair<webapps::ManifestId, SubAppsServiceRemoveResultType>>
  RemoveResultsToList(const RemoveResultsMojo& results) {
    std::vector<std::pair<webapps::ManifestId, SubAppsServiceRemoveResultType>>
        list;
    for (auto& result : results) {
      list.emplace_back(
          webapps::ManifestId(GetURLFromPath(result->manifest_id)),
          result->result_type);
    }
    return list;
  }

  bool UninstallNotificationShown() {
    return notification_display_service_
        ->GetNotification(SubAppsServiceImpl::kSubAppsUninstallNotificationId)
        .has_value();
  }

  base::SimpleTestClock* clock() { return &clock_; }

 protected:
  base::test::ScopedFeatureList features_{blink::features::kSubApps};
  webapps::AppId parent_app_id_;
  std::optional<webapps::ManifestId> parent_app_manifest_id_;
  mojo::Remote<SubAppsService> remote_;
  base::AutoReset<
      std::optional<SubAppsInstallDialogController::DialogActionForTesting>>
      dialog_override_;
  std::unique_ptr<net::EmbeddedTestServer> iwa_dev_server_;
  std::unique_ptr<NotificationDisplayServiceTester>
      notification_display_service_;
  base::SimpleTestClock clock_;
};

/********** End-to-end test (one is enough!). **********/

// Simple end-to-end test for add().
// NOTE: Only one E2E test is enough, test everything else through the Mojo
// interface (as all the other tests do).
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, EndToEndAdd) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();

  base::DictValue expected_installed;
  expected_installed.Set(kSub1, kSub1);
  expected_installed.Set(kSub2, kSub2);
  base::DictValue expected;
  expected.Set("installedApps", std::move(expected_installed));
  expected.Set("failedApps", base::DictValue());

  EXPECT_EQ(AddSubAppsJS(iwa_frame, {kSub1, kSub2}),
            base::Value(std::move(expected)));
  EXPECT_EQ(2ul, GetAllSubAppIds(parent_app_id_).size());
}

IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       ApiUndefinedForNonIsolatedApp) {
  auto* frame =
      ui_test_utils::NavigateToURL(browser(), GetURLFromPath(kParentAppPath));
  ASSERT_TRUE(frame);

  auto result = content::ExecJs(frame, "window.subApps.add([])");
  EXPECT_TRUE(result.failure_message());
  std::string message = result.failure_message();
  EXPECT_NE(message.find("Cannot read properties of undefined (reading 'add')"),
            std::string::npos);
}

/********** Tests for the Add API call. **********/

// Add a single sub-app and verify all sorts of things.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddSingle) {
  // Dependency graph:
  // NavigateToParentApp --> BindRemote --> CallAdd
  //                   \---------------->/
  // InstallParentApp ----------------->/
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();

  BindRemote(iwa_frame);
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

  ExpectCallAdd({{kSub1, SubAppsServiceAddResultType::kSuccess}}, {kSub1});

  // Verify a bunch of things for the newly installed sub-app.
  webapps::AppId sub_app_id = GenerateSubAppIdFromPath(kSub1, iwa_frame);

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::OpensInDedicatedWindow()));

  const WebApp* sub_app = provider().registrar_unsafe().GetAppById(sub_app_id);
  EXPECT_EQ(sub_app_id, sub_app->app_id());
  EXPECT_EQ(parent_app_id_, sub_app->parent_app_id());
  EXPECT_EQ(std::vector<webapps::AppId>{sub_app->app_id()},
            GetAllSubAppIds(parent_app_id_));
  EXPECT_TRUE(sub_app->IsSubAppInstalledApp());
  EXPECT_TRUE(sub_app->CanUserUninstallWebApp());
  EXPECT_EQ(GetURLFromPath(kSub1, iwa_frame), sub_app->start_url());
  if (provider().ui_manager().CanAddAppToQuickLaunchBar()) {
    EXPECT_FALSE(provider().ui_manager().IsAppInQuickLaunchBar(sub_app_id));
  }
}

// Verify that Add works if IWA is launched as standalone window.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddStandaloneWindow) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});
}

// Verify that adding the same app as standalone and as subapp results in two
// separate apps being registered with different manifest_id and app_id.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       AddSameAppAsSubAndStandalone) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  webapps::AppId standalone_app_id = InstallPwaFromPath(kSubAppPath);
  webapps::AppId sub_app_id = GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_EQ(sub_app_id, GetAllSubAppIds(parent_app_id_)[0]);

  const WebApp* sub_app = provider().registrar_unsafe().GetAppById(sub_app_id);
  const WebApp* standalone_app =
      provider().registrar_unsafe().GetAppById(standalone_app_id);

  EXPECT_TRUE(sub_app->HasOnlySource(WebAppManagement::kSubApp));

  // Manifest ids and app ids are different.
  EXPECT_NE(sub_app->manifest_id(), standalone_app->manifest_id());
  EXPECT_NE(sub_app_id, standalone_app_id);

  EXPECT_TRUE(CallRemove({kSubAppPath}).has_value());

  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id).has_value());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  // Inverting the order of installations.
  webapps::AppId standalone_app_id2 = InstallPwaFromPath(kSubAppPath2);
  webapps::AppId sub_app_id2 =
      GenerateSubAppIdFromPath(kSubAppPath2, iwa_frame);
  ExpectCallAdd({{kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath2});

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id2, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id2,
      WebAppFilter::InstalledInOperatingSystemForTesting()));

  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_EQ(sub_app_id2, GetAllSubAppIds(parent_app_id_)[0]);

  const WebApp* sub_app2 =
      provider().registrar_unsafe().GetAppById(sub_app_id2);
  const WebApp* standalone_app2 =
      provider().registrar_unsafe().GetAppById(standalone_app_id2);

  EXPECT_TRUE(sub_app2->HasOnlySource(WebAppManagement::kSubApp));

  // Manifest ids and app ids are different.
  EXPECT_NE(sub_app2->manifest_id(), standalone_app2->manifest_id());
  EXPECT_NE(sub_app_id2, standalone_app_id2);

  EXPECT_TRUE(CallRemove({kSubAppPath2}).has_value());
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id2).has_value());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id2,
      WebAppFilter::InstalledInOperatingSystemForTesting()));
}

// Add call should fail if the parent app isn't installed.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       AddFailParentAppNotInstalled) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  {
    auto* sync_bridge = &provider().sync_bridge_unsafe();
    auto update = sync_bridge->BeginUpdate();
    update->DeleteApp(parent_app_id_);
  }

  auto actual = CallAdd({kSubAppPath});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kWrongContext);
}

// Add call should fail if the call wasn't made from the context of parent app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       AddFailNotInParentAppContext) {
  IsolatedWebAppUrlInfo parent_app = InstallIwaParentApp();

  // Navigate to a non-isolated page.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GetURLFromPath("/title1.html")));
  auto* frame = render_frame_host();

  mojo::FakeMessageDispatchContext fake_dispatch_context;
  mojo::test::BadMessageObserver bad_message_observer;

  BindRemote(frame);

  EXPECT_THAT(bad_message_observer.WaitForBadMessage(),
              "No isolated context capability");
}

// Verify that Add fails when trying to add the parent app as sub-app of
// itself.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddFailForParentApp) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{"/", SubAppsServiceAddResultType::kRecursiveInstall}}, {"/"});
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that Add fails for an invalid (non-existing) sub-app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddFailNonExistent) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd(
      {{kSubAppPathInvalid, SubAppsServiceAddResultType::kInvalidManifest}},
      {kSubAppPathInvalid});
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

// Add call closes the mojo connection if the argument is wrong origin to the
// calling app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddFailWrongOrigin) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  base::test::TestFuture<void> disconnect_handler_future;
  remote_.set_disconnect_handler(disconnect_handler_future.GetCallback());
  // This call should never succeed and the disconnect handler should be called
  // instead.
  std::vector<std::string> sub_apps_mojo = {kDifferentDomain};
  remote_->Add(sub_apps_mojo,
               base::BindLambdaForTesting(
                   [](base::expected<
                       std::vector<blink::mojom::SubAppsServiceAddResultPtr>,
                       SubAppsServiceResultCode> results) {
                     ADD_FAILURE() << "Callback unexpectedly invoked.";
                   }));

  ASSERT_TRUE(disconnect_handler_future.Wait())
      << "Disconnect handler not invoked.";
}

// Make sure the Add API can't force manifest update. Add sub-app, verify
// display mode, then add the same one again with different display mode in the
// manifest, and verify that it didn't change.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddDoesntForceReinstall) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});
  webapps::AppId sub_app_id = GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::OpensInDedicatedWindow()));

  ExpectCallAdd(
      {{kSubAppPathMinimalUi, SubAppsServiceAddResultType::kAlreadyInstalled}},
      {kSubAppPathMinimalUi});
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::OpensInDedicatedWindow()));
}

// Add call should fail if calling app is already a sub app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, SubAppParentIsIwaParent) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();

  // Install sub_app_1
  BindRemote(iwa_frame);
  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  auto sub_apps = GetAllSubAppIds(parent_app_id_);
  ASSERT_EQ(1ul, sub_apps.size());
  webapps::AppId sub_app_1_id = sub_apps[0];

  // Inside of frame sub_app_1, bind remote and execute install of a new
  // sub_app_2 via Mojo.
  content::RenderFrameHost* sub_app_1_frame = OpenApp(sub_app_1_id);
  remote_.reset();
  BindRemote(sub_app_1_frame);
  auto actual = CallAdd({kSubAppPath2});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kWrongContext);

  // Assert that there is only 1 sub app.
  auto all_sub_apps = GetAllSubAppIds(parent_app_id_);
  EXPECT_EQ(1ul, all_sub_apps.size());
}

/******** Tests for the Add API call - adding multiple/zero sub-apps. ********/

// Add one sub-app, verify count is one. Add it again, still same count. Add a
// second sub-app, verify count is two.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddTwo) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());

  // Try to add first sub app again.
  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kAlreadyInstalled}},
                {kSubAppPath});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());

  // Add second sub app.
  ExpectCallAdd({{kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath2});
  EXPECT_EQ(2ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that adding multiple sub-apps works correctly.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddMultiple) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath2, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath3, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath, kSubAppPath2, kSubAppPath3});

  EXPECT_EQ(3ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that adding multiple sub-apps with overlapping scopes fails.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, OverlappingScopes) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd(
      {{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
       {kSubAppPathSameScope, SubAppsServiceAddResultType::kScopeOverlap}},
      {kSubAppPath, kSubAppPathSameScope});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that adding sub app with url with query params finishes with success.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddWithQueryParams) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd(
      {{"/sub1/page.html?alt", SubAppsServiceAddResultType::kSuccess}},
      {"/sub1/page.html?alt"});

  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that adding a sub-app with overlapping scope to an already installed
// sub-app fails.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       OverlappingScopeWithInstalled) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());

  ExpectCallAdd(
      {{kSubAppPathSameScope, SubAppsServiceAddResultType::kScopeOverlap}},
      {kSubAppPathSameScope});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that Adding a mix of valid and invalid sub-apps works.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       AddMultipleWithInvalidSubApps) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd(
      {{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
       {kSubAppPathInvalid, SubAppsServiceAddResultType::kInvalidManifest},
       {kSubAppPath3, SubAppsServiceAddResultType::kSuccess}},
      {kSubAppPath, kSubAppPathInvalid, kSubAppPath3});
  EXPECT_EQ(2ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that Add fails when trying to add a sub-app with a scope identical to
// the parent app's scope.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddFailIdenticalScope) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{"/identical_scope_sub_app_index.html",
                  SubAppsServiceAddResultType::kScopeOverlap}},
                {"/identical_scope_sub_app_index.html"});
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

// Verify that Add works correctly for 0 sub-apps to be installed.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, AddZero) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({}, {});
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

/******** Tests for the Add API call - dialog behaviour ********/

// Verify that all sub apps are returned with the failure result code when the
// permissions dialog is declined.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       DialogNotAcceptedReturnsAllSubApps) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  auto dialog_override =
      SubAppsInstallDialogController::SetAutomaticActionForTesting(
          SubAppsInstallDialogController::DialogActionForTesting::kCancel);

  auto actual = CallAdd({kSubAppPath, kSubAppPath2, kSubAppPath3});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kUserDeclined);
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       DialogEmbargoedIfDeclinedThreeTimes) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Always hit "Cancel" in the dialog.
  auto dialog_override =
      SubAppsInstallDialogController::SetAutomaticActionForTesting(
          SubAppsInstallDialogController::DialogActionForTesting::kCancel);

  std::vector<std::string> subapps = {
      kSubAppPath,
      kSubAppPath2,
      kSubAppPath3,
  };

  // Dismiss dialog three times.
  for (int i = 0; i < 3; i++) {
    auto actual = CallAdd(subapps);
    ASSERT_FALSE(actual.has_value());
    EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kUserDeclined);
    EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
  }

  EXPECT_TRUE(
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile())
          ->IsEmbargoed(iwa_frame->GetLastCommittedOrigin().GetURL(),
                        ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  dialog_override =
      SubAppsInstallDialogController::SetAutomaticActionForTesting(
          SubAppsInstallDialogController::DialogActionForTesting::kAccept);

  // Add call fails now even though we would accept because the dialog was
  // embargoed.
  auto actual = CallAdd(subapps);
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kUserDeclined);

  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}

IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, DialogEmbargoTiming) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Always hit "Cancel" in the dialog.
  auto dialog_override =
      SubAppsInstallDialogController::SetAutomaticActionForTesting(
          SubAppsInstallDialogController::DialogActionForTesting::kCancel);

  auto* auto_blocker =
      PermissionDecisionAutoBlockerFactory::GetForProfile(profile());
  auto_blocker->SetClockForTesting(clock());

  std::vector<std::string> subapps = {kSubAppPath, kSubAppPath2, kSubAppPath3};

  // Dismiss dialog three times.
  for (int i = 0; i < 3; i++) {
    auto actual = CallAdd(subapps);
    ASSERT_FALSE(actual.has_value());
    EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kUserDeclined);
    EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
  }

  // Check that embargo lasts for 10 minutes.
  EXPECT_TRUE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  clock()->Advance(base::Minutes(9));
  EXPECT_TRUE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  clock()->Advance(base::Minutes(1));
  EXPECT_FALSE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  // Dismiss forth time.
  auto actual = CallAdd(subapps);
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kUserDeclined);
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

  // Check that embargo now lasts for 7 days.
  EXPECT_TRUE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  clock()->Advance(base::Days(6));
  EXPECT_TRUE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));

  clock()->Advance(base::Days(7));
  EXPECT_FALSE(auto_blocker->IsEmbargoed(
      iwa_frame->GetLastCommittedOrigin().GetURL(),
      ContentSettingsType::SUB_APP_INSTALLATION_PROMPTS));
}

/********** Tests for uninstallation behaviour. **********/

// Verify that uninstalling an app with sub-apps causes sub-apps to be
// uninstalled as well.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       UninstallingParentAppUninstallsSubApps) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath2, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath3, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath, kSubAppPath2, kSubAppPath3});

  // Verify that sub-apps are installed.
  webapps::AppId sub_app_id_1 =
      GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);
  webapps::AppId sub_app_id_2 =
      GenerateSubAppIdFromPath(kSubAppPath2, iwa_frame);
  webapps::AppId sub_app_id_3 =
      GenerateSubAppIdFromPath(kSubAppPath3, iwa_frame);

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_1, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_2, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_3, WebAppFilter::InstalledInOperatingSystemForTesting()));

  UninstallParentApp();

  // Verify that both parent app and sub-apps are no longer installed.
  EXPECT_FALSE(provider()
                   .registrar_unsafe()
                   .GetInstallState(parent_app_id_)
                   .has_value());
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id_1).has_value());
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id_2).has_value());
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id_3).has_value());
}

// Verify that uninstalling one source of the parent app which has multiple
// sources of installation doesn't actually uninstall it (or the sub-apps it has
// added).
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       UninstallingParentAppSourceDoesntUninstallSubApps) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Add another source to the parent app.
  {
    ScopedRegistryUpdate update = provider().sync_bridge_unsafe().BeginUpdate();
    WebApp* web_app = update->UpdateApp(parent_app_id_);
    ASSERT_TRUE(web_app);
    web_app->AddSource(WebAppManagement::kDefault);
  }

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath, kSubAppPath2});

  // Verify that 2 sub-apps are installed.
  webapps::AppId sub_app_id_1 =
      GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);
  webapps::AppId sub_app_id_2 =
      GenerateSubAppIdFromPath(kSubAppPath2, iwa_frame);
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_1, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_2, WebAppFilter::InstalledInOperatingSystemForTesting()));

  UninstallParentAppBySource(WebAppManagement::kDefault);

  // Verify that the parent app and the sub-apps are still installed, only the
  // default install source is removed from the parent app.
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      parent_app_id_, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_FALSE(provider()
                   .registrar_unsafe()
                   .GetAppById(parent_app_id_)
                   ->IsPreinstalledApp());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_1, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id_2, WebAppFilter::InstalledInOperatingSystemForTesting()));
}

// Verify that uninstalling an app that has a sub-app with more than one install
// source removes all the "sub-app" install sources and uninstalls it.
IN_PROC_BROWSER_TEST_F(
    SubAppsServiceImplBrowserTest,
    UninstallingParentAppUninstallsOnlySubAppIfMultipleSources) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Install app as standalone app.
  webapps::AppId standalone_app_id = InstallPwaFromPath(kSubAppPath2);
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  // Add another sub-app to verify standalone app install/uninstall does not
  // affect normal sub-app uninstalls.
  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  webapps::AppId sub_app_id = GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  // Add standalone app as sub-app.
  ExpectCallAdd({{kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath2});

  EXPECT_EQ(2ul, GetAllSubAppIds(parent_app_id_).size());

  const WebApp* standalone_app =
      provider().registrar_unsafe().GetAppById(standalone_app_id);

  // Verify that the standalone install is NOT installed and registered as a
  // sub-app.
  EXPECT_NE(parent_app_id_, standalone_app->parent_app_id());
  EXPECT_TRUE(WebAppManagementTypes(
                  {WebAppManagement::kSync, WebAppManagement::kUserInstalled})
                  .HasAll(standalone_app->GetSources()));
  EXPECT_FALSE(standalone_app->IsSubAppInstalledApp());

  UninstallParentApp();

  // Verify that the second sub-app is uninstalled.
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(sub_app_id).has_value());

  // Verify that previous standalone is still installed.
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      standalone_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  // Verify that there are no apps registered as parent app's sub apps.
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_EQ(std::nullopt, standalone_app->parent_app_id());

  // Verify that the standalone app no longer has the sub-app install source.
  EXPECT_TRUE(WebAppManagementTypes(
                  {WebAppManagement::kSync, WebAppManagement::kUserInstalled})
                  .HasAll(standalone_app->GetSources()));
}

class SubAppsServiceImplLimitBrowserTest
    : public SubAppsServiceImplBrowserTest {
 public:
  SubAppsServiceImplLimitBrowserTest() {
    // Set limit to 1.
    scoped_feature_list.InitAndEnableFeatureWithParameters(kSubAppsInstallLimit,
                                                           {{"limit", "1"}});
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list;
};

IN_PROC_BROWSER_TEST_F(SubAppsServiceImplLimitBrowserTest,
                       AddFailExceedsLimit) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Add 2 sub-apps, all should fail.
  auto actual = CallAdd({kSubAppPath, kSubAppPath2});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kTotalLimitExceeded);

  // Add 1 more - success.
  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  // Add 1 more - failure.

  auto actual2 = CallAdd({kSubAppPath2});
  ASSERT_FALSE(actual2.has_value());
  EXPECT_EQ(actual2.error(), SubAppsServiceResultCode::kTotalLimitExceeded);
}

class SubAppsServiceImplPerPromptLimitBrowserTest
    : public SubAppsServiceImplBrowserTest {
 public:
  SubAppsServiceImplPerPromptLimitBrowserTest() {
    // Set per-prompt limit to 1.
    scoped_feature_list.InitAndEnableFeatureWithParameters(
        kSubAppsPerPromptLimit, {{"limit", "1"}});
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list;
};

IN_PROC_BROWSER_TEST_F(SubAppsServiceImplPerPromptLimitBrowserTest,
                       AddFailExceedsPerPromptLimit) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Add 2 sub-apps, all should fail because limit is 1.
  auto actual = CallAdd({kSubAppPath, kSubAppPath2});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kPerPromptLimitExceeded);

  // Add 1 - success.
  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  // Add 1 more - success.
  ExpectCallAdd({{kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath2});
}

/********** Tests for the List API call. **********/

// List call returns the correct value for three sub-apps.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, ListSuccess) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Empty list before adding any sub-apps.
  auto result = CallList();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(std::vector<SubAppsServiceListResultEntryPtr>{}, result.value());

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath2, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath3, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath, kSubAppPath2, kSubAppPath3});

  result = CallList();
  ASSERT_TRUE(result.has_value());

  // We need to use a set for comparison because the ordering changes between
  // invocations (due to embedded test server using a random port each time).
  base::flat_set<SubAppsServiceListResultEntryPtr> expected_set;
  expected_set.emplace(
      SubAppsServiceListResultEntry::New(kSubAppPath, kSubAppName));
  expected_set.emplace(
      SubAppsServiceListResultEntry::New(kSubAppPath2, kSubAppName2));
  expected_set.emplace(
      SubAppsServiceListResultEntry::New(kSubAppPath3, kSubAppName3));

  base::flat_set<SubAppsServiceListResultEntryPtr> actual_set(
      std::make_move_iterator(result.value().begin()),
      std::make_move_iterator(result.value().end()));

  // We see all three sub-apps now.
  EXPECT_EQ(expected_set, actual_set);
}

// Verify that the list call doesn't return a non-sub-apps installed app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       ListDoesntReturnNonSubApp) {
  // Regular install.
  InstallPwaFromPath(kSubAppPath);

  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // Sub-app install.
  ExpectCallAdd({{kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath2});

  std::vector<SubAppsServiceListResultEntryPtr> expected_result;
  expected_result.emplace_back(
      SubAppsServiceListResultEntry::New(kSubAppPath2, kSubAppName2));

  // Should only see the sub-app one here, not the standalone.
  auto result = CallList();
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(1ul, result.value().size());
  EXPECT_EQ(expected_result, result.value());
}

// List call returns failure if the parent app isn't installed.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       ListFailParentAppNotInstalled) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  {
    auto* sync_bridge = &provider().sync_bridge_unsafe();
    auto update = sync_bridge->BeginUpdate();
    update->DeleteApp(parent_app_id_);
  }

  auto result = CallList();
  ASSERT_FALSE(result.has_value());
  EXPECT_EQ(SubAppsServiceResultCode::kWrongContext, result.error());
}

// Verify that List does not return subapps installed outside of the parent app
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       ListReturnsOnlyAppsInstalledByTheCurrentParent) {
  content::RenderFrameHost* iwa_frame_1 = InstallAndOpenParentIwaApp();

  // Install a second IWA.
  ASSERT_OK_AND_ASSIGN(
      IsolatedWebAppUrlInfo parent_app_2,
      IsolatedWebAppBuilder(
          ManifestBuilder().AddPermissionsPolicy(
              network::mojom::PermissionsPolicyFeature::kSubApps, true, {}))
          .BuildBundle()
          ->Install(profile()));
  content::RenderFrameHost* iwa_frame_2 = OpenApp(parent_app_2.app_id());

  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      parent_app_id_, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      parent_app_2.app_id(),
      WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_NE(parent_app_id_, parent_app_2.app_id());

  // Call Add for both IWAs.
  base::DictValue expected_installed_1;
  expected_installed_1.Set(kSubAppPath, kSubAppPath);
  expected_installed_1.Set(kSubAppPath2, kSubAppPath2);
  base::DictValue expected_1;
  expected_1.Set("installedApps", std::move(expected_installed_1));
  expected_1.Set("failedApps", base::DictValue());

  EXPECT_EQ(AddSubAppsJS(iwa_frame_1, {kSubAppPath, kSubAppPath2}),
            base::Value(std::move(expected_1)));

  base::DictValue expected_empty;
  expected_empty.Set("installedApps", base::DictValue());
  expected_empty.Set("failedApps", base::DictValue());
  EXPECT_EQ(AddSubAppsJS(iwa_frame_2, {}),
            base::Value(std::move(expected_empty)));

  base::DictValue expected_installed_3;
  expected_installed_3.Set(kSubAppPath3, kSubAppPath3);
  base::DictValue expected_failed_3;
  expected_failed_3.Set(kSubAppPath, base::DictValue());
  base::DictValue expected_3;
  expected_3.Set("installedApps", std::move(expected_installed_3));
  expected_3.Set("failedApps", std::move(expected_failed_3));
  EXPECT_EQ(AddSubAppsJS(iwa_frame_1, {kSubAppPath, kSubAppPath3}),
            base::Value(std::move(expected_3)));

  // Check List results for the main app contains 3 sub-apps.
  auto list_result_1 = ListSubAppsJS(iwa_frame_1);

  const auto& dict_1 = list_result_1.ExtractDict();
  EXPECT_EQ(3ul, dict_1.size());
  EXPECT_TRUE(dict_1.contains(kSubAppPath));
  EXPECT_TRUE(dict_1.contains(kSubAppPath2));
  EXPECT_TRUE(dict_1.contains(kSubAppPath3));

  // Check List results for the second app is empty.
  auto list_result_2 = ListSubAppsJS(iwa_frame_2);
  EXPECT_TRUE(list_result_2.is_ok());
  EXPECT_EQ(0ul, list_result_2.ExtractDict().size());
}

/********** Tests for the Remove API call. **********/

// Remove works with one app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveOneApp) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  webapps::AppId app_id = GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  auto remove_actual = CallRemove({kSubAppPath});
  ASSERT_TRUE(remove_actual.has_value());
  EXPECT_EQ(SingleRemoveResultMojo(kSubAppPath,
                                   SubAppsServiceRemoveResultType::kSuccess),
            remove_actual.value());
  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_FALSE(
      provider().registrar_unsafe().GetInstallState(app_id).has_value());
  EXPECT_TRUE(UninstallNotificationShown());
}

// Remove works with a list of apps.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveListOfApps) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess},
                 {kSubAppPath2, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath, kSubAppPath2});

  EXPECT_EQ(2ul, GetAllSubAppIds(parent_app_id_).size());

  std::vector<std::pair<webapps::ManifestId, SubAppsServiceRemoveResultType>>
      expected_result;
  expected_result.emplace_back(webapps::ManifestId(GetURLFromPath(kSubAppPath)),
                               SubAppsServiceRemoveResultType::kSuccess);
  expected_result.emplace_back(
      webapps::ManifestId(GetURLFromPath(kSubAppPath2)),
      SubAppsServiceRemoveResultType::kSuccess);
  expected_result.emplace_back(
      webapps::ManifestId(GetURLFromPath(kSubAppPath3)),
      SubAppsServiceRemoveResultType::kNotFound);

  auto remove_result = CallRemove({kSubAppPath, kSubAppPath2, kSubAppPath3});
  ASSERT_TRUE(remove_result.has_value());
  EXPECT_THAT(RemoveResultsToList(remove_result.value()),
              testing::UnorderedElementsAreArray(expected_result));

  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

  webapps::ManifestId sub_app_id_1 =
      webapps::ManifestId(GetURLFromPath(kSubAppPath));
  webapps::ManifestId sub_app_id_2 =
      webapps::ManifestId(GetURLFromPath(kSubAppPath2));
  EXPECT_FALSE(provider()
                   .registrar_unsafe()
                   .GetInstallState(GenerateAppIdFromManifestId(sub_app_id_1))
                   .has_value());
  EXPECT_FALSE(provider()
                   .registrar_unsafe()
                   .GetInstallState(GenerateAppIdFromManifestId(sub_app_id_2))
                   .has_value());

  std::optional<message_center::Notification> uninstall_notification =
      notification_display_service_->GetNotification(
          SubAppsServiceImpl::kSubAppsUninstallNotificationId);
  ASSERT_TRUE(uninstall_notification.has_value());
  // Confirm the string generated for the notification title mentions the
  // correct number of uninstalls (i.e. successful uninstalls rather than
  // the number of requested installs).
  ASSERT_TRUE(uninstall_notification->title().find(u" 2 "));
  ASSERT_TRUE(uninstall_notification->never_timeout());
}

// Calling remove with an empty list doesn't crash.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveEmptyList) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  webapps::AppId sub_app_id = GenerateSubAppIdFromPath(kSubAppPath, iwa_frame);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));

  EXPECT_TRUE(CallRemove({}).has_value());
  EXPECT_EQ(1ul, GetAllSubAppIds(parent_app_id_).size());
  EXPECT_TRUE(provider().registrar_unsafe().AppMatches(
      sub_app_id, WebAppFilter::InstalledInOperatingSystemForTesting()));
  EXPECT_FALSE(UninstallNotificationShown());
}

// Remove fails for a regular installed app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveFailRegularApp) {
  InstallPwaFromPath(kSubAppPath);

  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  auto actual = CallRemove({kSubAppPath});
  ASSERT_TRUE(actual.has_value());
  EXPECT_EQ(SingleRemoveResultMojo(kSubAppPath,
                                   SubAppsServiceRemoveResultType::kNotFound),
            actual.value());
  EXPECT_FALSE(UninstallNotificationShown());
}

// Remove fails for a sub-app with a different parent_app_id.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveFailWrongParent) {
  content::RenderFrameHost* iwa_frame_1 = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame_1);

  ExpectCallAdd({{kSubAppPath, SubAppsServiceAddResultType::kSuccess}},
                {kSubAppPath});

  // Install a second IWA.
  ASSERT_OK_AND_ASSIGN(
      IsolatedWebAppUrlInfo parent_app_2,
      IsolatedWebAppBuilder(
          ManifestBuilder().AddPermissionsPolicy(
              network::mojom::PermissionsPolicyFeature::kSubApps, true, {}))
          .BuildBundle()
          ->Install(profile()));
  content::RenderFrameHost* iwa_frame_2 = OpenApp(parent_app_2.app_id());
  remote_.reset();
  BindRemote(iwa_frame_2);

  auto actual = CallRemove({kSubAppPath});
  ASSERT_TRUE(actual.has_value());
  EXPECT_EQ(SubAppsServiceRemoveResultType::kNotFound,
            actual.value()[0]->result_type);
  EXPECT_FALSE(UninstallNotificationShown());
}

// Remove call returns failure if the parent app isn't installed.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       RemoveFailCallingAppNotInstalled) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  // In order to trigger the edge case of the parent app being uninstalled in
  // between an API call making it to the backend, just remove it from the
  // database here, allowing the parent window to remain active to make the
  // API call.
  {
    auto* sync_bridge = &provider().sync_bridge_unsafe();
    auto update = sync_bridge->BeginUpdate();
    update->DeleteApp(parent_app_id_);
  }

  auto actual = CallRemove({kSubAppPath});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(), SubAppsServiceResultCode::kWrongContext);
  EXPECT_FALSE(UninstallNotificationShown());
}

// Remove call closes the mojo connection if the argument is wrong origin to the
// calling app.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest, RemoveFailWrongOrigin) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  base::test::TestFuture<void> disconnect_handler_future;
  remote_.set_disconnect_handler(disconnect_handler_future.GetCallback());
  // This call should never succeed and the disconnect handler should be called
  // instead.
  remote_->Remove(
      {kDifferentDomain},
      base::BindLambdaForTesting(
          [](base::expected<
              std::vector<blink::mojom::SubAppsServiceRemoveResultPtr>,
              SubAppsServiceResultCode> result) {
            ADD_FAILURE() << "Callback unexpectedly invoked.";
          }));
  ASSERT_TRUE(disconnect_handler_future.Wait())
      << "Disconnect handler not invoked.";
  EXPECT_FALSE(UninstallNotificationShown());
}

// Remove with a cross-origin path followed by a valid path should not crash.
// Regression test: RemoveSubApp() calls ReportBadMessageAndDeleteThis() for the
// cross-origin path, deleting `this`. Without the weak_ptr check after each
// iteration, the loop continues and accesses freed memory.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplBrowserTest,
                       RemoveWrongOriginFollowedByValidPath) {
  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  base::test::TestFuture<void> disconnect_handler_future;
  remote_.set_disconnect_handler(disconnect_handler_future.GetCallback());
  remote_->Remove(
      {kDifferentDomain, kSubAppPath},
      base::BindLambdaForTesting(
          [](base::expected<
              std::vector<blink::mojom::SubAppsServiceRemoveResultPtr>,
              SubAppsServiceResultCode> result) {
            ADD_FAILURE() << "Callback unexpectedly invoked.";
          }));
  ASSERT_TRUE(disconnect_handler_future.Wait())
      << "Disconnect handler not invoked.";
  EXPECT_FALSE(UninstallNotificationShown());
}

/******** Tests for policy-disabled scenarios ********/

// Test class for SubApps API when WebAppInstallByUserEnabled policy is
// disabled.
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
class SubAppsServiceImplPolicyDisabledBrowserTest
    : public SubAppsServiceImplBrowserTest {
 public:
  SubAppsServiceImplPolicyDisabledBrowserTest() = default;
  SubAppsServiceImplPolicyDisabledBrowserTest(
      const SubAppsServiceImplPolicyDisabledBrowserTest&) = delete;

  void SetUpInProcessBrowserTestFixture() override {
    SubAppsServiceImplBrowserTest::SetUpInProcessBrowserTestFixture();

    // Set up the policy provider to disable web app installs
    policy_provider_.SetDefaultReturns(
        /*is_initialization_complete_return=*/true,
        /*is_first_policy_load_complete_return=*/true);
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
        &policy_provider_);

    // Create policy map with disabled web app user installs
    policy::PolicyMap policies;
    policies.Set(policy::key::kWebAppInstallByUserEnabled,
                 policy::POLICY_LEVEL_MANDATORY, policy::POLICY_SCOPE_USER,
                 policy::POLICY_SOURCE_ENTERPRISE_DEFAULT, base::Value(false),
                 nullptr);
    policy_provider_.UpdateChromePolicy(policies);
  }

 private:
  testing::NiceMock<policy::MockConfigurationPolicyProvider> policy_provider_;
};

// Verify that the Add API fails when WebAppInstallByUserEnabled policy is
// disabled.
IN_PROC_BROWSER_TEST_F(SubAppsServiceImplPolicyDisabledBrowserTest,
                       AddFailsWhenPolicyDisabled) {
  // Verify the policy is disabled from startup
  ASSERT_FALSE(AreWebAppsUserInstallable(
      Profile::FromBrowserContext(browser()->GetProfile())));

  content::RenderFrameHost* iwa_frame = InstallAndOpenParentIwaApp();
  BindRemote(iwa_frame);

  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());

  // Attempt to add sub-apps should fail due to policy
  auto actual = CallAdd({kSubAppPath, kSubAppPath2});
  ASSERT_FALSE(actual.has_value());
  EXPECT_EQ(actual.error(),
            SubAppsServiceResultCode::kWebAppsNotUserInstallable);

  // Verify no sub-apps were actually installed

  EXPECT_EQ(0ul, GetAllSubAppIds(parent_app_id_).size());
}
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

}  // namespace web_app
