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

#include "base/functional/bind.h"
#include "base/test/with_feature_override.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/notifications/notification_permission_context.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/browser/permission_settings_registry.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/content_settings/core/common/content_settings_utils.h"
#include "components/content_settings/core/common/features.h"
#include "components/permissions/contexts/geolocation_permission_context.h"
#include "components/permissions/contexts/midi_permission_context.h"
#include "components/permissions/permission_decision.h"
#include "components/permissions/permission_manager.h"
#include "components/permissions/permission_request_id.h"
#include "components/permissions/permission_util.h"
#include "components/permissions/resolvers/content_setting_permission_resolver.h"
#include "content/public/browser/permission_descriptor_util.h"
#include "content/public/browser/permission_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "services/network/public/cpp/permissions_policy/origin_with_possible_wildcards.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/geolocation/geolocation_permission_context_delegate_android.h"
#else
#include "chrome/browser/geolocation/geolocation_permission_context_delegate.h"
#endif

// Integration tests for querying permissions that have a permissions policy
// set. These tests are not meant to cover every edge case as the
// PermissionsPolicy class itself is tested thoroughly in
// permissions_policy_unittest.cc and in
// render_frame_host_permissions_policy_unittest.cc. Instead they are meant to
// ensure that integration with content::PermissionContextBase
// works correctly.
class PermissionContextBasePermissionsPolicyTest
    : public ChromeRenderViewHostTestHarness {
 public:
  void EnableBlockMidiByDefault() {
    feature_list_.InitAndEnableFeature(blink::features::kBlockMidiByDefault);
  }
  PermissionContextBasePermissionsPolicyTest() = default;

  void SetUp() override { ChromeRenderViewHostTestHarness::SetUp(); }

 protected:
  static constexpr const char* kOrigin1 = "https://google.com";
  static constexpr const char* kOrigin2 = "https://maps.google.com";

  content::RenderFrameHost* GetMainRFH(const char* origin) {
    content::RenderFrameHost* result = web_contents()->GetPrimaryMainFrame();
    content::RenderFrameHostTester::For(result)
        ->InitializeRenderFrameIfNeeded();
    SimulateNavigation(&result, GURL(origin));
    return result;
  }

  content::RenderFrameHost* AddChildRFH(
      content::RenderFrameHost* parent,
      const char* origin,
      network::mojom::PermissionsPolicyFeature feature =
          network::mojom::PermissionsPolicyFeature::kNotFound) {
    network::ParsedPermissionsPolicy frame_policy = {};
    if (feature != network::mojom::PermissionsPolicyFeature::kNotFound) {
      frame_policy.emplace_back(
          feature,
          std::vector({*network::OriginWithPossibleWildcards::FromOrigin(
              url::Origin::Create(GURL(origin)))}),
          /*self_if_matches=*/std::nullopt,
          /*matches_all_origins=*/false,
          /*matches_opaque_src=*/false);
    }
    content::RenderFrameHost* result =
        content::RenderFrameHostTester::For(parent)->AppendChildWithPolicy(
            "", frame_policy);
    content::RenderFrameHostTester::For(result)
        ->InitializeRenderFrameIfNeeded();
    SimulateNavigation(&result, GURL(origin));
    return result;
  }

  // The header policy should only be set once on page load, so we refresh the
  // page to simulate that.
  void RefreshPageAndSetHeaderPolicy(
      content::RenderFrameHost** rfh,
      network::mojom::PermissionsPolicyFeature feature,
      const std::vector<std::string>& origins) {
    content::RenderFrameHost* current = *rfh;
    auto navigation = content::NavigationSimulator::CreateRendererInitiated(
        current->GetLastCommittedURL(), current);
    std::vector<network::OriginWithPossibleWildcards> parsed_origins;
    for (const std::string& origin : origins) {
      parsed_origins.emplace_back(
          *network::OriginWithPossibleWildcards::FromOrigin(
              url::Origin::Create(GURL(origin))));
    }
    navigation->SetPermissionsPolicyHeader(
        {{feature, parsed_origins, /*self_if_matches=*/std::nullopt,
          /*matches_all_origins=*/false,
          /*matches_opaque_src=*/false}});
    navigation->Commit();
    *rfh = navigation->GetFinalRenderFrameHost();
  }

  ContentSetting GetPermissionForFrame(permissions::PermissionContextBase* pcb,
                                       content::RenderFrameHost* rfh) {
    return permissions::PermissionUtil::PermissionStatusToContentSetting(
        pcb->GetPermissionStatus(
               content::PermissionDescriptorUtil::
                   CreatePermissionDescriptorForPermissionType(
                       permissions::PermissionUtil::
                           ContentSettingsTypeToPermissionType(
                               pcb->content_settings_type())),
               rfh, rfh->GetLastCommittedURL(),
               web_contents()->GetPrimaryMainFrame()->GetLastCommittedURL())
            .status);
  }

  PermissionStatus RequestPermissionForFrame(
      permissions::PermissionContextBase* pcb,
      content::RenderFrameHost* rfh,
      blink::PermissionType permission) {
    permissions::PermissionRequestID id(
        rfh, permission_request_id_generator_.GenerateNextId());
    pcb->RequestPermission(
        std::make_unique<permissions::PermissionRequestData>(
            content::PermissionDescriptorUtil::
                CreatePermissionDescriptorForPermissionType(permission),
            id, /*user_gesture=*/true, rfh->GetLastCommittedURL()),
        base::BindOnce(&PermissionContextBasePermissionsPolicyTest::
                           RequestPermissionForFrameFinished,
                       base::Unretained(this)));
    EXPECT_NE(PermissionStatus::ASK, last_request_result_);
    PermissionStatus result = last_request_result_;
    last_request_result_ = PermissionStatus::ASK;
    return result;
  }

  std::unique_ptr<permissions::GeolocationPermissionContext>
  MakeGeolocationPermissionContext() {
    return std::make_unique<permissions::GeolocationPermissionContext>(
        profile(),
#if BUILDFLAG(IS_ANDROID)
        std::make_unique<GeolocationPermissionContextDelegateAndroid>(profile())
#else
        std::make_unique<GeolocationPermissionContextDelegate>(profile())
#endif
    );
  }

 private:
  base::test::ScopedFeatureList feature_list_;
  void RequestPermissionForFrameFinished(content::PermissionResult result) {
    last_request_result_ = result.status;
  }

  void SimulateNavigation(content::RenderFrameHost** rfh, const GURL& url) {
    auto navigation_simulator =
        content::NavigationSimulator::CreateRendererInitiated(url, *rfh);
    navigation_simulator->Commit();
    *rfh = navigation_simulator->GetFinalRenderFrameHost();
  }

  PermissionStatus last_request_result_ = PermissionStatus::ASK;
  permissions::PermissionRequestID::RequestLocalId::Generator
      permission_request_id_generator_;
};

