// Copyright 2023 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/safe_browsing/phishy_interaction_tracker.h"

#include <string>
#include <utility>

#include "base/metrics/statistics_recorder.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/safe_browsing/chrome_ping_manager_factory.h"
#include "chrome/browser/safe_browsing/chrome_safe_browsing_blocking_page_factory.h"
#include "chrome/browser/safe_browsing/chrome_ui_manager_delegate.h"
#include "chrome/browser/safe_browsing/safe_browsing_service.h"
#include "chrome/browser/safe_browsing/test_safe_browsing_service.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "components/input/native_web_keyboard_event.h"
#include "components/safe_browsing/content/browser/content_unsafe_resource_util.h"
#include "components/safe_browsing/core/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/mock_render_process_host.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

using content::WebContents;

using safe_browsing::PhishyInteractionTracker;

using testing::_;
using testing::Mock;
using testing::StrictMock;

namespace {

constexpr char kBadURL[] = "https://www.phishing.com";

class MockSafeBrowsingUIManager : public safe_browsing::SafeBrowsingUIManager {
 public:
  MockSafeBrowsingUIManager()
      : SafeBrowsingUIManager(
            std::make_unique<
                safe_browsing::ChromeSafeBrowsingUIManagerDelegate>(),
            std::make_unique<
                safe_browsing::ChromeSafeBrowsingBlockingPageFactory>(),
            chrome::ChromeUINewTabURLAsGURL()) {}

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

  MOCK_METHOD1(DisplayBlockingPage, void(const UnsafeResource& resource));

  bool IsUrlAllowlistedOrPendingForWebContents(
      const GURL& url,
      content::NavigationEntry* entry,
      WebContents* web_contents,
      bool allowlist_only,
      safe_browsing::SBThreatType* threat_type) override {
    *threat_type = safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_PHISHING;
    return true;
  }

 protected:
  ~MockSafeBrowsingUIManager() override = default;
};

}  // namespace

class PhishyInteractionTrackerTest : public ChromeRenderViewHostTestHarness {
 public:
  typedef security_interstitials::UnsafeResource UnsafeResource;

  PhishyInteractionTrackerTest() = default;
  ~PhishyInteractionTrackerTest() override = default;

  void SetUp() override {
    browser_process_ = TestingBrowserProcess::GetGlobal();

    sb_service_ =
        base::MakeRefCounted<safe_browsing::TestSafeBrowsingService>();
    sb_service_->SetUseTestUrlLoaderFactory(true);
    // Set sb_service before the ChromeRenderViewHostTestHarness::SetUp(),
    // because it is needed to construct ping manager.
    browser_process_->SetSafeBrowsingService(sb_service_.get());

    ChromeRenderViewHostTestHarness::SetUp();

    ui_manager_ = new StrictMock<MockSafeBrowsingUIManager>();
    phishy_interaction_tracker_ =
        base::WrapUnique(new PhishyInteractionTracker(web_contents()));
    phishy_interaction_tracker_->SetUIManagerForTesting(ui_manager_.get());
    phishy_interaction_tracker_->HandlePageChanged();
  }

  void TearDown() override {
    browser_process_->SetSafeBrowsingService(nullptr);
    // Delete the tracker object on the UI thread and release the
    // SafeBrowsingService.
    sb_service_.reset();
    ui_manager_.reset();
    phishy_interaction_tracker_.reset();
    ChromeRenderViewHostTestHarness::TearDown();
  }

  Profile* profile() {
    return Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  }

  security_interstitials::UnsafeResource MakeUnsafeResource(const char* url) {
    security_interstitials::UnsafeResource resource;
    resource.url = GURL(url);
    resource.threat_type =
        safe_browsing::SBThreatType::SB_THREAT_TYPE_URL_CLIENT_SIDE_PHISHING;
    return resource;
  }

  std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest>
  GetActualRequest(const network::ResourceRequest& request) {
    std::string request_string = GetUploadData(request);
    auto actual_request =
        std::make_unique<safe_browsing::ClientSafeBrowsingReportRequest>();
    actual_request->ParseFromString(request_string);
    return actual_request;
  }

  void TriggerClickEvent() {
    blink::WebMouseEvent mouse_event = blink::WebMouseEvent(
        blink::WebInputEvent::Type::kMouseDown, gfx::PointF(), gfx::PointF(),
        blink::WebPointerProperties::Button::kBack, 0, 0,
        base::TimeTicks::Now());
    phishy_interaction_tracker_->HandleInputEvent(mouse_event);
  }

