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

#include "components/safe_browsing/content/browser/ui_manager.h"

#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/mock_callback.h"
#include "base/values.h"
#include "components/prefs/testing_pref_service.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "components/safe_browsing/content/browser/safe_browsing_blocking_page.h"
#include "components/safe_browsing/content/browser/safe_browsing_blocking_page_factory.h"
#include "components/safe_browsing/content/browser/safe_browsing_controller_client.h"
#include "components/safe_browsing/core/browser/db/util.h"
#include "components/safe_browsing/core/browser/db/v4_protocol_manager_util.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "components/security_interstitials/content/security_interstitial_controller_client.h"
#include "components/security_interstitials/content/settings_page_helper.h"
#include "components/security_interstitials/core/base_safe_browsing_error_ui.h"
#include "components/security_interstitials/core/metrics_helper.h"
#include "components/security_interstitials/core/unsafe_resource.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "services/network/public/cpp/cross_thread_pending_shared_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using content::BrowserThread;

static const char* kGoodURL = "https://www.good.com";
static const char* kGoodIpAddress = "http://1.2.3.4/";
static const char* kBadURL = "https://www.malware.com";
static const char* kBadURLWithAlternateScheme = "http://www.malware.com";
static const char* kBadURLWithPath = "https://www.malware.com/index.html";
static const char* kIpAddress = "https://195.127.0.11/uploads";
static const char* kIpAddressAlternatePathAndScheme =
    "http://195.127.0.11/otherPath";
static const char* kRedirectURL = "https://www.test1.com";
static const char* kAnotherRedirectURL = "https://www.test2.com";

namespace safe_browsing {

class SafeBrowsingCallbackWaiter {
 public:
  SafeBrowsingCallbackWaiter() = default;

  bool callback_called() const { return callback_called_; }
  bool proceed() const { return proceed_; }
  bool showed_interstitial() const { return showed_interstitial_; }
  bool has_post_commit_interstitial_skipped() const {
    return has_post_commit_interstitial_skipped_;
  }

  void OnBlockingPageDone(
      security_interstitials::UnsafeResource::UrlCheckResult result) {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    callback_called_ = true;
    proceed_ = result.proceed;
    showed_interstitial_ = result.showed_interstitial;
    has_post_commit_interstitial_skipped_ =
        result.has_post_commit_interstitial_skipped;
    loop_.Quit();
  }

  void WaitForCallback() {
    DCHECK_CURRENTLY_ON(BrowserThread::UI);
    loop_.Run();
  }

