// 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 "third_party/blink/renderer/modules/mediastream/media_devices.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "media/base/media_permission.h"
#include "media/base/media_switches.h"
#include "media/base/output_device_info.h"
#include "media/base/video_types.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video/video_capture_device_descriptor.h"
#include "media/capture/video_capture_types.h"
#include "media/media_buildflags.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/mojom/media/capture_handle_config.mojom-blink.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-blink-forward.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-blink.h"
#include "third_party/blink/public/mojom/mediastream/media_devices.mojom-shared.h"
#include "third_party/blink/renderer/bindings/core/v8/idl_types.h"
#include "third_party/blink/renderer/bindings/core/v8/native_value_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/to_v8_traits.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_union_boolean_string.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_output_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_capture_handle_config.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_crop_target.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_double_range.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_long_range.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_device_info.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_device_kind.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_media_track_capabilities.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_restriction_target.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_boolean_mediatrackconstraints.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_user_media_stream_constraints.h"
#include "third_party/blink/renderer/core/dom/events/event.h"
#include "third_party/blink/renderer/core/dom/events/event_listener.h"
#include "third_party/blink/renderer/core/dom/events/native_event_listener.h"
#include "third_party/blink/renderer/core/event_type_names.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_iframe_element.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
#include "third_party/blink/renderer/modules/mediastream/crop_target.h"
#include "third_party/blink/renderer/modules/mediastream/input_device_info.h"
#include "third_party/blink/renderer/modules/mediastream/media_device_info.h"
#include "third_party/blink/renderer/modules/mediastream/media_permission_testing_platform.h"
#include "third_party/blink/renderer/modules/mediastream/media_stream_constraints_util_audio.h"
#include "third_party/blink/renderer/modules/mediastream/restriction_target.h"
#include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/testing_platform_support.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/functional.h"
#include "ui/gfx/geometry/mojom/geometry.mojom.h"

