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

// The StarboardRenderer class does not perform much active logic. Most of its
// functionality is separated into other classes, which are stored as data
// members of StarboardRenderer. As such, these tests are more like integration
// tests, which verify that the sub-components of StarboardRenderer
// (GeometryChangeHandler, StarboardPlayerManager, etc.) all work together and
// implement the functionality expected of a ::media::Renderer.
//
// One important detail here is that StarboardRenderer does not actually do any
// rendering or decoding directly. That is done by the starboard library itself.
// Here, MockStarboardApiWrapper is used to verify that the correct data is
// being passed to starboard.

#include "chromecast/starboard/media/renderer/starboard_renderer.h"

#include <array>
#include <memory>
#include <utility>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/gtest_util.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chromecast/base/metrics/mock_cast_metrics_helper.h"
#include "chromecast/media/service/mojom/video_geometry_setter.mojom.h"
#include "chromecast/media/service/video_geometry_setter_service.h"
#include "chromecast/starboard/media/cdm/starboard_drm_key_tracker.h"
#include "chromecast/starboard/media/cdm/starboard_drm_wrapper.h"
#include "chromecast/starboard/media/media/mock_starboard_api_wrapper.h"
#include "chromecast/starboard/media/media/starboard_api_wrapper.h"
#include "chromecast/starboard/media/media/test_matchers.h"
#include "media/base/audio_codecs.h"
#include "media/base/buffering_state.h"
#include "media/base/decoder_buffer.h"
#include "media/base/demuxer_stream.h"
#include "media/base/mock_filters.h"
#include "media/base/pipeline_status.h"
#include "media/base/test_helpers.h"
#include "media/base/video_codecs.h"
#include "mojo/core/embedder/embedder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/display/test/test_screen.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/overlay_transform.h"