 private:
  bool callback_called_ = false;
  bool proceed_ = false;
  bool showed_interstitial_ = false;
  bool has_post_commit_interstitial_skipped_ = false;
  base::RunLoop loop_;
};

// A test blocking page that does not create windows.
class TestSafeBrowsingBlockingPage : public SafeBrowsingBlockingPage {
 public:
  TestSafeBrowsingBlockingPage(BaseUIManager* manager,
                               content::WebContents* web_contents,
                               const GURL& main_frame_url,
                               const UnsafeResourceList& unsafe_resources,
                               bool is_proceed_anyway_disabled = false)
      : SafeBrowsingBlockingPage(
            manager,
            web_contents,
            main_frame_url,
            unsafe_resources,
            std::make_unique<safe_browsing::SafeBrowsingControllerClient>(
                web_contents,
                std::make_unique<security_interstitials::MetricsHelper>(
                    unsafe_resources[0].url,
                    BaseBlockingPage::GetReportingInfo(
                        unsafe_resources,
                        /*blocked_page_shown_timestamp=*/std::nullopt),
                    /*history_service=*/nullptr),
                /*prefs=*/nullptr,
                manager->app_locale(),
                manager->default_safe_page(),
                /*settings_helper=*/nullptr),
            BaseSafeBrowsingErrorUI::SBErrorDisplayOptions(
                BaseBlockingPage::IsMainPageResourceLoadPending(
                    unsafe_resources),
                false,  // is_extended_reporting_opt_in_allowed
                false,  // is_off_the_record
                false,  // is_extended_reporting_enabled
                false,  // is_extended_reporting_policy_managed
                false,  // is_enhanced_protection_enabled
                is_proceed_anyway_disabled,  // is_proceed_anyway_disabled
                false,                       // should_open_links_in_new_tab
                true,                        // always_show_back_to_safety
                false,                 // is_enhanced_protection_message_enabled
                false,                 // is_safe_browsing_managed
                "cpn_safe_browsing"),  // help_center_article_link
            /*history_service=*/nullptr,
            /*navigation_observer_manager=*/nullptr,
            /*metrics_collector=*/nullptr,
            /*trigger_manager=*/nullptr,
            is_proceed_anyway_disabled,
            /*is_safe_browsing_surveys_enabled=*/true,
            /*trust_safety_sentiment_service_trigger=*/base::NullCallback(),
            /*ignore_auto_revocation_notifications_trigger=*/
            base::NullCallback()) {
    // Don't delay details at all for the unittest.
    SetThreatDetailsProceedDelayForTesting(0);
    DontCreateViewForTesting();
  }
};

// A factory that creates TestSafeBrowsingBlockingPages.
class TestSafeBrowsingBlockingPageFactory
    : public SafeBrowsingBlockingPageFactory {
 public:
  TestSafeBrowsingBlockingPageFactory() = default;
  ~TestSafeBrowsingBlockingPageFactory() override = default;

  SafeBrowsingBlockingPage* CreateSafeBrowsingPage(
      BaseUIManager* delegate,
      content::WebContents* web_contents,
      const GURL& main_frame_url,
      const SafeBrowsingBlockingPage::UnsafeResourceList& unsafe_resources,
      std::optional<base::TimeTicks> blocked_page_shown_timestamp) override {
    return new TestSafeBrowsingBlockingPage(delegate, web_contents,
                                            main_frame_url, unsafe_resources);
  }
  security_interstitials::SecurityInterstitialPage* CreateEnterpriseWarnPage(
      BaseUIManager* ui_manager,
      content::WebContents* web_contents,
      const GURL& main_frame_url,
      const SafeBrowsingBlockingPage::UnsafeResourceList& unsafe_resources)
      override {
    NOTREACHED();
  }

  security_interstitials::SecurityInterstitialPage* CreateEnterpriseBlockPage(
      BaseUIManager* ui_manager,
      content::WebContents* web_contents,
      const GURL& main_frame_url,
      const SafeBrowsingBlockingPage::UnsafeResourceList& unsafe_resources)
      override {
    NOTREACHED();
  }
};

class TestSafeBrowsingUIManagerDelegate
    : public SafeBrowsingUIManager::Delegate {
 public:
  TestSafeBrowsingUIManagerDelegate() {
    safe_browsing::RegisterProfilePrefs(pref_service_.registry());
  }

  ~TestSafeBrowsingUIManagerDelegate() override = default;

  // SafeBrowsingUIManager::Delegate:
  std::string GetApplicationLocale() override { return "en-us"; }
  void TriggerSecurityInterstitialShownExtensionEventIfDesired(
      content::WebContents* web_contents,
      const GURL& page_url,
      const std::string& reason,
      int net_error_code) override {}
  void TriggerSecurityInterstitialProceededExtensionEventIfDesired(
      content::WebContents* web_contents,
      const GURL& page_url,
      const std::string& reason,
      int net_error_code) override {}
  void TriggerUrlFilteringInterstitialExtensionEventIfDesired(
      content::WebContents* web_contents,
      const GURL& page_url,
      const std::string& threat_type,
      safe_browsing::RTLookupResponse rt_lookup_response) override {}
  prerender::NoStatePrefetchContents* GetNoStatePrefetchContentsIfExists(
      content::WebContents* web_contents) override {
    return nullptr;
  }
  bool IsHostingExtension(content::WebContents* web_contents) override {
    return is_hosting_extension_;
  }
  PrefService* GetPrefs(content::BrowserContext* browser_context) override {
    return &pref_service_;
  }
  history::HistoryService* GetHistoryService(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }
  PingManager* GetPingManager(
      content::BrowserContext* browser_context) override {
    return nullptr;
  }
  bool IsMetricsAndCrashReportingEnabled() override { return false; }

  void set_is_hosting_extension(bool is_hosting_extension) {
    is_hosting_extension_ = is_hosting_extension;
  }

 private:
  bool is_hosting_extension_ = false;
  TestingPrefServiceSimple pref_service_;
};

class SafeBrowsingUIManagerTest : public content::RenderViewHostTestHarness {
 public:
  SafeBrowsingUIManagerTest() {
    auto ui_manager_delegate =
        std::make_unique<TestSafeBrowsingUIManagerDelegate>();
    raw_ui_manager_delegate_ = ui_manager_delegate.get();
    ui_manager_ = new SafeBrowsingUIManager(
        std::move(ui_manager_delegate),
        std::make_unique<TestSafeBrowsingBlockingPageFactory>(),
        GURL("chrome://new-tab-page/"));
  }

