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

#include "content/browser/media/capture/web_contents_frame_tracker.h"

#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "content/browser/media/capture/mouse_cursor_overlay_controller.h"
#include "content/browser/media/capture/web_contents_video_capture_device.h"
#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "media/base/media_switches.h"
#include "media/capture/mojom/video_capture_types.mojom.h"
#include "media/capture/video/video_capture_feedback.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/screen_info.h"

namespace content {
namespace {

using testing::_;
using testing::NiceMock;
using testing::StrictMock;

constexpr viz::FrameSinkId kInitSinkId(123, 456);

// Standardized screen resolutions to test common scenarios.
constexpr gfx::Size kSizeZero{0, 0};
constexpr gfx::Size kSize720p{1280, 720};
constexpr gfx::Size kSize1080p{1920, 1080};
constexpr gfx::Size kSizeWsxgaPlus{1680, 1050};

class SimpleContext : public WebContentsFrameTracker::Context {
 public:
  SimpleContext() = default;
  ~SimpleContext() override = default;

  // WebContentsFrameTracker::Context overrides.
  std::optional<gfx::Rect> GetScreenBounds() override { return screen_bounds_; }

  WebContentsImpl::CaptureTarget GetCaptureTarget() override {
    return WebContentsImpl::CaptureTarget{frame_sink_id_, gfx::NativeView{}};
  }
  void IncrementCapturerCount(const gfx::Size& capture_size) override {
    ++capturer_count_;
    last_capture_size_ = capture_size;
  }
  void DecrementCapturerCount() override { --capturer_count_; }

  void SetCaptureScaleOverride(float scale) override {
    scale_override_ = scale;
  }
  float GetCaptureScaleOverride() const override { return scale_override_; }
  int capturer_count() const { return capturer_count_; }
  const gfx::Size& last_capture_size() const { return last_capture_size_; }

  void set_frame_sink_id(viz::FrameSinkId frame_sink_id) {
    frame_sink_id_ = frame_sink_id;
  }
  void set_screen_bounds(std::optional<gfx::Rect> screen_bounds) {
    screen_bounds_ = std::move(screen_bounds);
  }
  float scale_override() const { return scale_override_; }

 private:
  int capturer_count_ = 0;
  viz::FrameSinkId frame_sink_id_ = kInitSinkId;
  gfx::Size last_capture_size_;
  std::optional<gfx::Rect> screen_bounds_;
  float scale_override_ = 1.0f;
};

// The capture device is mostly for interacting with the frame tracker. We do
// care about the frame tracker pushing back target updates, however.
class MockCaptureDevice : public WebContentsVideoCaptureDevice {
 public:
  MOCK_METHOD2(OnTargetChanged,
               void(const std::optional<viz::VideoCaptureTarget>&, uint32_t));
  MOCK_METHOD0(OnTargetPermanentlyLost, void());

  base::WeakPtr<MockCaptureDevice> AsWeakPtr() {
    return weak_ptr_factory_.GetWeakPtr();
  }

 private:
  base::WeakPtrFactory<MockCaptureDevice> weak_ptr_factory_{this};
};

// This test class is intentionally quite similar to
// |WebContentsVideoCaptureDevice|, and provides convenience methods for calling
// into the |WebContentsFrameTracker|, which interacts with UI thread objects
// and needs to be called carefully on the UI thread.
class WebContentsFrameTrackerTest : public RenderViewHostTestHarness {
 protected:
  WebContentsFrameTrackerTest() = default;

  void SetUp() override {
    RenderViewHostTestHarness::SetUp();

    // The tests assume that they are running on the main thread (which is
    // equivalent to the browser's UI thread) so that they can make calls on the
    // tracker object synchronously.
    ASSERT_TRUE(
        content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));

    // Views in the web context are incredibly fragile and prone to
    // non-deterministic test failures, so we use TestWebContents here.
    web_contents_ = TestWebContents::Create(browser_context(), nullptr);
    device_ = std::make_unique<StrictMock<MockCaptureDevice>>();

