// Copyright 2018 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 <utility>
#include <vector>

#include "base/memory/read_only_shared_memory_region.h"
#include "base/test/scoped_task_environment.h"
#include "content/browser/devtools/devtools_video_consumer.h"
#include "content/public/test/test_utils.h"
#include "media/base/limits.h"
#include "mojo/public/cpp/base/shared_memory_utils.h"
#include "mojo/public/cpp/bindings/binding.h"
#include "testing/gmock/include/gmock/gmock.h"

using testing::_;

namespace content {
namespace {

// Capture parameters.
constexpr gfx::Size kResolution = gfx::Size(320, 180);  // Arbitrarily chosen.
constexpr media::VideoPixelFormat kFormat = media::PIXEL_FORMAT_I420;

// A non-zero FrameSinkId to prevent validation errors when
// DevToolsVideoConsumer::ChangeTarget(viz::FrameSinkId) is called
// (which eventually fails in FrameSinkVideoCapturerStubDispatch::Accept).
constexpr viz::FrameSinkId kInitialFrameSinkId = viz::FrameSinkId(1, 1);

}  // namespace

// Mock for the FrameSinkVideoCapturer running in the VIZ process.
class MockFrameSinkVideoCapturer : public viz::mojom::FrameSinkVideoCapturer {
 public:
  MockFrameSinkVideoCapturer() : binding_(this) {}

  bool is_bound() const { return binding_.is_bound(); }

  void Bind(viz::mojom::FrameSinkVideoCapturerRequest request) {
    DCHECK(!binding_.is_bound());
    binding_.Bind(std::move(request));
  }

  void Reset() {
    binding_.Close();
    consumer_.reset();
  }

  // This is never called.
  MOCK_METHOD2(SetFormat,
               void(media::VideoPixelFormat format,
                    const gfx::ColorSpace& color_space));
  void SetMinCapturePeriod(base::TimeDelta min_capture_period) final {
    min_capture_period_ = min_capture_period;
    MockSetMinCapturePeriod(min_capture_period_);
  }
  MOCK_METHOD1(MockSetMinCapturePeriod,
               void(base::TimeDelta min_capture_period));
  void SetMinSizeChangePeriod(base::TimeDelta min_period) final {
    min_period_ = min_period;
    MockSetMinSizeChangePeriod(min_period_);
  }
  MOCK_METHOD1(MockSetMinSizeChangePeriod, void(base::TimeDelta min_period));
  void SetResolutionConstraints(const gfx::Size& min_frame_size,
                                const gfx::Size& max_frame_size,
                                bool use_fixed_aspect_ratio) final {
    min_frame_size_ = min_frame_size;
    max_frame_size_ = max_frame_size;
    MockSetResolutionConstraints(min_frame_size_, max_frame_size_, true);
  }
  MOCK_METHOD3(MockSetResolutionConstraints,
               void(const gfx::Size& min_frame_size,
                    const gfx::Size& max_frame_size,
                    bool use_fixed_aspect_ratio));
  // This is never called.
  MOCK_METHOD1(SetAutoThrottlingEnabled, void(bool));
  void ChangeTarget(
      const base::Optional<viz::FrameSinkId>& frame_sink_id) final {
    frame_sink_id_ = frame_sink_id ? *frame_sink_id : viz::FrameSinkId();
    MockChangeTarget(frame_sink_id_);
  }
  MOCK_METHOD1(MockChangeTarget, void(const viz::FrameSinkId& frame_sink_id));
  void Start(viz::mojom::FrameSinkVideoConsumerPtr consumer) final {
    DCHECK(!consumer_);
    consumer_ = std::move(consumer);
    MockStart(consumer_.get());
  }
  MOCK_METHOD1(MockStart, void(viz::mojom::FrameSinkVideoConsumer* consumer));
  void Stop() final {
    binding_.Close();
    consumer_.reset();
    MockStop();
  }
  MOCK_METHOD0(MockStop, void());
  MOCK_METHOD0(RequestRefreshFrame, void());
  MOCK_METHOD2(CreateOverlay,
               void(int32_t stacking_index,
                    viz::mojom::FrameSinkVideoCaptureOverlayRequest request));

  // Const accessors to get the cached variables.
  base::TimeDelta min_capture_period() const { return min_capture_period_; }
  base::TimeDelta min_period() const { return min_period_; }
  gfx::Size min_frame_size() const { return min_frame_size_; }
  gfx::Size max_frame_size() const { return max_frame_size_; }
  viz::FrameSinkId frame_sink_id() const { return frame_sink_id_; }

 private:
  // These variables are cached when they are received from
  // DevToolsVideoConsumer.
  base::TimeDelta min_capture_period_;
  base::TimeDelta min_period_;
  gfx::Size min_frame_size_;
  gfx::Size max_frame_size_;
  viz::FrameSinkId frame_sink_id_;
  viz::mojom::FrameSinkVideoConsumerPtr consumer_;