TEST_F(PermissionContextBasePermissionsPolicyTest, DefaultPolicy) {
  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);
  content::RenderFrameHost* child = AddChildRFH(parent, kOrigin2);

  // Midi is ask by default in the top level frame but not in subframes.
  permissions::MidiPermissionContext midi(profile());
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK, GetPermissionForFrame(&midi, child));

  // Geolocation is ask by default in top level frames but not in subframes.
  auto geolocation = MakeGeolocationPermissionContext();
  EXPECT_EQ(CONTENT_SETTING_ASK,
            GetPermissionForFrame(geolocation.get(), parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            GetPermissionForFrame(geolocation.get(), child));

  // Notifications is ask by default in top level frames but not in subframes.
  NotificationPermissionContext notifications(profile());
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&notifications, parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            GetPermissionForFrame(&notifications, child));
}

TEST_F(PermissionContextBasePermissionsPolicyTest,
       DefaultPolicyBlockMidiByDefault) {
  EnableBlockMidiByDefault();

  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);
  content::RenderFrameHost* child = AddChildRFH(parent, kOrigin2);

  // Midi is disallowed by default in the top level frame and blocked in
  // subframes.
  permissions::MidiPermissionContext midi(profile());
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK, GetPermissionForFrame(&midi, child));
}

TEST_F(PermissionContextBasePermissionsPolicyTest, DisabledTopLevelFrame) {
  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);

  // Disable midi in the top level frame.
  RefreshPageAndSetHeaderPolicy(
      &parent, network::mojom::PermissionsPolicyFeature::kMidiFeature,
      std::vector<std::string>());
  content::RenderFrameHost* child = AddChildRFH(parent, kOrigin2);
  permissions::MidiPermissionContext midi(profile());
  EXPECT_EQ(CONTENT_SETTING_BLOCK, GetPermissionForFrame(&midi, parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK, GetPermissionForFrame(&midi, child));

  // Disable geolocation in the top level frame.
  RefreshPageAndSetHeaderPolicy(
      &parent, network::mojom::PermissionsPolicyFeature::kGeolocation,
      std::vector<std::string>());
  child = AddChildRFH(parent, kOrigin2);
  auto geolocation = MakeGeolocationPermissionContext();
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            GetPermissionForFrame(geolocation.get(), parent));
  EXPECT_EQ(CONTENT_SETTING_BLOCK,
            GetPermissionForFrame(geolocation.get(), child));
}