namespace blink {

using ::blink::mojom::blink::MediaDeviceInfoPtr;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::StrictMock;
using MediaDeviceType = ::blink::mojom::MediaDeviceType;

namespace {

constexpr char kInvalidSinkId[] = "invalid_sink_id";
constexpr char kValidSinkId[] = "valid_sink_id";

String MaxLengthCaptureHandle() {
  String maxHandle = "0123456789abcdef";  // 16 characters.
  while (maxHandle.length() < 1024) {
    maxHandle = maxHandle + maxHandle;
  }
  CHECK_EQ(maxHandle.length(), 1024u) << "Malformed test.";
  return maxHandle;
}

class MockMediaDevicesDispatcherHost final
    : public mojom::blink::MediaDevicesDispatcherHost {
 public:
  MockMediaDevicesDispatcherHost()
      : enumeration_({
            // clang-format off
            {
              {"fake_audio_input_1", "Fake Audio Input 1", "common_group_1"},
              {"fake_audio_input_2", "Fake Audio Input 2", "common_group_2"},
              {"fake_audio_input_3", "Fake Audio Input 3", "audio_input_group"},
            }, {
              {"fake_video_input_1", "Fake Video Input 1", "common_group_1",
               media::VideoCaptureControlSupport(),
               blink::mojom::FacingMode::kNone,
               media::CameraAvailability::kAvailable},
              {"fake_video_input_2", "Fake Video Input 2", "video_input_group",
               media::VideoCaptureControlSupport(),
               blink::mojom::FacingMode::kUser, std::nullopt},
              {"fake_video_input_3", "Fake Video Input 3", "video_input_group 2",
               media::VideoCaptureControlSupport(),
               blink::mojom::FacingMode::kUser,
               media::CameraAvailability::
                    kUnavailableExclusivelyUsedByOtherApplication},
            },
            {
              {"fake_audio_output_1", "Fake Audio Output 1", "common_group_1"},
              {"fake_audio_putput_2", "Fake Audio Output 2", "common_group_2"},
            }
            // clang-format on
        }) {
    mojom::blink::VideoInputDeviceCapabilitiesPtr video_capabilities =
        mojom::blink::VideoInputDeviceCapabilities::New();
    video_capabilities->device_id = String(enumeration_[1][0].device_id);
    video_capabilities->group_id = String(enumeration_[1][0].group_id);
    video_capabilities->facing_mode =
        enumeration_[1][0].video_facing;  // mojom::blink::FacingMode::kNone;
    video_capabilities->formats.push_back(media::VideoCaptureFormat(
        gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
    video_capabilities->availability =
        static_cast<media::mojom::CameraAvailability>(
            *enumeration_[1][0].availability);
    video_input_capabilities_.push_back(std::move(video_capabilities));

    video_capabilities = mojom::blink::VideoInputDeviceCapabilities::New();
    video_capabilities->device_id = String(enumeration_[1][1].device_id);
    video_capabilities->group_id = String(enumeration_[1][1].group_id);
    video_capabilities->formats.push_back(media::VideoCaptureFormat(
        gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
    video_capabilities->facing_mode = enumeration_[1][1].video_facing;
    media::VideoCaptureFormat format;
    video_input_capabilities_.push_back(std::move(video_capabilities));

    video_capabilities = mojom::blink::VideoInputDeviceCapabilities::New();
    video_capabilities->device_id = String(enumeration_[1][2].device_id);
    video_capabilities->group_id = String(enumeration_[1][2].group_id);
    video_capabilities->formats.push_back(media::VideoCaptureFormat(
        gfx::Size(640, 480), 30.0, media::VideoPixelFormat::PIXEL_FORMAT_I420));
    video_capabilities->formats.push_back(
        media::VideoCaptureFormat(gfx::Size(1920, 1080), 60.0,
                                  media::VideoPixelFormat::PIXEL_FORMAT_I420));
    video_capabilities->facing_mode = enumeration_[1][2].video_facing;
    video_capabilities->availability =
        static_cast<media::mojom::CameraAvailability>(
            *enumeration_[1][2].availability);
    video_input_capabilities_.push_back(std::move(video_capabilities));

    mojom::blink::AudioInputDeviceCapabilitiesPtr audio_capabilities =
        mojom::blink::AudioInputDeviceCapabilities::New();
    audio_capabilities->device_id = String(enumeration_[0][0].device_id);
    audio_capabilities->group_id = String(enumeration_[0][0].group_id);
    audio_capabilities->parameters =
        media::AudioParameters::UnavailableDeviceParams();
    audio_capabilities->is_valid = true;
    audio_input_capabilities_.push_back(std::move(audio_capabilities));

    audio_capabilities = mojom::blink::AudioInputDeviceCapabilities::New();
    audio_capabilities->device_id = String(enumeration_[0][1].device_id);
    audio_capabilities->group_id = String(enumeration_[0][1].group_id);
    audio_capabilities->parameters =
        media::AudioParameters::UnavailableDeviceParams();
    audio_capabilities->parameters.set_effects(
        media::AudioParameters::PlatformEffectsMask::VOICE_ISOLATION_SUPPORTED);
    audio_capabilities->is_valid = true;
    audio_input_capabilities_.push_back(std::move(audio_capabilities));

    audio_capabilities = mojom::blink::AudioInputDeviceCapabilities::New();
    audio_capabilities->device_id = String(enumeration_[0][2].device_id);
    audio_capabilities->group_id = String(enumeration_[0][2].group_id);
    audio_capabilities->parameters =
        media::AudioParameters::UnavailableDeviceParams();
    audio_capabilities->parameters.set_effects(
        media::AudioParameters::PlatformEffectsMask::ECHO_CANCELLER);
    audio_capabilities->is_valid = true;
    audio_input_capabilities_.push_back(std::move(audio_capabilities));
  }

  ~MockMediaDevicesDispatcherHost() override {
    EXPECT_FALSE(expected_capture_handle_config_);
  }

  void EnumerateDevices(bool request_audio_input,
                        bool request_video_input,
                        bool request_audio_output,
                        bool request_video_input_capabilities,
                        bool request_audio_input_capabilities,
                        EnumerateDevicesCallback callback) override {
    Vector<Vector<WebMediaDeviceInfo>> enumeration(static_cast<size_t>(
        blink::mojom::blink::MediaDeviceType::kNumMediaDeviceTypes));
    Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>
        video_input_capabilities;
    Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>
        audio_input_capabilities;
    if (request_audio_input) {
      wtf_size_t index = static_cast<wtf_size_t>(
          blink::mojom::blink::MediaDeviceType::kMediaAudioInput);
      enumeration[index] = enumeration_[index];

      if (request_audio_input_capabilities) {
        for (const auto& c : audio_input_capabilities_) {
          mojom::blink::AudioInputDeviceCapabilitiesPtr capabilities =
              mojom::blink::AudioInputDeviceCapabilities::New();
          *capabilities = *c;
          audio_input_capabilities.push_back(std::move(capabilities));
        }
      }
    }
    if (request_video_input) {
      wtf_size_t index = static_cast<wtf_size_t>(
          blink::mojom::blink::MediaDeviceType::kMediaVideoInput);
      enumeration[index] = enumeration_[index];

      if (request_video_input_capabilities) {
        for (const auto& c : video_input_capabilities_) {
          mojom::blink::VideoInputDeviceCapabilitiesPtr capabilities =
              mojom::blink::VideoInputDeviceCapabilities::New();
          *capabilities = *c;
          video_input_capabilities.push_back(std::move(capabilities));
        }
      }
    }
    if (request_audio_output) {
      wtf_size_t index = static_cast<wtf_size_t>(
          blink::mojom::blink::MediaDeviceType::kMediaAudioOutput);
      enumeration[index] = enumeration_[index];
    }
    std::move(callback).Run(std::move(enumeration),
                            std::move(video_input_capabilities),
                            std::move(audio_input_capabilities));
  }

  void SelectAudioOutput(
      const String& device_id,
      SelectAudioOutputCallback select_audio_output_callback) override {
    mojom::blink::SelectAudioOutputResultPtr result =
        mojom::blink::SelectAudioOutputResult::New();
    if (device_id == "test_device_id") {
      result->status = blink::mojom::AudioOutputStatus::kSuccess;
      result->device_info.device_id = "test_device_id";
      result->device_info.label = "Test Speaker";
      result->device_info.group_id = "test_group_id";
    } else {
      result->status = blink::mojom::AudioOutputStatus::kNoPermission;
    }
    std::move(select_audio_output_callback).Run(std::move(result));
  }

  void GetVideoInputCapabilities(GetVideoInputCapabilitiesCallback) override {
    NOTREACHED();
  }

  void GetAllVideoInputDeviceFormats(
      const String&,
      GetAllVideoInputDeviceFormatsCallback) override {
    NOTREACHED();
  }

  void GetAvailableVideoInputDeviceFormats(
      const String&,
      GetAvailableVideoInputDeviceFormatsCallback) override {
    NOTREACHED();
  }

  void GetAudioInputCapabilities(GetAudioInputCapabilitiesCallback) override {
    NOTREACHED();
  }

  void AddMediaDevicesListener(
      bool subscribe_audio_input,
      bool subscribe_video_input,
      bool subscribe_audio_output,
      mojo::PendingRemote<mojom::blink::MediaDevicesListener> listener)
      override {
    listener_.Bind(std::move(listener));
  }

  void SetCaptureHandleConfig(
      mojom::blink::CaptureHandleConfigPtr config) override {
    CHECK(config);

    auto expected_config = std::move(expected_capture_handle_config_);
    expected_capture_handle_config_ = nullptr;
    CHECK(expected_config);

    // TODO(crbug.com/1208868): Define CaptureHandleConfig traits that compare
    // |permitted_origins| using SecurityOrigin::IsSameOriginWith(), thereby
    // allowing this block to be replaced by a single EXPECT_EQ. (This problem
    // only manifests in Blink.)
    EXPECT_EQ(config->expose_origin, expected_config->expose_origin);
    EXPECT_EQ(config->capture_handle, expected_config->capture_handle);
    EXPECT_EQ(config->all_origins_permitted,
              expected_config->all_origins_permitted);
    CHECK_EQ(config->permitted_origins.size(),
             expected_config->permitted_origins.size());
    for (wtf_size_t i = 0; i < config->permitted_origins.size(); ++i) {
      EXPECT_TRUE(config->permitted_origins[i]->IsSameOriginWith(
          expected_config->permitted_origins[i].get()));
    }
  }

  void SetPreferredSinkId(const String& sink_id,
                          SetPreferredSinkIdCallback callback) override {
    if (sink_id == kValidSinkId) {
      std::move(callback).Run(
          static_cast<media::mojom::blink::OutputDeviceStatus>(
              output_device_status_));
    } else {
      std::move(callback).Run(
          static_cast<media::mojom::blink::OutputDeviceStatus>(
              media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_FOUND));
    }
  }

  void CloseFocusWindowOfOpportunity(const String& label) override {}

  void ProduceSubCaptureTargetId(
      SubCaptureTarget::Type type,
      ProduceSubCaptureTargetIdCallback callback) override {
    auto it = next_ids_.find(type);
    if (it == next_ids_.end()) {
      GTEST_FAIL();
    }
    std::vector<String>& queue = it->second;
    CHECK(!queue.empty());
    String next_id = queue.front();
    queue.erase(queue.begin());
    std::move(callback).Run(std::move(next_id));
  }

  void SetNextId(SubCaptureTarget::Type type, String next_id) {
    std::vector<String>& queue = next_ids_[type];
    queue.push_back(std::move(next_id));
  }

  void SetOutputDeviceStatus(media::OutputDeviceStatus status) {
    output_device_status_ = status;
  }

  void ExpectSetCaptureHandleConfig(
      mojom::blink::CaptureHandleConfigPtr config) {
    CHECK(config);
    CHECK(!expected_capture_handle_config_) << "Unfulfilled expectation.";
    expected_capture_handle_config_ = std::move(config);
  }

  mojom::blink::CaptureHandleConfigPtr expected_capture_handle_config() {
    return std::move(expected_capture_handle_config_);
  }

  mojo::PendingRemote<mojom::blink::MediaDevicesDispatcherHost>
  CreatePendingRemoteAndBind() {
    mojo::PendingRemote<mojom::blink::MediaDevicesDispatcherHost> remote;
    receiver_.Bind(remote.InitWithNewPipeAndPassReceiver());
    return remote;
  }

  void CloseBinding() { receiver_.reset(); }

  mojo::Remote<mojom::blink::MediaDevicesListener>& listener() {
    return listener_;
  }

  const Vector<Vector<WebMediaDeviceInfo>>& enumeration() const {
    return enumeration_;
  }

  void NotifyDeviceChanges() {
    listener()->OnDevicesChanged(MediaDeviceType::kMediaAudioInput,
                                 enumeration_[static_cast<wtf_size_t>(
                                     MediaDeviceType::kMediaAudioInput)]);
    listener()->OnDevicesChanged(MediaDeviceType::kMediaVideoInput,
                                 enumeration_[static_cast<wtf_size_t>(
                                     MediaDeviceType::kMediaVideoInput)]);
    listener()->OnDevicesChanged(MediaDeviceType::kMediaAudioOutput,
                                 enumeration_[static_cast<wtf_size_t>(
                                     MediaDeviceType::kMediaAudioOutput)]);
  }

  Vector<WebMediaDeviceInfo>& AudioInputDevices() {
    return enumeration_[static_cast<wtf_size_t>(
        MediaDeviceType::kMediaAudioInput)];
  }
  Vector<WebMediaDeviceInfo>& VideoInputDevices() {
    return enumeration_[static_cast<wtf_size_t>(
        MediaDeviceType::kMediaVideoInput)];
  }
  Vector<WebMediaDeviceInfo>& AudioOutputDevices() {
    return enumeration_[static_cast<wtf_size_t>(
        MediaDeviceType::kMediaAudioOutput)];
  }

  const Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>&
  VideoInputCapabilities() {
    return video_input_capabilities_;
  }
  const Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>&
  AudioInputCapabilities() {
    return audio_input_capabilities_;
  }

 private:
  mojo::Remote<mojom::blink::MediaDevicesListener> listener_;
  mojo::Receiver<mojom::blink::MediaDevicesDispatcherHost> receiver_{this};
  mojom::blink::CaptureHandleConfigPtr expected_capture_handle_config_;
  std::map<SubCaptureTarget::Type, std::vector<String>> next_ids_;
  media::OutputDeviceStatus output_device_status_ =
      media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_OK;
  Vector<Vector<WebMediaDeviceInfo>> enumeration_{static_cast<size_t>(
      blink::mojom::blink::MediaDeviceType::kNumMediaDeviceTypes)};
  Vector<mojom::blink::VideoInputDeviceCapabilitiesPtr>
      video_input_capabilities_;
  Vector<mojom::blink::AudioInputDeviceCapabilitiesPtr>
      audio_input_capabilities_;
};

class MockDeviceChangeEventListener : public NativeEventListener {
 public:
  MOCK_METHOD(void, Invoke, (ExecutionContext*, Event*));
};

V8MediaDeviceKind::Enum ToEnum(MediaDeviceType type) {
  switch (type) {
    case MediaDeviceType::kMediaAudioInput:
      return V8MediaDeviceKind::Enum::kAudioinput;
    case blink::MediaDeviceType::kMediaVideoInput:
      return V8MediaDeviceKind::Enum::kVideoinput;
    case blink::MediaDeviceType::kMediaAudioOutput:
      return V8MediaDeviceKind::Enum::kAudiooutput;
    case blink::MediaDeviceType::kNumMediaDeviceTypes:
      break;
  }
  NOTREACHED();
}

void VerifyFacingMode(const Vector<String>& js_facing_mode,
                      blink::mojom::FacingMode cpp_facing_mode) {
  switch (cpp_facing_mode) {
    case blink::mojom::FacingMode::kNone:
      EXPECT_TRUE(js_facing_mode.empty());
      break;
    case blink::mojom::FacingMode::kUser:
      EXPECT_THAT(js_facing_mode, ElementsAre("user"));
      break;
    case blink::mojom::FacingMode::kEnvironment:
      EXPECT_THAT(js_facing_mode, ElementsAre("environment"));
      break;
    case blink::mojom::FacingMode::kLeft:
      EXPECT_THAT(js_facing_mode, ElementsAre("left"));
      break;
    case blink::mojom::FacingMode::kRight:
      EXPECT_THAT(js_facing_mode, ElementsAre("right"));
      break;
  }
}

void VerifyDeviceInfo(const MediaDeviceInfo* device,
                      const WebMediaDeviceInfo& expected,
                      MediaDeviceType type) {
  EXPECT_EQ(device->deviceId(), String(expected.device_id));
  EXPECT_EQ(device->groupId(), String(expected.group_id));
  EXPECT_EQ(device->label(), String(expected.label));
  EXPECT_EQ(device->kind(), ToEnum(type));
}

void VerifyVideoInputCapabilities(
    const MediaDeviceInfo* device,
    const WebMediaDeviceInfo& expected_device_info,
    const mojom::blink::VideoInputDeviceCapabilitiesPtr&
        expected_capabilities) {
  CHECK_EQ(device->kind(), V8MediaDeviceKind::Enum::kVideoinput);
  const InputDeviceInfo* info = static_cast<const InputDeviceInfo*>(device);
  MediaTrackCapabilities* capabilities = info->getCapabilities();
  EXPECT_EQ(capabilities->hasFacingMode(), expected_device_info.IsAvailable());
  if (capabilities->hasFacingMode()) {
    VerifyFacingMode(capabilities->facingMode(),
                     expected_device_info.video_facing);
  }
  EXPECT_EQ(capabilities->hasDeviceId(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasGroupId(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasWidth(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasHeight(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasAspectRatio(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasFrameRate(), expected_device_info.IsAvailable());
  if (expected_device_info.IsAvailable()) {
    int max_expected_width = 0;
    int max_expected_height = 0;
    float max_expected_frame_rate = 0.0;
    for (const auto& format : expected_capabilities->formats) {
      max_expected_width =
          std::max(max_expected_width, format.frame_size.width());
      max_expected_height =
          std::max(max_expected_height, format.frame_size.height());
      max_expected_frame_rate =
          std::max(max_expected_frame_rate, format.frame_rate);
    }
    EXPECT_EQ(capabilities->deviceId().Utf8(), expected_device_info.device_id);
    EXPECT_EQ(capabilities->groupId().Utf8(), expected_device_info.group_id);
    EXPECT_EQ(capabilities->width()->min(), 1);
    EXPECT_EQ(capabilities->width()->max(), max_expected_width);
    EXPECT_EQ(capabilities->height()->min(), 1);
    EXPECT_EQ(capabilities->height()->max(), max_expected_height);
    EXPECT_EQ(capabilities->aspectRatio()->min(), 1.0 / max_expected_height);
    EXPECT_EQ(capabilities->aspectRatio()->max(), max_expected_width);
    EXPECT_EQ(capabilities->frameRate()->min(), 1.0);
    EXPECT_EQ(capabilities->frameRate()->max(), max_expected_frame_rate);
  }
}

EchoCancellationMode ToEchoCancellationMode(
    const V8UnionBooleanOrString* value) {
  if (value->IsBoolean()) {
    return value->GetAsBoolean() ? EchoCancellationMode::kBrowserDecides
                                 : EchoCancellationMode::kDisabled;
  }
  CHECK(value->IsString());
  if (value->GetAsString() == "remote-only") {
    return EchoCancellationMode::kRemoteOnly;
  }
  CHECK_EQ(value->GetAsString(), "all");
  return EchoCancellationMode::kAll;
}

void VerifyAudioInputCapabilities(
    const MediaDeviceInfo* device,
    const WebMediaDeviceInfo& expected_device_info,
    const mojom::blink::AudioInputDeviceCapabilitiesPtr&
        expected_capabilities) {
  CHECK_EQ(device->kind(), V8MediaDeviceKind::Enum::kAudioinput);
  const InputDeviceInfo* info = static_cast<const InputDeviceInfo*>(device);
  MediaTrackCapabilities* capabilities = info->getCapabilities();
  EXPECT_EQ(capabilities->hasDeviceId(), expected_device_info.IsAvailable());
  EXPECT_EQ(capabilities->hasGroupId(), expected_device_info.IsAvailable());
  if (expected_device_info.IsAvailable()) {
    EXPECT_EQ(capabilities->deviceId().Utf8(), expected_device_info.device_id);
    EXPECT_EQ(capabilities->groupId().Utf8(), expected_device_info.group_id);
    Vector<EchoCancellationMode> echo_cancellation;
    for (auto value : capabilities->echoCancellation()) {
      echo_cancellation.push_back(ToEchoCancellationMode(value));
    }
    EXPECT_TRUE(std::ranges::contains(echo_cancellation,
                                      EchoCancellationMode::kBrowserDecides));
    EXPECT_TRUE(std::ranges::contains(echo_cancellation,
                                      EchoCancellationMode::kDisabled));
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
    EXPECT_TRUE(std::ranges::contains(echo_cancellation,
                                      EchoCancellationMode::kRemoteOnly));
#endif
    int effects = expected_capabilities->parameters.effects();
    // On some platforms, capabilities are not queried because it is costly.
    // In this case, device parameters are unknown. See crbug.com/40945999
    if (!base::FeatureList::IsEnabled(
            kEnumerateDevicesRequestAudioCapabilities)) {
      effects = media::AudioParameters::PlatformEffectsMask::NO_EFFECTS;
    }
    if (EchoCanceller::IsSystemWideAecAvailable(effects)) {
      EXPECT_TRUE(
          std::ranges::contains(echo_cancellation, EchoCancellationMode::kAll));
    }
    EXPECT_TRUE(capabilities->hasVoiceIsolation());
    Vector<bool> voice_isolation = capabilities->voiceIsolation();
    if (IsVoiceIsolationSupported() &&
        (effects & media::AudioParameters::VOICE_ISOLATION_SUPPORTED)) {
      EXPECT_TRUE(std::ranges::contains(voice_isolation, true));
      EXPECT_TRUE(std::ranges::contains(voice_isolation, false));
    } else {
      EXPECT_FALSE(std::ranges::contains(voice_isolation, true));
      EXPECT_TRUE(std::ranges::contains(voice_isolation, false));
    }
  }
}

SubCaptureTarget* ToSubCaptureTarget(const blink::ScriptValue& value) {
  if (CropTarget* crop_target =
          V8CropTarget::ToWrappable(value.GetIsolate(), value.V8Value())) {
    return crop_target;
  }

  if (RestrictionTarget* restriction_target = V8RestrictionTarget::ToWrappable(
          value.GetIsolate(), value.V8Value())) {
    return restriction_target;
  }

  NOTREACHED();
}

bool ProduceSubCaptureTargetAndGetPromise(V8TestingScope& scope,
                                          SubCaptureTarget::Type type,
                                          MediaDevices* media_devices,
                                          Element* element) {
  switch (type) {
    case SubCaptureTarget::Type::kCropTarget:
      return !media_devices
                  ->ProduceCropTarget(scope.GetScriptState(), element,
                                      scope.GetExceptionState())
                  .IsEmpty();

    case SubCaptureTarget::Type::kRestrictionTarget:
      return !media_devices
                  ->ProduceRestrictionTarget(scope.GetScriptState(), element,
                                             scope.GetExceptionState())
                  .IsEmpty();
  }
}

void ProduceSubCaptureTargetAndGetTester(
    V8TestingScope& scope,
    SubCaptureTarget::Type type,
    MediaDevices* media_devices,
    Element* element,
    std::optional<ScriptPromiseTester>& tester) {
  switch (type) {
    case SubCaptureTarget::Type::kCropTarget:
      tester.emplace(
          scope.GetScriptState(),
          media_devices->ProduceCropTarget(scope.GetScriptState(), element,
                                           scope.GetExceptionState()));
      return;
    case SubCaptureTarget::Type::kRestrictionTarget:
      tester.emplace(
          scope.GetScriptState(),
          media_devices->ProduceRestrictionTarget(
              scope.GetScriptState(), element, scope.GetExceptionState()));
      return;
  }
}

class MockMediaPermission : public media::MediaPermission {
 public:
  MockMediaPermission() = default;

  void HasPermission(Type type,
                     PermissionStatusCB permission_status_cb) override {
    bool has_permission = false;
    if (type == Type::kAudioCapture) {
      has_permission = has_microphone_permission_;
    } else if (type == Type::kVideoCapture) {
      has_permission = has_camera_permission_;
    }

    std::move(permission_status_cb).Run(has_permission);
  }

  void RequestPermission(Type type,
                         PermissionStatusCB permission_status_cb) override {}

  bool IsEncryptedMediaEnabled() override { return false; }

#if BUILDFLAG(IS_WIN)
  void IsHardwareSecureDecryptionAllowed(
      IsHardwareSecureDecryptionAllowedCB cb) override {}
#endif  // BUILDFLAG(IS_WIN)

  void SetCameraPermission(bool has_permission) {
    has_camera_permission_ = has_permission;
  }

  void SetMicrophonePermission(bool has_permission) {
    has_microphone_permission_ = has_permission;
  }

 private:
  bool has_camera_permission_ = true;
  bool has_microphone_permission_ = true;
};

}  // namespace

class MediaDevicesTest : public PageTestBase {
 public:
  MediaDevicesTest()
      : platform_(std::make_unique<MockMediaPermission>()),
        dispatcher_host_(std::make_unique<MockMediaDevicesDispatcherHost>()) {}

  MediaDevices* GetMediaDevices(LocalDOMWindow& window) {
    if (!media_devices_) {
      media_devices_ = MakeGarbageCollected<MediaDevices>(*window.navigator());
      media_devices_->SetDispatcherHostForTesting(
          dispatcher_host_->CreatePendingRemoteAndBind());
    }
    return media_devices_;
  }

  void CloseBinding() { dispatcher_host_->CloseBinding(); }

  void OnListenerConnectionError() { listener_connection_error_ = true; }
  bool listener_connection_error() const { return listener_connection_error_; }

  ScopedTestingPlatformSupport<MediaPermissionTestingPlatform,
                               std::unique_ptr<media::MediaPermission>>&
  platform() {
    return platform_;
  }

  MockMediaDevicesDispatcherHost& dispatcher_host() {
    DCHECK(dispatcher_host_);
    return *dispatcher_host_;
  }

  void AddDeviceChangeListener(EventListener* event_listener) {
    GetMediaDevices(*GetDocument().domWindow())
        ->addEventListener(event_type_names::kDevicechange, event_listener);
    platform()->RunUntilIdle();
  }

  void RemoveDeviceChangeListener(EventListener* event_listener) {
    GetMediaDevices(*GetDocument().domWindow())
        ->removeEventListener(event_type_names::kDevicechange, event_listener,
                              /*use_capture=*/false);
    platform()->RunUntilIdle();
  }

  void NotifyDeviceChanges() {
    dispatcher_host().NotifyDeviceChanges();
    platform()->RunUntilIdle();
  }

  void RunEnumerateDevicesTest();

  void ExpectEnumerateDevicesHistogramReport(
      EnumerateDevicesResult expected_result) {
    histogram_tester_.ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.Result", 1);
    histogram_tester_.ExpectUniqueSample(
        "Media.MediaDevices.EnumerateDevices.Result", expected_result, 1);
    histogram_tester_.ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.Latency", 1);
  }

  DOMException* CallAndValidateSetPreferredSinkId(const char* sink_id,
                                                  bool expect_fulfilled) {
    V8TestingScope scope;

    auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
    ScriptPromiseTester tester(
        scope.GetScriptState(),
        media_devices->setPreferredSinkId(scope.GetScriptState(), sink_id,
                                          scope.GetExceptionState()));
    tester.WaitUntilSettled();

    if (expect_fulfilled) {
      EXPECT_TRUE(tester.IsFulfilled());
    } else {
      EXPECT_TRUE(tester.IsRejected());
    }

    return V8DOMException::ToWrappable(scope.GetIsolate(),
                                       tester.Value().V8Value());
  }

  void SetCameraPermission(bool has_permission) {
    static_cast<MockMediaPermission*>(
        platform()->GetWebRTCMediaPermission(nullptr))
        ->SetCameraPermission(has_permission);
  }

  void SetMicrophonePermission(bool has_permission) {
    static_cast<MockMediaPermission*>(
        platform()->GetWebRTCMediaPermission(nullptr))
        ->SetMicrophonePermission(has_permission);
  }

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

 private:
  ScopedTestingPlatformSupport<MediaPermissionTestingPlatform,
                               std::unique_ptr<media::MediaPermission>>
      platform_;
  std::unique_ptr<MockMediaDevicesDispatcherHost> dispatcher_host_;
  bool listener_connection_error_ = false;
  Persistent<MediaDevices> media_devices_;
  base::HistogramTester histogram_tester_;
};

TEST_F(MediaDevicesTest, GetUserMediaCanBeCalled) {
  V8TestingScope scope;
  UserMediaStreamConstraints* constraints =
      UserMediaStreamConstraints::Create();
  auto promise = GetMediaDevices(scope.GetWindow())
                     ->getUserMedia(scope.GetScriptState(), constraints,
                                    scope.GetExceptionState());
  // We return the created promise before it was resolved/rejected.
  ASSERT_FALSE(promise.IsEmpty());
  // We expect a type error because the given constraints are empty.
  EXPECT_EQ(scope.GetExceptionState().Code(),
            ToExceptionCode(ESErrorType::kTypeError));
  VLOG(1) << "Exception message is" << scope.GetExceptionState().Message();
}

void MediaDevicesTest::RunEnumerateDevicesTest() {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ScriptPromiseTester tester(
      scope.GetScriptState(),
      media_devices->enumerateDevices(scope.GetScriptState(),
                                      scope.GetExceptionState()));
  tester.WaitUntilSettled();
  EXPECT_TRUE(tester.IsFulfilled());

  auto device_infos = NativeValueTraits<IDLArray<MediaDeviceInfo>>::NativeValue(
      scope.GetIsolate(), tester.Value().V8Value(), scope.GetExceptionState());
  ASSERT_FALSE(scope.GetExceptionState().HadException());

  ExpectEnumerateDevicesHistogramReport(EnumerateDevicesResult::kOk);

  const auto& video_input_capabilities =
      dispatcher_host().VideoInputCapabilities();
  const auto& audio_input_capabilities =
      dispatcher_host().AudioInputCapabilities();
  for (wtf_size_t i = 0, result_index = 0, video_input_index = 0,
                  audio_input_index = 0;
       i < static_cast<wtf_size_t>(MediaDeviceType::kNumMediaDeviceTypes);
       ++i) {
    for (const auto& expected_device_info :
         dispatcher_host().enumeration()[i]) {
      testing::Message message;
      message << "Verifying result index " << result_index;
      SCOPED_TRACE(message);
      VerifyDeviceInfo(device_infos[result_index], expected_device_info,
                       static_cast<MediaDeviceType>(i));
      if (i == static_cast<wtf_size_t>(MediaDeviceType::kMediaVideoInput)) {
        VerifyVideoInputCapabilities(
            device_infos[result_index], expected_device_info,
            video_input_capabilities[video_input_index]);
        video_input_index++;
      } else if (i ==
                 static_cast<wtf_size_t>(MediaDeviceType::kMediaAudioInput)) {
        VerifyAudioInputCapabilities(
            device_infos[result_index], expected_device_info,
            audio_input_capabilities[audio_input_index]);
        audio_input_index++;
      }
      result_index++;
    }
  }
}

TEST_F(MediaDevicesTest, EnumerateDevices) {
  RunEnumerateDevicesTest();
}

#if BUILDFLAG(CHROME_WIDE_ECHO_CANCELLATION)
// Verifies that when voice isolation is supported by the browser (via
// `kWebRtcVoiceIsolationDenoiser`), `VerifyAudioInputCapabilities()` checks
// that audio input devices reporting `VOICE_ISOLATION_SUPPORTED` in their
// platform effects expose `capabilities.voiceIsolation = {true, false}`, while
// devices without the effect only expose `{false}`.
TEST_F(MediaDevicesTest, EnumerateDevicesWithVoiceIsolation) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndEnableFeature(media::kWebRtcVoiceIsolationDenoiser);
  RunEnumerateDevicesTest();
}
#endif

TEST_F(MediaDevicesTest, EnumerateDevicesAfterConnectionError) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  // Simulate a connection error by closing the binding.
  CloseBinding();
  platform()->RunUntilIdle();

  ScriptPromiseTester tester(
      scope.GetScriptState(),
      media_devices->enumerateDevices(scope.GetScriptState(),
                                      scope.GetExceptionState()));
  tester.WaitUntilSettled();
  EXPECT_TRUE(tester.IsRejected());
  ExpectEnumerateDevicesHistogramReport(
      EnumerateDevicesResult::kErrorMediaDevicesDispatcherHostDisconnected);
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigAfterConnectionError) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  // Simulate a connection error by closing the binding.
  CloseBinding();
  platform()->RunUntilIdle();

  // Note: SetCaptureHandleConfigEmpty proves the following is a valid call.
  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());
  platform()->RunUntilIdle();
}

TEST_F(MediaDevicesTest, ObserveDeviceChangeEvent) {
  EXPECT_FALSE(dispatcher_host().listener());

  // Subscribe to the devicechange event.
  StrictMock<MockDeviceChangeEventListener>* event_listener =
      MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
  AddDeviceChangeListener(event_listener);
  EXPECT_TRUE(dispatcher_host().listener());
  dispatcher_host().listener().set_disconnect_handler(
      BindOnce(&MediaDevicesTest::OnListenerConnectionError, Unretained(this)));

  // Send a device change notification from the dispatcher host. The event is
  // not fired because devices did not actually change.
  NotifyDeviceChanges();

  // Adding a new device fires the event.
  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioInputDevices().push_back(WebMediaDeviceInfo(
      "new_fake_audio_input_device", "new_fake_label", "new_fake_group"));
  NotifyDeviceChanges();

  // Renaming a device ID fires the event.
  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id";
  NotifyDeviceChanges();

  // Renaming a group ID fires the event.
  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioOutputDevices().begin()->group_id = "new_group_id";
  NotifyDeviceChanges();

  // Renaming a label fires the event.
  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioOutputDevices().begin()->label = "new_label";
  NotifyDeviceChanges();

  // Changing availability fires the event.
  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().VideoInputDevices().begin()->availability =
      media::CameraAvailability::kUnavailableExclusivelyUsedByOtherApplication;
  NotifyDeviceChanges();

  // Changing facing mode does not file the event.
  EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
  dispatcher_host().VideoInputDevices().begin()->video_facing =
      blink::mojom::FacingMode::kLeft;
  NotifyDeviceChanges();

  // Unsubscribe.
  RemoveDeviceChangeListener(event_listener);
  EXPECT_TRUE(listener_connection_error());

  // Sending a device change notification after unsubscribe does not fire the
  // event.
  dispatcher_host().AudioInputDevices().push_back(WebMediaDeviceInfo(
      "yet_another_input_device", "yet_another_label", "yet_another_group"));
  NotifyDeviceChanges();
}

TEST_F(MediaDevicesTest, RemoveDeviceFiresDeviceChange) {
  StrictMock<MockDeviceChangeEventListener>* event_listener =
      MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
  AddDeviceChangeListener(event_listener);

  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().VideoInputDevices().EraseAt(0);
  NotifyDeviceChanges();
}

TEST_F(MediaDevicesTest, RenameDeviceIDFiresDeviceChange) {
  StrictMock<MockDeviceChangeEventListener>* event_listener =
      MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
  AddDeviceChangeListener(event_listener);

  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioOutputDevices().begin()->device_id = "new_device_id";
  NotifyDeviceChanges();
}

TEST_F(MediaDevicesTest, RenameLabelFiresDeviceChange) {
  StrictMock<MockDeviceChangeEventListener>* event_listener =
      MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
  AddDeviceChangeListener(event_listener);

  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioOutputDevices().begin()->label = "new_label";
  NotifyDeviceChanges();
}

TEST_F(MediaDevicesTest, ObserveDeviceChangeEventPermissions) {
  StrictMock<MockDeviceChangeEventListener>* event_listener =
      MakeGarbageCollected<StrictMock<MockDeviceChangeEventListener>>();
  AddDeviceChangeListener(event_listener);

  SetCameraPermission(false);
  SetMicrophonePermission(true);

  EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
  dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id";
  NotifyDeviceChanges();

  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id";
  NotifyDeviceChanges();

  SetCameraPermission(true);
  SetMicrophonePermission(false);

  EXPECT_CALL(*event_listener, Invoke(_, _));
  dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id_2";
  NotifyDeviceChanges();

  EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
  dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id_2";
  NotifyDeviceChanges();

  SetCameraPermission(false);
  SetMicrophonePermission(false);

  EXPECT_CALL(*event_listener, Invoke(_, _)).Times(0);
  dispatcher_host().VideoInputDevices().begin()->device_id = "new_device_id_3";
  NotifyDeviceChanges();
  dispatcher_host().AudioInputDevices().begin()->device_id = "new_device_id_3";
  NotifyDeviceChanges();
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigEmpty) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = false;
  expected_config->capture_handle = "";
  expected_config->all_origins_permitted = false;
  expected_config->permitted_origins = {};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigWithExposeOrigin) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setExposeOrigin(true);

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = true;
  expected_config->capture_handle = "";
  expected_config->all_origins_permitted = false;
  expected_config->permitted_origins = {};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithHandle) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setHandle("0xabcdef0123456789");

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = false;
  expected_config->capture_handle = "0xabcdef0123456789";
  expected_config->all_origins_permitted = false;
  expected_config->permitted_origins = {};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithMaxHandle) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  const String maxHandle = MaxLengthCaptureHandle();

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setHandle(maxHandle);

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = false;
  expected_config->capture_handle = maxHandle;
  expected_config->all_origins_permitted = false;
  expected_config->permitted_origins = {};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest,
       SetCaptureHandleConfigCaptureWithOverMaxHandleRejected) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setHandle(MaxLengthCaptureHandle() + "a");  // Over max length.

  // Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  ASSERT_TRUE(scope.GetExceptionState().HadException());
  EXPECT_EQ(scope.GetExceptionState().Code(),
            ToExceptionCode(ESErrorType::kTypeError));
}

TEST_F(MediaDevicesTest,
       SetCaptureHandleConfigCaptureWithPermittedOriginsWildcard) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setPermittedOrigins({"*"});

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = false;
  expected_config->capture_handle = "";
  expected_config->all_origins_permitted = true;
  expected_config->permitted_origins = {};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest, SetCaptureHandleConfigCaptureWithPermittedOrigins) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setPermittedOrigins(
      {"https://chromium.org", "ftp://chromium.org:1234"});

  // Expected output.
  auto expected_config = mojom::blink::CaptureHandleConfig::New();
  expected_config->expose_origin = false;
  expected_config->capture_handle = "";
  expected_config->all_origins_permitted = false;
  expected_config->permitted_origins = {
      SecurityOrigin::CreateFromString("https://chromium.org"),
      SecurityOrigin::CreateFromString("ftp://chromium.org:1234")};
  dispatcher_host().ExpectSetCaptureHandleConfig(std::move(expected_config));

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  EXPECT_FALSE(scope.GetExceptionState().HadException());
}

TEST_F(MediaDevicesTest,
       SetCaptureHandleConfigCaptureWithWildcardAndSomethingElseRejected) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setPermittedOrigins({"*", "https://chromium.org"});

  // Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  ASSERT_TRUE(scope.GetExceptionState().HadException());
  EXPECT_EQ(scope.GetExceptionState().Code(),
            ToExceptionCode(DOMExceptionCode::kNotSupportedError));
}

