// 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 "content/browser/fenced_frame/fenced_frame_reporter.h"

#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <type_traits>
#include <utility>

#include "base/memory/scoped_refptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/test/test_content_browser_client.h"
#include "content/public/test/test_renderer_host.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_key.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/data_element.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
#include "third_party/blink/public/common/fenced_frame/redacted_fenced_frame_config.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content {
namespace {

using ::testing::_;

class InterestGroupEnabledContentBrowserClient
    : public TestContentBrowserClient {
 public:
  // ContentBrowserClient overrides:
  // This is needed so that the interest group related APIs can run without
  // failing with the result AuctionResult::kSellerRejected.
  bool IsPrivacySandboxReportingDestinationAttested(
      content::BrowserContext* browser_context,
      const url::Origin& destination_origin,
      content::PrivacySandboxInvokingAPI invoking_api) override {
    return true;
  }
};

class FencedFrameReporterTest : public RenderViewHostTestHarness {
 public:
  FencedFrameReporterTest() {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/
        {blink::features::kFencedFramesAutomaticBeaconCredentials,
         blink::features::kFencedFramesReportEventHeaderChanges},
        /*disabled_features=*/{});
  }

  scoped_refptr<network::SharedURLLoaderFactory> shared_url_loader_factory() {
    return test_url_loader_factory_.GetSafeWeakWrapper();
  }

  void SetUp() override {
    old_content_browser_client_ =
        SetBrowserClientForTesting(&test_content_browser_client_);
    RenderViewHostTestHarness::SetUp();
    NavigateAndCommit(main_frame_url_);
  }

  void TearDown() override {
    SetBrowserClientForTesting(old_content_browser_client_);
    RenderViewHostTestHarness::TearDown();
  }

  void ValidateRequest(const network::ResourceRequest& request,
                       const GURL& expected_url,
                       const std::optional<std::string>& event_data) {
    EXPECT_EQ(request.url, expected_url);
    EXPECT_EQ(request.mode, network::mojom::RequestMode::kCors);
    EXPECT_EQ(request.credentials_mode, network::mojom::CredentialsMode::kOmit);
    EXPECT_TRUE(request.trusted_params->isolation_info.network_isolation_key()
                    .IsTransient());
    EXPECT_EQ(request.referrer, main_frame_origin_.GetURL());
    EXPECT_NE(request.referrer, main_frame_url_);
    EXPECT_EQ(
        request.referrer_policy,
        net::ReferrerPolicy::REDUCE_GRANULARITY_ON_TRANSITION_CROSS_ORIGIN);

    // Checks specific to DestinationURL events.
    if (!event_data.has_value()) {
      EXPECT_EQ(request.method, net::HttpRequestHeaders::kGetMethod);
      EXPECT_EQ(request.request_initiator, main_frame_origin_);
      return;
    }

    // Checks specific to DestinationEnum + AutomaticBeacon events.
    EXPECT_EQ(request.request_initiator, report_url_declarer_origin_);
    EXPECT_EQ(request.method, net::HttpRequestHeaders::kPostMethod);

    EXPECT_EQ(request.headers.GetHeader(net::HttpRequestHeaders::kContentType),
              "text/plain;charset=UTF-8");

    ASSERT_TRUE(request.request_body);
    ASSERT_EQ(request.request_body->elements()->size(), 1u);
    ASSERT_EQ((*request.request_body->elements())[0].type(),
              network::DataElement::Tag::kBytes);
    EXPECT_EQ((*request.request_body->elements())[0]
                  .As<network::DataElementBytes>()
                  .AsStringPiece(),
              *event_data);
  }

 protected:
  RenderFrameHostImpl* main_rfh_impl() {
    return static_cast<RenderFrameHostImpl*>(main_rfh());
  }

  const base::HistogramTester& histogram_tester() const {
    return histogram_tester_;
  }

  network::TestURLLoaderFactory test_url_loader_factory_;

  const GURL main_frame_url_{"https://main_frame.test/mypage.html"};
  const GURL report_url_declarer_{"https://report_declarer.test/"};
  const GURL report_destination_{"https://report_destination.test"};
  const GURL report_destination2_{"https://report_destination2.test"};
  const GURL report_destination3_{"https://report_destination3.test"};
  const url::Origin main_frame_origin_ = url::Origin::Create(main_frame_url_);
  const url::Origin report_url_declarer_origin_ =
      url::Origin::Create(report_url_declarer_);
  const url::Origin report_destination_origin_ =
      url::Origin::Create(report_destination_);
  const url::Origin report_destination2_origin_ =
      url::Origin::Create(report_destination2_);
  const url::Origin report_destination3_origin_ =
      url::Origin::Create(report_destination3_);

  InterestGroupEnabledContentBrowserClient test_content_browser_client_;
  raw_ptr<ContentBrowserClient> old_content_browser_client_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  base::HistogramTester histogram_tester_;
};

