// 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/chrome_content_browser_client.h"
#include "chrome/browser/notifications/non_persistent_notification_handler.h"
#include "chrome/browser/notifications/notification_permission_context.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/permission_util.h"
#include "components/permissions/request_type.h"
#include "components/permissions/test/mock_permission_prompt_factory.h"
#include "components/permissions/test/mock_permission_request.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/common/content_client.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_builders.h"

namespace {

using Permission = ukm::builders::Permission;

const char kTestFilePath[] =
    "/notifications/notification_permission_checker.html";
const char kTesterHost[] = "notification.com";
const char kIsolatedEmbedderHost[] = "isolated.com";
const char kEmbedderHost[] = "normal.com";

// A ChromeContentBrowserClient that returns a non-default
// StoragePartitionConfig for the given Origin.
class StoragePartitioningChromeContentBrowserClient
    : public ChromeContentBrowserClient {
 public:
  explicit StoragePartitioningChromeContentBrowserClient(
      const std::string& partitioned_host)
      : partitioned_host_(partitioned_host) {}

  ~StoragePartitioningChromeContentBrowserClient() override = default;

  content::StoragePartitionConfig GetStoragePartitionConfigForSite(
      content::BrowserContext* browser_context,
      const GURL& site) override {
    if (site.GetHost() == partitioned_host_) {
      return content::StoragePartitionConfig::Create(
          browser_context, partitioned_host_, /*partition_name=*/"",
          /*in_memory=*/false);
    }
    return ChromeContentBrowserClient::GetStoragePartitionConfigForSite(
        browser_context, site);
  }

 private:
  std::string partitioned_host_;
};

}  // namespace

class NotificationPermissionBrowserTest : public InProcessBrowserTest {
 public:
  NotificationPermissionBrowserTest()
      : partitioning_client_(kIsolatedEmbedderHost) {}

  NotificationPermissionBrowserTest(const NotificationPermissionBrowserTest&) =
      delete;
  NotificationPermissionBrowserTest& operator=(
      const NotificationPermissionBrowserTest&) = delete;
  ~NotificationPermissionBrowserTest() override {
    CHECK_EQ(&partitioning_client_,
             SetBrowserClientForTesting(original_client_));
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);

    https_server_ = std::make_unique<net::EmbeddedTestServer>(
        net::EmbeddedTestServer::TYPE_HTTPS);
    https_server_->ServeFilesFromSourceDirectory(server_root_);
    EXPECT_TRUE(https_server_->Start());

    original_client_ = SetBrowserClientForTesting(&partitioning_client_);
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    mock_cert_verifier_.SetUpCommandLine(command_line);
  }

  void SetUpInProcessBrowserTestFixture() override {
    mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
  }

  void TearDownInProcessBrowserTestFixture() override {
    mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
  }

 protected:
  void GrantNotificationPermissionForTest(const GURL& url) const {
    NotificationPermissionContext::UpdatePermission(
        browser()->GetProfile(), url.DeprecatedGetOriginAsURL(),
        CONTENT_SETTING_ALLOW);
  }

  GURL TesterUrl() const {
    return https_server_->GetURL(kTesterHost, kTestFilePath);
  }

  GURL IsolatedEmbedderUrl() const {
    return https_server_->GetURL(kIsolatedEmbedderHost, kTestFilePath);
  }

  GURL EmbedderUrl() const {
    return https_server_->GetURL(kEmbedderHost, kTestFilePath);
  }

  content::WebContents* GetActiveWebContents() const {
    return browser()->GetTabStripModel()->GetActiveWebContents();
  }

  content::RenderFrameHost* CreateChildIframe(
      content::RenderFrameHost* parent_rfh,
      const GURL& iframe_src) {
    // For now assume this is the only child iframe.
    EXPECT_FALSE(ChildFrameAt(parent_rfh, 0));

    EXPECT_EQ("iframe loaded",
              EvalJs(parent_rfh, content::JsReplace(R"(
                new Promise((resolve, reject) => {
                  const iframe = document.createElement('iframe');
                  iframe.id = 'child_iframe';
                  iframe.src = $1;
                  iframe.onload = _ => { resolve('iframe loaded') };
                  iframe.onerror = e => { reject(e) };
                  document.body.appendChild(iframe);
                }))",
                                                    iframe_src)));

    content::RenderFrameHost* iframe = ChildFrameAt(parent_rfh, 0);
    EXPECT_TRUE(iframe);
    EXPECT_EQ(iframe_src, iframe->GetLastCommittedURL());
    return iframe;
  }

 private:
  const base::FilePath server_root_{FILE_PATH_LITERAL("chrome/test/data")};
  std::unique_ptr<net::EmbeddedTestServer> https_server_;
  content::ContentMockCertVerifier mock_cert_verifier_;

  raw_ptr<content::ContentBrowserClient> original_client_ = nullptr;
  StoragePartitioningChromeContentBrowserClient partitioning_client_;
};