TEST_F(MediaDevicesTest,
       SetCaptureHandleConfigCaptureWithMalformedOriginRejected) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());

  CaptureHandleConfig* input_config =
      MakeGarbageCollected<CaptureHandleConfig>();
  input_config->setPermittedOrigins(
      {"https://chromium.org:99999"});  // Invalid.

  // Note: dispatcher_host().ExpectSetCaptureHandleConfig() not called.

  media_devices->setCaptureHandleConfig(scope.GetScriptState(), input_config,
                                        scope.GetExceptionState());

  platform()->RunUntilIdle();

  ASSERT_TRUE(scope.GetExceptionState().HadException());
  EXPECT_EQ(scope.GetExceptionState().Code(),
            ToExceptionCode(DOMExceptionCode::kNotSupportedError));
}

TEST_F(MediaDevicesTest, SetPreferredSinkIdWithValidId) {
  CallAndValidateSetPreferredSinkId(kValidSinkId, /*expect_fulfilled=*/true);
}

TEST_F(MediaDevicesTest, SetPreferredSinkIdWithInvalidId) {
  DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
      kInvalidSinkId, /*expect_fulfilled=*/false);

  EXPECT_EQ(dom_exception->code(),
            static_cast<uint16_t>(DOMExceptionCode::kNotFoundError));
}

