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

#include <array>
#include <memory>

#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/permissions/permission_request_manager.h"
#include "components/permissions/test/mock_permission_prompt_factory.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/webrtc/media_stream_devices_controller.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"

using testing::_;

namespace policy {

class MediaStreamDevicesControllerBrowserTest
    : public PolicyTest,
      public testing::WithParamInterface<bool> {
 public:
  void SetUpOnMainThread() override {
    PolicyTest::SetUpOnMainThread();

    ASSERT_TRUE(embedded_test_server()->Start());
    request_url_ = embedded_test_server()->GetURL("/simple.html");
    request_pattern_ = request_url_.DeprecatedGetOriginAsURL().spec();
    ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), request_url_));

    // Testing both the new (PermissionManager) and old code-paths is not simple
    // since we are already using WithParamInterface. We only test whichever one
    // is enabled in chrome_features.cc since we won't keep the old path around
    // for long once we flip the flag.
    permissions::PermissionRequestManager* manager =
        permissions::PermissionRequestManager::FromWebContents(
            browser()->tab_strip_model()->GetActiveWebContents());
    prompt_factory_ =
        std::make_unique<permissions::MockPermissionPromptFactory>(manager);
    prompt_factory_->set_response_type(
        permissions::PermissionRequestManager::ACCEPT_ALL);
  }

  void TearDownOnMainThread() override { prompt_factory_.reset(); }

  content::MediaStreamRequest CreateRequest(
      blink::mojom::MediaStreamType audio_request_type,
      blink::mojom::MediaStreamType video_request_type) {
    content::WebContents* web_contents =
        browser()->tab_strip_model()->GetActiveWebContents();
    EXPECT_EQ(request_url_,
              web_contents->GetPrimaryMainFrame()->GetLastCommittedURL());
    int render_process_id =
        web_contents->GetPrimaryMainFrame()->GetProcess()->GetDeprecatedID();
    int render_frame_id = web_contents->GetPrimaryMainFrame()->GetRoutingID();
    return content::MediaStreamRequest(
        render_process_id, render_frame_id, 0,
        url::Origin::Create(request_url_), false, blink::MEDIA_GENERATE_STREAM,
        /*requested_audio_device_ids=*/{}, /*requested_video_device_ids=*/{},
        audio_request_type, video_request_type,
        /*disable_local_echo=*/false,
        /*request_pan_tilt_zoom_permission=*/false,
        /*captured_surface_control_active=*/false);
  }

  // Configure a given policy map. The |policy_name| is the name of either the
  // audio or video capture allow policy and must never be nullptr.
  // |allowlist_policy| and |allow_rule| are optional.  If nullptr, no allowlist
  // policy is set.  If non-nullptr, the allowlist policy is set to contain
  // either the |allow_rule| (if non-nullptr) or an "allow all" wildcard.
  void ConfigurePolicyMap(PolicyMap* policies,
                          const char* policy_name,
                          const char* allowlist_policy,
                          const char* allow_rule) {
    policies->Set(policy_name, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
                  POLICY_SOURCE_CLOUD, base::Value(policy_value_), nullptr);

    if (allowlist_policy) {
      // Add an entry to the allowlist that allows the specified URL regardless
      // of the setting of kAudioCapturedAllowed.
      base::ListValue list;
      if (allow_rule) {
        list.Append(allow_rule);
        request_url_allowed_via_allowlist_ = true;
      } else {
        list.Append(ContentSettingsPattern::Wildcard().ToString());
        // We should ignore all wildcard entries in the allowlist, so even
        // though we've added an entry, it should be ignored and our expectation
        // is that the request has not been allowed via the allowlist.
        request_url_allowed_via_allowlist_ = false;
      }
      policies->Set(allowlist_policy, POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER,
                    POLICY_SOURCE_CLOUD, base::Value(std::move(list)), nullptr);
    }
  }

  void Accept(const blink::mojom::StreamDevicesSet& stream_devices_set,
              blink::mojom::MediaStreamRequestResult result,
              bool blocked_by_permissions_policy,
              ContentSetting audio_setting,
              ContentSetting video_setting) {
    if (result == blink::mojom::MediaStreamRequestResult::OK) {
      ASSERT_EQ(stream_devices_set.stream_devices.size(), 1u);
      const blink::mojom::StreamDevices& devices =
          *stream_devices_set.stream_devices[0];
      if (policy_value_ || request_url_allowed_via_allowlist_) {
        ASSERT_NE(devices.audio_device.has_value(),
                  devices.video_device.has_value());
        if (devices.audio_device.has_value()) {
          ASSERT_EQ("fake_dev", devices.audio_device.value().id);
        } else if (devices.video_device.has_value()) {
          ASSERT_EQ("fake_dev", devices.video_device.value().id);
        }
      } else {
        ASSERT_FALSE(devices.audio_device.has_value());
        ASSERT_FALSE(devices.video_device.has_value());
      }
    } else {
      ASSERT_EQ(0u, stream_devices_set.stream_devices.size());
    }
  }

  void FinishAudioTest(std::string requested_device_id) {
    content::MediaStreamRequest request(
        CreateRequest(blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE,
                      blink::mojom::MediaStreamType::NO_SERVICE));
    request.requested_audio_device_ids = {requested_device_id};
    // TODO(raymes): Test MEDIA_DEVICE_OPEN (Pepper) which grants both webcam
    // and microphone permissions at the same time.
    webrtc::MediaStreamDevicesController::RequestPermissions(
        request, MediaCaptureDevicesDispatcher::GetInstance(),
        base::BindOnce(&MediaStreamDevicesControllerBrowserTest::Accept,
                       base::Unretained(this)));
    quit_closure_.Run();
  }

  void FinishVideoTest(std::string requested_device_id) {
    content::MediaStreamRequest request(
        CreateRequest(blink::mojom::MediaStreamType::NO_SERVICE,
                      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE));
    request.requested_video_device_ids = {requested_device_id};
    // TODO(raymes): Test MEDIA_DEVICE_OPEN (Pepper) which grants both webcam
    // and microphone permissions at the same time.
    webrtc::MediaStreamDevicesController::RequestPermissions(
        request, MediaCaptureDevicesDispatcher::GetInstance(),
        base::BindOnce(&MediaStreamDevicesControllerBrowserTest::Accept,
                       base::Unretained(this)));
    quit_closure_.Run();
  }

  std::unique_ptr<permissions::MockPermissionPromptFactory> prompt_factory_;
  bool policy_value_ = GetParam();
  bool request_url_allowed_via_allowlist_ = false;
  GURL request_url_;
  std::string request_pattern_;
  base::RepeatingClosure quit_closure_;
};

