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

#include "content/browser/renderer_host/media/service_video_capture_provider.h"

#include "base/run_loop.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_task_environment.h"
#include "base/threading/thread.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "services/video_capture/public/interfaces/device_factory.mojom.h"
#include "services/video_capture/public/interfaces/device_factory_provider.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::Mock;
using testing::Invoke;
using testing::_;

namespace content {

class MockServiceConnector
    : public ServiceVideoCaptureProvider::ServiceConnector {
 public:
  MockServiceConnector() {}

  MOCK_METHOD1(BindFactoryProvider,
               void(video_capture::mojom::DeviceFactoryProviderPtr* provider));
};

class MockDeviceFactoryProvider
    : public video_capture::mojom::DeviceFactoryProvider {
 public:
  void ConnectToDeviceFactory(
      video_capture::mojom::DeviceFactoryRequest request) override {
    DoConnectToDeviceFactory(request);
  }

  MOCK_METHOD1(SetShutdownDelayInSeconds, void(float seconds));
  MOCK_METHOD1(DoConnectToDeviceFactory,
               void(video_capture::mojom::DeviceFactoryRequest& request));
};

class MockDeviceFactory : public video_capture::mojom::DeviceFactory {
 public:
  void GetDeviceInfos(GetDeviceInfosCallback callback) override {
    DoGetDeviceInfos(callback);
  }
  void CreateDevice(const std::string& device_id,
                    video_capture::mojom::DeviceRequest device_request,
                    CreateDeviceCallback callback) override {
    DoCreateDevice(device_id, &device_request, callback);
  }

  MOCK_METHOD1(DoGetDeviceInfos, void(GetDeviceInfosCallback& callback));
  MOCK_METHOD3(DoCreateDevice,
               void(const std::string& device_id,
                    video_capture::mojom::DeviceRequest* device_request,
                    CreateDeviceCallback& callback));
};

class ServiceVideoCaptureProviderTest : public testing::Test {
 public:
  ServiceVideoCaptureProviderTest()
      : factory_provider_binding_(&mock_device_factory_provider_),
        device_factory_binding_(&mock_device_factory_) {}
  ~ServiceVideoCaptureProviderTest() override {}

 protected:
  void SetUp() override {
    auto mock_service_connector = base::MakeUnique<MockServiceConnector>();
    mock_service_connector_ = mock_service_connector.get();
    provider_ = base::MakeUnique<ServiceVideoCaptureProvider>(
        std::move(mock_service_connector));

    ON_CALL(*mock_service_connector_, BindFactoryProvider(_))
        .WillByDefault(
            Invoke([this](video_capture::mojom::DeviceFactoryProviderPtr*
                              device_factory_provider) {
              if (factory_provider_binding_.is_bound())
                factory_provider_binding_.Close();
              factory_provider_binding_.Bind(
                  mojo::MakeRequest(device_factory_provider));
            }));
    ON_CALL(mock_device_factory_provider_, DoConnectToDeviceFactory(_))
        .WillByDefault(
            Invoke([this](video_capture::mojom::DeviceFactoryRequest& request) {
              if (device_factory_binding_.is_bound())
                device_factory_binding_.Close();
              device_factory_binding_.Bind(std::move(request));
              wait_for_connection_to_service_.Quit();
            }));
  }

  void TearDown() override {}

  base::test::ScopedTaskEnvironment scoped_task_environment_;
  MockServiceConnector* mock_service_connector_;
  MockDeviceFactoryProvider mock_device_factory_provider_;
  mojo::Binding<video_capture::mojom::DeviceFactoryProvider>
      factory_provider_binding_;
  MockDeviceFactory mock_device_factory_;
  mojo::Binding<video_capture::mojom::DeviceFactory> device_factory_binding_;
  std::unique_ptr<ServiceVideoCaptureProvider> provider_;
  base::MockCallback<VideoCaptureProvider::GetDeviceInfosCallback> results_cb_;
  base::MockCallback<
      video_capture::mojom::DeviceFactory::GetDeviceInfosCallback>
      service_cb_;
  base::RunLoop wait_for_connection_to_service_;