namespace chromecast {
namespace media {
namespace {

using ::base::test::RunOnceCallback;
using ::media::DemuxerStream;
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::AtLeast;
using ::testing::DoAll;
using ::testing::DoubleEq;
using ::testing::InSequence;
using ::testing::MockFunction;
using ::testing::NiceMock;
using ::testing::NotNull;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::SaveArg;
using ::testing::WithArg;

// Runs any pending tasks that have been posted to the current sequence.
void RunPendingTasks() {
  base::RunLoop run_loop;
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, run_loop.QuitClosure());
  run_loop.Run();
}

// Returns a valid audio config with values arbitrarily set. The values will
// match the values of GetStarboardAudioConfig.
::media::AudioDecoderConfig GetChromiumAudioConfig() {
  return ::media::AudioDecoderConfig(
      ::media::AudioCodec::kAAC, ::media::SampleFormat::kSampleFormatS32,
      ::media::ChannelLayoutConfig::Stereo(), 48000, /*extra_data=*/{},
      ::media::EncryptionScheme::kUnencrypted);
}

// Returns a valid video config with values arbitrarily set. The values will
// match the values of GetStarboardVideoConfig.
::media::VideoDecoderConfig GetChromiumVideoConfig() {
  ::media::VideoDecoderConfig video_config(
      ::media::VideoCodec::kH264, ::media::VideoCodecProfile::H264PROFILE_HIGH,
      ::media::VideoDecoderConfig::AlphaMode::kIsOpaque,
      ::media::VideoColorSpace(1, 1, 1, gfx::ColorSpace::RangeID::LIMITED),
      ::media::VideoTransformation(), gfx::Size(3840, 2160),
      gfx::Rect(0, 0, 3838, 2121), gfx::Size(1920, 1080), /*extra_data=*/{},
      ::media::EncryptionScheme::kUnencrypted);
  video_config.set_level(42);
  return video_config;
}

// Returns a valid starboard audio config with values arbitrarily set. The
// values will match the values of GetChromiumAudioConfig.
StarboardAudioSampleInfo GetStarboardAudioConfig() {
  return StarboardAudioSampleInfo{
      .codec = kStarboardAudioCodecAac,
      .mime = R"-(audio/mp4; codecs="mp4a.40.5")-",
      .format_tag = 0,
      .number_of_channels = 2,
      .samples_per_second = 48000,
      .average_bytes_per_second = (32 / 8) * 48000 * 2,
      .block_alignment = 4,
      .bits_per_sample = 32,
      .audio_specific_config_size = 0,
      .audio_specific_config = nullptr,
  };
}

// Returns a valid starboard video config with values arbitrarily set. The
// values will match the values of GetChromiumVideoConfig.
StarboardVideoSampleInfo GetStarboardVideoConfig() {
  return StarboardVideoSampleInfo{
      .codec = kStarboardVideoCodecH264,
      .mime = R"-(video/mp4; codecs="avc1.64002A")-",
      .max_video_capabilities = "",
      .is_key_frame = false,
      .frame_width = 3840,
      .frame_height = 2160,
      .color_metadata =
          StarboardColorMetadata{
              // These 0 fields signify "unknown" to starboard.
              .bits_per_channel = 0,
              .chroma_subsampling_horizontal = 0,
              .chroma_subsampling_vertical = 0,
              .cb_subsampling_horizontal = 0,
              .cb_subsampling_vertical = 0,
              .chroma_siting_horizontal = 0,
              .chroma_siting_vertical = 0,
              // No HDR metadata, so everything is 0.
              .mastering_metadata = StarboardMediaMasteringMetadata{},
              .max_cll = 0,
              .max_fall = 0,
              .primaries = 1,  // BT.709
              .transfer = 1,   // BT.709
              .matrix = 1,     // BT.709
              .range = 1,      // broadcast range
              .custom_primary_matrix = {0},
          },
  };
}

// Some default configs.
const StarboardAudioSampleInfo kSbAudioConfig = GetStarboardAudioConfig();
const StarboardVideoSampleInfo kSbVideoConfig = GetStarboardVideoConfig();

// Some default audio/video data.
constexpr auto kAudioData = std::to_array<uint8_t>({1, 1, 1, 2, 2, 2});
constexpr auto kVideoData = std::to_array<uint8_t>({1, 2, 3, 4, 5, 6, 7});

// A test fixture is used to abstract away boilerplate (e.g. setting up the task
// environment, creating mocks).
class StarboardRendererTest : public ::testing::Test {
 protected:
  StarboardRendererTest() {
    mojo::core::Init();

    audio_stream_.set_audio_decoder_config(GetChromiumAudioConfig());
    video_stream_.set_video_decoder_config(GetChromiumVideoConfig());

    ON_CALL(starboard_for_drm_, CreateDrmSystem)
        .WillByDefault(Return(&drm_system_));
    StarboardDrmWrapper::SetSingletonForTesting(&starboard_for_drm_);

    ON_CALL(media_resource_, GetAllStreams)
        .WillByDefault(Return(std::vector<raw_ptr<DemuxerStream>>(
            {&audio_stream_, &video_stream_})));
    ON_CALL(media_resource_, GetFirstStream(DemuxerStream::Type::AUDIO))
        .WillByDefault(Return(&audio_stream_));
    ON_CALL(media_resource_, GetFirstStream(DemuxerStream::Type::VIDEO))
        .WillByDefault(Return(&video_stream_));

    ON_CALL(*starboard_,
            CreatePlayer(
                Pointee(MatchesPlayerCreationParam(StarboardPlayerCreationParam{
                    .drm_system = nullptr,
                    .audio_sample_info = kSbAudioConfig,
                    .video_sample_info = kSbVideoConfig,
                    .output_mode = StarboardPlayerOutputMode::
                        kStarboardPlayerOutputModePunchOut})),
                _))
        .WillByDefault(
            DoAll(SaveArg<1>(&sb_player_callbacks_), Return(&sb_player_)));
    ON_CALL(*starboard_, SeekTo(&sb_player_, _, _))
        .WillByDefault(SaveArg<2>(&current_seek_ticket_));

    // Since matchers compare data ptrs by address, it does not matter that all
    // of these buffers use the same audio data (the data is copied, so each
    // DecoderBuffer's data ptr will be different).
    scoped_refptr<::media::DecoderBuffer> audio_buffer_1 =
        ::media::DecoderBuffer::CopyFrom(kAudioData);
    audio_buffer_1->set_timestamp(base::Seconds(0));

    scoped_refptr<::media::DecoderBuffer> audio_buffer_2 =
        ::media::DecoderBuffer::CopyFrom(kAudioData);
    audio_buffer_2->set_timestamp(base::Seconds(1));

    scoped_refptr<::media::DecoderBuffer> audio_buffer_3 =
        ::media::DecoderBuffer::CopyFrom(kAudioData);
    audio_buffer_3->set_timestamp(base::Seconds(5));

    audio_buffers_ = {
        std::move(audio_buffer_1),
        std::move(audio_buffer_2),
        std::move(audio_buffer_3),
    };

    scoped_refptr<::media::DecoderBuffer> video_buffer_1 =
        ::media::DecoderBuffer::CopyFrom(kVideoData);
    video_buffer_1->set_timestamp(base::Seconds(0));

    scoped_refptr<::media::DecoderBuffer> video_buffer_2 =
        ::media::DecoderBuffer::CopyFrom(kVideoData);
    video_buffer_2->set_timestamp(base::Seconds(1));

    scoped_refptr<::media::DecoderBuffer> video_buffer_3 =
        ::media::DecoderBuffer::CopyFrom(kVideoData);
    video_buffer_3->set_timestamp(base::Seconds(5));

    scoped_refptr<::media::DecoderBuffer> video_buffer_4 =
        ::media::DecoderBuffer::CopyFrom(kVideoData);
    video_buffer_4->set_timestamp(base::Seconds(20));

    video_buffers_ = {
        std::move(video_buffer_1),
        std::move(video_buffer_2),
        std::move(video_buffer_3),
        std::move(video_buffer_4),
    };

    CHECK_EQ(audio_buffers_.size(), sb_audio_buffers_.size());
    for (size_t i = 0; i < audio_buffers_.size(); ++i) {
      const auto& audio_buffer = audio_buffers_[i];
      sb_audio_buffers_[i] = StarboardSampleInfo{
          .type = 0,
          .buffer = base::span(*audio_buffer).data(),
          .buffer_size = static_cast<int>(audio_buffer->size()),
          .timestamp = audio_buffer->timestamp().InMicroseconds(),
          .side_data = base::span<const StarboardSampleSideData>(),
          .audio_sample_info = kSbAudioConfig,
          .drm_info = nullptr,
      };
    }

    CHECK_EQ(video_buffers_.size(), sb_video_buffers_.size());
    for (size_t i = 0; i < video_buffers_.size(); ++i) {
      const auto& video_buffer = video_buffers_[i];
      sb_video_buffers_[i] = StarboardSampleInfo{
          .type = 1,
          .buffer = base::span(*video_buffer).data(),
          .buffer_size = static_cast<int>(video_buffer->size()),
          .timestamp = video_buffer->timestamp().InMicroseconds(),
          .side_data = base::span<const StarboardSampleSideData>(),
          .video_sample_info = kSbVideoConfig,
          .drm_info = nullptr,
      };
    }
  }

