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

#include "components/viz/service/frame_sinks/video_detector.h"

#include <algorithm>
#include <memory>
#include <set>
#include <utility>

#include "base/compiler_specific.h"
#include "base/containers/circular_deque.h"
#include "base/feature_list.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "components/viz/common/features.h"
#include "components/viz/common/quads/surface_draw_quad.h"
#include "components/viz/common/quads/texture_draw_quad.h"
#include "components/viz/common/resources/transferable_resource.h"
#include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
#include "components/viz/service/display/display_resource_provider_software.h"
#include "components/viz/service/display/surface_aggregator.h"
#include "components/viz/service/frame_sinks/compositor_frame_sink_support.h"
#include "components/viz/service/frame_sinks/frame_sink_manager_impl.h"
#include "components/viz/test/compositor_frame_helpers.h"
#include "components/viz/test/fake_compositor_frame_sink_client.h"
#include "components/viz/test/surface_id_allocator_set.h"
#include "gpu/command_buffer/service/scheduler.h"
#include "gpu/command_buffer/service/shared_image/shared_image_manager.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "services/viz/public/mojom/compositing/video_detector_observer.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/rect.h"

namespace viz {

namespace {

// Implementation that just records video state changes.
class TestObserver : public mojom::VideoDetectorObserver {
 public:
  TestObserver() = default;

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

  void Bind(mojo::PendingReceiver<mojom::VideoDetectorObserver> receiver) {
    receiver_.Bind(std::move(receiver));
  }

  bool IsEmpty() {
    receiver_.FlushForTesting();
    return states_.empty();
  }

  void Reset() {
    receiver_.FlushForTesting();
    states_.clear();
  }

  // Pops and returns the earliest-received state.
  bool PopState() {
    receiver_.FlushForTesting();
    CHECK(!states_.empty());
    uint8_t first_state = states_.front();
    states_.pop_front();
    return first_state;
  }

  // mojom::VideoDetectorObserver implementation.
  void OnVideoActivityStarted() override { states_.push_back(true); }
  void OnVideoActivityEnded() override { states_.push_back(false); }

 private:
  // States in the order they were received.
  base::circular_deque<bool> states_;

  mojo::Receiver<mojom::VideoDetectorObserver> receiver_{this};
};

}  // namespace

class VideoDetectorTest : public testing::Test {
 public:
  VideoDetectorTest()
      : surface_aggregator_(frame_sink_manager_.surface_manager(),
                            &resource_provider_,
                            false) {}

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

  ~VideoDetectorTest() override = default;

  void SetUp() override {
    mock_task_runner_ = base::MakeRefCounted<base::TestMockTimeTaskRunner>(
        base::Time() + base::Seconds(1), base::TimeTicks() + base::Seconds(1));

    detector_ = frame_sink_manager_.CreateVideoDetectorForTesting(
        mock_task_runner_->GetMockTickClock(), mock_task_runner_);

    mojo::PendingRemote<mojom::VideoDetectorObserver> video_detector_observer;
    observer_.Bind(video_detector_observer.InitWithNewPipeAndPassReceiver());
    detector_->AddObserver(std::move(video_detector_observer));

    root_frame_sink_ = CreateFrameSink();
    ParentLocalSurfaceIdAllocator* allocator =
        allocators_.GetAllocator(root_frame_sink_->frame_sink_id());
    allocator->GenerateId();
    root_frame_sink_->SubmitCompositorFrame(
        allocator->GetCurrentLocalSurfaceId(), MakeDefaultCompositorFrame());
  }

 protected:
  // Constants placed here for convenience.
  static constexpr int kMinFps = VideoDetector::kMinFramesPerSecond;
  static constexpr gfx::Rect kMinRect =
      gfx::Rect(VideoDetector::kMinDamageWidth,
                VideoDetector::kMinDamageHeight);
  static constexpr base::TimeDelta kMinDuration =
      VideoDetector::kMinVideoDuration;
  static constexpr base::TimeDelta kTimeout = VideoDetector::kMaxVideoTimeout;

  // Move |detector_|'s idea of the current time forward by |delta|.
  void AdvanceTime(base::TimeDelta delta) {
    mock_task_runner_->FastForwardBy(delta);
  }

  void CreateDisplayFrame() {
    surface_aggregator_.Aggregate(root_frame_sink_->last_activated_surface_id(),
                                  mock_task_runner_->NowTicks(),
                                  gfx::OVERLAY_TRANSFORM_NONE);
  }

  void EmbedClient(CompositorFrameSinkSupport* frame_sink) {
    embedded_clients_.insert(frame_sink);
    SubmitRootFrame();
  }