TEST_F(MediaDevicesTest, SetPreferredSinkIAuthorizationDenied) {
  dispatcher_host().SetOutputDeviceStatus(
      media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_NOT_AUTHORIZED);

  DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
      kValidSinkId, /*expect_fulfilled=*/false);

  EXPECT_EQ(dom_exception->name(), "NotAllowedError");
}

TEST_F(MediaDevicesTest, SetPreferredSinkTimeout) {
  dispatcher_host().SetOutputDeviceStatus(
      media::OutputDeviceStatus::OUTPUT_DEVICE_STATUS_ERROR_TIMED_OUT);

  DOMException* dom_exception = CallAndValidateSetPreferredSinkId(
      kValidSinkId, /*expect_fulfilled=*/false);

  EXPECT_EQ(dom_exception->code(),
            static_cast<uint16_t>(DOMExceptionCode::kTimeoutError));
}

// Regression test for crbug.com/403348706. This ensures that device change
// events, queued before the LocalFrame's ExecutionContext was destroyed,
// resolve without crashing the renderer.
TEST_F(MediaDevicesTest,
       DeviceChangeEventsDoNotCrashWhenExecutionContextDestroyed) {
  // Simulate resolution of a `MaybeFireDeviceChangeEvent()` task.
  MediaDevices* media_devices = GetMediaDevices(*GetDocument().domWindow());
  media_devices->MaybeFireDeviceChangeEvent(true);

  // Navigate the local frame's document, this will replace and destroy the
  // frame's document and dom window, and consequently the observed
  // ExecutionContext.
  Document& initial_document = GetDocument();
  LocalDOMWindow* initial_dom_window = GetDocument().domWindow();
  NavigateTo(KURL("https://example.com"));
  EXPECT_NE(GetDocument(), initial_document);
  EXPECT_NE(GetDocument().domWindow(), initial_dom_window);

  // Simulate the resolution of a `MaybeFireDeviceChangeEvent()` task, queued
  // before the observed context was destroyed. This should resolve without
  // crashing.
  media_devices->MaybeFireDeviceChangeEvent(true);
}