// Tests that undelegated permissions which have their default/prompt value on
// an origin are automatically denied in documents from that origin when
// loaded as a cross-origin iframe.
IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       UndelegatedPermissionDeniedIfNotGrantedToOrigin) {
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("default", EvalJs(main_frame, "getNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(main_frame, "getServiceWorkerNotificationPermission()"));

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), EmbedderUrl()));
  main_frame = GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("default", EvalJs(main_frame, "getNotificationPermission()"));

  content::RenderFrameHost* iframe = CreateChildIframe(main_frame, TesterUrl());
  EXPECT_EQ("denied", EvalJs(iframe, "getNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(iframe, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("denied", EvalJs(iframe, "getPushPermission()"));
  // TODO(crbug.com/40254041): This should return 'denied'.
  EXPECT_EQ("prompt", EvalJs(iframe, "getServiceWorkerPushPermission()"));
}

// Tests that undelegated permissions aren't delegated to an embedded frame
// as other permissions are. If 'example.com' was granted notification
// permissions by the user when it was a top-level frame, then it retains that
// permission when iframed in another page, regardless of the other page's
// permission status.
IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       UndelegatedPermissionsAreNotDelegated) {
  GrantNotificationPermissionForTest(TesterUrl());

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("granted", EvalJs(main_frame, "getNotificationPermission()"));
  EXPECT_EQ("granted",
            EvalJs(main_frame, "getServiceWorkerNotificationPermission()"));

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), EmbedderUrl()));
  main_frame = GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("default", EvalJs(main_frame, "getNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(main_frame, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("prompt", EvalJs(main_frame, "getPushPermission()"));
  // TODO(crbug.com/40254041): This should return 'denied'.
  EXPECT_EQ("prompt", EvalJs(main_frame, "getServiceWorkerPushPermission()"));

  content::RenderFrameHost* iframe = CreateChildIframe(main_frame, TesterUrl());
  EXPECT_EQ("granted", EvalJs(iframe, "getNotificationPermission()"));
  EXPECT_EQ("granted",
            EvalJs(iframe, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("granted", EvalJs(iframe, "getPushPermission()"));
  EXPECT_EQ("granted", EvalJs(iframe, "getServiceWorkerPushPermission()"));
}

// Tests that iframes not using their normal StoragePartition don't have
// notification permission, even if they would have permission outside of an
// isolated app.
IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       IframesInNonDefaultPartitionDontGetPermission) {
  GrantNotificationPermissionForTest(TesterUrl());

  // Verify that TesterUrl() has notification/push permission.
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("granted", EvalJs(main_frame, "getNotificationPermission()"));
  EXPECT_EQ("granted",
            EvalJs(main_frame, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("granted", EvalJs(main_frame, "queryNotificationPermission()"));
  EXPECT_EQ("granted",
            EvalJs(main_frame, "queryServiceWorkerNotificationPermission()"));
  EXPECT_EQ("granted", EvalJs(main_frame, "getPushPermission()"));
  EXPECT_EQ("granted", EvalJs(main_frame, "getServiceWorkerPushPermission()"));

  // Load a site that uses a dedicated StoragePartition and verify that it has
  // default notification/push permissions.
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), IsolatedEmbedderUrl()));
  main_frame = GetActiveWebContents()->GetPrimaryMainFrame();
  EXPECT_EQ("default", EvalJs(main_frame, "getNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(main_frame, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("prompt", EvalJs(main_frame, "queryNotificationPermission()"));
  EXPECT_EQ("prompt",
            EvalJs(main_frame, "queryServiceWorkerNotificationPermission()"));
  EXPECT_EQ("prompt", EvalJs(main_frame, "getPushPermission()"));
  EXPECT_EQ("prompt", EvalJs(main_frame, "getServiceWorkerPushPermission()"));

  // Load TesterUrl() in an iframe inside the dedicated StoragePartition page.
  // Even though TesterUrl() has notification/push permission when in a main
  // frame, it shouldn't when it's embedded in a different StoragePartition.
  content::RenderFrameHost* iframe = CreateChildIframe(main_frame, TesterUrl());
  EXPECT_EQ("denied", EvalJs(iframe, "getNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(iframe, "getServiceWorkerNotificationPermission()"));
  EXPECT_EQ("denied", EvalJs(iframe, "queryNotificationPermission()"));
  EXPECT_EQ("denied",
            EvalJs(iframe, "queryServiceWorkerNotificationPermission()"));
  EXPECT_EQ("denied", EvalJs(iframe, "requestNotificationPermission()"));
  EXPECT_EQ("denied", EvalJs(iframe, "getPushPermission()"));
  EXPECT_EQ("denied", EvalJs(iframe, "getServiceWorkerPushPermission()"));
  EXPECT_EQ(
      "a JavaScript error: \"NotAllowedError: "
      "Registration failed - permission denied\"\n",
      EvalJs(iframe, "requestPushPermission()").ExtractError());
}

// Test that the Notifications.NonPersistentNotificationThirdPartyCount metric
// triggers in third-party contexts. Note: This test doesn't exactly fit with
// the others in this class, but the helper methods here are exactly what we
// needed and this test will be removed once the metric is removed.
IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       NonPersistentNotificationThirdPartyCountMetricTest) {
  GrantNotificationPermissionForTest(TesterUrl());

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();

  base::HistogramTester histogram_tester;
  const std::string histogram_name =
      "Notifications.NonPersistentNotificationThirdPartyCount";

  const std::string show_notification_js = R"(new Promise((resolve) => {
     const notification = new Notification("done");
     notification.onshow = () => {
       const title = notification.title;
       notification.close();
       resolve(title);
     };
   });)";

  EXPECT_EQ("done", EvalJs(main_frame, show_notification_js));

  histogram_tester.ExpectBucketCount(histogram_name, false, 1);
  histogram_tester.ExpectBucketCount(histogram_name, true, 0);

  content::RenderFrameHost* iframe = CreateChildIframe(main_frame, TesterUrl());

  EXPECT_EQ("done", EvalJs(iframe, show_notification_js));

  histogram_tester.ExpectBucketCount(histogram_name, false, 2);
  histogram_tester.ExpectBucketCount(histogram_name, true, 0);

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), EmbedderUrl()));
  main_frame = GetActiveWebContents()->GetPrimaryMainFrame();
  iframe = CreateChildIframe(main_frame, TesterUrl());

  EXPECT_EQ("done", EvalJs(iframe, show_notification_js));

  histogram_tester.ExpectBucketCount(histogram_name, false, 2);
  histogram_tester.ExpectBucketCount(histogram_name, true, 1);
}