  mojo::Binding<viz::mojom::FrameSinkVideoCapturer> binding_;
};

// Represents the FrameSinkVideoConsumerFrameCallbacks instance in the VIZ
// process.
class MockFrameSinkVideoConsumerFrameCallbacks
    : public viz::mojom::FrameSinkVideoConsumerFrameCallbacks {
 public:
  MockFrameSinkVideoConsumerFrameCallbacks() : binding_(this) {}

  void Bind(viz::mojom::FrameSinkVideoConsumerFrameCallbacksRequest request) {
    binding_.Bind(std::move(request));
  }

  MOCK_METHOD0(Done, void());
  MOCK_METHOD1(ProvideFeedback, void(double utilization));

 private:
  mojo::Binding<viz::mojom::FrameSinkVideoConsumerFrameCallbacks> binding_;
};

// Mock for the classes like TracingHandler that receive frames from
// DevToolsVideoConsumer via the OnFrameCapturedCallback.
class MockDevToolsVideoFrameReceiver {
 public:
  MOCK_METHOD1(OnFrameFromVideoConsumerMock,
               void(scoped_refptr<media::VideoFrame> frame));

  MockDevToolsVideoFrameReceiver() : weak_factory_(this) {}

  scoped_refptr<media::VideoFrame> TakeFrameAt(int i) {
    return std::move(frames_[i]);
  }

  void OnFrameFromVideoConsumer(scoped_refptr<media::VideoFrame> frame) {
    OnFrameFromVideoConsumerMock(frame);
    frames_.push_back(std::move(frame));
  }

  std::unique_ptr<DevToolsVideoConsumer> CreateDevToolsVideoConsumer() {
    return std::make_unique<DevToolsVideoConsumer>(base::BindRepeating(
        &MockDevToolsVideoFrameReceiver::OnFrameFromVideoConsumer,
        weak_factory_.GetWeakPtr()));
  }

 private:
  std::vector<scoped_refptr<media::VideoFrame>> frames_;
  base::WeakPtrFactory<MockDevToolsVideoFrameReceiver> weak_factory_;
};

class DevToolsVideoConsumerTest : public testing::Test {
 public:
  DevToolsVideoConsumerTest() : weak_factory_(this) {}

  void SetUp() override {
    consumer_ = receiver_.CreateDevToolsVideoConsumer();

    consumer_->SetFrameSinkId(kInitialFrameSinkId);
  }

  void SimulateFrameCapture(base::ReadOnlySharedMemoryRegion data) {
    viz::mojom::FrameSinkVideoConsumerFrameCallbacksPtr callbacks_ptr;
    callbacks.Bind(mojo::MakeRequest(&callbacks_ptr));

    media::mojom::VideoFrameInfoPtr info = media::mojom::VideoFrameInfo::New(
        base::TimeDelta(), base::Value(base::Value::Type::DICTIONARY), kFormat,
        kResolution, gfx::Rect(kResolution), gfx::ColorSpace::CreateREC709(),
        nullptr);

    consumer_->OnFrameCaptured(std::move(data), std::move(info),
                               gfx::Rect(kResolution), gfx::Rect(kResolution),
                               std::move(callbacks_ptr));
  }

  void StartCaptureWithMockCapturer() {
    consumer_->InnerStartCapture(CreateMockCapturer());
  }

  bool IsValidMinAndMaxFrameSize(gfx::Size min_frame_size,
                                 gfx::Size max_frame_size) {
    return consumer_->IsValidMinAndMaxFrameSize(min_frame_size, max_frame_size);
  }

  static gfx::Size GetVideoConsumerDefaultMinFrameSize() {
    return DevToolsVideoConsumer::kDefaultMinFrameSize;
  }

  static gfx::Size GetVideoConsumerDefaultMaxFrameSize() {
    return DevToolsVideoConsumer::kDefaultMaxFrameSize;
  }

  // Getters for |consumer_|'s private variables.
  base::TimeDelta GetMinCapturePeriod() const {
    return consumer_->min_capture_period_;
  }
  gfx::Size GetMinFrameSize() const { return consumer_->min_frame_size_; }
  gfx::Size GetMaxFrameSize() const { return consumer_->max_frame_size_; }
  viz::FrameSinkId GetFrameSinkId() const { return consumer_->frame_sink_id_; }

 protected:
  MockFrameSinkVideoCapturer capturer_;
  MockFrameSinkVideoConsumerFrameCallbacks callbacks;
  MockDevToolsVideoFrameReceiver receiver_;
  std::unique_ptr<DevToolsVideoConsumer> consumer_;