  void TriggerKeyEvent() {
    input::NativeWebKeyboardEvent key_event(
        blink::WebKeyboardEvent::Type::kChar,
        blink::WebInputEvent::kNoModifiers, base::TimeTicks::Now());
    phishy_interaction_tracker_->HandleInputEvent(key_event);
  }

  void TriggerPasteEvent() { phishy_interaction_tracker_->HandlePasteEvent(); }

  void SetNullDelayForTest() {
    phishy_interaction_tracker_->SetInactivityDelayForTesting(
        base::Milliseconds(0));
  }

  void VerifyInteraction(
      const safe_browsing::ClientSafeBrowsingReportRequest& report,
      const safe_browsing::ClientSafeBrowsingReportRequest::
          PhishySiteInteraction::PhishySiteInteractionType&
              expected_interaction_type,
      const int& expected_occurrence_count) {
    // Find the interaction within the report by comparing
    // security_interstitial_interaction.
    for (const auto& interaction : report.phishy_site_interactions()) {
      if (interaction.phishy_site_interaction_type() ==
          expected_interaction_type) {
        EXPECT_EQ(interaction.occurrence_count(), expected_occurrence_count);
        if (expected_occurrence_count == 1) {
          EXPECT_EQ(interaction.first_interaction_timestamp_msec(),
                    interaction.last_interaction_timestamp_msec());
        } else {
          EXPECT_LE(interaction.first_interaction_timestamp_msec(),
                    interaction.last_interaction_timestamp_msec());
        }
        // Return once the interaction type is found and verified.
        return;
      }
    }
  }

  void VerifyPhishyInteractionReport(
      const safe_browsing::ClientSafeBrowsingReportRequest& report,
      int expected_click_occurrences,
      int expected_key_occurrences,
      int expected_paste_occurrences) {
    ASSERT_EQ(report.type(), safe_browsing::ClientSafeBrowsingReportRequest::
                                 PHISHY_SITE_INTERACTIONS);
    ASSERT_EQ(report.phishy_site_interactions().size(), 3);
    VerifyInteraction(report,
                      safe_browsing::ClientSafeBrowsingReportRequest::
                          PhishySiteInteraction::PHISHY_CLICK_EVENT,
                      expected_click_occurrences);
    VerifyInteraction(report,
                      safe_browsing::ClientSafeBrowsingReportRequest::
                          PhishySiteInteraction::PHISHY_KEY_EVENT,
                      expected_key_occurrences);
    VerifyInteraction(report,
                      safe_browsing::ClientSafeBrowsingReportRequest::
                          PhishySiteInteraction::PHISHY_PASTE_EVENT,
                      expected_paste_occurrences);
  }

 protected:
  raw_ptr<TestingBrowserProcess> browser_process_;

  scoped_refptr<safe_browsing::TestSafeBrowsingService> sb_service_;
  std::unique_ptr<PhishyInteractionTracker> phishy_interaction_tracker_;
  scoped_refptr<MockSafeBrowsingUIManager> ui_manager_;
  safe_browsing::ChromePingManagerAllowerForTesting allow_ping_manager_;
};

TEST_F(PhishyInteractionTrackerTest, CheckHistogramCountsOnPhishyUserEvents) {
  base::HistogramTester histogram_tester_;

  security_interstitials::UnsafeResource resource = MakeUnsafeResource(kBadURL);
  safe_browsing::SBThreatType threat_type;
  EXPECT_TRUE(ui_manager_->IsUrlAllowlistedOrPendingForWebContents(
      resource.url, /*entry=*/nullptr,
      safe_browsing::unsafe_resource_util::GetWebContentsForResource(resource),
      true, &threat_type));

  const std::string phishy_interaction_histogram = "SafeBrowsing.PhishySite.";
  const int kExpectedClickEventCount = 3;
  const int kExpectedKeyEventCount = 5;
  const int kExpectedPasteEventCount = 2;
  // Trigger kExpectedClickEventCount mouse events.
  for (int i = 0; i < kExpectedClickEventCount; ++i) {
    TriggerClickEvent();
  }
  // Trigger kExpectedKeyEventCount key events.
  for (int i = 0; i < kExpectedKeyEventCount; ++i) {
    TriggerKeyEvent();
  }
  // Trigger kExpectedPasteEventCount - 1 paste events so we can trigger a
  // paste below.
  for (int i = 0; i < kExpectedPasteEventCount - 1; ++i) {
    TriggerPasteEvent();
  }
  // Set a null delay so that histograms get logged after this last user event.
  SetNullDelayForTest();
  TriggerPasteEvent();

  base::RunLoop run_loop;
  base::StatisticsRecorder::ScopedHistogramSampleObserver observer(
      "SafeBrowsing.PhishySite.PasteEventCount", run_loop.QuitClosure());
  run_loop.Run();

  histogram_tester_.ExpectUniqueSample(
      phishy_interaction_histogram + "ClickEventCount",
      kExpectedClickEventCount, 1);
  histogram_tester_.ExpectUniqueSample(
      phishy_interaction_histogram + "KeyEventCount", kExpectedKeyEventCount,
      1);
  histogram_tester_.ExpectUniqueSample(
      phishy_interaction_histogram + "PasteEventCount",
      kExpectedPasteEventCount, 1);
}