  ~StarboardRendererTest() override = default;

  // This should be destructed last.
  base::test::TaskEnvironment task_environment_;
  scoped_refptr<base::SequencedTaskRunner> task_runner_ =
      base::SequencedTaskRunner::GetCurrentDefault();
  NiceMock<::media::MockMediaResource> media_resource_;
  NiceMock<::media::MockDemuxerStream> audio_stream_ =
      NiceMock<::media::MockDemuxerStream>(DemuxerStream::Type::AUDIO);
  NiceMock<::media::MockDemuxerStream> video_stream_ =
      NiceMock<::media::MockDemuxerStream>(DemuxerStream::Type::VIDEO);
  NiceMock<::media::MockRendererClient> client_;
  NiceMock<chromecast::metrics::MockCastMetricsHelper> cast_metrics_helper_;
  MockFunction<void(::media::PipelineStatus)> pipeline_status_fn_;
  std::unique_ptr<NiceMock<MockStarboardApiWrapper>> starboard_ =
      std::make_unique<NiceMock<MockStarboardApiWrapper>>();

  // This will be set to the callbacks received by the mock Starboard. These
  // callbacks would normally be used by starboard, e.g. to request new buffers.
  // We can call these callbacks to simulate that behavior.
  const StarboardPlayerCallbackHandler* sb_player_callbacks_ = nullptr;

