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

#include "base/command_line.h"
#include "base/strings/stringprintf.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/video_capture_service.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "media/base/media_switches.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/video_capture/public/cpp/mock_producer.h"
#include "services/video_capture/public/mojom/devices_changed_observer.mojom.h"
#include "services/video_capture/public/mojom/producer.mojom.h"
#include "services/video_capture/public/mojom/video_capture_service.mojom.h"
#include "services/video_capture/public/mojom/video_source_provider.mojom.h"
#include "services/video_capture/public/mojom/virtual_device.mojom.h"

namespace content {

namespace {

enum class VirtualDeviceType { kSharedMemory, kTexture };

struct TestParams {
  VirtualDeviceType device_type;
};

static const char kVideoCaptureHtmlFile[] = "/media/video_capture_test.html";
static const char kEnumerateVideoCaptureDevicesAndVerify[] =
    "enumerateVideoCaptureDevicesAndVerifyCount(%d)";
static const char kRegisterForDeviceChangeEvent[] =
    "registerForDeviceChangeEvent()";
static const char kWaitForDeviceChangeEvent[] = "waitForDeviceChangeEvent()";
static const char kResetHasReceivedChangedEventFlag[] =
    "resetHasReceivedChangedEventFlag()";

}  // anonymous namespace

// Integration test that obtains a connection to the video capture service via
// the Browser process' service manager. It then registers and unregisters
// virtual devices at the service and checks in JavaScript that the list of
// enumerated devices changes correspondingly.
class WebRtcVideoCaptureServiceEnumerationBrowserTest
    : public ContentBrowserTest,
      public testing::WithParamInterface<TestParams>,
      public video_capture::mojom::DevicesChangedObserver {
 public:
  WebRtcVideoCaptureServiceEnumerationBrowserTest() = default;

  WebRtcVideoCaptureServiceEnumerationBrowserTest(
      const WebRtcVideoCaptureServiceEnumerationBrowserTest&) = delete;
  WebRtcVideoCaptureServiceEnumerationBrowserTest& operator=(
      const WebRtcVideoCaptureServiceEnumerationBrowserTest&) = delete;

  ~WebRtcVideoCaptureServiceEnumerationBrowserTest() override {}

  void ConnectToService() {
    mojo::PendingRemote<video_capture::mojom::DevicesChangedObserver> observer;
    devices_changed_observer_receiver_.Bind(
        observer.InitWithNewPipeAndPassReceiver());
    GetVideoCaptureService().ConnectToVideoSourceProvider(
        video_source_provider_.BindNewPipeAndPassReceiver());
    video_source_provider_->RegisterVirtualDevicesChangedObserver(
        std::move(observer),
        false /*raise_event_if_virtual_devices_already_present*/);
  }

  void AddVirtualDevice(const std::string& device_id) {
    media::VideoCaptureDeviceInfo info;
    info.descriptor.device_id = device_id;
    info.descriptor.set_display_name("Virtual Chromium " + device_id);
    info.descriptor.capture_api = media::VideoCaptureApi::VIRTUAL_DEVICE;

    base::RunLoop wait_loop;
    closure_to_be_called_on_devices_changed_ = wait_loop.QuitClosure();
    switch (GetParam().device_type) {
      case VirtualDeviceType::kSharedMemory: {
        mojo::PendingRemote<video_capture::mojom::SharedMemoryVirtualDevice>
            virtual_device;
        mojo::PendingRemote<video_capture::mojom::Producer> producer;
        auto mock_producer = std::make_unique<video_capture::MockProducer>(
            producer.InitWithNewPipeAndPassReceiver());
        video_source_provider_->AddSharedMemoryVirtualDevice(
            info, std::move(producer),
            virtual_device.InitWithNewPipeAndPassReceiver());
        shared_memory_devices_by_id_.insert(std::make_pair(
            device_id, std::make_pair(std::move(virtual_device),
                                      std::move(mock_producer))));
        break;
      }
      case VirtualDeviceType::kTexture: {
        mojo::PendingRemote<video_capture::mojom::TextureVirtualDevice>
            virtual_device;
        video_source_provider_->AddTextureVirtualDevice(
            info, virtual_device.InitWithNewPipeAndPassReceiver());
        texture_devices_by_id_.insert(
            std::make_pair(device_id, std::move(virtual_device)));
        break;
      }
    }
    // Wait for confirmation from the service.
    wait_loop.Run();
  }

  void RemoveVirtualDevice(const std::string& device_id) {
    base::RunLoop wait_loop;
    closure_to_be_called_on_devices_changed_ = wait_loop.QuitClosure();
    switch (GetParam().device_type) {
      case VirtualDeviceType::kSharedMemory:
        shared_memory_devices_by_id_.erase(device_id);
        break;
      case VirtualDeviceType::kTexture:
        texture_devices_by_id_.erase(device_id);
        break;
    }

    // Wait for confirmation from the service.
    wait_loop.Run();
  }

  void DisconnectFromService() { video_source_provider_.reset(); }

  void EnumerateDevicesInRendererAndVerifyDeviceCount(
      int expected_device_count) {
    const std::string javascript_to_execute = base::StringPrintf(
        kEnumerateVideoCaptureDevicesAndVerify, expected_device_count);
    ASSERT_TRUE(ExecJs(shell(), javascript_to_execute));
  }

  void RegisterForDeviceChangeEventInRenderer() {
    ASSERT_TRUE(ExecJs(shell(), kRegisterForDeviceChangeEvent));
  }

  void WaitForDeviceChangeEventInRenderer() {
    ASSERT_TRUE(ExecJs(shell(), kWaitForDeviceChangeEvent));
  }

  void ResetHasReceivedChangedEventFlag() {
    ASSERT_TRUE(ExecJs(shell(), kResetHasReceivedChangedEventFlag));
  }

  // Implementation of video_capture::mojom::DevicesChangedObserver:
  void OnDevicesChanged() override {
    if (closure_to_be_called_on_devices_changed_) {
      std::move(closure_to_be_called_on_devices_changed_).Run();
    }
  }

 protected:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    // Note: We are not planning to actually use any fake device, but we want
    // to avoid enumerating or otherwise calling into real capture devices.
    command_line->RemoveSwitch(switches::kUseFakeDeviceForMediaStream);
    command_line->AppendSwitchASCII(switches::kUseFakeDeviceForMediaStream,
                                    "device-count=0");
    command_line->AppendSwitch(switches::kUseFakeUIForMediaStream);
  }