  ~SafeBrowsingUIManagerTest() override = default;

  void SetUp() override {
    content::RenderViewHostTestHarness::SetUp();
    SafeBrowsingUIManager::CreateAllowlistForTesting(web_contents());
  }

  bool IsAllowlistedForMalware(const char* url) {
    auto* primary_main_frame = web_contents()->GetPrimaryMainFrame();
    return ui_manager_->IsAllowlisted(
        GURL(url),
        security_interstitials::UnsafeResourceLocator::
            CreateForRenderFrameToken(
                primary_main_frame->GetGlobalId().child_id.value(),
                primary_main_frame->GetFrameToken().value()),
        /*navigation_id=*/std::nullopt,
        SBThreatType::SB_THREAT_TYPE_URL_MALWARE,
        safe_browsing::ThreatSource::UNKNOWN);
  }

  bool IsAllowlisted(security_interstitials::UnsafeResource resource) {
    return ui_manager_->IsAllowlisted(
        resource.url, resource.rfh_locator, resource.navigation_id,
        resource.threat_type, resource.threat_source);
  }

  void AddToAllowlist(security_interstitials::UnsafeResource resource,
                      bool pending) {
    ui_manager_->AddToAllowlistUrlSet(resource.url, resource.navigation_id,
                                      web_contents(), pending,
                                      resource.threat_type);
  }

