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

#include <stdint.h>

#include <map>
#include <memory>
#include <string>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/location.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "content/browser/media/media_devices_util.h"
#include "content/browser/renderer_host/media/fake_video_capture_provider.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/browser/renderer_host/media/media_stream_ui_proxy.h"
#include "content/browser/renderer_host/media/video_capture_host.h"
#include "content/browser/renderer_host/media/video_capture_manager.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_content_browser_client.h"
#include "media/audio/audio_system_impl.h"
#include "media/audio/mock_audio_manager.h"
#include "media/audio/test_audio_thread.h"
#include "media/base/media_switches.h"
#include "media/capture/video_capture_types.h"
#include "mojo/public/cpp/bindings/receiver.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"

using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtMost;
using ::testing::DoAll;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::StrictMock;

namespace content {

namespace {

void VideoInputDevicesEnumerated(
    base::OnceClosure quit_closure,
    const MediaDeviceSaltAndOrigin& salt_and_origin,
    blink::WebMediaDeviceInfoArray* out,
    const MediaDeviceEnumeration& enumeration) {
  for (const auto& info : enumeration[static_cast<size_t>(
           blink::mojom::MediaDeviceType::kMediaVideoInput)]) {
    std::string device_id =
        GetHMACForRawMediaDeviceID(salt_and_origin, info.device_id);
    out->emplace_back(device_id, info.label, std::string());
  }
  std::move(quit_closure).Run();
}

// Id used to identify the capture session between renderer and
// video_capture_host. This is an arbitrary value.
const base::UnguessableToken& DeviceId() {
  static const base::UnguessableToken device_id(
      base::UnguessableToken::CreateForTesting(555, 555));
  return device_id;
}

}  // namespace

ACTION_P2(ExitMessageLoop, task_runner, quit_closure) {
  task_runner->PostTask(FROM_HERE, quit_closure);
}

class MockRenderFrameHostDelegate
    : public VideoCaptureHost::RenderFrameHostDelegate {
 public:
  MOCK_METHOD0(NotifyStreamAdded, void());
  MOCK_METHOD0(NotifyStreamRemoved, void());
  MOCK_CONST_METHOD0(render_frame_host_id, GlobalRenderFrameHostId());
};

// This is an integration test of VideoCaptureHost in conjunction with
// MediaStreamManager, VideoCaptureManager, VideoCaptureController, and
// VideoCaptureDevice.
class VideoCaptureTest : public testing::Test,
                         public media::mojom::VideoCaptureObserver {
 public:
  VideoCaptureTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP),
        task_runner_(base::SingleThreadTaskRunner::GetCurrentDefault()) {}

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

  ~VideoCaptureTest() override { audio_manager_->Shutdown(); }

  void SetUp() override {
    SetBrowserClientForTesting(&browser_client_);
    audio_manager_ = std::make_unique<media::MockAudioManager>(
        std::make_unique<media::TestAudioThread>());
    audio_system_ =
        std::make_unique<media::AudioSystemImpl>(audio_manager_.get());

    media_stream_manager_ = std::make_unique<MediaStreamManager>(
        audio_system_.get(), std::make_unique<FakeVideoCaptureProvider>());
    media_stream_manager_->UseFakeUIFactoryForTests(base::BindRepeating(
        &VideoCaptureTest::CreateFakeUI, base::Unretained(this)));

    // Create a Host and connect it to a simulated IPC channel.
    host_ = std::make_unique<VideoCaptureHost>(
        GlobalRenderFrameHostId(1, 1) /* render_frame_host_id */,
        media_stream_manager_.get());
    host_receiver_ =
        std::make_unique<mojo::Receiver<media::mojom::VideoCaptureHost>>(
            host_.get(), host_remote_.BindNewPipeAndPassReceiver());

    OpenSession();
  }

  void TearDown() override {
    Mock::VerifyAndClearExpectations(host_.get());
    EXPECT_TRUE(host_->controllers_.empty());

    CloseSession();

    host_receiver_.reset();
    host_.reset();
  }

