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

#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"

#include <memory>

#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/media/media_access_handler.h"
#include "chrome/browser/media/prefs/capture_device_ranking.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/mediastream/media_stream_request.h"

using testing::_;

namespace {

class MockMediaAccessHandler : public MediaAccessHandler {
 public:
  explicit MockMediaAccessHandler(
      const blink::mojom::MediaStreamType supported_type)
      : supported_type_(supported_type) {}

  bool SupportsStreamType(content::RenderFrameHost* render_frame_host,
                          const blink::mojom::MediaStreamType type,
                          const extensions::Extension* extension) override {
    return supported_type_ == type;
  }

  MOCK_METHOD4(CheckMediaAccessPermission,
               bool(content::RenderFrameHost* render_frame_host,
                    const url::Origin& security_origin,
                    blink::mojom::MediaStreamType type,
                    const extensions::Extension* extension));
  MOCK_METHOD4(HandleRequest,
               void(content::WebContents* web_contents,
                    const content::MediaStreamRequest& request,
                    content::MediaResponseCallback callback,
                    const extensions::Extension* extension));
  MOCK_METHOD4(UpdateVideoScreenCaptureStatus,
               void(int render_process_id,
                    int render_frame_id,
                    int page_request_id,
                    bool is_secure));

  const blink::mojom::MediaStreamType supported_type_;
};

blink::MediaStreamDevices CreateFakeDevices(
    blink::mojom::MediaStreamType type) {
  blink::MediaStreamDevices devices;
  devices.reserve(3);
  for (size_t i = 0; i < devices.capacity(); ++i) {
    devices.emplace_back(type, "id_" + base::NumberToString(i),
                         "name " + base::NumberToString(i));
  }
  return devices;
}

std::vector<std::string> GetIds(const blink::MediaStreamDevices& devices,
                                size_t start_index) {
  CHECK_LT(start_index, devices.size());
  std::vector<std::string> device_ids;
  for (auto it = devices.begin() + start_index; it != devices.end(); ++it) {
    device_ids.push_back(it->id);
  }
  return device_ids;
}

}  // namespace

class MediaCaptureDevicesDispatcherTest
    : public ChromeRenderViewHostTestHarness {
 public:
  MediaCaptureDevicesDispatcherTest() = default;
  ~MediaCaptureDevicesDispatcherTest() override = default;

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    dispatcher_ = MediaCaptureDevicesDispatcher::GetInstance();
  }

  std::vector<std::unique_ptr<MediaAccessHandler>>& media_access_handlers() {
    return dispatcher_->media_access_handlers_;
  }

  void UpdateVideoScreenCaptureStatus(
      const blink::mojom::MediaStreamType type) {
    dispatcher_->UpdateVideoScreenCaptureStatus(0, 0, 0, type, false);
  }

  void UpdateAudioDevicePreferenceRanking(
      const typename blink::MediaStreamDevices::const_iterator
          preferred_device_iter) {
    CHECK(profile()->GetPrefs());
    media_prefs::UpdateAudioDevicePreferenceRanking(
        *profile()->GetPrefs(), preferred_device_iter,
        dispatcher_->GetAudioCaptureDevices());
  }

  void UpdateVideoDevicePreferenceRanking(
      const typename blink::MediaStreamDevices::const_iterator
          preferred_device_iter) {
    CHECK(profile()->GetPrefs());
    media_prefs::UpdateVideoDevicePreferenceRanking(
        *profile()->GetPrefs(), preferred_device_iter,
        dispatcher_->GetVideoCaptureDevices());
  }

 protected:
  raw_ptr<MediaCaptureDevicesDispatcher> dispatcher_;
};

TEST_F(MediaCaptureDevicesDispatcherTest,
       DISABLED_LoopsAllMediaAccessHandlers) {
  media_access_handlers().clear();

  // Add two handlers.
  auto stream_type1 = blink::mojom::MediaStreamType::DISPLAY_VIDEO_CAPTURE;
  auto mock1 = std::make_unique<MockMediaAccessHandler>(stream_type1);
  MockMediaAccessHandler* handler1 = mock1.get();
  media_access_handlers().push_back(std::move(mock1));
  auto stream_type2 = blink::mojom::MediaStreamType::GUM_DESKTOP_VIDEO_CAPTURE;
  auto mock2 = std::make_unique<MockMediaAccessHandler>(stream_type2);
  MockMediaAccessHandler* handler2 = mock2.get();
  media_access_handlers().push_back(std::move(mock2));

  // Expect both to be called.
  EXPECT_CALL(*handler1, UpdateVideoScreenCaptureStatus(_, _, _, _));
  UpdateVideoScreenCaptureStatus(stream_type1);
  EXPECT_CALL(*handler2, UpdateVideoScreenCaptureStatus(_, _, _, _));
  UpdateVideoScreenCaptureStatus(stream_type2);
}