  void AddToAllowlistForMalware(const char* url, bool pending) {
    ui_manager_->AddToAllowlistUrlSet(GURL(url), /*navigation_id=*/std::nullopt,
                                      web_contents(), pending,
                                      SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
  }

  void RemoveAllowlistUrlSetThreatType(const GURL& url,
                                       bool from_pending_only,
                                       SBThreatType threat_type) {
    ui_manager_->RemoveAllowlistUrlSetThreatType(
        url, /*navigation_id=*/std::nullopt, web_contents(), from_pending_only,
        threat_type);
  }

  security_interstitials::UnsafeResource MakeUnsafeResource(
      const char* url,
      const SBThreatType threat_type =
          SBThreatType::SB_THREAT_TYPE_URL_MALWARE) {
    auto* primary_main_frame = web_contents()->GetPrimaryMainFrame();
    return MakeUnsafeResource(url, primary_main_frame->GetGlobalId(),
                              primary_main_frame->GetFrameToken(), threat_type);
  }

  security_interstitials::UnsafeResource MakeUnsafeResource(
      const char* url,
      content::GlobalRenderFrameHostId frame_id,
      const blink::LocalFrameToken& frame_token,
      const SBThreatType threat_type) {
    security_interstitials::UnsafeResource resource;
    resource.url = GURL(url);
    resource.rfh_locator = security_interstitials::UnsafeResourceLocator::
        CreateForRenderFrameToken(frame_id.child_id.value(),
                                  frame_token.value());
    resource.threat_type = threat_type;
    return resource;
  }

  void StartNavigation(const char* url) {
    auto navigation = content::NavigationSimulator::CreateBrowserInitiated(
        GURL(url), web_contents());
    navigation->Start();
  }

  security_interstitials::UnsafeResource MakeUnsafeResourceAndStartNavigation(
      const char* url) {
    security_interstitials::UnsafeResource resource = MakeUnsafeResource(url);

    // The WC doesn't have a URL without a navigation. A main-frame malware
    // unsafe resource must be a pending navigation.
    StartNavigation(url);
    return resource;
  }

  void SimulateBlockingPageDone(
      const std::vector<security_interstitials::UnsafeResource>& resources,
      bool proceed) {
    GURL main_frame_url;
    content::NavigationEntry* entry =
        web_contents()->GetController().GetVisibleEntry();
    if (entry) {
      main_frame_url = entry->GetURL();
    }

    ui_manager_->OnBlockingPageDone(resources, proceed, web_contents(),
                                    main_frame_url,
                                    true /* showed_interstitial */);
  }

 protected:
  SafeBrowsingUIManager* ui_manager() { return ui_manager_.get(); }
  TestSafeBrowsingUIManagerDelegate* ui_manager_delegate() {
    return raw_ui_manager_delegate_;
  }

 private:
  scoped_refptr<SafeBrowsingUIManager> ui_manager_;
  raw_ptr<TestSafeBrowsingUIManagerDelegate> raw_ui_manager_delegate_ = nullptr;
};

TEST_F(SafeBrowsingUIManagerTest, Allowlist) {
  StartNavigation(kBadURL);
  AddToAllowlistForMalware(kBadURL, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURL));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresSitesNotAdded) {
  StartNavigation(kGoodURL);
  EXPECT_FALSE(IsAllowlistedForMalware(kGoodURL));
}

TEST_F(SafeBrowsingUIManagerTest,
       PendingAllowlistOnlyAddedOnceForSameNavigation) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  resource.navigation_id = 1;
  // AddToAllowlist is called twice with the same navigation id.
  AddToAllowlist(resource, /*pending=*/true);
  AddToAllowlist(resource, /*pending=*/true);
  EXPECT_FALSE(IsAllowlisted(resource));

  SBThreatType threat_type;
  content::NavigationEntry* entry =
      web_contents()->GetController().GetVisibleEntry();
  ASSERT_TRUE(entry);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      resource.url, entry,
      unsafe_resource_util::GetWebContentsForResource(resource),
      /*allowlist_only=*/false, &threat_type));

  std::vector<security_interstitials::UnsafeResource> resources;
  resources.push_back(resource);
  // Calling OnBlockingPageDone once should be sufficient to remove the
  // URL from the pending allowlist.
  SimulateBlockingPageDone(resources, /*proceed=*/false);

  EXPECT_FALSE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      resource.url, entry,
      unsafe_resource_util::GetWebContentsForResource(resource),
      /*allowlist_only=*/false, &threat_type));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistRemembersThreatType) {
  StartNavigation(kBadURL);
  AddToAllowlistForMalware(kBadURL, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURL));
  SBThreatType threat_type;
  content::NavigationEntry* entry =
      web_contents()->GetController().GetVisibleEntry();
  ASSERT_TRUE(entry);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      GURL(kBadURL), entry, web_contents(), true, &threat_type));
  EXPECT_EQ(SBThreatType::SB_THREAT_TYPE_URL_MALWARE, threat_type);
}