#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
// This test logically belongs to the ProduceSubCaptureTargetTest suite,
// but does not require parameterization.
TEST_F(MediaDevicesTest, DistinctIdsForDistinctTypes) {
  ScopedElementCaptureForTest scoped_element_capture(true);
  V8TestingScope scope;
  MediaDevices* const media_devices =
      GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  dispatcher_host().SetNextId(SubCaptureTarget::Type::kCropTarget,
                              String("983bf2ff-7410-416c-808a-78421cbd8fdc"));
  dispatcher_host().SetNextId(SubCaptureTarget::Type::kRestrictionTarget,
                              String("70db842e-5326-42c1-86b2-e3b2f74e97d2"));

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));
  const auto first_promise = media_devices->ProduceCropTarget(
      scope.GetScriptState(), div, scope.GetExceptionState());
  ScriptPromiseTester first_tester(scope.GetScriptState(), first_promise);
  first_tester.WaitUntilSettled();
  EXPECT_TRUE(first_tester.IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  // The second call to |produceSubCaptureTargetId|, given the different type,
  // should return a different ID.
  const auto second_promise = media_devices->ProduceRestrictionTarget(
      scope.GetScriptState(), div, scope.GetExceptionState());
  ScriptPromiseTester second_tester(scope.GetScriptState(), second_promise);
  second_tester.WaitUntilSettled();
  EXPECT_TRUE(second_tester.IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  const String first_result = ToSubCaptureTarget(first_tester.Value())->GetId();
  ASSERT_FALSE(first_result.empty());

  const String second_result =
      ToSubCaptureTarget(second_tester.Value())->GetId();
  ASSERT_FALSE(second_result.empty());

  EXPECT_NE(first_result, second_result);
}
#endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)

TEST_F(MediaDevicesTest, MetricsFailedEnumerateDevicesThenGetUserMedia) {
  {
    V8TestingScope scope;
    MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
    media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/false);
    media_devices->ReportSuccessfulGetUserMedia();
    histogram_tester().ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::kFailedEnumerateDevicesFirst,
        1);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kFailedEnumerateDevicesThenGetUserMedia,
        1);
  }
  histogram_tester().ExpectUniqueSample(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
      EnumerateDevicesFirstStateOnContextDestroyed::kFailed, 1);
}

TEST_F(MediaDevicesTest, MetricsSuccessfulEnumerateDevicesThenGetUserMedia) {
  {
    V8TestingScope scope;
    MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
    media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/true);
    media_devices->ReportSuccessfulGetUserMedia();
    histogram_tester().ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kSuccessfulEnumerateDevicesFirst,
        1);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kSuccessfulEnumerateDevicesThenGetUserMedia,
        1);
  }
  histogram_tester().ExpectUniqueSample(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
      EnumerateDevicesFirstStateOnContextDestroyed::
          kSuccessfulFollowedByGetUserMedia,
      1);
}

TEST_F(MediaDevicesTest, MetricsGetUserMediaThenSuccessfulEnumerateDevices) {
  {
    V8TestingScope scope;
    MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
    media_devices->ReportSuccessfulGetUserMedia();
    media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/true);
    histogram_tester().ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kGetUserMediaThenSuccessfulEnumerateDevices,
        1);
  }
  histogram_tester().ExpectUniqueSample(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
      EnumerateDevicesFirstStateOnContextDestroyed::
          kSuccessfulAfterGetUserMedia,
      1);
}

TEST_F(MediaDevicesTest, MetricsGetUserMediaThenFailedEnumerateDevices) {
  {
    V8TestingScope scope;
    MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
    media_devices->ReportSuccessfulGetUserMedia();
    media_devices->ReportCompletedEnumerateDevices(/*is_successful=*/false);
    histogram_tester().ExpectTotalCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction", 2);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
    histogram_tester().ExpectBucketCount(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kGetUserMediaThenFailedEnumerateDevices,
        1);
  }
  histogram_tester().ExpectUniqueSample(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
      EnumerateDevicesFirstStateOnContextDestroyed::kFailed, 1);
}