  void Initialize() {
    DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    embedded_test_server()->StartAcceptingConnections();

    EXPECT_TRUE(NavigateToURL(
        shell(), GURL(embedded_test_server()->GetURL(kVideoCaptureHtmlFile))));
  }

  std::map<std::string,
           mojo::PendingRemote<video_capture::mojom::TextureVirtualDevice>>
      texture_devices_by_id_;
  std::map<std::string,
           std::pair<mojo::PendingRemote<
                         video_capture::mojom::SharedMemoryVirtualDevice>,
                     std::unique_ptr<video_capture::MockProducer>>>
      shared_memory_devices_by_id_;

 private:
  mojo::Receiver<video_capture::mojom::DevicesChangedObserver>
      devices_changed_observer_receiver_{this};
  mojo::Remote<video_capture::mojom::VideoSourceProvider>
      video_source_provider_;
  base::OnceClosure closure_to_be_called_on_devices_changed_;
};

IN_PROC_BROWSER_TEST_P(WebRtcVideoCaptureServiceEnumerationBrowserTest,
                       SingleAddedVirtualDeviceGetsEnumerated) {
  Initialize();
  ConnectToService();

  // Exercise
  AddVirtualDevice("virtual-chromium-test");
  EnumerateDevicesInRendererAndVerifyDeviceCount(1);

  // Tear down
  RemoveVirtualDevice("virtual-chromium-test");
  DisconnectFromService();
}

// TODO(https://crbug.com/352672009): Flaky on Mac.
// TODO(https://crbug.com/352092989): Flaky on Windows.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_RemoveVirtualDeviceAfterItHasBeenEnumerated \
  DISABLED_RemoveVirtualDeviceAfterItHasBeenEnumerated
#else
#define MAYBE_RemoveVirtualDeviceAfterItHasBeenEnumerated \
  RemoveVirtualDeviceAfterItHasBeenEnumerated
#endif
IN_PROC_BROWSER_TEST_P(WebRtcVideoCaptureServiceEnumerationBrowserTest,
                       MAYBE_RemoveVirtualDeviceAfterItHasBeenEnumerated) {
  Initialize();
  ConnectToService();

  AddVirtualDevice("virtual-chromium-test-1");
  AddVirtualDevice("virtual-chromium-test-2");
  EnumerateDevicesInRendererAndVerifyDeviceCount(2);
  RemoveVirtualDevice("virtual-chromium-test-1");
  EnumerateDevicesInRendererAndVerifyDeviceCount(1);
  RemoveVirtualDevice("virtual-chromium-test-2");
  EnumerateDevicesInRendererAndVerifyDeviceCount(0);

  // Tear down
  DisconnectFromService();
}

// The mediadevices.ondevicechange event is currently not supported on Android.
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_AddingAndRemovingVirtualDeviceTriggersMediaElementOnDeviceChange \
  DISABLED_AddingAndRemovingVirtualDeviceTriggersMediaElementOnDeviceChange
#else
#define MAYBE_AddingAndRemovingVirtualDeviceTriggersMediaElementOnDeviceChange \
  AddingAndRemovingVirtualDeviceTriggersMediaElementOnDeviceChange
#endif

IN_PROC_BROWSER_TEST_P(
    WebRtcVideoCaptureServiceEnumerationBrowserTest,
    MAYBE_AddingAndRemovingVirtualDeviceTriggersMediaElementOnDeviceChange) {
  Initialize();
  ConnectToService();
  RegisterForDeviceChangeEventInRenderer();
  // Waiting for enumeration ensures that the browser everything is primed for
  // device change events.
  EnumerateDevicesInRendererAndVerifyDeviceCount(0);

  // Exercise
  AddVirtualDevice("virtual-chromium-test");

  WaitForDeviceChangeEventInRenderer();
  ResetHasReceivedChangedEventFlag();

  RemoveVirtualDevice("virtual-chromium-test");
  WaitForDeviceChangeEventInRenderer();

  // Tear down
  DisconnectFromService();
}

INSTANTIATE_TEST_SUITE_P(
    All,
    WebRtcVideoCaptureServiceEnumerationBrowserTest,
    ::testing::Values(TestParams{VirtualDeviceType::kSharedMemory},
                      TestParams{VirtualDeviceType::kTexture}));

}  // namespace content