 private:
  std::unique_ptr<viz::ClientFrameSinkVideoCapturer> CreateMockCapturer() {
    return std::make_unique<viz::ClientFrameSinkVideoCapturer>(
        base::BindRepeating(
            [](base::WeakPtr<DevToolsVideoConsumerTest> self,
               viz::mojom::FrameSinkVideoCapturerRequest request) {
              self->capturer_.Bind(std::move(request));
            },
            weak_factory_.GetWeakPtr()));
  }

  base::test::ScopedTaskEnvironment task_environment_;
  base::WeakPtrFactory<DevToolsVideoConsumerTest> weak_factory_;
};

// Tests that the OnFrameFromVideoConsumer callbacks is called when
// OnFrameCaptured is passed a valid buffer with valid mapping.
TEST_F(DevToolsVideoConsumerTest, CallbacksAreCalledWhenBufferValid) {
  // On valid buffer the |receiver_| gets a frame via OnFrameFromVideoConsumer.
  EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(1);

  auto region = mojo::CreateReadOnlySharedMemoryRegion(
                    media::VideoFrame::AllocationSize(kFormat, kResolution))
                    .region;
  ASSERT_TRUE(region.IsValid());
  SimulateFrameCapture(std::move(region));
  base::RunLoop().RunUntilIdle();
}

// Tests that only the OnFrameFromVideoConsumer callback is not called when
// OnFrameCaptured is passed an invalid buffer.
TEST_F(DevToolsVideoConsumerTest, CallbackIsNotCalledWhenBufferIsNotValid) {
  // On invalid buffer, the |receiver_| doesn't get a frame.
  EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(0);

  SimulateFrameCapture(base::ReadOnlySharedMemoryRegion());
  base::RunLoop().RunUntilIdle();
}

// Tests that the OnFrameFromVideoConsumer callback is not called when
// OnFrameCaptured is passed a buffer with less-than-expected size.
TEST_F(DevToolsVideoConsumerTest, CallbackIsNotCalledWhenBufferIsTooSmall) {
  // On invalid mapping, the |receiver_| doesn't get a frame.
  EXPECT_CALL(receiver_, OnFrameFromVideoConsumerMock(_)).Times(0);

  constexpr size_t too_few_number_of_bytes = 4;
  ASSERT_LT(too_few_number_of_bytes,
            media::VideoFrame::AllocationSize(kFormat, kResolution));
  auto region =
      mojo::CreateReadOnlySharedMemoryRegion(too_few_number_of_bytes).region;
  ASSERT_TRUE(region.IsValid());
  SimulateFrameCapture(std::move(region));
  base::RunLoop().RunUntilIdle();
}

// Tests that starting capture calls |capturer_| functions, and capture can be
// restarted. This test is important as it ensures that when restarting capture,
// a FrameSinkVideoCapturerPtrInfo is bound to |capturer_| and it verifies that
// resources used in the previous StartCapture aren't reused.
TEST_F(DevToolsVideoConsumerTest, StartCaptureCallsSetFunctions) {
  // Starting capture should call these |capturer_| functions once.
  EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_));
  EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_));
  EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _));
  EXPECT_CALL(capturer_, MockChangeTarget(_));
  EXPECT_CALL(capturer_, MockStart(_));
  StartCaptureWithMockCapturer();
  base::RunLoop().RunUntilIdle();

  // Stop capturing.
  consumer_->StopCapture();

  // Reset the mock to allow the next consumer to connect.
  capturer_.Reset();

  // Start capturing again, and expect that these |capturer_| functions are
  // called once. This will re-bind the |capturer_| and ensures that destroyed
  // resources aren't being reused.
  EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_));
  EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_));
  EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _));
  EXPECT_CALL(capturer_, MockChangeTarget(_));
  EXPECT_CALL(capturer_, MockStart(_));
  StartCaptureWithMockCapturer();
  base::RunLoop().RunUntilIdle();
}