IN_PROC_BROWSER_TEST_P(MediaStreamDevicesControllerBrowserTest,
                       AudioCaptureAllowed) {
  blink::MediaStreamDevices audio_devices;
  blink::MediaStreamDevice fake_audio_device(
      blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, "fake_dev",
      "Fake Audio Device");
  audio_devices.push_back(fake_audio_device);

  PolicyMap policies;
  ConfigurePolicyMap(&policies, key::kAudioCaptureAllowed, nullptr, nullptr);
  UpdateProviderPolicy(policies);

  content::GetIOThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(
          &MediaCaptureDevicesDispatcher::SetTestAudioCaptureDevices,
          base::Unretained(MediaCaptureDevicesDispatcher::GetInstance()),
          audio_devices),
      base::BindOnce(&MediaStreamDevicesControllerBrowserTest::FinishAudioTest,
                     base::Unretained(this), fake_audio_device.id));

  base::RunLoop loop;
  quit_closure_ = loop.QuitWhenIdleClosure();
  loop.Run();
}

IN_PROC_BROWSER_TEST_P(MediaStreamDevicesControllerBrowserTest,
                       AudioCaptureAllowedUrls) {
  blink::MediaStreamDevices audio_devices;
  blink::MediaStreamDevice fake_audio_device(
      blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE, "fake_dev",
      "Fake Audio Device");
  audio_devices.push_back(fake_audio_device);

  auto allow_pattern = std::to_array<const char*>({
      request_pattern_.c_str(),
      // This will set an allow-all policy allowlist.  Since we do not allow
      // setting an allow-all entry in the allowlist, this entry should be
      // ignored and therefore the request should be denied.
      nullptr,
  });

  for (size_t i = 0; i < std::size(allow_pattern); ++i) {
    PolicyMap policies;
    ConfigurePolicyMap(&policies, key::kAudioCaptureAllowed,
                       key::kAudioCaptureAllowedUrls, allow_pattern[i]);
    UpdateProviderPolicy(policies);

    content::GetIOThreadTaskRunner({})->PostTaskAndReply(
        FROM_HERE,
        base::BindOnce(
            &MediaCaptureDevicesDispatcher::SetTestAudioCaptureDevices,
            base::Unretained(MediaCaptureDevicesDispatcher::GetInstance()),
            audio_devices),
        base::BindOnce(
            &MediaStreamDevicesControllerBrowserTest::FinishAudioTest,
            base::Unretained(this), fake_audio_device.id));

    base::RunLoop loop;
    quit_closure_ = loop.QuitWhenIdleClosure();
    loop.Run();
  }
}