  // It is undefined behavior to set expectations on a mock after its mock
  // functions have been called. Thus, to be safe we use a separate mock
  // starboard for the StarboardDrmWrapper. All expectations are set before it
  // is passed to the StarboardDrmWrapper (in this fixture's ctor).
  NiceMock<MockStarboardApiWrapper> starboard_for_drm_;

  // Since SbPlayer is used as an opaque void* by cast, we can use any type
  // here. All that matters is the address.
  int sb_player_ = 1;
  // Same for SbDrmSystem.
  int drm_system_ = 2;
  // Captured by starboard when a seek is performed.
  int current_seek_ticket_ = -1;
  display::test::TestScreen test_screen_ =
      display::test::TestScreen(/*create_display=*/true,
                                /*register_screen=*/true);
  VideoGeometrySetterService geometry_setter_service_;

  // We use std::array here so we can have compile-time bounds checking (via
  // std::get()) and avoid for-loops when using these arrays in tests (to keep
  // EXPECT/ASSERT in top-level TEST bodies).
  //
  // Chromium buffers.
  std::array<scoped_refptr<::media::DecoderBuffer>, 3> audio_buffers_;
  std::array<scoped_refptr<::media::DecoderBuffer>, 4> video_buffers_;

  // Starboard buffers corresponding to the chromium buffers.
  std::array<StarboardSampleInfo, 3> sb_audio_buffers_;
  std::array<StarboardSampleInfo, 4> sb_video_buffers_;