  void OpenSession() {
    const GlobalRenderFrameHostId render_frame_host_id{1, 1};
    const int requester_id = 1;
    const int page_request_id = 1;
    const url::Origin security_origin =
        url::Origin::Create(GURL("http://test.com"));

    ASSERT_TRUE(opened_device_label_.empty());

    // Enumerate video devices.
    blink::WebMediaDeviceInfoArray video_devices;
    {
      base::RunLoop run_loop;
      MediaDevicesManager::BoolDeviceTypes devices_to_enumerate;
      devices_to_enumerate[static_cast<size_t>(
          blink::mojom::MediaDeviceType::kMediaVideoInput)] = true;
      base::test::TestFuture<const MediaDeviceSaltAndOrigin&> future;
      GetMediaDeviceSaltAndOrigin(render_frame_host_id, future.GetCallback());
      MediaDeviceSaltAndOrigin salt_and_origin = future.Get();
      media_stream_manager_->media_devices_manager()->EnumerateDevices(
          devices_to_enumerate,
          base::BindOnce(&VideoInputDevicesEnumerated, run_loop.QuitClosure(),
                         std::move(salt_and_origin), &video_devices));
      run_loop.Run();
    }
    ASSERT_FALSE(video_devices.empty());

    // Open the first device.
    {
      base::test::TestFuture<const MediaDeviceSaltAndOrigin&> future;
      GetMediaDeviceSaltAndOrigin(render_frame_host_id, future.GetCallback());
      MediaDeviceSaltAndOrigin salt_and_origin = future.Get();

      base::RunLoop run_loop;
      media_stream_manager_->OpenDevice(
          render_frame_host_id, requester_id, page_request_id,
          video_devices[0].device_id,
          blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, salt_and_origin,
          base::BindOnce(&VideoCaptureTest::OnDeviceOpened,
                         base::Unretained(this), run_loop.QuitClosure()),
          MediaStreamManager::DeviceStoppedCallback());
      run_loop.Run();
    }
    ASSERT_FALSE(opened_session_id_.is_empty());
  }

  void CloseSession() {
    if (opened_device_label_.empty())
      return;
    media_stream_manager_->CancelRequest(opened_device_label_);
    opened_device_label_.clear();
    opened_session_id_ = base::UnguessableToken();
  }

 protected:
  // media::mojom::VideoCaptureObserver implementation.
  void OnStateChanged(media::mojom::VideoCaptureResultPtr result) override {
    if (result->which() == media::mojom::VideoCaptureResult::Tag::kState)
      DoOnStateChanged(result->get_state());
    else
      DoOnVideoCaptureError(result->get_error_code());
  }
  MOCK_METHOD1(DoOnStateChanged, void(media::mojom::VideoCaptureState));
  MOCK_METHOD1(DoOnVideoCaptureError, void(media::VideoCaptureError));

  void OnNewBuffer(int32_t buffer_id,
                   media::mojom::VideoBufferHandlePtr buffer_handle) override {
    DoOnNewBuffer(buffer_id);
  }
  MOCK_METHOD1(DoOnNewBuffer, void(int32_t));
  void OnBufferReady(media::mojom::ReadyBufferPtr buffer) override {
    DoOnBufferReady(buffer->buffer_id);
  }
  MOCK_METHOD1(DoOnBufferReady, void(int32_t));
  MOCK_METHOD1(OnBufferDestroyed, void(int32_t));
  MOCK_METHOD1(OnFrameDropped, void(media::VideoCaptureFrameDropReason));
  MOCK_METHOD1(OnNewCaptureVersion, void(const media::CaptureVersion&));