TEST_F(SafeBrowsingUIManagerTest, Allowlisted_RedirectChain) {
  security_interstitials::UnsafeResource resource = MakeUnsafeResource(
      kBadURL, SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING);
  AddToAllowlist(resource, /*pending=*/false);

  auto navigation = content::NavigationSimulator::CreateBrowserInitiated(
      GURL(kGoodURL), web_contents());
  navigation->Start();
  navigation->Redirect(GURL(kBadURL));
  navigation->Redirect(GURL(kAnotherRedirectURL));
  navigation->Commit();

  content::NavigationEntry* entry =
      web_contents()->GetController().GetVisibleEntry();

  ASSERT_TRUE(entry);
  EXPECT_EQ(entry->GetRedirectChain().size(), 3u);
  resource.url = GURL(kAnotherRedirectURL);
  // Although kAnotherRedirectURL is not directly allowlisted, IsAllowlisted
  // should return true because one of the URLs on its redirect chain is
  // allowlisted.
  EXPECT_TRUE(IsAllowlisted(resource));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresPath) {
  StartNavigation(kBadURL);
  AddToAllowlistForMalware(kBadURL, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURL));

  content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURLWithPath));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresScheme) {
  StartNavigation(kBadURL);
  AddToAllowlistForMalware(kBadURL, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURL));

  content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
  EXPECT_TRUE(IsAllowlistedForMalware(kBadURLWithAlternateScheme));
  EXPECT_FALSE(IsAllowlistedForMalware(kGoodURL));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistIPAddress) {
  StartNavigation(kIpAddress);
  AddToAllowlistForMalware(kIpAddress, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(kIpAddress));

  content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
  EXPECT_TRUE(IsAllowlistedForMalware(kIpAddressAlternatePathAndScheme));
  EXPECT_FALSE(IsAllowlistedForMalware(kGoodIpAddress));
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoresThreatType) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  AddToAllowlist(resource, /*pending=*/false);
  EXPECT_TRUE(IsAllowlisted(resource));

  security_interstitials::UnsafeResource resource_phishing =
      MakeUnsafeResource(kBadURL);
  resource_phishing.threat_type = SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
  EXPECT_TRUE(IsAllowlisted(resource_phishing));
}

// Regression test for crbug.com/502520875.
TEST_F(SafeBrowsingUIManagerTest, AllowlistIgnoredForNonBypassableThreatType) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  AddToAllowlist(resource, /*pending=*/false);
  EXPECT_TRUE(IsAllowlisted(resource));

  auto navigation = content::NavigationSimulator::CreateBrowserInitiated(
      GURL(kBadURL), web_contents());
  navigation->Start();

  security_interstitials::UnsafeResource resource_policy_block =
      MakeUnsafeResource(kBadURL);
  resource_policy_block.threat_type =
      SBThreatType::SB_THREAT_TYPE_MANAGED_POLICY_BLOCK;
  resource_policy_block.navigation_id =
      navigation->GetNavigationHandle()->GetNavigationId();
  EXPECT_FALSE(IsAllowlisted(resource_policy_block));
}

TEST_F(SafeBrowsingUIManagerTest, CallbackProceed) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  std::vector<security_interstitials::UnsafeResource> resources;
  resources.push_back(resource);
  SimulateBlockingPageDone(resources, true);
  EXPECT_TRUE(IsAllowlisted(resource));
  waiter.WaitForCallback();
  EXPECT_TRUE(waiter.callback_called());
  EXPECT_TRUE(waiter.proceed());
}

TEST_F(SafeBrowsingUIManagerTest, UICallbackDontProceed) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  std::vector<security_interstitials::UnsafeResource> resources;
  resources.push_back(resource);
  SimulateBlockingPageDone(resources, false);
  EXPECT_FALSE(IsAllowlisted(resource));
  waiter.WaitForCallback();
  EXPECT_TRUE(waiter.callback_called());
  EXPECT_FALSE(waiter.proceed());
}

namespace {

// A WebContentsDelegate that records whether
// VisibleSecurityStateChanged() was called.
class SecurityStateWebContentsDelegate : public content::WebContentsDelegate {
 public:
  SecurityStateWebContentsDelegate() = default;

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

  ~SecurityStateWebContentsDelegate() override = default;