  void SubmitRootFrame() {
    CompositorFrame frame = MakeDefaultCompositorFrame();
    CompositorRenderPass* render_pass = frame.render_pass_list.back().get();
    SharedQuadState* shared_quad_state =
        render_pass->CreateAndAppendSharedQuadState();
    for (CompositorFrameSinkSupport* frame_sink : embedded_clients_) {
      SurfaceDrawQuad* quad =
          render_pass->CreateAndAppendDrawQuad<SurfaceDrawQuad>();
      quad->SetNew(
          shared_quad_state, gfx::Rect(0, 0, 10, 10), gfx::Rect(0, 0, 5, 5),
          SurfaceRange(std::nullopt, frame_sink->last_activated_surface_id()),
          SkColors::kMagenta, /*stretch_content_to_fill_bounds=*/false);
    }
    root_frame_sink_->SubmitCompositorFrame(
        root_frame_sink_->last_activated_local_surface_id(), std::move(frame));
  }

  void SendUpdate(CompositorFrameSinkSupport* frame_sink,
                  const gfx::Rect& damage,
                  bool may_contain_video,
                  bool use_per_quad_damage) {
    LocalSurfaceId local_surface_id =
        frame_sink->last_activated_local_surface_id();
    if (!local_surface_id.is_valid()) {
      ParentLocalSurfaceIdAllocator* allocator =
          allocators_.GetAllocator(frame_sink->frame_sink_id());
      allocator->GenerateId();
      local_surface_id = allocator->GetCurrentLocalSurfaceId();
    }
    frame_sink->SubmitCompositorFrame(
        local_surface_id,
        use_per_quad_damage
            ? MakeDamagedCompositorFrameWithPerQuadDamage(damage,
                                                          may_contain_video)
            : MakeDamagedCompositorFrame(damage, may_contain_video));
  }

  // Report updates to |client| of area |damage| at a rate of
  // |updates_per_second| over |duration|. The first update will be sent
  // immediately and time will have advanced by |duration| upon returning.
  void SendUpdates(CompositorFrameSinkSupport* frame_sink,
                   const gfx::Rect& damage,
                   bool may_contain_video,
                   bool use_per_quad_damage,
                   int updates_per_second,
                   base::TimeDelta duration) {
    const base::TimeDelta time_between_updates =
        base::Seconds(1.0 / updates_per_second);
    for (base::TimeDelta d; d < duration; d += time_between_updates) {
      SendUpdate(frame_sink, damage, may_contain_video, use_per_quad_damage);
      CreateDisplayFrame();
      AdvanceTime(std::min(time_between_updates, duration - d));
    }
  }

  std::unique_ptr<CompositorFrameSinkSupport> CreateFrameSink() {
    constexpr bool is_root = false;
    static uint32_t client_id = 1;
    FrameSinkId frame_sink_id(client_id++, 0);
    frame_sink_manager_.RegisterFrameSinkId(frame_sink_id,
                                            true /* report_activation */);
    auto frame_sink = std::make_unique<CompositorFrameSinkSupport>(
        &frame_sink_client_, &frame_sink_manager_, frame_sink_id, is_root);
    SendUpdate(frame_sink.get(), gfx::Rect(), /*may_contain_video*/ false,
               /*use_per_quad_damage*/ false);
    return frame_sink;
  }

  TestObserver observer_;
  scoped_refptr<base::TestMockTimeTaskRunner> mock_task_runner_;

 protected:
  CompositorFrame MakeDamagedCompositorFrame(const gfx::Rect& damage,
                                             bool may_contain_video) {
    constexpr gfx::Rect kFrameSinkRect(10000, 10000);
    auto frame =
        CompositorFrameBuilder().AddRenderPass(kFrameSinkRect, damage).Build();
    frame.metadata.may_contain_video = may_contain_video;

    return frame;
  }

  CompositorFrame MakeDamagedCompositorFrameWithPerQuadDamage(
      const gfx::Rect& damage,
      bool may_contain_video) {
    constexpr gfx::Rect kFrameSinkRect(10000, 10000);
    auto frame =
        CompositorFrameBuilder()
            .AddRenderPass(RenderPassBuilder(kFrameSinkRect)
                               .AddTextureQuad(kFrameSinkRect, ResourceId(1234))
                               .SetQuadDamageRect(kFrameSinkRect))
            .PopulateResources()
            .Build();
    frame.metadata.may_contain_video = may_contain_video;

    return frame;
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  gpu::SharedImageManager shared_image_manager_;
  gpu::SyncPointManager sync_point_manager_;
  gpu::Scheduler gpu_scheduler_{&sync_point_manager_};
  FrameSinkManagerImpl frame_sink_manager_{FrameSinkManagerImpl::InitParams()};
  DisplayResourceProviderSoftware resource_provider_{&shared_image_manager_,
                                                     &gpu_scheduler_};
  FakeCompositorFrameSinkClient frame_sink_client_;
  SurfaceIdAllocatorSet allocators_;
  SurfaceAggregator surface_aggregator_;
  std::unique_ptr<CompositorFrameSinkSupport> root_frame_sink_;
  std::set<raw_ptr<CompositorFrameSinkSupport, SetExperimental>>
      embedded_clients_;
  raw_ptr<VideoDetector> detector_;
};


constexpr gfx::Rect VideoDetectorTest::kMinRect;
constexpr base::TimeDelta VideoDetectorTest::kMinDuration;
constexpr base::TimeDelta VideoDetectorTest::kTimeout;

// Verify that VideoDetector does not report clients with small damage rects.
TEST_F(VideoDetectorTest, DontReportWhenDamageTooSmall) {
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());