  void StartCapture() {
    base::RunLoop run_loop;
    media::VideoCaptureParams params;
    params.requested_format = media::VideoCaptureFormat(
        gfx::Size(352, 288), 30, media::PIXEL_FORMAT_I420);

    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::STARTED));
    EXPECT_CALL(*this, DoOnNewBuffer(_))
        .Times(AnyNumber())
        .WillRepeatedly(Return());
    EXPECT_CALL(*this, DoOnBufferReady(_))
        .Times(AnyNumber())
        .WillRepeatedly(ExitMessageLoop(task_runner_, run_loop.QuitClosure()));

    host_remote_->Start(DeviceId(), opened_session_id_, params,
                        observer_receiver_.BindNewPipeAndPassRemote());

    // Ensure that the browser context has been retrevied and the observer is
    // connected.
    host_remote_.FlushForTesting();
    observer_receiver_.FlushForTesting();

    run_loop.Run();
  }

  void StartCaptureWithInvalidSession() {
    media::VideoCaptureParams params;
    params.requested_format = media::VideoCaptureFormat(
        gfx::Size(352, 288), 30, media::PIXEL_FORMAT_I420);

    EXPECT_CALL(*this,
                DoOnVideoCaptureError(
                    media::VideoCaptureError::kVideoCaptureControllerInvalid))
        .Times(1);
    host_remote_->Start(DeviceId(), base::UnguessableToken::Create(), params,
                        observer_receiver_.BindNewPipeAndPassRemote());

    // Ensure that the browser context has been retrevied and the observer is
    // connected.
    host_remote_.FlushForTesting();
    observer_receiver_.FlushForTesting();
  }

  void StartAndImmediateStopCapture() {
    // Quickly start and then stop capture, without giving much chance for
    // asynchronous capture operations to produce frames.
    InSequence s;
    base::RunLoop run_loop;

    media::VideoCaptureParams params;
    params.requested_format = media::VideoCaptureFormat(
        gfx::Size(352, 288), 30, media::PIXEL_FORMAT_I420);

    // |STARTED| is reported asynchronously, which may not be received if
    // capture is stopped immediately.
    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::STARTED))
        .Times(AtMost(1));
    host_remote_->Start(DeviceId(), opened_session_id_, params,
                        observer_receiver_.BindNewPipeAndPassRemote());

    // Ensure that the browser context has been retrevied and the observer is
    // connected.
    host_remote_.FlushForTesting();
    observer_receiver_.FlushForTesting();

    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::STOPPED));
    host_remote_->Stop(DeviceId());
    host_remote_.FlushForTesting();
    run_loop.RunUntilIdle();
  }

  void PauseResumeCapture() {
    InSequence s;
    base::RunLoop run_loop;

    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::PAUSED));
    host_remote_->Pause(DeviceId());
    host_remote_.FlushForTesting();

    media::VideoCaptureParams params;
    params.requested_format = media::VideoCaptureFormat(
        gfx::Size(352, 288), 30, media::PIXEL_FORMAT_I420);

    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::RESUMED));
    host_remote_->Resume(DeviceId(), opened_session_id_, params);
    host_remote_.FlushForTesting();
    run_loop.RunUntilIdle();
  }

  void StopCapture() {
    base::RunLoop run_loop;

    EXPECT_CALL(*this,
                DoOnStateChanged(media::mojom::VideoCaptureState::STOPPED))
        .WillOnce(ExitMessageLoop(task_runner_, run_loop.QuitClosure()));
    host_remote_->Stop(DeviceId());
    host_remote_.FlushForTesting();

    run_loop.Run();

    EXPECT_TRUE(host_->controllers_.empty());
  }

  void WaitForOneCapturedBuffer() {
    base::RunLoop run_loop;

    EXPECT_CALL(*this, DoOnBufferReady(_))
        .Times(AnyNumber())
        .WillOnce(ExitMessageLoop(task_runner_, run_loop.QuitClosure()))
        .RetiresOnSaturation();
    run_loop.Run();
  }

  void SimulateError() {
    EXPECT_CALL(
        *this,
        DoOnVideoCaptureError(
            media::VideoCaptureError::kIntentionalErrorRaisedByUnitTest));
    host_->OnError(DeviceId(),
                   media::VideoCaptureError::kIntentionalErrorRaisedByUnitTest);
    base::RunLoop().RunUntilIdle();
  }

  MediaStreamManager* media_stream_manager() const {
    return media_stream_manager_.get();
  }

 private:
  std::unique_ptr<FakeMediaStreamUIProxy> CreateFakeUI() {
    return std::make_unique<FakeMediaStreamUIProxy>(
        /*tests_use_fake_render_frame_hosts=*/true);
  }

  void OnDeviceOpened(base::OnceClosure quit_closure,
                      bool success,
                      const std::string& label,
                      const blink::MediaStreamDevice& opened_device) {
    if (success) {
      opened_device_label_ = label;
      opened_session_id_ = opened_device.session_id();
    }
    std::move(quit_closure).Run();
  }

  std::unique_ptr<media::AudioManager> audio_manager_;
  std::unique_ptr<media::AudioSystem> audio_system_;

  // |media_stream_manager_| needs to outlive |task_environment_| because it is
  // a CurrentThread::DestructionObserver.
  std::unique_ptr<MediaStreamManager> media_stream_manager_;
  const content::BrowserTaskEnvironment task_environment_;
  content::TestBrowserContext browser_context_;
  content::TestContentBrowserClient browser_client_;
  const scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
  base::UnguessableToken opened_session_id_;
  std::string opened_device_label_;

  std::unique_ptr<VideoCaptureHost> host_;
  mojo::Remote<media::mojom::VideoCaptureHost> host_remote_;
  std::unique_ptr<mojo::Receiver<media::mojom::VideoCaptureHost>>
      host_receiver_;
  mojo::Receiver<media::mojom::VideoCaptureObserver> observer_receiver_{this};
};