// Tests that non-persistent notifications (i.e. doesn't use
// the Push API) records PermissionUsage and Notification UKMs.
IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       NonPersistentNotificationRecordsUkms) {
  GrantNotificationPermissionForTest(TesterUrl());

  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  content::RenderFrameHost* main_frame =
      GetActiveWebContents()->GetPrimaryMainFrame();

  ukm::TestAutoSetUkmRecorder ukm_recorder;

  const std::string show_notification_js = R"(new Promise((resolve) => {
     const notification = new Notification("done");
     notification.onshow = () => {
       const title = notification.title;
       notification.close();
       resolve(title);
     };
   });)";

  EXPECT_EQ("done", EvalJs(main_frame, show_notification_js));
  const auto usage_entries = ukm_recorder.GetEntriesByName("PermissionUsage");
  ASSERT_EQ(1u, usage_entries.size());
}

IN_PROC_BROWSER_TEST_F(NotificationPermissionBrowserTest,
                       DisablePermissionRecordsUkms) {
  ukm::TestAutoSetUkmRecorder ukm_recorder;

  auto* manager = permissions::PermissionRequestManager::FromWebContents(
      GetActiveWebContents());
  std::unique_ptr<permissions::MockPermissionPromptFactory> bubble_factory =
      std::make_unique<permissions::MockPermissionPromptFactory>(manager);
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), TesterUrl()));
  manager->AddRequest(GetActiveWebContents()->GetPrimaryMainFrame(),
                      std::make_unique<permissions::MockPermissionRequest>(
                          permissions::RequestType::kNotifications));
  bubble_factory->WaitForPermissionBubble();
  manager->Accept(/*prompt_options=*/std::monostate());

  GrantNotificationPermissionForTest(TesterUrl());

  std::unique_ptr<NotificationHandler> handler =
      std::make_unique<NonPersistentNotificationHandler>();
  handler->DisableNotifications(browser()->GetProfile(), TesterUrl(),
                                /*notification_id=*/std::nullopt,
                                /*is_suspicious=*/false);

  const auto action_entries = ukm_recorder.GetEntriesByName("Permission");
  ASSERT_EQ(2u, action_entries.size());

  // The revocation event uses the notification source_id type
  EXPECT_EQ(ukm::SourceIdType::NOTIFICATION_ID,
            ukm::GetSourceIdType(action_entries[1]->source_id));

  // Expect one GRANT and one REVOKE event
  ukm_recorder.ExpectEntryMetric(
      action_entries[0], Permission::kActionName,
      static_cast<int>(permissions::PermissionAction::GRANTED));
  ukm_recorder.ExpectEntryMetric(
      action_entries[1], Permission::kActionName,
      static_cast<int>(permissions::PermissionAction::REVOKED));
}