TEST_F(MediaDevicesTest, MetricsEnumerateDevicesOnly) {
  {
    V8TestingScope scope;
    ScriptPromiseTester(scope.GetScriptState(),
                        GetMediaDevices(scope.GetWindow())
                            ->enumerateDevices(scope.GetScriptState(),
                                               scope.GetExceptionState()))
        .WaitUntilSettled();
    histogram_tester().ExpectUniqueSample(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::
            kSuccessfulEnumerateDevicesFirst,
        1);
  }
  histogram_tester().ExpectUniqueSample(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed",
      EnumerateDevicesFirstStateOnContextDestroyed::
          kSuccessfulNeverGetUserMedia,
      1);
}

TEST_F(MediaDevicesTest, MetricsGetUserMediaOnly) {
  {
    V8TestingScope scope;
    MediaDevices* const media_devices = GetMediaDevices(scope.GetWindow());
    // A full getUserMedia() call cannot be mocked in this test, so just use the
    // report function.
    media_devices->ReportSuccessfulGetUserMedia();
    histogram_tester().ExpectUniqueSample(
        "Media.MediaDevices.EnumerateDevices.GetUserMediaInteraction",
        EnumerateDevicesGetUserMediaInteraction::kGetUserMediaFirst, 1);
  }
  histogram_tester().ExpectTotalCount(
      "Media.MediaDevices.EnumerateDevices.FirstStateOnContextDestroyed", 0);
}

class ProduceSubCaptureTargetTest
    : public MediaDevicesTest,
      public testing::WithParamInterface<
          std::pair<SubCaptureTarget::Type, bool>> {
 public:
  ProduceSubCaptureTargetTest()
      : type_(std::get<0>(GetParam())),
        scoped_element_capture_(std::get<1>(GetParam())) {}
  ~ProduceSubCaptureTargetTest() override = default;

  const SubCaptureTarget::Type type_;
  ScopedElementCaptureForTest scoped_element_capture_;
};

INSTANTIATE_TEST_SUITE_P(
    _,
    ProduceSubCaptureTargetTest,
    ::testing::Values(std::make_pair(SubCaptureTarget::Type::kCropTarget,
                                     /* Element Capture enabled: */ false),
                      std::make_pair(SubCaptureTarget::Type::kCropTarget,
                                     /* Element Capture enabled: */ true),
                      std::make_pair(SubCaptureTarget::Type::kRestrictionTarget,
                                     /* Element Capture enabled: */ true)));

TEST_P(ProduceSubCaptureTargetTest, IdWithValidElement) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
    <iframe id='test-iframe' src="about:blank"></iframe>
    <p id='test-p'>
      <var id='test-var'>e</var> equals mc<sup id='test-sup'>2</sup>, or is
      <wbr id='test-wbr'>it mc<sub id='test-sub'>2</sub>?
      <u id='test-u'>probz</u>.
    </p>
    <select id='test-select'></select>

    <svg id='test-svg' width="400" height="110">
      <rect id='test-rect' width="300" height="100"/>
    </svg>

    <math id='test-math' xmlns='http://www.w3.org/1998/Math/MathML'>
    </math>
  )HTML");

  Document& document = GetDocument();
  static const std::vector<const char*> kElementIds{
      "test-div",    "test-iframe", "test-p",    "test-var",
      "test-sup",    "test-wbr",    "test-sub",  "test-u",
      "test-select", "test-svg",    "test-rect", "test-math"};

  for (const char* id : kElementIds) {
    Element* const element = document.getElementById(AtomicString(id));
    dispatcher_host().SetNextId(
        type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));
    std::optional<ScriptPromiseTester> tester;
    ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, element,
                                        tester);
    ASSERT_TRUE(tester);
    tester->WaitUntilSettled();
    EXPECT_TRUE(tester->IsFulfilled())
        << "Failed promise for element id=" << id;
    EXPECT_FALSE(scope.GetExceptionState().HadException());
  }
}

TEST_P(ProduceSubCaptureTargetTest, IdRejectedIfDifferentWindow) {
  V8TestingScope scope;
  // Intentionally sets up a MediaDevices object in a different window.
  auto* media_devices = GetMediaDevices(scope.GetWindow());
  ASSERT_TRUE(media_devices);

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
    <iframe id='test-iframe' src="about:blank" />
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));
  bool got_promise =
      ProduceSubCaptureTargetAndGetPromise(scope, type_, media_devices, div);
  platform()->RunUntilIdle();
  EXPECT_FALSE(got_promise);
  EXPECT_TRUE(scope.GetExceptionState().HadException());
  EXPECT_EQ(scope.GetExceptionState().CodeAs<DOMExceptionCode>(),
            DOMExceptionCode::kNotSupportedError);
  EXPECT_EQ(
      scope.GetExceptionState().Message(),
      String("The Element and the MediaDevices object must be same-window."));
}

TEST_P(ProduceSubCaptureTargetTest, RejectsIfFencedFrame) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  // Set the page to behave as a fenced frame root.
  GetDocument().GetPage()->SetIsMainFrameFencedFrameRoot();
  ASSERT_TRUE(GetDocument().GetFrame()->IsInFencedFrameTree());

  SetBodyContent("<div id='test-div'></div>");
  Element* const div = GetDocument().getElementById(AtomicString("test-div"));

  bool got_promise =
      ProduceSubCaptureTargetAndGetPromise(scope, type_, media_devices, div);
  EXPECT_FALSE(got_promise);
  EXPECT_TRUE(scope.GetExceptionState().HadException());
  EXPECT_EQ(scope.GetExceptionState().CodeAs<DOMExceptionCode>(),
            DOMExceptionCode::kNotAllowedError);
  EXPECT_EQ(
      scope.GetExceptionState().Message(),
      type_ == SubCaptureTarget::Type::kCropTarget
          ? String(
                "CropTarget.fromElement is not allowed in a fenced frame tree.")
          : String("RestrictionTarget.fromElement is not allowed in a fenced "
                   "frame tree."));
}

TEST_P(ProduceSubCaptureTargetTest, DuplicateId) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  // This ID should be used for the single ID produced.
  dispatcher_host().SetNextId(type_,
                              String("983bf2ff-7410-416c-808a-78421cbd8fdc"));

  // This ID should never be encountered.
  dispatcher_host().SetNextId(type_,
                              String("70db842e-5326-42c1-86b2-e3b2f74e97d2"));

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));
  std::optional<ScriptPromiseTester> first_tester;
  ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div,
                                      first_tester);
  ASSERT_TRUE(first_tester);
  first_tester->WaitUntilSettled();
  EXPECT_TRUE(first_tester->IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  // The second call to |produceSubCaptureTargetId| should return the same ID.
  std::optional<ScriptPromiseTester> second_tester;
  ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div,
                                      second_tester);
  ASSERT_TRUE(second_tester);
  second_tester->WaitUntilSettled();
  EXPECT_TRUE(second_tester->IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  const String first_result =
      ToSubCaptureTarget(first_tester->Value())->GetId();
  ASSERT_FALSE(first_result.empty());

  const String second_result =
      ToSubCaptureTarget(second_tester->Value())->GetId();
  ASSERT_FALSE(second_result.empty());

  EXPECT_EQ(first_result, second_result);
}

TEST_P(ProduceSubCaptureTargetTest,
       DuplicateCallBeforePromiseResolutionSameWorld) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  dispatcher_host().SetNextId(type_,
                              String("983bf2ff-7410-416c-808a-78421cbd8fdc"));

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));

  v8::Local<v8::Promise> first_v8_promise;
  v8::Local<v8::Promise> second_v8_promise;
  std::optional<ScriptPromiseTester> first_tester;
  std::optional<ScriptPromiseTester> second_tester;

  if (type_ == SubCaptureTarget::Type::kCropTarget) {
    auto p1 = media_devices->ProduceCropTarget(scope.GetScriptState(), div,
                                               scope.GetExceptionState());
    first_v8_promise = p1.V8Promise();
    first_tester.emplace(scope.GetScriptState(), p1);

    auto p2 = media_devices->ProduceCropTarget(scope.GetScriptState(), div,
                                               scope.GetExceptionState());
    second_v8_promise = p2.V8Promise();
    second_tester.emplace(scope.GetScriptState(), p2);
  } else {
    auto p1 = media_devices->ProduceRestrictionTarget(
        scope.GetScriptState(), div, scope.GetExceptionState());
    first_v8_promise = p1.V8Promise();
    first_tester.emplace(scope.GetScriptState(), p1);

    auto p2 = media_devices->ProduceRestrictionTarget(
        scope.GetScriptState(), div, scope.GetExceptionState());
    second_v8_promise = p2.V8Promise();
    second_tester.emplace(scope.GetScriptState(), p2);
  }

  // The two promises must be distinct objects.
  EXPECT_FALSE(first_v8_promise.IsEmpty());
  EXPECT_FALSE(second_v8_promise.IsEmpty());
  EXPECT_NE(first_v8_promise, second_v8_promise);

  first_tester->WaitUntilSettled();
  second_tester->WaitUntilSettled();

  EXPECT_TRUE(first_tester->IsFulfilled());
  EXPECT_TRUE(second_tester->IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  const String first_result =
      ToSubCaptureTarget(first_tester->Value())->GetId();
  const String second_result =
      ToSubCaptureTarget(second_tester->Value())->GetId();

  EXPECT_EQ(first_result, "983bf2ff-7410-416c-808a-78421cbd8fdc");
  EXPECT_EQ(first_result, second_result);
}