// Construct and destruct all objects. This is a non trivial sequence.
TEST_F(VideoCaptureTest, ConstructAndDestruct) {}

TEST_F(VideoCaptureTest, StartAndImmediateStop) {
  StartAndImmediateStopCapture();
}

TEST_F(VideoCaptureTest, StartAndCaptureAndStop) {
  StartCapture();
  WaitForOneCapturedBuffer();
  WaitForOneCapturedBuffer();
  StopCapture();
}

TEST_F(VideoCaptureTest, StartAndErrorAndStop) {
  StartCapture();
  SimulateError();
  StopCapture();
}

TEST_F(VideoCaptureTest, StartWithInvalidSessionId) {
  StartCaptureWithInvalidSession();
  StopCapture();
}

TEST_F(VideoCaptureTest, StartAndCaptureAndError) {
  EXPECT_CALL(*this, DoOnStateChanged(media::mojom::VideoCaptureState::STOPPED))
      .Times(0);
  StartCapture();
  WaitForOneCapturedBuffer();
  SimulateError();
  base::PlatformThread::Sleep(base::Milliseconds(200));
}

TEST_F(VideoCaptureTest, StartAndPauseAndResumeAndStop) {
  StartCapture();
  PauseResumeCapture();
  StopCapture();
}

TEST_F(VideoCaptureTest, CloseSessionWithoutStopping) {
  StartCapture();

  // When the session is closed via the stream without stopping capture, the
  // ENDED event is sent.
  EXPECT_CALL(*this, DoOnStateChanged(media::mojom::VideoCaptureState::ENDED));
  CloseSession();
  base::RunLoop().RunUntilIdle();
}

// Tests if RenderProcessHostDelegate methods are called as often as as
// expected.
TEST_F(VideoCaptureTest, IncrementMatchesDecrementCalls) {
  std::unique_ptr<MockRenderFrameHostDelegate> mock_delegate =
      std::make_unique<MockRenderFrameHostDelegate>();
  MockRenderFrameHostDelegate* const mock_delegate_ptr = mock_delegate.get();
  EXPECT_CALL(*mock_delegate_ptr, render_frame_host_id())
      .WillRepeatedly(Return(GlobalRenderFrameHostId()));
  std::unique_ptr<VideoCaptureHost> host =
      std::make_unique<VideoCaptureHost>(std::move(mock_delegate), nullptr);

  const int kNumNotifyCalls = 3;
  EXPECT_CALL(*mock_delegate_ptr, NotifyStreamAdded()).Times(kNumNotifyCalls);
  EXPECT_CALL(*mock_delegate_ptr, NotifyStreamRemoved()).Times(kNumNotifyCalls);

  EXPECT_EQ(0u, host->number_of_active_streams_);
  for (int i = 0; i < kNumNotifyCalls; ++i)
    host->NotifyStreamAdded();
  EXPECT_EQ(kNumNotifyCalls, static_cast<int>(host->number_of_active_streams_));
  host->NotifyStreamRemoved();
  host->NotifyAllStreamsRemoved();
  EXPECT_EQ(0u, host->number_of_active_streams_);
}

TEST_F(VideoCaptureTest, RegisterAndUnregisterWithMediaStreamManager) {
  {
    mojo::Remote<media::mojom::VideoCaptureHost> client;
    VideoCaptureHost::Create(
        GlobalRenderFrameHostId() /* render_frame_host_id */,
        media_stream_manager(), client.BindNewPipeAndPassReceiver());
    EXPECT_TRUE(client.is_bound());
    EXPECT_EQ(media_stream_manager()->num_video_capture_hosts(), 1u);
  }

  base::RunLoop().RunUntilIdle();
  // At this point, the pipe is closed and the VideoCaptureHost should be
  // removed from MediaStreamManager.
  EXPECT_EQ(media_stream_manager()->num_video_capture_hosts(), 0u);
}

}  // namespace content