  {
    // Send damages with a smaller width than |kMinRect|. Make sure video
    // activity isn't detected.
    gfx::Rect rect = kMinRect;
    rect.Inset(gfx::Insets::TLBR(0, 0, 0, 1));
    SendUpdates(frame_sink.get(), rect, /*may_contain_video=*/true,
                /*use_per_quad_damage=*/false, 2 * kMinFps, 2 * kMinDuration);
    EXPECT_TRUE(observer_.IsEmpty());
  }

  {
    // Send damages with a smaller height than |kMinRect|. Make sure video
    // activity isn't detected.
    gfx::Rect rect = kMinRect;
    rect.Inset(gfx::Insets::TLBR(0, 0, 0, 1));
    SendUpdates(frame_sink.get(), rect, /*may_contain_video=*/true,
                /*use_per_quad_damage=*/false, 2 * kMinFps, 2 * kMinDuration);
    EXPECT_TRUE(observer_.IsEmpty());
  }
}

// Verify that VideoDetector does not report clients with a low frame rate.
TEST_F(VideoDetectorTest, DontReportWhenFramerateTooLow) {
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, kMinFps - 5, 2 * kMinDuration);
  EXPECT_TRUE(observer_.IsEmpty());
}

// Verify that VideoDetector does not report clients until they have played for
// the minimum necessary duration.
TEST_F(VideoDetectorTest, DontReportWhenNotPlayingLongEnough) {
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, 2 * kMinFps, 0.5 * kMinDuration);
  EXPECT_TRUE(observer_.IsEmpty());

  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, 2 * kMinFps, 0.6 * kMinDuration);
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());
}

// Verify that VideoDetector does not report clients that are not visible
// on screen.
TEST_F(VideoDetectorTest, DontReportWhenClientHidden) {
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();

  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, kMinFps + 5, 2 * kMinDuration);
  EXPECT_TRUE(observer_.IsEmpty());

  // Make the client visible.
  observer_.Reset();
  AdvanceTime(kTimeout);
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, kMinFps + 5, 2 * kMinDuration);
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());
}

TEST_F(VideoDetectorTest, DoesNotReportNonVideoFrames) {
  const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/false,
              /*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
  EXPECT_TRUE(observer_.IsEmpty());
}

// Turn video activity on and off. Make sure the observers are notified
// properly.
TEST_F(VideoDetectorTest, ReportStartAndStop) {
  const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());

  AdvanceTime(kTimeout);
  EXPECT_FALSE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());

  // Start playing again.
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/false, kMinFps + 5, kDuration);
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());

  AdvanceTime(kTimeout);
  EXPECT_FALSE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());
}

// If there are multiple clients playing video, make sure that observers only
// receive a single notification.
TEST_F(VideoDetectorTest, ReportOnceForMultipleClients) {
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink1 = CreateFrameSink();
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink2 = CreateFrameSink();
  EmbedClient(frame_sink1.get());
  EmbedClient(frame_sink2.get());

  // Even if there's video playing in both clients, the observer should only
  // receive a single notification.
  constexpr int fps = 2 * kMinFps;
  constexpr base::TimeDelta time_between_updates = base::Seconds(1.0 / fps);
  for (base::TimeDelta d; d < 2 * kMinDuration; d += time_between_updates) {
    SendUpdate(frame_sink1.get(), kMinRect, /*may_contain_video=*/true,
               /*use_per_quad_damage=*/false);
    SendUpdate(frame_sink2.get(), kMinRect, /*may_contain_video=*/true,
               /*use_per_quad_damage=*/false);
    AdvanceTime(time_between_updates);
    CreateDisplayFrame();
  }
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());
}

TEST_F(VideoDetectorTest, ReportBasedOnPerQuadDamage) {
  const base::TimeDelta kDuration = kMinDuration + base::Milliseconds(100);
  std::unique_ptr<CompositorFrameSinkSupport> frame_sink = CreateFrameSink();
  EmbedClient(frame_sink.get());
  SendUpdates(frame_sink.get(), kMinRect, /*may_contain_video=*/true,
              /*use_per_quad_damage=*/true, kMinFps + 5, kDuration);
  EXPECT_TRUE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());

  AdvanceTime(kTimeout);
  EXPECT_FALSE(observer_.PopState());
  EXPECT_TRUE(observer_.IsEmpty());
}

}  // namespace viz
