// 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 <optional>

#include "base/auto_reset.h"
#include "base/time/time.h"
#include "chrome/browser/extensions/extension_service_test_base.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/extensions/tab_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "components/crx_file/id_util.h"
#include "components/sessions/content/session_tab_helper.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/web_contents_tester.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/host_access_request_helper.h"
#include "extensions/browser/permissions/active_tab_permission_granter.h"
#include "extensions/browser/permissions/permissions_updater.h"
#include "extensions/browser/permissions/scripting_permissions_modifier.h"
#include "extensions/browser/permissions/site_permissions_helper.h"
#include "extensions/browser/permissions_manager.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/url_pattern.h"
#include "extensions/test/permissions_manager_waiter.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

class HostAccessRequestsHelperUnittest : public ExtensionServiceTestBase {
 public:
  HostAccessRequestsHelperUnittest() = default;
  ~HostAccessRequestsHelperUnittest() override = default;

  HostAccessRequestsHelperUnittest(const HostAccessRequestsHelperUnittest&) =
      delete;
  HostAccessRequestsHelperUnittest& operator=(
      const HostAccessRequestsHelperUnittest&) = delete;

  // Installs an extension with `host_permission` and withhelds them.
  scoped_refptr<const Extension> InstallExtensionAndWithholdHostPermissions(
      const std::string& name,
      const std::string& host_permission);

  // Installs an extension with activeTab permission.
  scoped_refptr<const Extension> InstallExtensionWithActiveTab(
      const std::string& name);

  // Adds a new tab with `url` to the tab strip, and returns the WebContents
  // associated with it.
  content::WebContents* AddTab(const GURL& url);

  PermissionsManager* permissions_manager() { return permissions_manager_; }

  void SetUp() override;
  void TearDown() override;

 private:
  std::vector<std::unique_ptr<content::WebContents>> web_contents_collection_;
  raw_ptr<PermissionsManager> permissions_manager_ = nullptr;
  std::optional<base::AutoReset<base::TimeDelta>> cooldown_reset_;
};

void HostAccessRequestsHelperUnittest::SetUp() {
  ExtensionServiceTestBase::SetUp();
  InitializeEmptyExtensionService();
  permissions_manager_ = PermissionsManager::Get(profile());
  cooldown_reset_.emplace(
      HostAccessRequestsHelper::SetCooldownForTesting(base::Seconds(0)));
}

void HostAccessRequestsHelperUnittest::TearDown() {
  cooldown_reset_.reset();
  web_contents_collection_.clear();
  permissions_manager_ = nullptr;
  ExtensionServiceTestBase::TearDown();
}

scoped_refptr<const Extension>
HostAccessRequestsHelperUnittest::InstallExtensionAndWithholdHostPermissions(
    const std::string& name,
    const std::string& host_permission) {
  auto extension = ExtensionBuilder(name)
                       .AddHostPermission(host_permission)
                       .SetID(crx_file::id_util::GenerateId(name))
                       .Build();

  PermissionsUpdater updater(profile());
  updater.InitializePermissions(extension.get());

  ScriptingPermissionsModifier(profile(), extension)
      .SetWithholdHostPermissions(true);

  registrar()->AddExtension(extension);
  return extension;
}

scoped_refptr<const Extension>
HostAccessRequestsHelperUnittest::InstallExtensionWithActiveTab(
    const std::string& name) {
  auto extension = ExtensionBuilder(name)
                       .SetID(crx_file::id_util::GenerateId(name))
                       .AddAPIPermission("activeTab")
                       .Build();

  PermissionsUpdater updater(profile());
  updater.InitializePermissions(extension.get());

  registrar()->AddExtension(extension);
  return extension;
}

content::WebContents* HostAccessRequestsHelperUnittest::AddTab(
    const GURL& url) {
  std::unique_ptr<content::WebContents> test_web_contents(
      content::WebContentsTester::CreateTestWebContents(profile(), nullptr));
  content::WebContents* raw_contents = test_web_contents.get();

  sessions::SessionTabHelper::CreateForWebContents(
      raw_contents, sessions::SessionTabHelper::DelegateLookup());
  int tab_id = sessions::SessionTabHelper::IdForTab(raw_contents).id();
  ActiveTabPermissionGranter::CreateForWebContents(raw_contents, tab_id,
                                                   profile());
  extensions::TabHelper::CreateForWebContents(raw_contents);

  web_contents_collection_.push_back(std::move(test_web_contents));

  raw_contents->WasShown();
  content::NavigationSimulator::NavigateAndCommitFromBrowser(raw_contents, url);
  EXPECT_EQ(url, raw_contents->GetLastCommittedURL());

  return raw_contents;
}