  bool visible_security_state_changed() const {
    return visible_security_state_changed_;
  }

  void ClearVisibleSecurityStateChanged() {
    visible_security_state_changed_ = false;
  }

  // WebContentsDelegate:
  void VisibleSecurityStateChanged(content::WebContents* source) override {
    visible_security_state_changed_ = true;
  }

 private:
  bool visible_security_state_changed_ = false;
};

}  // namespace

// Tests that the WebContentsDelegate is notified of a visible security
// state change when a blocking page is shown.
TEST_F(SafeBrowsingUIManagerTest, VisibleSecurityStateChanged) {
  SecurityStateWebContentsDelegate delegate;
  web_contents()->SetDelegate(&delegate);

  // Simulate a blocking page.
  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);
  // Needed for showing the blocking page.
  resource.threat_source = safe_browsing::ThreatSource::ANDROID_SAFEBROWSING;

  NavigateAndCommit(GURL(kBadURL));

  delegate.ClearVisibleSecurityStateChanged();
  EXPECT_FALSE(delegate.visible_security_state_changed());
  ui_manager()->DisplayBlockingPage(resource);
  EXPECT_TRUE(delegate.visible_security_state_changed());

  // Simulate proceeding through the blocking page.
  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  std::vector<security_interstitials::UnsafeResource> resources;
  resources.push_back(resource);

  delegate.ClearVisibleSecurityStateChanged();
  EXPECT_FALSE(delegate.visible_security_state_changed());
  SimulateBlockingPageDone(resources, true);
  EXPECT_TRUE(delegate.visible_security_state_changed());

  waiter.WaitForCallback();
  EXPECT_TRUE(waiter.callback_called());
  EXPECT_TRUE(waiter.proceed());
  EXPECT_TRUE(IsAllowlisted(resource));
}

TEST_F(SafeBrowsingUIManagerTest, ShowBlockPageNoCallback) {
  SecurityStateWebContentsDelegate delegate;
  web_contents()->SetDelegate(&delegate);

  // Simulate a blocking page.
  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);
  // Needed for showing the blocking page.
  resource.threat_source = safe_browsing::ThreatSource::ANDROID_SAFEBROWSING;

  // This call caused a crash in https://crbug.com/1058094. Just verify that we
  // don't crash anymore.
  ui_manager()->DisplayBlockingPage(resource);
}

TEST_F(SafeBrowsingUIManagerTest, NoInterstitialInExtensions) {
  // Pretend the current web contents is in an extension.
  ui_manager_delegate()->set_is_hosting_extension(true);

  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);

  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  ui_manager()->StartDisplayingBlockingPage(resource);
  waiter.WaitForCallback();
  EXPECT_FALSE(waiter.proceed());
  EXPECT_FALSE(waiter.showed_interstitial());
}

TEST_F(SafeBrowsingUIManagerTest, DisplayInterstitial) {
  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);

  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  ui_manager()->StartDisplayingBlockingPage(resource);
  waiter.WaitForCallback();
  EXPECT_FALSE(waiter.proceed());
  EXPECT_TRUE(waiter.showed_interstitial());
  EXPECT_TRUE(waiter.has_post_commit_interstitial_skipped());
}

TEST_F(SafeBrowsingUIManagerTest, DisplayInterstitial_PostCommitInterstitial) {
  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);
  resource.threat_source = safe_browsing::ThreatSource::ANDROID_SAFEBROWSING;
  // Make it a post commit interstitial.
  resource.threat_type = SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;

  SafeBrowsingCallbackWaiter waiter;
  resource.callback =
      base::BindRepeating(&SafeBrowsingCallbackWaiter::OnBlockingPageDone,
                          base::Unretained(&waiter));
  resource.callback_sequence = content::GetUIThreadTaskRunner({});
  ui_manager()->StartDisplayingBlockingPage(resource);
  waiter.WaitForCallback();
  EXPECT_FALSE(waiter.proceed());
  EXPECT_FALSE(waiter.showed_interstitial());
  EXPECT_FALSE(waiter.has_post_commit_interstitial_skipped());
}