    // All tests should call target changed as part of initialization.
    EXPECT_CALL(*device_, OnTargetChanged(_, _)).Times(1);

    // This PostTask technically isn't necessary since we're already on the main
    // thread which is equivalent to the browser's UI thread, but it's a bit
    // cleaner to do so in case we want to switch to a different threading model
    // for the tests in the future.
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsFrameTrackerTest::SetUpOnUIThread,
                       base::Unretained(this),
                       base::SingleThreadTaskRunner::GetCurrentDefault()));
    RunAllTasksUntilIdle();
  }

  void SetUpOnUIThread(
      const scoped_refptr<base::SequencedTaskRunner> device_task_runner) {
    auto context = std::make_unique<SimpleContext>();
    raw_context_ = context.get();
    SetScreenSize(kSize1080p);
    tracker_ = std::make_unique<WebContentsFrameTracker>(
        device_task_runner, device_->AsWeakPtr(), controller());
    tracker_->SetWebContentsAndContextForTesting(web_contents_.get(),
                                                 std::move(context));
  }

  void TearDown() override {
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsFrameTrackerTest::TearDownOnUIThread,
                       base::Unretained(this)));
    RunAllTasksUntilIdle();
    RenderViewHostTestHarness::TearDown();
  }

  void TearDownOnUIThread() {
    tracker_.reset();
    device_.reset();
    web_contents_.reset();
  }

  void SetScreenSize(const gfx::Size& size) {
    raw_context_->set_screen_bounds(gfx::Rect{size});
  }

  void SetFrameSinkId(viz::FrameSinkId id) {
    raw_context_->set_frame_sink_id(id);
  }

  void StartTrackerOnUIThread(const gfx::Size& capture_size) {
    // Using base::Unretained for the tracker is presumed safe due to using
    // RunAllTasksUntilIdle in TearDown.
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsFrameTracker::WillStartCapturingWebContents,
                       base::Unretained(tracker_.get()), capture_size,
                       true /* is_high_dpi_enabled */));
  }

  void StopTrackerOnUIThread() {
    // Using base::Unretained for the tracker is presumed safe due to using
    // RunAllTasksUntilIdle in TearDown.
    GetUIThreadTaskRunner({})->PostTask(
        FROM_HERE,
        base::BindOnce(&WebContentsFrameTracker::DidStopCapturingWebContents,
                       base::Unretained(tracker_.get())));
  }

  // The controller is ignored on iOS, and must be initialized on all
  // other platforms.
  MouseCursorOverlayController* controller() {
#if BUILDFLAG(IS_IOS)
    return nullptr;
#else
    return &controller_;
#endif
  }
  WebContentsFrameTracker* tracker() { return tracker_.get(); }
  SimpleContext* context() { return raw_context_; }
  StrictMock<MockCaptureDevice>* device() { return device_.get(); }

 private:
#if !BUILDFLAG(IS_IOS)
  MouseCursorOverlayController controller_;
#endif

  std::unique_ptr<TestWebContents> web_contents_;
  std::unique_ptr<StrictMock<MockCaptureDevice>> device_;
  std::unique_ptr<WebContentsFrameTracker> tracker_;

  // Save because the pointed-to location should not change during testing.
  raw_ptr<SimpleContext, AcrossTasksDanglingUntriaged> raw_context_;
};

TEST_F(WebContentsFrameTrackerTest, CalculatesPreferredSizeClampsToView) {
  SetScreenSize(kSize720p);
  EXPECT_EQ(kSize720p, tracker()->CalculatePreferredSize(kSize720p));
  EXPECT_EQ(kSize720p, tracker()->CalculatePreferredSize(kSize1080p));
}

TEST_F(WebContentsFrameTrackerTest,
       CalculatesPreferredSizeNoLargerThanCaptureSize) {
  SetScreenSize(kSize1080p);
  EXPECT_EQ(kSize720p, tracker()->CalculatePreferredSize(kSize720p));
  EXPECT_EQ(kSize1080p, tracker()->CalculatePreferredSize(kSize1080p));
}