TEST_P(ProduceSubCaptureTargetTest,
       DuplicateCallBeforePromiseResolutionDifferentWorlds) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  dispatcher_host().SetNextId(type_,
                              String("983bf2ff-7410-416c-808a-78421cbd8fdc"));

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));

  constexpr int32_t kIsolatedWorldId = 1;
  DOMWrapperWorld* isolated_world = DOMWrapperWorld::EnsureIsolatedWorld(
      scope.GetIsolate(), kIsolatedWorldId);
  scope.GetFrame().GetWindowProxy(*isolated_world);  // Force initialization.
  ScriptState* isolated_script_state =
      ToScriptState(&scope.GetFrame(), *isolated_world);
  ASSERT_TRUE(isolated_script_state);

  // First call from the isolated world.
  std::optional<ScriptPromiseTester> isolated_tester;
  {
    ScriptState::Scope isolated_scope(isolated_script_state);
    DummyExceptionStateForTesting exception_state;
    if (type_ == SubCaptureTarget::Type::kCropTarget) {
      auto p = media_devices->ProduceCropTarget(isolated_script_state, div,
                                                exception_state);
      EXPECT_FALSE(p.IsEmpty());
      EXPECT_EQ(p.V8Promise()->GetCreationContextChecked(),
                isolated_script_state->GetContext());
      isolated_tester.emplace(isolated_script_state, p);
    } else {
      auto p = media_devices->ProduceRestrictionTarget(isolated_script_state,
                                                       div, exception_state);
      EXPECT_FALSE(p.IsEmpty());
      EXPECT_EQ(p.V8Promise()->GetCreationContextChecked(),
                isolated_script_state->GetContext());
      isolated_tester.emplace(isolated_script_state, p);
    }
    EXPECT_FALSE(exception_state.HadException());
  }
  ASSERT_TRUE(isolated_tester);

  // Second call from the main world before the first promise is resolved.
  std::optional<ScriptPromiseTester> main_tester;
  if (type_ == SubCaptureTarget::Type::kCropTarget) {
    auto p = media_devices->ProduceCropTarget(scope.GetScriptState(), div,
                                              scope.GetExceptionState());
    EXPECT_FALSE(p.IsEmpty());
    EXPECT_EQ(p.V8Promise()->GetCreationContextChecked(), scope.GetContext());
    main_tester.emplace(scope.GetScriptState(), p);
  } else {
    auto p = media_devices->ProduceRestrictionTarget(
        scope.GetScriptState(), div, scope.GetExceptionState());
    EXPECT_FALSE(p.IsEmpty());
    EXPECT_EQ(p.V8Promise()->GetCreationContextChecked(), scope.GetContext());
    main_tester.emplace(scope.GetScriptState(), p);
  }
  ASSERT_TRUE(main_tester);

  isolated_tester->WaitUntilSettled();
  main_tester->WaitUntilSettled();

  EXPECT_TRUE(isolated_tester->IsFulfilled());
  EXPECT_TRUE(main_tester->IsFulfilled());
  String isolated_result;
  {
    ScriptState::Scope isolated_scope(isolated_script_state);
    isolated_result = ToSubCaptureTarget(isolated_tester->Value())->GetId();
  }
  const String main_result = ToSubCaptureTarget(main_tester->Value())->GetId();

  EXPECT_EQ(isolated_result, "983bf2ff-7410-416c-808a-78421cbd8fdc");
  EXPECT_EQ(isolated_result, main_result);

  isolated_world->Dispose();
}

TEST_P(ProduceSubCaptureTargetTest, CorrectTokenClassInstantiated) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));
  dispatcher_host().SetNextId(
      type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));

  std::optional<ScriptPromiseTester> tester;
  ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div, tester);
  ASSERT_TRUE(tester);
  tester->WaitUntilSettled();
  ASSERT_TRUE(tester->IsFulfilled());
  ASSERT_FALSE(scope.GetExceptionState().HadException());

  // Type instantiated if and only if it's the expected type.
  const blink::ScriptValue value = tester->Value();
  EXPECT_EQ(!!V8CropTarget::ToWrappable(value.GetIsolate(), value.V8Value()),
            type_ == SubCaptureTarget::Type::kCropTarget);
  EXPECT_EQ(
      !!V8RestrictionTarget::ToWrappable(value.GetIsolate(), value.V8Value()),
      type_ == SubCaptureTarget::Type::kRestrictionTarget);
}

TEST_P(ProduceSubCaptureTargetTest, IdStringFormat) {
  V8TestingScope scope;
  auto* media_devices = GetMediaDevices(*GetDocument().domWindow());
  ASSERT_TRUE(media_devices);

  SetBodyContent(R"HTML(
    <div id='test-div'></div>
  )HTML");

  Document& document = GetDocument();
  Element* const div = document.getElementById(AtomicString("test-div"));
  dispatcher_host().SetNextId(
      type_, String(base::Uuid::GenerateRandomV4().AsLowercaseString()));
  std::optional<ScriptPromiseTester> tester;
  ProduceSubCaptureTargetAndGetTester(scope, type_, media_devices, div, tester);
  ASSERT_TRUE(tester);
  tester->WaitUntilSettled();
  EXPECT_TRUE(tester->IsFulfilled());
  EXPECT_FALSE(scope.GetExceptionState().HadException());

  const SubCaptureTarget* const target = ToSubCaptureTarget(tester->Value());
  const String& id = target->GetId();
  EXPECT_TRUE(id.ContainsOnlyAsciiOrEmpty());
  EXPECT_TRUE(base::Uuid::ParseLowercase(id.Ascii()).is_valid());
}

class MediaDevicesSimTest : public SimTest {};

TEST_F(MediaDevicesSimTest, SetPreferredSinkIdRejectsInIframe) {
  SimRequest main_resource("https://example.com", "text/html");
  LoadURL("https://example.com");
  main_resource.Complete(R"(
    <iframe id="test-iframe" src="about:blank"></iframe>
  )");

  auto* iframe = To<HTMLIFrameElement>(
      GetDocument().getElementById(AtomicString("test-iframe")));
  ASSERT_TRUE(iframe);
  auto* child_frame = To<LocalFrame>(iframe->ContentFrame());
  ASSERT_TRUE(child_frame);

  LocalDOMWindow* child_window = child_frame->DomWindow();
  ASSERT_TRUE(child_window);
  ASSERT_FALSE(child_frame->IsOutermostMainFrame());

  ScriptState* child_script_state = ToScriptStateForMainWorld(child_frame);
  ScriptState::Scope child_scope(child_script_state);

  MediaDevices* child_media_devices =
      MakeGarbageCollected<MediaDevices>(*child_window->navigator());

  DummyExceptionStateForTesting exception_state;
  child_media_devices->setPreferredSinkId(child_script_state, kValidSinkId,
                                          exception_state);

  EXPECT_TRUE(exception_state.HadException());
  EXPECT_EQ(exception_state.Code(),
            ToExceptionCode(DOMExceptionCode::kInvalidStateError));
  EXPECT_EQ(exception_state.Message(),
            "Can only be called from the top-level document.");
}

// TODO(crbug.com/1418194): Add tests after MediaDevicesDispatcherHost
// has been updated.

}  // namespace blink