TEST_F(PhishyInteractionTrackerTest,
       CheckPhishyUserInteractionClientReport_WithoutSBERDeprecation) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      safe_browsing::kExtendedReportingRemovePrefDependency);
  safe_browsing::SetExtendedReportingPrefForTests(profile()->GetPrefs(), true);
  const int kExpectedClickEventCount = 4;
  const int kExpectedKeyEventCount = 1;
  const int kExpectedPasteEventCount = 3;
  auto* ping_manager =
      safe_browsing::ChromePingManagerFactory::GetForBrowserContext(profile());
  network::TestURLLoaderFactory test_url_loader_factory;
  base::RunLoop run_loop;
  test_url_loader_factory.SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest>
            actual_request = GetActualRequest(request);
        VerifyPhishyInteractionReport(
            *actual_request.get(), kExpectedClickEventCount,
            kExpectedKeyEventCount, kExpectedPasteEventCount);
        run_loop.Quit();
      }));
  ping_manager->SetURLLoaderFactoryForTesting(
      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
          &test_url_loader_factory));

  // Trigger kExpectedClickEventCount mouse events.
  for (int i = 0; i < kExpectedClickEventCount; ++i) {
    TriggerClickEvent();
  }
  // Trigger kExpectedKeyEventCount key events.
  for (int i = 0; i < kExpectedKeyEventCount; ++i) {
    TriggerKeyEvent();
  }
  // Trigger kExpectedPasteEventCount - 1 paste events so we can trigger a
  // paste below.
  for (int i = 0; i < kExpectedPasteEventCount - 1; ++i) {
    TriggerPasteEvent();
  }
  // Set a null delay so that histograms get logged after this last user event.
  SetNullDelayForTest();
  TriggerPasteEvent();

  run_loop.Run();
}

TEST_F(PhishyInteractionTrackerTest, CheckPhishyUserInteractionClientReport) {
  base::test::ScopedFeatureList feature_list;
  // Feature is enabled, so reporting relies on the ESB pref.
  feature_list.InitAndEnableFeature(
      safe_browsing::kExtendedReportingRemovePrefDependency);

  safe_browsing::SetExtendedReportingPrefForTests(profile()->GetPrefs(), false);
  safe_browsing::SetSafeBrowsingState(
      profile()->GetPrefs(),
      safe_browsing::SafeBrowsingState::ENHANCED_PROTECTION);

  const int kExpectedClickEventCount = 4;
  const int kExpectedKeyEventCount = 1;
  const int kExpectedPasteEventCount = 3;
  auto* ping_manager =
      safe_browsing::ChromePingManagerFactory::GetForBrowserContext(profile());
  network::TestURLLoaderFactory test_url_loader_factory;
  base::RunLoop run_loop;
  test_url_loader_factory.SetInterceptor(
      base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
        std::unique_ptr<safe_browsing::ClientSafeBrowsingReportRequest>
            actual_request = GetActualRequest(request);
        VerifyPhishyInteractionReport(
            *actual_request.get(), kExpectedClickEventCount,
            kExpectedKeyEventCount, kExpectedPasteEventCount);
        run_loop.Quit();
      }));
  ping_manager->SetURLLoaderFactoryForTesting(
      base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
          &test_url_loader_factory));

  // Trigger kExpectedClickEventCount mouse events.
  for (int i = 0; i < kExpectedClickEventCount; ++i) {
    TriggerClickEvent();
  }
  // Trigger kExpectedKeyEventCount key events.
  for (int i = 0; i < kExpectedKeyEventCount; ++i) {
    TriggerKeyEvent();
  }
  // Trigger kExpectedPasteEventCount - 1 paste events so we can trigger a
  // paste below.
  for (int i = 0; i < kExpectedPasteEventCount - 1; ++i) {
    TriggerPasteEvent();
  }
  // Set a null delay so that histograms get logged after this last user event.
  SetNullDelayForTest();
  TriggerPasteEvent();

  run_loop.Run();
}