TEST_F(MediaCaptureDevicesDispatcherTest,
       GetPreferredAudioDeviceForBrowserContext) {
  const auto kFakeAudioDevices =
      CreateFakeDevices(blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
  dispatcher_->SetTestAudioCaptureDevices(kFakeAudioDevices);
  UpdateAudioDevicePreferenceRanking(
      dispatcher_->GetAudioCaptureDevices().end() - 1);
  UpdateAudioDevicePreferenceRanking(
      dispatcher_->GetAudioCaptureDevices().begin());
  // Ranking at this point is [device_0, device_2, device_1].

  // Exclude the first device from the eligible list to exercise filtering.
  const auto eligible_device_ids = GetIds(kFakeAudioDevices, 1);
  const auto preferred_device =
      dispatcher_->GetPreferredAudioDeviceForBrowserContext(
          browser_context(), eligible_device_ids);
  ASSERT_TRUE(preferred_device->IsSameDevice(kFakeAudioDevices.back()));
}

TEST_F(
    MediaCaptureDevicesDispatcherTest,
    GetPreferredAudioDeviceForBrowserContext_EmptyEligibleDevicesReturnsDefault) {
  const auto kFakeAudioDevices =
      CreateFakeDevices(blink::mojom::MediaStreamType::DEVICE_AUDIO_CAPTURE);
  dispatcher_->SetTestAudioCaptureDevices(kFakeAudioDevices);
  UpdateAudioDevicePreferenceRanking(
      dispatcher_->GetAudioCaptureDevices().end() - 1);
  UpdateAudioDevicePreferenceRanking(
      dispatcher_->GetAudioCaptureDevices().begin());
  // Ranking at this point is [device_0, device_2, device_1].

  // Pass empty eligible devices to disable filtering.
  const auto preferred_device =
      dispatcher_->GetPreferredAudioDeviceForBrowserContext(browser_context(),
                                                            {});
  // Preferred device should be the most preferred device in the ranking without
  // any filtering. That device is device_0.
  ASSERT_TRUE(preferred_device->IsSameDevice(kFakeAudioDevices.front()));
}

TEST_F(MediaCaptureDevicesDispatcherTest,
       GetPreferredVideoDeviceForBrowserContext) {
  const auto kFakeVideoDevices =
      CreateFakeDevices(blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
  dispatcher_->SetTestVideoCaptureDevices(kFakeVideoDevices);
  UpdateVideoDevicePreferenceRanking(
      dispatcher_->GetVideoCaptureDevices().end() - 1);
  UpdateVideoDevicePreferenceRanking(
      dispatcher_->GetVideoCaptureDevices().begin());
  // Ranking at this point is [device_0, device_2, device_1].

  // Exclude the first device from the eligible list to exercise filtering.
  const auto eligible_device_ids = GetIds(kFakeVideoDevices, 1);
  const auto preferred_device =
      dispatcher_->GetPreferredVideoDeviceForBrowserContext(
          browser_context(), eligible_device_ids);
  ASSERT_TRUE(preferred_device->IsSameDevice(kFakeVideoDevices.back()));
}

TEST_F(
    MediaCaptureDevicesDispatcherTest,
    GetPreferredVideoDeviceForBrowserContext_EmptyEligibleDevicesReturnsDefault) {
  const auto kFakeVideoDevices =
      CreateFakeDevices(blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE);
  dispatcher_->SetTestVideoCaptureDevices(kFakeVideoDevices);
  UpdateVideoDevicePreferenceRanking(
      dispatcher_->GetVideoCaptureDevices().end() - 1);
  UpdateVideoDevicePreferenceRanking(
      dispatcher_->GetVideoCaptureDevices().begin());
  // Ranking at this point is [device_0, device_2, device_1].

  // Exclude the first device from the eligible list to exercise filtering.
  const auto preferred_device =
      dispatcher_->GetPreferredVideoDeviceForBrowserContext(browser_context(),
                                                            {});
  ASSERT_TRUE(preferred_device->IsSameDevice(kFakeVideoDevices.front()));
}