TEST_F(PermissionContextBasePermissionsPolicyTest, EnabledForChildFrame) {
  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);

  // Enable midi for the child frame.
  content::RenderFrameHost* child = AddChildRFH(
      parent, kOrigin2, network::mojom::PermissionsPolicyFeature::kMidiFeature);
  permissions::MidiPermissionContext midi(profile());
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, parent));
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, child));

  // Enable geolocation for the child frame.
  child = AddChildRFH(parent, kOrigin2,
                      network::mojom::PermissionsPolicyFeature::kGeolocation);
  auto geolocation = MakeGeolocationPermissionContext();
  EXPECT_EQ(CONTENT_SETTING_ASK,
            GetPermissionForFrame(geolocation.get(), parent));
  EXPECT_EQ(CONTENT_SETTING_ASK,
            GetPermissionForFrame(geolocation.get(), child));
}

TEST_F(PermissionContextBasePermissionsPolicyTest,
       EnabledForChildFrameBlockMidiByDefault) {
  EnableBlockMidiByDefault();

  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);

  // Enable midi for the child frame.
  content::RenderFrameHost* child = AddChildRFH(
      parent, kOrigin2, network::mojom::PermissionsPolicyFeature::kMidiFeature);
  permissions::MidiPermissionContext midi(profile());
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, parent));
  EXPECT_EQ(CONTENT_SETTING_ASK, GetPermissionForFrame(&midi, child));
}

class PermissionContextBasePermissionsPolicyGeolocationTest
    : public base::test::WithFeatureOverride,
      public PermissionContextBasePermissionsPolicyTest {
 public:
  PermissionContextBasePermissionsPolicyGeolocationTest()
      : base::test::WithFeatureOverride(
            content_settings::features::kApproximateGeolocationPermission) {}
};

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
    PermissionContextBasePermissionsPolicyGeolocationTest);

TEST_P(PermissionContextBasePermissionsPolicyGeolocationTest,
       RequestPermission) {
  content::RenderFrameHost* parent = GetMainRFH(kOrigin1);

  ContentSettingsType geolocation_type =
      content_settings::GeolocationContentSettingsType();
  HostContentSettingsMapFactory::GetForProfile(profile())
      ->SetDefaultPermissionSetting(
          geolocation_type,
          content_settings::PermissionSettingsRegistry::GetInstance()
              ->Get(geolocation_type)
              ->delegate()
              .ToPermissionSetting(CONTENT_SETTING_ALLOW));

  // Request geolocation in the top level frame, request should work.
  auto geolocation = MakeGeolocationPermissionContext();
  EXPECT_EQ(PermissionStatus::GRANTED,
            RequestPermissionForFrame(geolocation.get(), parent,
                                      blink::PermissionType::GEOLOCATION));

  // Disable geolocation in the top level frame.
  RefreshPageAndSetHeaderPolicy(
      &parent, network::mojom::PermissionsPolicyFeature::kGeolocation,
      std::vector<std::string>());

  // Request should fail.
  EXPECT_EQ(PermissionStatus::DENIED,
            RequestPermissionForFrame(geolocation.get(), parent,
                                      blink::PermissionType::GEOLOCATION));
}