TEST_F(SafeBrowsingUIManagerTest, InvalidRenderFrameHostId) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);

  // Clobber the resource's RenderFrameHost id so that subsequent WebContents
  // lookups return null. This simulates the destruction of a RenderFrameHost
  // (for the purposes of a WebContents lookup) which SafeBrowsing needs to
  // handle.
  content::GlobalRenderFrameHostId invalid_rfh_id;
  resource.rfh_locator =
      security_interstitials::UnsafeResourceLocator::CreateForRenderFrameToken(
          invalid_rfh_id.child_id.value(), base::UnguessableToken::Create());
  ASSERT_FALSE(unsafe_resource_util::GetWebContentsForResource(resource));

  EXPECT_FALSE(IsAllowlisted(resource));
}

// Regression test for https://g-issues.chromium.org/issues/327838835
TEST_F(SafeBrowsingUIManagerTest,
       DontSendClientSafeBrowsingWarningShownReportNullWebContents) {
  ASSERT_FALSE(
      ui_manager()->ShouldSendClientSafeBrowsingWarningShownReport(nullptr));
}

TEST_F(SafeBrowsingUIManagerTest,
       AllowlistSetSeverestThreatTypeInRedirectChain) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResource(kGoodURL, SBThreatType::SB_THREAT_TYPE_API_ABUSE);
  AddToAllowlist(resource, true);

  auto navigation = content::NavigationSimulator::CreateBrowserInitiated(
      GURL(kGoodURL), web_contents());
  navigation->Start();
  navigation->Redirect(GURL(kRedirectURL));
  navigation->Redirect(GURL(kAnotherRedirectURL));
  navigation->Commit();

  content::NavigationEntry* entry =
      web_contents()->GetController().GetVisibleEntry();

  ASSERT_TRUE(entry);
  EXPECT_EQ(entry->GetRedirectChain().size(), 3u);

  // The threat type for the final redirected url should be set to the first url
  // in the chain.
  SBThreatType threat_type;
  security_interstitials::UnsafeResource final_resource =
      MakeUnsafeResource(kAnotherRedirectURL);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      final_resource.url, entry,
      unsafe_resource_util::GetWebContentsForResource(final_resource), false,
      &threat_type));
  EXPECT_EQ(threat_type, resource.threat_type);

  security_interstitials::UnsafeResource redirect_resource =
      MakeUnsafeResource(kRedirectURL, SBThreatType::SB_THREAT_TYPE_BILLING);
  AddToAllowlist(redirect_resource, true);

  // The second redirect url has a less severe threat type, the final
  // url's threat type should still be the first one in the chain.
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      final_resource.url, entry,
      unsafe_resource_util::GetWebContentsForResource(final_resource), false,
      &threat_type));
  EXPECT_EQ(threat_type, resource.threat_type);

  redirect_resource = MakeUnsafeResource(
      kRedirectURL, SBThreatType::SB_THREAT_TYPE_MANAGED_POLICY_BLOCK);
  AddToAllowlist(redirect_resource, true);

  // Now that the second redirect url has a  more severe threat type, the final
  // url's threat type should be set to that one.
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      final_resource.url, entry,
      unsafe_resource_util::GetWebContentsForResource(final_resource), false,
      &threat_type));
  EXPECT_EQ(threat_type, redirect_resource.threat_type);
}

TEST_F(SafeBrowsingUIManagerTest, AllowlistViewSource) {
  const char* view_source_url = "view-source:https://www.malware.com";
  StartNavigation(view_source_url);
  AddToAllowlistForMalware(view_source_url, /*pending=*/false);
  EXPECT_TRUE(IsAllowlistedForMalware(view_source_url));
  EXPECT_FALSE(IsAllowlistedForMalware(kBadURL));

  content::WebContentsTester::For(web_contents())->CommitPendingNavigation();
  EXPECT_TRUE(IsAllowlistedForMalware(view_source_url));
  EXPECT_FALSE(IsAllowlistedForMalware(kBadURL));
}