// ReportingDestination has no map.
TEST_F(FencedFrameReporterTest, NoReportNoMap) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(
          shared_url_loader_factory(), browser_context(),
          report_url_declarer_origin_,
          /*reporting_url_map=*/{{"event_type", report_destination_}});
  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;

  // A Shared Storage FencedFrameReporter has no map for FLEDGE destinations.
  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(),
      error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'Buyer'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSeller, main_rfh_impl(),
      error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'Seller'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kDirectSeller, main_rfh_impl(),
      error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'DirectSeller'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kComponentSeller,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'ComponentSeller'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  EXPECT_FALSE(reporter->SendReport(
      DestinationURLEvent(report_destination_),
      blink::FencedFrame::ReportingDestination::kBuyer, main_rfh_impl(),
      error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'Buyer'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  // No requests should have been made.
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);
}

// ReportingDestination has an empty map.
TEST_F(FencedFrameReporterTest, NoReportEmptyMap) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(shared_url_loader_factory(),
                                                  browser_context(),
                                                  report_url_declarer_origin_,
                                                  /*reporting_url_map=*/{});
  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;
  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame did not register reporting metadata for destination "
            "'SharedStorageSelectUrl'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  // No requests should have been made.
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);
}

// Non-empty reporting URL map, but passed in event type isn't registered.
TEST_F(FencedFrameReporterTest, NoReportEventTypeNotRegistered) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(
          shared_url_loader_factory(), browser_context(),
          report_url_declarer_origin_,
          /*reporting_url_map=*/
          {{"registered_event_type", report_destination_}});
  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;
  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("unregistered_event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(
      error_message,
      "This frame did not register reporting url for destination "
      "'SharedStorageSelectUrl' and event_type 'unregistered_event_type'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kWarning);

  // No requests should have been made.
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);
}

// Event types map to disallowed URLs (empty URLs, non-HTTP/HTTPS URLs).
TEST_F(FencedFrameReporterTest, NoReportBadUrl) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(
          shared_url_loader_factory(), browser_context(),
          report_url_declarer_origin_,
          /*reporting_url_map=*/
          {{"no_url", GURL()},
           {"data_url", GURL("data:,only http is allowed")}});
  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;
  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("no_url", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame registered invalid reporting url for destination "
            "'SharedStorageSelectUrl' and event_type 'no_url'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError);

  EXPECT_FALSE(reporter->SendReport(
      DestinationEnumEvent("data_url", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(error_message,
            "This frame registered invalid reporting url for destination "
            "'SharedStorageSelectUrl' and event_type 'data_url'.");
  EXPECT_EQ(console_message_level, blink::mojom::ConsoleMessageLevel::kError);

  // No requests should have been made.
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);
}

TEST_F(FencedFrameReporterTest, SendReports) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(
          shared_url_loader_factory(), browser_context(),
          report_url_declarer_origin_,
          /*reporting_url_map=*/
          {{"event_type", report_destination_},
           {"event_type2", report_destination2_}});

  // Make a report.
  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;
  EXPECT_TRUE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 1);
  ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request,
                  report_destination_, "event_data");

  // Make another report to the same URL with different data. Should also
  // succeed.
  EXPECT_TRUE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data2"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 2);
  ValidateRequest((*test_url_loader_factory_.pending_requests())[1].request,
                  report_destination_, "event_data2");

  // Make a report using another event type.
  EXPECT_TRUE(reporter->SendReport(
      DestinationEnumEvent("event_type2", "event_data3"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 3);
  ValidateRequest((*test_url_loader_factory_.pending_requests())[2].request,
                  report_destination2_, "event_data3");
}

TEST_F(FencedFrameReporterTest, SendReportsEnumWithSimulatedResponse) {
  scoped_refptr<FencedFrameReporter> reporter =
      FencedFrameReporter::CreateForSharedStorage(
          shared_url_loader_factory(), browser_context(),
          report_url_declarer_origin_,
          /*reporting_url_map=*/
          {{"event_type", report_destination_},
           {"event_type2", report_destination2_}});

  std::string error_message;
  blink::mojom::ConsoleMessageLevel console_message_level =
      blink::mojom::ConsoleMessageLevel::kError;

  // Make an enum report that succeeds.
  EXPECT_TRUE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 1);
  ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request,
                  report_destination_, "event_data");

  test_url_loader_factory_.SimulateResponseForPendingRequest(
      report_destination_.spec(), "");
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);

  // Make an enum report that fails due to HTTP status 404.
  EXPECT_TRUE(reporter->SendReport(
      DestinationEnumEvent("event_type", "event_data"),
      blink::FencedFrame::ReportingDestination::kSharedStorageSelectUrl,
      main_rfh_impl(), error_message, console_message_level));
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 1);
  ValidateRequest((*test_url_loader_factory_.pending_requests())[0].request,
                  report_destination_, "event_data");

  test_url_loader_factory_.SimulateResponseForPendingRequest(
      report_destination_.spec(), "", net::HTTP_NOT_FOUND);
  EXPECT_EQ(test_url_loader_factory_.NumPending(), 0);
}

}  // namespace
}  // namespace content