// Tests that calling 'Set' functions in DevToolsVideoConsumer before
// |capturer_| is initialized results in the passed values being cached.
// When capture is later started (and |capturer_| initialized), these cached
// values should be used and sent to the |capturer_|.
TEST_F(DevToolsVideoConsumerTest, CapturerIsPassedCachedValues) {
  // These values are chosen so that they are valid, and different from
  // the default values in DevToolsVideoConsumer.
  constexpr base::TimeDelta kNewMinCapturePeriod = base::TimeDelta();
  const gfx::Size kNewMinFrameSize =
      gfx::Size(GetVideoConsumerDefaultMinFrameSize().width() + 1,
                GetVideoConsumerDefaultMinFrameSize().height() + 1);
  const gfx::Size kNewMaxFrameSize =
      gfx::Size(GetVideoConsumerDefaultMaxFrameSize().width() + 1,
                GetVideoConsumerDefaultMaxFrameSize().width() + 1);
  constexpr viz::FrameSinkId kNewFrameSinkId = viz::FrameSinkId(2, 2);

  // Right now, |capturer_| has not been created via StartCapture, so
  // calling these functions should not call the |capturer_|, but the
  // values that are passed in should be cached.
  EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_)).Times(0);
  EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_)).Times(0);
  EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _)).Times(0);
  EXPECT_CALL(capturer_, MockChangeTarget(_)).Times(0);
  EXPECT_CALL(capturer_, MockStart(_)).Times(0);
  consumer_->SetMinCapturePeriod(kNewMinCapturePeriod);
  consumer_->SetMinAndMaxFrameSize(kNewMinFrameSize, kNewMaxFrameSize);
  consumer_->SetFrameSinkId(kNewFrameSinkId);
  base::RunLoop().RunUntilIdle();
  // Verify that new values are cached.
  EXPECT_EQ(GetMinCapturePeriod(), kNewMinCapturePeriod);
  EXPECT_EQ(GetMinFrameSize(), kNewMinFrameSize);
  EXPECT_EQ(GetMaxFrameSize(), kNewMaxFrameSize);
  EXPECT_EQ(GetFrameSinkId(), kNewFrameSinkId);

  // Starting capture now, will result in the cached values being sent to
  // |capturer_|. So, expect that these calls are made and verify the values.
  EXPECT_CALL(capturer_, MockSetMinCapturePeriod(_));
  EXPECT_CALL(capturer_, MockSetMinSizeChangePeriod(_));
  EXPECT_CALL(capturer_, MockSetResolutionConstraints(_, _, _));
  EXPECT_CALL(capturer_, MockChangeTarget(_));
  EXPECT_CALL(capturer_, MockStart(_));
  StartCaptureWithMockCapturer();
  base::RunLoop().RunUntilIdle();
  // Verify that the previously cached values are sent to |capturer_|.
  EXPECT_EQ(capturer_.min_capture_period(), kNewMinCapturePeriod);
  EXPECT_EQ(capturer_.min_frame_size(), kNewMinFrameSize);
  EXPECT_EQ(capturer_.max_frame_size(), kNewMaxFrameSize);
  EXPECT_EQ(capturer_.frame_sink_id(), kNewFrameSinkId);
}

// Tests that DevToolsVideoConsumer::IsValidMinAndMaxFrameSize adheres to the
// limits set by media::limits::kMaxDimension
TEST_F(DevToolsVideoConsumerTest, IsValidMinAndMaxFrameSize) {
  // Choosing valid frame sizes with
  // kNormalMinSize.height() > kNormalMaxSize.width() so that width
  // and height are not interchanged in this test.
  constexpr gfx::Size kNormalMinSize = gfx::Size(50, 150);
  constexpr gfx::Size kNormalMaxSize = gfx::Size(100, 200);

  // Testing success cases.
  EXPECT_TRUE(IsValidMinAndMaxFrameSize(kNormalMinSize, kNormalMaxSize));
  // Non-zero frames that are equal should pass.
  EXPECT_TRUE(IsValidMinAndMaxFrameSize(kNormalMinSize, kNormalMaxSize));
  // Swapping width and height of frames should pass.
  EXPECT_TRUE(IsValidMinAndMaxFrameSize(
      gfx::Size(kNormalMinSize.height(), kNormalMinSize.width()),
      gfx::Size(kNormalMaxSize.height(), kNormalMaxSize.width())));

  // Testing failure cases.
  // |min_frame_size|.width() should be > 0
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(gfx::Size(0, kNormalMinSize.height()),
                                         kNormalMaxSize));
  // |min_frame_size|.height() should be > 0
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(gfx::Size(kNormalMinSize.width(), 0),
                                         kNormalMaxSize));
  // |min_frame_size|.width() should be <= |max_frame_size|.width()
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(
      gfx::Size(kNormalMaxSize.width() + 1, kNormalMinSize.height()),
      kNormalMaxSize));
  // |max_frame_size|.height() should be <= |max_frame_size|.height()
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(
      gfx::Size(kNormalMinSize.width(), kNormalMaxSize.height() + 1),
      kNormalMaxSize));
  // |max_frame_size|.height() should be <= media::limits::kMaxDimension
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(
      kNormalMinSize,
      gfx::Size(kNormalMaxSize.width(), media::limits::kMaxDimension + 1)));
  // |max_frame_size|.width() should be <= media::limits::kMaxDimension
  EXPECT_FALSE(IsValidMinAndMaxFrameSize(
      kNormalMinSize,
      gfx::Size(media::limits::kMaxDimension + 1, kNormalMaxSize.height())));
}

}  // namespace content