TEST_F(SafeBrowsingUIManagerTest, RemoveAllowlistUrlSetThreatType_Pending) {
  const GURL url("https://suspicious.com");
  ui_manager()->AddToAllowlistUrlSet(
      url, /*navigation_id=*/std::nullopt, web_contents(), /*is_pending=*/true,
      SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);

  SBThreatType threat_type;
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/false,
      &threat_type));
  EXPECT_EQ(threat_type, SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);

  // Calling RemoveAllowlistUrlSetThreatType with a non-matching threat type
  // (e.g. SB_THREAT_TYPE_URL_MALWARE) does not remove the entry.
  RemoveAllowlistUrlSetThreatType(url, /*from_pending_only=*/true,
                                  SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/false,
      &threat_type));

  // Calling RemoveAllowlistUrlSetThreatType with the matching threat type
  // removes it.
  RemoveAllowlistUrlSetThreatType(
      url, /*from_pending_only=*/true,
      SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);
  EXPECT_FALSE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/false,
      &threat_type));
}

TEST_F(SafeBrowsingUIManagerTest, RemoveAllowlistUrlSetThreatType_NonPending) {
  const GURL url("https://suspicious.com");
  ui_manager()->AddToAllowlistUrlSet(
      url, /*navigation_id=*/std::nullopt, web_contents(), /*is_pending=*/false,
      SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);

  SBThreatType threat_type;
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/true,
      &threat_type));
  EXPECT_EQ(threat_type, SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);

  // Calling with from_pending_only=true does not remove a non-pending entry.
  RemoveAllowlistUrlSetThreatType(
      url, /*from_pending_only=*/true,
      SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/true,
      &threat_type));

  // Non-matching threat type does not remove the non-pending entry.
  RemoveAllowlistUrlSetThreatType(url, /*from_pending_only=*/false,
                                  SBThreatType::SB_THREAT_TYPE_URL_MALWARE);
  EXPECT_TRUE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/true,
      &threat_type));

  // Matching threat type with from_pending_only=false removes it.
  RemoveAllowlistUrlSetThreatType(
      url, /*from_pending_only=*/false,
      SBThreatType::SB_THREAT_TYPE_WARNABLE_SUSPICIOUS_SITE);
  EXPECT_FALSE(ui_manager()->IsUrlAllowlistedOrPendingForWebContents(
      url, /*entry=*/nullptr, web_contents(), /*allowlist_only=*/true,
      &threat_type));
}

TEST_F(SafeBrowsingUIManagerTest,
       ProceedAnywayDisabled_CommandReceivedDoesNotAllowlist) {
  security_interstitials::UnsafeResource resource =
      MakeUnsafeResourceAndStartNavigation(kBadURL);
  resource.threat_source = ThreatSource::LOCAL_PVER4;
  std::vector<security_interstitials::UnsafeResource> resources = {resource};
  TestSafeBrowsingBlockingPage blocking_page(
      ui_manager(), web_contents(), GURL(kBadURL), resources,
      /*is_proceed_anyway_disabled=*/true);

  // 1. Verify frontend string population: disableKeyboardOverride must be true.
  base::DictValue load_time_data;
  blocking_page.sb_error_ui()->PopulateStringsForHtml(load_time_data);
  std::optional<bool> disable_keyboard =
      load_time_data.FindBool("disableKeyboardOverride");
  ASSERT_TRUE(disable_keyboard.has_value());
  EXPECT_TRUE(disable_keyboard.value());

  // 2. Verify backend command handling: CMD_PROCEED (1) must not allowlist URL.
  blocking_page.CommandReceived("1");
  EXPECT_FALSE(IsAllowlisted(resource));
}

}  // namespace safe_browsing