TEST_F(WebContentsFrameTrackerTest,
       CalculatesPreferredSizeWithCorrectAspectRatio) {
  SetScreenSize(kSizeWsxgaPlus);

  // 720P is strictly less than WSXGA+, so should be unchanged.
  EXPECT_EQ(kSize720p, tracker()->CalculatePreferredSize(kSize720p));

  // 1080P is larger, so should be scaled appropriately.
  EXPECT_EQ((gfx::Size{1680, 945}),
            tracker()->CalculatePreferredSize(kSize1080p));

  // Wider capture size.
  EXPECT_EQ((gfx::Size{1680, 525}),
            tracker()->CalculatePreferredSize(gfx::Size{3360, 1050}));

  // Taller capture size.
  EXPECT_EQ((gfx::Size{500, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1000, 2100}));
}

TEST_F(WebContentsFrameTrackerTest,
       CalculatesPreferredSizeAspectRatioWithNoOffByOneErrors) {
  SetScreenSize(kSizeWsxgaPlus);

  // Wider capture size.
  EXPECT_EQ((gfx::Size{1680, 525}),
            tracker()->CalculatePreferredSize(gfx::Size{3360, 1050}));
  EXPECT_EQ((gfx::Size{1680, 525}),
            tracker()->CalculatePreferredSize(gfx::Size{3360, 1051}));
  EXPECT_EQ((gfx::Size{1680, 526}),
            tracker()->CalculatePreferredSize(gfx::Size{3360, 1052}));
  EXPECT_EQ((gfx::Size{1680, 525}),
            tracker()->CalculatePreferredSize(gfx::Size{3361, 1052}));
  EXPECT_EQ((gfx::Size{1680, 666}),
            tracker()->CalculatePreferredSize(gfx::Size{5897, 2339}));

  // Taller capture size.
  EXPECT_EQ((gfx::Size{500, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1000, 2100}));
  EXPECT_EQ((gfx::Size{499, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1000, 2101}));
  EXPECT_EQ((gfx::Size{499, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1000, 2102}));
  EXPECT_EQ((gfx::Size{500, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1001, 2102}));
  EXPECT_EQ((gfx::Size{500, 1050}),
            tracker()->CalculatePreferredSize(gfx::Size{1002, 2102}));

  // Some larger and prime factor cases to sanity check.
  EXPECT_EQ((gfx::Size{1680, 565}),
            tracker()->CalculatePreferredSize(gfx::Size{21841, 7351}));
  EXPECT_EQ((gfx::Size{1680, 565}),
            tracker()->CalculatePreferredSize(gfx::Size{21841, 7349}));
  EXPECT_EQ((gfx::Size{1680, 565}),
            tracker()->CalculatePreferredSize(gfx::Size{21839, 7351}));
  EXPECT_EQ((gfx::Size{1680, 565}),
            tracker()->CalculatePreferredSize(gfx::Size{21839, 7349}));

  EXPECT_EQ((gfx::Size{1680, 670}),
            tracker()->CalculatePreferredSize(gfx::Size{139441, 55651}));
  EXPECT_EQ((gfx::Size{1680, 670}),
            tracker()->CalculatePreferredSize(gfx::Size{139439, 55651}));
  EXPECT_EQ((gfx::Size{1680, 670}),
            tracker()->CalculatePreferredSize(gfx::Size{139441, 55649}));
  EXPECT_EQ((gfx::Size{1680, 670}),
            tracker()->CalculatePreferredSize(gfx::Size{139439, 55649}));

  // Finally, just check for roundoff errors.
  SetScreenSize(gfx::Size{1000, 1000});
  EXPECT_EQ((gfx::Size{1000, 333}),
            tracker()->CalculatePreferredSize(gfx::Size{3000, 1000}));
}

TEST_F(WebContentsFrameTrackerTest,
       CalculatesPreferredSizeLeavesCaptureSizeIfSmallerThanScreen) {
  // Smaller in both directions, but different aspect ratio, should be
  // unchanged.
  SetScreenSize(kSize1080p);
  EXPECT_EQ(kSizeWsxgaPlus, tracker()->CalculatePreferredSize(kSizeWsxgaPlus));
}