 private:
  DISALLOW_COPY_AND_ASSIGN(ServiceVideoCaptureProviderTest);
};

// Tests that if connection to the service is lost during an outstanding call
// to GetDeviceInfos(), the callback passed into GetDeviceInfos() still gets
// invoked.
TEST_F(ServiceVideoCaptureProviderTest,
       GetDeviceInfosAsyncInvokesCallbackWhenLosingConnection) {
  base::RunLoop run_loop;

  video_capture::mojom::DeviceFactory::GetDeviceInfosCallback
      callback_to_be_called_by_service;
  base::RunLoop wait_for_call_to_arrive_at_service;
  EXPECT_CALL(mock_device_factory_, DoGetDeviceInfos(_))
      .WillOnce(Invoke(
          [&callback_to_be_called_by_service,
           &wait_for_call_to_arrive_at_service](
              video_capture::mojom::DeviceFactory::GetDeviceInfosCallback&
                  callback) {
            // Hold on to the callback so we can drop it later.
            callback_to_be_called_by_service = std::move(callback);
            wait_for_call_to_arrive_at_service.Quit();
          }));
  base::RunLoop wait_for_callback_from_service;
  EXPECT_CALL(results_cb_, Run(_))
      .WillOnce(Invoke(
          [&wait_for_callback_from_service](
              const std::vector<media::VideoCaptureDeviceInfo>& results) {
            EXPECT_EQ(0u, results.size());
            wait_for_callback_from_service.Quit();
          }));

  // Exercise
  provider_->GetDeviceInfosAsync(results_cb_.Get());
  wait_for_call_to_arrive_at_service.Run();

  // Simulate that the service goes down by cutting the connections.
  device_factory_binding_.Close();
  factory_provider_binding_.Close();

  wait_for_callback_from_service.Run();
}

// Tests that |ServiceVideoCaptureProvider| closes the connection to the service
// after successfully processing a single request to GetDeviceInfos().
TEST_F(ServiceVideoCaptureProviderTest,
       ClosesServiceConnectionAfterGetDeviceInfos) {
  // Setup part 1
  video_capture::mojom::DeviceFactory::GetDeviceInfosCallback
      callback_to_be_called_by_service;
  base::RunLoop wait_for_call_to_arrive_at_service;
  EXPECT_CALL(mock_device_factory_, DoGetDeviceInfos(_))
      .WillOnce(Invoke(
          [&callback_to_be_called_by_service,
           &wait_for_call_to_arrive_at_service](
              video_capture::mojom::DeviceFactory::GetDeviceInfosCallback&
                  callback) {
            // Hold on to the callback so we can drop it later.
            callback_to_be_called_by_service = std::move(callback);
            wait_for_call_to_arrive_at_service.Quit();
          }));

  // Exercise part 1: Make request to the service
  provider_->GetDeviceInfosAsync(results_cb_.Get());
  wait_for_call_to_arrive_at_service.Run();

  // Setup part 2: Now that the connection to the service is established, we can
  // listen for disconnects.
  base::RunLoop wait_for_connection_to_device_factory_to_close;
  base::RunLoop wait_for_connection_to_device_factory_provider_to_close;
  device_factory_binding_.set_connection_error_handler(
      base::BindOnce([](base::RunLoop* run_loop) { run_loop->Quit(); },
                     &wait_for_connection_to_device_factory_to_close));
  factory_provider_binding_.set_connection_error_handler(
      base::BindOnce([](base::RunLoop* run_loop) { run_loop->Quit(); },
                     &wait_for_connection_to_device_factory_provider_to_close));

  // Exercise part 2: The service responds
  std::vector<media::VideoCaptureDeviceInfo> arbitrarily_empty_results;
  base::ResetAndReturn(&callback_to_be_called_by_service)
      .Run(arbitrarily_empty_results);

  // Verification: Expect |provider_| to close the connection to the service.
  wait_for_connection_to_device_factory_to_close.Run();
  wait_for_connection_to_device_factory_provider_to_close.Run();
}

// Tests that |ServiceVideoCaptureProvider| does not close the connection to the
// service while at least one previously handed out VideoCaptureDeviceLauncher
// instance is still alive. Then confirms that it closes the connection as soon
// as the last VideoCaptureDeviceLauncher instance is released.
TEST_F(ServiceVideoCaptureProviderTest,
       KeepsServiceConnectionWhileDeviceLauncherAlive) {
  ON_CALL(mock_device_factory_, DoGetDeviceInfos(_))
      .WillByDefault(Invoke([](video_capture::mojom::DeviceFactory::
                                   GetDeviceInfosCallback& callback) {
        std::vector<media::VideoCaptureDeviceInfo> arbitrarily_empty_results;
        base::ResetAndReturn(&callback).Run(arbitrarily_empty_results);
      }));

  // Exercise part 1: Create a device launcher and hold on to it.
  auto device_launcher_1 = provider_->CreateDeviceLauncher();
  wait_for_connection_to_service_.Run();

  // Monitor if connection gets closed
  bool connection_has_been_closed = false;
  device_factory_binding_.set_connection_error_handler(base::BindOnce(
      [](bool* connection_has_been_closed) {
        *connection_has_been_closed = true;
      },
      &connection_has_been_closed));
  factory_provider_binding_.set_connection_error_handler(base::BindOnce(
      [](bool* connection_has_been_closed) {
        *connection_has_been_closed = true;
      },
      &connection_has_been_closed));

  // Exercise part 2: Make a few GetDeviceInfosAsync requests
  base::RunLoop wait_for_get_device_infos_response_1;
  base::RunLoop wait_for_get_device_infos_response_2;
  provider_->GetDeviceInfosAsync(base::BindOnce(
      [](base::RunLoop* run_loop,
         const std::vector<media::VideoCaptureDeviceInfo>&) {
        run_loop->Quit();
      },
      &wait_for_get_device_infos_response_1));
  provider_->GetDeviceInfosAsync(base::BindOnce(
      [](base::RunLoop* run_loop,
         const std::vector<media::VideoCaptureDeviceInfo>&) {
        run_loop->Quit();
      },
      &wait_for_get_device_infos_response_2));
  wait_for_get_device_infos_response_1.Run();
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_FALSE(connection_has_been_closed);
  wait_for_get_device_infos_response_2.Run();
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_FALSE(connection_has_been_closed);

  // Exercise part 3: Create and release another device launcher
  auto device_launcher_2 = provider_->CreateDeviceLauncher();
  device_launcher_2.reset();
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_FALSE(connection_has_been_closed);

  // Exercise part 3: Release the initial device launcher.
  device_launcher_1.reset();
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_TRUE(connection_has_been_closed);
}

// Tests that |ServiceVideoCaptureProvider| does not close the connection to the
// service while at least one answer to a GetDeviceInfos() request is still
// pending. Confirms that it closes the connection as soon as the last pending
// request is answered.
TEST_F(ServiceVideoCaptureProviderTest,
       DoesNotCloseServiceConnectionWhileGetDeviceInfoResponsePending) {
  // When GetDeviceInfos gets called, hold on to the callbacks, but do not
  // yet invoke them.
  std::vector<video_capture::mojom::DeviceFactory::GetDeviceInfosCallback>
      callbacks_to_be_called_by_service;
  ON_CALL(mock_device_factory_, DoGetDeviceInfos(_))
      .WillByDefault(Invoke(
          [&callbacks_to_be_called_by_service](
              video_capture::mojom::DeviceFactory::GetDeviceInfosCallback&
                  callback) {
            callbacks_to_be_called_by_service.push_back(std::move(callback));
          }));

  // Make initial call to GetDeviceInfosAsync(). The service does not yet
  // respond.
  provider_->GetDeviceInfosAsync(
      base::BindOnce([](const std::vector<media::VideoCaptureDeviceInfo>&) {}));
  // Make an additional call to GetDeviceInfosAsync().
  provider_->GetDeviceInfosAsync(
      base::BindOnce([](const std::vector<media::VideoCaptureDeviceInfo>&) {}));
  {
    base::RunLoop give_mojo_chance_to_process;
    give_mojo_chance_to_process.RunUntilIdle();
  }
  ASSERT_EQ(2u, callbacks_to_be_called_by_service.size());

  // Monitor if connection gets closed
  bool connection_has_been_closed = false;
  device_factory_binding_.set_connection_error_handler(base::BindOnce(
      [](bool* connection_has_been_closed) {
        *connection_has_been_closed = true;
      },
      &connection_has_been_closed));
  factory_provider_binding_.set_connection_error_handler(base::BindOnce(
      [](bool* connection_has_been_closed) {
        *connection_has_been_closed = true;
      },
      &connection_has_been_closed));

  // The service now responds to the first request.
  std::vector<media::VideoCaptureDeviceInfo> arbitrarily_empty_results;
  base::ResetAndReturn(&callbacks_to_be_called_by_service[0])
      .Run(arbitrarily_empty_results);
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_FALSE(connection_has_been_closed);

  // Create and release a device launcher
  auto device_launcher = provider_->CreateDeviceLauncher();
  device_launcher.reset();
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_FALSE(connection_has_been_closed);

  // The service now responds to the second request.
  base::ResetAndReturn(&callbacks_to_be_called_by_service[1])
      .Run(arbitrarily_empty_results);
  {
    base::RunLoop give_provider_chance_to_disconnect;
    give_provider_chance_to_disconnect.RunUntilIdle();
  }
  ASSERT_TRUE(connection_has_been_closed);
}

}  // namespace content