IN_PROC_BROWSER_TEST_P(MediaStreamDevicesControllerBrowserTest,
                       VideoCaptureAllowed) {
  blink::MediaStreamDevices video_devices;
  blink::MediaStreamDevice fake_video_device(
      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, "fake_dev",
      "Fake Video Device");
  video_devices.push_back(fake_video_device);

  PolicyMap policies;
  ConfigurePolicyMap(&policies, key::kVideoCaptureAllowed, nullptr, nullptr);
  UpdateProviderPolicy(policies);

  content::GetIOThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(
          &MediaCaptureDevicesDispatcher::SetTestVideoCaptureDevices,
          base::Unretained(MediaCaptureDevicesDispatcher::GetInstance()),
          video_devices),
      base::BindOnce(&MediaStreamDevicesControllerBrowserTest::FinishVideoTest,
                     base::Unretained(this), std::move(fake_video_device.id)));

  base::RunLoop loop;
  quit_closure_ = loop.QuitWhenIdleClosure();
  loop.Run();
}

IN_PROC_BROWSER_TEST_P(MediaStreamDevicesControllerBrowserTest,
                       VideoCaptureAllowedUrls) {
  blink::MediaStreamDevices video_devices;
  blink::MediaStreamDevice fake_video_device(
      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, "fake_dev",
      "Fake Video Device");
  video_devices.push_back(fake_video_device);

  auto allow_pattern = std::to_array<const char*>({
      request_pattern_.c_str(),
      // This will set an allow-all policy allowlist.  Since we do not allow
      // setting an allow-all entry in the allowlist, this entry should be
      // ignored and therefore the request should be denied.
      nullptr,
  });

  for (size_t i = 0; i < std::size(allow_pattern); ++i) {
    PolicyMap policies;
    ConfigurePolicyMap(&policies, key::kVideoCaptureAllowed,
                       key::kVideoCaptureAllowedUrls, allow_pattern[i]);
    UpdateProviderPolicy(policies);

    content::GetIOThreadTaskRunner({})->PostTaskAndReply(
        FROM_HERE,
        base::BindOnce(
            &MediaCaptureDevicesDispatcher::SetTestVideoCaptureDevices,
            base::Unretained(MediaCaptureDevicesDispatcher::GetInstance()),
            video_devices),
        base::BindOnce(
            &MediaStreamDevicesControllerBrowserTest::FinishVideoTest,
            base::Unretained(this), fake_video_device.id));

    base::RunLoop loop;
    quit_closure_ = loop.QuitWhenIdleClosure();
    loop.Run();
  }
}

INSTANTIATE_TEST_SUITE_P(MediaStreamDevicesControllerBrowserTestInstance,
                         MediaStreamDevicesControllerBrowserTest,
                         testing::Bool());

}  // namespace policy