// Tests host access requests are properly added and removed.
TEST_F(HostAccessRequestsHelperUnittest, AddAndRemoveRequests) {
  auto extension_A =
      InstallExtensionAndWithholdHostPermissions("Extension A", "<all_urls>");
  auto extension_B =
      InstallExtensionAndWithholdHostPermissions("Extension B", "<all_urls>");

  content::WebContents* web_contents = AddTab(GURL("http://www.example.com/"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Try to remove a non-existent host access request. Verify nothing happens.
  EXPECT_EQ(
      permissions_manager()->RemoveHostAccessRequest(tab_id, extension_A->id()),
      PermissionsManager::RemoveRequestResult::kNotFound);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Add host access request for extension A. Verify only extension A has an
  // active request.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id,
                                              *extension_A);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Add host access request for extension B. Verify both extensions have active
  // requests.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id,
                                              *extension_B);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Remove host access request for extension A. Verify only extension B has an
  // active request.
  EXPECT_EQ(
      permissions_manager()->RemoveHostAccessRequest(tab_id, extension_A->id()),
      PermissionsManager::RemoveRequestResult::kSuccess);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));
}

// Tests that host access requests which include a filter are properly added
// and removed.
TEST_F(HostAccessRequestsHelperUnittest,
       AddAndRemoveRequestsWithPatternFilter) {
  auto extension =
      InstallExtensionAndWithholdHostPermissions("Extension", "<all_urls>");
  URLPattern matching_filter(Extension::kValidHostPermissionSchemes,
                             "http://www.matching.com/");
  URLPattern non_matching_filter(Extension::kValidHostPermissionSchemes,
                                 "http://www.non-matching.com/");

  content::WebContents* web_contents = AddTab(GURL("http://www.matching.com/"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add a host access request with filter that does not match the current web
  // contents. Verify request is not active.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              non_matching_filter);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Add a host access request with filter that matches the current web
  // contents. Verify request is active.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              matching_filter);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Remove a host access request with filter that doesn't match the current web
  // contents. Verify request is active.
  permissions_manager()->RemoveHostAccessRequest(tab_id, extension->id(),
                                                 non_matching_filter);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Remove a host access request with filter that matches the current web
  // contents. Verify request is not active.
  permissions_manager()->RemoveHostAccessRequest(tab_id, extension->id(),
                                                 matching_filter);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Add back a host access request with filter that matches the current web
  // contents (so we can test removal without filter). Verify request is active.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              matching_filter);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Remove a host access request for extension without specifying a filter.
  // Verify request is no longer active, since a request without filter matches
  // all patterns.
  EXPECT_EQ(
      permissions_manager()->RemoveHostAccessRequest(tab_id, extension->id()),
      PermissionsManager::RemoveRequestResult::kSuccess);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Test request is not added when extension only has activeTab permission.
TEST_F(HostAccessRequestsHelperUnittest,
       InvalidRequest_ExtensionOnlyHasActiveTabPermission) {
  auto extension = InstallExtensionWithActiveTab("Extension");

  content::WebContents* web_contents = AddTab(GURL("http://www.example.com"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension. Verify request is not active.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Tests that host access requests dismissed by the user are not active
// requests.
TEST_F(HostAccessRequestsHelperUnittest, UserDismissedRequest) {
  auto extension =
      InstallExtensionAndWithholdHostPermissions("Extension", "<all_urls>");

  content::WebContents* web_contents = AddTab(GURL("http://www.example.com/"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension A. Verify extension has an active
  // request.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Dismiss host access request for extension. Verify request is not an active
  // request.
  permissions_manager()->UserDismissedHostAccessRequest(web_contents, tab_id,
                                                        extension->id());
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Tests request is removed on cross-origin navigations.
TEST_F(HostAccessRequestsHelperUnittest,
       RequestRemovedOnCrossOriginNavigation) {
  auto extension =
      InstallExtensionAndWithholdHostPermissions("Extension", "<all_urls>");

  content::WebContents* web_contents =
      AddTab(GURL("http://www.same-origin.com/a"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Same-origin navigation should retain request.
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents, GURL("http://www.same-origin.com/b"));
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Cross-origin navigation should remove request.
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents, GURL("http://www.cross-origin.com/c"));
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Tests that a request with matching filter pattern are active on same origin
// navigations.
TEST_F(HostAccessRequestsHelperUnittest, RequestUpdatedOnPageNavigations) {
  auto extension =
      InstallExtensionAndWithholdHostPermissions("Extension", "<all_urls>");

  content::WebContents* web_contents = AddTab(GURL("http://www.example.com/"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension. Verify request is active.
  std::optional<URLPattern> filter;
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              filter);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Add host access request for extension with a filter that doesn't match the
  // current web contents but has the same origin. Verify request is not
  // active, since new request should override the previous one.
  filter = URLPattern(Extension::kValidHostPermissionSchemes, "*://*/path");
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              filter);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Navigate to a host that matches the filter. Verify request is active.
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents, GURL("http://www.example.com/path"));
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Add host access request for extension with a filter that doesn't have the
  // same origin as the current web contents. Verify request is not active.
  filter = URLPattern(Extension::kValidHostPermissionSchemes,
                      "http://www.other.com/path");
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension,
                                              filter);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Test request is removed when extension is granted "always on this host" host
// access.
TEST_F(HostAccessRequestsHelperUnittest,
       RequestRemovedWhenExtensionHasSiteAccess) {
  auto extension =
      InstallExtensionAndWithholdHostPermissions("Extension", "<all_urls>");

  content::WebContents* web_contents =
      AddTab(GURL("http://www.same-origin.com/a"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Grant "always on this host" access to the extension.
  PermissionsManagerWaiter waiter(PermissionsManager::Get(profile()));
  SitePermissionsHelper permissions(profile());
  permissions.UpdateSiteAccess(
      *extension, web_contents, PermissionsManager::UserSiteAccess::kOnSite,
      web_contents->GetPrimaryMainFrame()->GetLastCommittedOrigin());
  waiter.WaitForExtensionPermissionsUpdate();

  // Request should be removed since extension has granted host access.
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Test request is removed when extension is granted one-time host access.
TEST_F(HostAccessRequestsHelperUnittest,
       RequestRemovedWhenExtensionHasGrantedActiveTab) {
  // Add extension with host permissions and activeTab, and withhold the host
  // permissions.
  const std::string extension_name = "Extension";
  auto extension = ExtensionBuilder(extension_name)
                       .AddHostPermission("http://www.example.com/")
                       .AddAPIPermission("activeTab")
                       .SetID(crx_file::id_util::GenerateId(extension_name))
                       .Build();
  registrar()->AddExtension(extension);
  ScriptingPermissionsModifier(profile(), extension)
      .SetWithholdHostPermissions(true);

  content::WebContents* web_contents = AddTab(GURL("http://www.example.com"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for extension.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id, *extension);
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));

  // Grant tab permission to extension.
  ActiveTabPermissionGranter* active_tab_permission_granter =
      ActiveTabPermissionGranter::FromWebContents(web_contents);
  ASSERT_TRUE(active_tab_permission_granter);
  PermissionsManagerWaiter waiter(PermissionsManager::Get(profile()));
  active_tab_permission_granter->GrantIfRequested(extension.get());
  waiter.WaitForActiveTabPermissionGranted(extension->id());

  // Request should be removed since extension has granted host access.
  // Even though the extension was only granted activeTab (and not persistent
  // host access, as would be the case if the user accepted the request), we no
  // longer show the host access request. This is to avoid a user seeing a
  // request after having granted one-time access to an extension.
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension->id()));
}

// Test request is removed when extension is unloaded.
TEST_F(HostAccessRequestsHelperUnittest,
       RequestRemovedWhenExtensionIsUnloaded) {
  auto extension_A =
      InstallExtensionAndWithholdHostPermissions("Extension A", "<all_urls>");
  auto extension_B =
      InstallExtensionAndWithholdHostPermissions("Extension B", "<all_urls>");

  content::WebContents* web_contents =
      AddTab(GURL("http://www.same-origin.com/a"));
  int tab_id = ExtensionTabUtil::GetTabId(web_contents);

  // Add host access request for both extensions.
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id,
                                              *extension_A);
  permissions_manager()->AddHostAccessRequest(web_contents, tab_id,
                                              *extension_B);
  ASSERT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  ASSERT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Uninstall extension A. Verify only extension B should have a host access
  // request.
  registrar()->UninstallExtension(
      extension_A->id(), extensions::UNINSTALL_REASON_FOR_TESTING, nullptr);
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_TRUE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Disable extension B. Verify no extension should have a host access request.
  registrar()->DisableExtension(
      extension_B->id(), {extensions::disable_reason::DISABLE_USER_ACTION});
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));

  // Enable extension B. Verify no extension has a host access request. Request
  // is not persisted when extension is re-enabled, the extension needs to add
  // the request again.
  registrar()->EnableExtension(extension_B->id());
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_A->id()));
  EXPECT_FALSE(permissions_manager()->HasActiveHostAccessRequest(
      tab_id, extension_B->id()));
}

}  // namespace extensions