TEST_F(WebContentsFrameTrackerTest,
       CalculatesPreferredSizeWithZeroValueProperly) {
  // If a capture dimension is zero, the preferred size should be (0, 0).
  EXPECT_EQ((kSizeZero), tracker()->CalculatePreferredSize(gfx::Size{0, 1000}));
  EXPECT_EQ((kSizeZero), tracker()->CalculatePreferredSize(kSizeZero));
  EXPECT_EQ((kSizeZero), tracker()->CalculatePreferredSize(gfx::Size{1000, 0}));

  // If a screen dimension is zero, the preferred size should be (0, 0). This
  // probably means the tab isn't being drawn anyway.
  SetScreenSize(gfx::Size{1920, 0});
  EXPECT_EQ(kSizeZero, tracker()->CalculatePreferredSize(kSize720p));
  SetScreenSize(gfx::Size{0, 1080});
  EXPECT_EQ(kSizeZero, tracker()->CalculatePreferredSize(kSize720p));
  SetScreenSize(kSizeZero);
  EXPECT_EQ(kSizeZero, tracker()->CalculatePreferredSize(kSize720p));
}

TEST_F(WebContentsFrameTrackerTest, UpdatesPreferredSizeOnWebContents) {
  StartTrackerOnUIThread(kSize720p);
  RunAllTasksUntilIdle();

  // In this case, the capture size requested is smaller than the screen size,
  // so it should be used.
  EXPECT_EQ(kSize720p, context()->last_capture_size());
  EXPECT_EQ(context()->capturer_count(), 1);

  // When we stop the tracker, the web contents issues a preferred size change
  // of the "old" size--so it shouldn't change.
  StopTrackerOnUIThread();
  RunAllTasksUntilIdle();
  EXPECT_EQ(kSize720p, context()->last_capture_size());
  EXPECT_EQ(context()->capturer_count(), 0);
}

TEST_F(WebContentsFrameTrackerTest, NotifiesOfLostTargets) {
  EXPECT_CALL(*device(), OnTargetPermanentlyLost()).Times(1);
  tracker()->WebContentsDestroyed();
  RunAllTasksUntilIdle();
}

// We test target changing for all other tests as part of set up, but also
// test the observer callbacks here.
TEST_F(WebContentsFrameTrackerTest, NotifiesOfTargetChanges) {
  const viz::FrameSinkId kNewId(42, 1337);
  SetFrameSinkId(kNewId);
  EXPECT_CALL(
      *device(),
      OnTargetChanged(std::make_optional<viz::VideoCaptureTarget>(kNewId),
                      /*sub_capture_target_version=*/0))
      .Times(1);

  // The tracker doesn't actually use the frame host information, just
  // posts a possible target change.
  tracker()->RenderFrameHostChanged(nullptr, nullptr);
  RunAllTasksUntilIdle();
}

TEST_F(WebContentsFrameTrackerTest,
       CroppingChangesTargetParametersAndInvokesCallback) {
  const base::Token kCropId(19831230, 19840730);

  // Expect the callback handed to Crop() to be invoke with kSuccess.
  bool success = false;
  base::OnceCallback<void(media::mojom::ApplySubCaptureTargetResult)> callback =
      base::BindOnce(
          [](bool* success, media::mojom::ApplySubCaptureTargetResult result) {
            *success =
                (result == media::mojom::ApplySubCaptureTargetResult::kSuccess);
          },
          &success);

  // Expect OnTargetChanged() to be invoked once with the crop-ID.
  EXPECT_CALL(*device(),
              OnTargetChanged(std::make_optional<viz::VideoCaptureTarget>(
                                  kInitSinkId, kCropId),
                              /*sub_capture_target_version=*/1))
      .Times(1);

  tracker()->ApplySubCaptureTarget(
      media::mojom::SubCaptureTargetType::kCropTarget, kCropId,
      /*sub_capture_target_version=*/1, std::move(callback));

  RunAllTasksUntilIdle();
  EXPECT_TRUE(success);
}

}  // namespace
}  // namespace content