  const base::UnguessableToken plane_id_ = base::UnguessableToken::Create();
};

TEST_F(StarboardRendererTest,
       ReadsFromDemuxerStreamsAndPushesBuffersToStarboard) {
  constexpr base::TimeDelta kSeekTime = base::Seconds(0);

  // Since we do not simulate geometry_setter_service_ receiving any geometry
  // changes, the player's bounds should end up getting set to the screen
  // resolution (full screen).
  EXPECT_CALL(
      *starboard_,
      SetPlayerBounds(
          &sb_player_, 0,
          static_cast<int>(display::test::TestScreen::kDefaultScreenBounds.x()),
          static_cast<int>(display::test::TestScreen::kDefaultScreenBounds.y()),
          static_cast<int>(
              display::test::TestScreen::kDefaultScreenBounds.width()),
          static_cast<int>(
              display::test::TestScreen::kDefaultScreenBounds.height())))
      .Times(1);
  EXPECT_CALL(client_, OnBufferingStateChange(
                           ::media::BufferingState::BUFFERING_HAVE_ENOUGH, _))
      .Times(AtLeast(1));
  EXPECT_CALL(*starboard_, SeekTo(&sb_player_, kSeekTime.InMicroseconds(), _))
      .WillOnce(SaveArg<2>(&current_seek_ticket_));

  // Audio buffer expectations.
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeAudio,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<0>(sb_audio_buffers_)))))
      .Times(1);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeAudio,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<1>(sb_audio_buffers_)))))
      .Times(1);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeAudio,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<2>(sb_audio_buffers_)))))
      .Times(1);
  EXPECT_CALL(*starboard_,
              WriteEndOfStream(&sb_player_,
                               StarboardMediaType::kStarboardMediaTypeAudio))
      .Times(1);

  // Video buffer expectations.
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeVideo,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<0>(sb_video_buffers_)))))
      .Times(1);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeVideo,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<1>(sb_video_buffers_)))))
      .Times(1);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeVideo,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<2>(sb_video_buffers_)))))
      .Times(1);
  EXPECT_CALL(
      *starboard_,
      WriteSample(&sb_player_, StarboardMediaType::kStarboardMediaTypeVideo,
                  ElementsAre(MatchesStarboardSampleInfo(
                      std::get<3>(sb_video_buffers_)))))
      .Times(1);
  EXPECT_CALL(*starboard_,
              WriteEndOfStream(&sb_player_,
                               StarboardMediaType::kStarboardMediaTypeVideo))
      .Times(1);

  // Provide the audio buffers from the audio DemuxerStream, ending with EOS.
  EXPECT_CALL(audio_stream_, OnRead)
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<0>(audio_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<1>(audio_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<2>(audio_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {::media::DecoderBuffer::CreateEOSBuffer()})));

  // Provide the video buffers from the video DemuxerStream, ending with EOS.
  EXPECT_CALL(video_stream_, OnRead)
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<0>(video_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<1>(video_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<2>(video_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {std::get<3>(video_buffers_)})))
      .WillOnce(
          RunOnceCallback<0>(DemuxerStream::Status::kOk,
                             std::vector<scoped_refptr<::media::DecoderBuffer>>(
                                 {::media::DecoderBuffer::CreateEOSBuffer()})));

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  EXPECT_CALL(pipeline_status_fn_,
              Call(HasStatusCode(::media::PipelineStatusCodes::PIPELINE_OK)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  renderer.StartPlayingFrom(kSeekTime);

  ASSERT_THAT(sb_player_callbacks_, NotNull());
  ASSERT_THAT(sb_player_callbacks_->context, NotNull());
  ASSERT_THAT(sb_player_callbacks_->decoder_status_fn, NotNull());
  ASSERT_THAT(sb_player_callbacks_->player_status_fn, NotNull());

  // Simulate starboard requesting audio buffers. The +1 is to represent the
  // expected EOS after all buffers have been returned.
  for (size_t i = 0; i < audio_buffers_.size() + 1; ++i) {
    sb_player_callbacks_->decoder_status_fn(
        &sb_player_, sb_player_callbacks_->context,
        StarboardMediaType::kStarboardMediaTypeAudio,
        StarboardDecoderState::kStarboardDecoderStateNeedsData,
        current_seek_ticket_);
    RunPendingTasks();
  }

  // Simulate starboard requesting video buffers. The +1 is to represent the
  // expected EOS after all buffers have been returned.
  for (size_t i = 0; i < video_buffers_.size() + 1; ++i) {
    sb_player_callbacks_->decoder_status_fn(
        &sb_player_, sb_player_callbacks_->context,
        StarboardMediaType::kStarboardMediaTypeVideo,
        StarboardDecoderState::kStarboardDecoderStateNeedsData,
        current_seek_ticket_);
    RunPendingTasks();
  }

  // This should inform the RendererClient that we have enough data buffered.
  sb_player_callbacks_->player_status_fn(
      &sb_player_, sb_player_callbacks_->context,
      StarboardPlayerState::kStarboardPlayerStatePresenting,
      current_seek_ticket_);
}

TEST_F(StarboardRendererTest,
       FlushSeeksToCurrentMediaTimeAndSetsPlaybackRateToZero) {
  constexpr base::TimeDelta kSeekTime = base::Seconds(0);
  constexpr base::TimeDelta kMediaTime = base::Seconds(1);
  constexpr double kPlaybackRate = 1.1;

  EXPECT_CALL(*starboard_, SeekTo(&sb_player_, kSeekTime.InMicroseconds(), _))
      .WillOnce(SaveArg<2>(&current_seek_ticket_));
  EXPECT_CALL(*starboard_, SeekTo(&sb_player_, kMediaTime.InMicroseconds(), _))
      .WillOnce(SaveArg<2>(&current_seek_ticket_));

  // The playback rate should be set to zero twice:
  // * Once when we start playback
  // * Once when we flush
  EXPECT_CALL(*starboard_, SetPlaybackRate(&sb_player_, DoubleEq(0))).Times(2);
  // The playback rate should be set to 1.1 when we explicitly set the playback
  // rate.
  EXPECT_CALL(*starboard_,
              SetPlaybackRate(&sb_player_, DoubleEq(kPlaybackRate)))
      .Times(1);
  EXPECT_CALL(*starboard_, GetPlayerInfo(&sb_player_, NotNull()))
      .Times(AtLeast(1))
      .WillRepeatedly(
          WithArg<1>([kMediaTime](StarboardPlayerInfo* player_info) {
            *player_info = {};
            player_info->current_media_timestamp_micros =
                kMediaTime.InMicroseconds();
          }));

  // Ignore unrelated metrics calls.
  EXPECT_CALL(cast_metrics_helper_, RecordApplicationEvent(_))
      .Times(AnyNumber());
  // Should be called at flush, but not at destruction.
  EXPECT_CALL(cast_metrics_helper_,
              RecordApplicationEvent("Cast.Platform.Ended"))
      .Times(1);
  // Should be called when setting the playback rate to a nonzero value.
  EXPECT_CALL(cast_metrics_helper_,
              RecordApplicationEvent("Cast.Platform.Playing"))
      .Times(AtLeast(1));

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  EXPECT_CALL(pipeline_status_fn_,
              Call(HasStatusCode(::media::PipelineStatusCodes::PIPELINE_OK)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  renderer.StartPlayingFrom(kSeekTime);
  renderer.SetPlaybackRate(kPlaybackRate);

  MockFunction<void()> flush_cb;
  EXPECT_CALL(flush_cb, Call).Times(1);
  renderer.Flush(base::BindLambdaForTesting(flush_cb.AsStdFunction()));
  RunPendingTasks();
}

TEST_F(StarboardRendererTest, SetVolumeForwardsVolumeChangeToStarboard) {
  constexpr base::TimeDelta kSeekTime = base::Seconds(0);
  constexpr float kVolume = 0.77f;

  EXPECT_CALL(*starboard_, SetVolume(&sb_player_, DoubleEq(kVolume))).Times(1);

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  EXPECT_CALL(pipeline_status_fn_,
              Call(HasStatusCode(::media::PipelineStatusCodes::PIPELINE_OK)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  renderer.StartPlayingFrom(kSeekTime);
  renderer.SetVolume(kVolume);
}

TEST_F(StarboardRendererTest, ForwardsGeometryChangesToStarboard) {
  const gfx::RectF geometry_1(0, 0, 1920, 1080);
  const gfx::RectF geometry_2(0, 0, 720, 1280);
  const gfx::OverlayTransform transform =
      gfx::OverlayTransform::OVERLAY_TRANSFORM_NONE;

  {
    InSequence s;
    EXPECT_CALL(
        *starboard_,
        SetPlayerBounds(&sb_player_, 0, static_cast<int>(geometry_1.x()),
                        static_cast<int>(geometry_1.y()),
                        static_cast<int>(geometry_1.width()),
                        static_cast<int>(geometry_1.height())))
        .Times(1);

    EXPECT_CALL(
        *starboard_,
        SetPlayerBounds(&sb_player_, 0, static_cast<int>(geometry_2.x()),
                        static_cast<int>(geometry_2.y()),
                        static_cast<int>(geometry_2.width()),
                        static_cast<int>(geometry_2.height())))
        .Times(1);
  }

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);
  RunPendingTasks();

  static_cast<mojom::VideoGeometrySetter*>(&geometry_setter_service_)
      ->SetVideoGeometry(geometry_1, transform, plane_id_);
  RunPendingTasks();

  EXPECT_CALL(pipeline_status_fn_,
              Call(HasStatusCode(::media::PipelineStatusCodes::PIPELINE_OK)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  static_cast<mojom::VideoGeometrySetter*>(&geometry_setter_service_)
      ->SetVideoGeometry(geometry_2, transform, plane_id_);
  RunPendingTasks();
}

TEST_F(StarboardRendererTest, GetMediaTimeReadsCurrentMediaTimeFromStarboard) {
  constexpr base::TimeDelta kMediaTime = base::Seconds(1);

  EXPECT_CALL(*starboard_, GetPlayerInfo(&sb_player_, NotNull()))
      .Times(AtLeast(1))
      .WillRepeatedly(
          WithArg<1>([kMediaTime](StarboardPlayerInfo* player_info) {
            *player_info = {};
            player_info->current_media_timestamp_micros =
                kMediaTime.InMicroseconds();
          }));

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  EXPECT_CALL(pipeline_status_fn_,
              Call(HasStatusCode(::media::PipelineStatusCodes::PIPELINE_OK)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  EXPECT_EQ(renderer.GetMediaTime(), kMediaTime);
}

// Regression test for crbug.com/503617302.
TEST_F(StarboardRendererTest, DoesNotSetSbPlayerOnInitializationFailure) {
  const gfx::RectF geometry(0, 0, 1920, 1080);
  const gfx::OverlayTransform transform =
      gfx::OverlayTransform::OVERLAY_TRANSFORM_NONE;

  // Set up the mock to fail player creation.
  EXPECT_CALL(*starboard_, CreatePlayer(_, _)).WillOnce(Return(nullptr));

  // The player's bounds should NOT be set after a failed initialization, even
  // if geometry changes occur.
  EXPECT_CALL(*starboard_, SetPlayerBounds(_, _, _, _, _, _)).Times(0);

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);
  RunPendingTasks();

  EXPECT_CALL(
      pipeline_status_fn_,
      Call(HasStatusCode(
          ::media::PipelineStatusCodes::PIPELINE_ERROR_INITIALIZATION_FAILED)))
      .Times(1);
  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  // This geometry change should be ignored by the geometry_change_handler_,
  // which was never initialized with a player.
  static_cast<mojom::VideoGeometrySetter*>(&geometry_setter_service_)
      ->SetVideoGeometry(geometry, transform, plane_id_);
  RunPendingTasks();
}

TEST_F(StarboardRendererTest, Initialize_Twice_Crashes) {
  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  EXPECT_CHECK_DEATH(renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction())));
}

TEST_F(StarboardRendererTest, SetPlaybackRateReportsMetric) {
  // Ignore unrelated metrics calls.
  EXPECT_CALL(cast_metrics_helper_, RecordApplicationEvent(_))
      .Times(AnyNumber());
  // Should be called at destruction.
  EXPECT_CALL(cast_metrics_helper_,
              RecordApplicationEvent("Cast.Platform.Ended"))
      .Times(1);
  // Should be called when setting the playback rate to a nonzero value.
  EXPECT_CALL(cast_metrics_helper_,
              RecordApplicationEvent("Cast.Platform.Playing"))
      .Times(1);
  // Should be called when setting the playback rate to 0.
  EXPECT_CALL(cast_metrics_helper_,
              RecordApplicationEvent("Cast.Platform.Pause"))
      .Times(1);

  StarboardRenderer renderer(std::move(starboard_), task_runner_, plane_id_,
                             /*enable_buffering=*/true,
                             &geometry_setter_service_, &cast_metrics_helper_);

  renderer.Initialize(
      &media_resource_, &client_,
      base::BindLambdaForTesting(pipeline_status_fn_.AsStdFunction()));
  RunPendingTasks();

  renderer.SetPlaybackRate(1.0);
  renderer.SetPlaybackRate(0.0);
}

}  // namespace
}  // namespace media
}  // namespace chromecast
