// 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 "third_party/blink/renderer/modules/webcodecs/video_encoder.h"

#include "base/run_loop.h"
#include "media/base/mock_filters.h"
#include "media/video/video_encoder_info.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_rect_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_cssimagevalue_htmlcanvaselement_htmlimageelement_htmlvideoelement_imagebitmap_offscreencanvas_svgimageelement_videoframe.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_encoder_config.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_encoder_encode_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_encoder_init.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_video_frame_init.h"
#include "third_party/blink/renderer/core/html/canvas/image_data.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/core/testing/mock_function_scope.h"
#include "third_party/blink/renderer/core/typed_arrays/dom_array_buffer.h"
#include "third_party/blink/renderer/modules/webcodecs/codec_pressure_manager.h"
#include "third_party/blink/renderer/modules/webcodecs/codec_pressure_manager_provider.h"
#include "third_party/blink/renderer/modules/webcodecs/video_encoder.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"

namespace blink {

namespace {

using testing::_;
using testing::ByMove;
using testing::DoAll;
using testing::Return;
using testing::SaveArg;
using testing::WithArgs;

ACTION_P(RunClosure, closure) {
  scheduler::GetSequencedTaskRunnerForTesting()->PostTask(FROM_HERE,
                                                          std::move(closure));
}

class MockVideoEncoder : public VideoEncoder {
 public:
  MockVideoEncoder(ScriptState* script_state,
                   const VideoEncoderInit* init,
                   ExceptionState& exception_state)
      : VideoEncoder(script_state, init, exception_state) {}
  ~MockVideoEncoder() override = default;

  MOCK_METHOD((media::EncoderStatus::Or<std::unique_ptr<media::VideoEncoder>>),
              CreateMediaVideoEncoder,
              (const ParsedConfig& config,
               media::GpuVideoAcceleratorFactories* gpu_factories,
               bool& is_platform_encoder),
              (override));
  MOCK_METHOD(std::unique_ptr<media::VideoEncoderMetricsProvider>,
              CreateVideoEncoderMetricsProvider,
              (),
              (const));

  // CallOnMediaENcoderInfoChanged() is necessary for VideoEncoderTest to call
  // VideoEncoder::OnMediaEncoderInfoChanged() because the function is a private
  // and VideoEncoderTest is not a friend of VideoEncoder.
  void CallOnMediaEncoderInfoChanged(
      const media::VideoEncoderInfo& encoder_info) {
    VideoEncoder::OnMediaEncoderInfoChanged(encoder_info);
  }
};

class VideoEncoderTest : public testing::Test {
 public:
  VideoEncoderTest() = default;
  ~VideoEncoderTest() override = default;
  test::TaskEnvironment task_environment_;
};

constexpr gfx::Size kEncodeSize(80, 60);

TEST_F(VideoEncoderTest, GetReadbackYuvColorSpace) {
  EXPECT_EQ(GetReadbackYuvColorSpace(gfx::ColorSpace()),
            gfx::ColorSpace::CreateREC709());

  EXPECT_EQ(GetReadbackYuvColorSpace(gfx::ColorSpace::CreateSRGB()),
            gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT709,
                            gfx::ColorSpace::TransferID::SRGB,
                            gfx::ColorSpace::MatrixID::BT709,
                            gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(
      GetReadbackYuvColorSpace(gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::SRGB,
          gfx::ColorSpace::MatrixID::GBR, gfx::ColorSpace::RangeID::FULL)),
      gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::SRGB,
          gfx::ColorSpace::MatrixID::BT709, gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(
      GetReadbackYuvColorSpace(gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::BT470M,
          gfx::ColorSpace::TransferID::GAMMA22, gfx::ColorSpace::MatrixID::RGB,
          gfx::ColorSpace::RangeID::FULL)),
      gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT470M,
                      gfx::ColorSpace::TransferID::GAMMA22,
                      gfx::ColorSpace::MatrixID::FCC,
                      gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(
      GetReadbackYuvColorSpace(gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::BT470BG,
          gfx::ColorSpace::TransferID::GAMMA28, gfx::ColorSpace::MatrixID::RGB,
          gfx::ColorSpace::RangeID::FULL)),
      gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT470BG,
                      gfx::ColorSpace::TransferID::GAMMA28,
                      gfx::ColorSpace::MatrixID::BT470BG,
                      gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(
      GetReadbackYuvColorSpace(gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::SMPTE170M,
          gfx::ColorSpace::TransferID::SMPTE170M,
          gfx::ColorSpace::MatrixID::RGB, gfx::ColorSpace::RangeID::FULL)),
      gfx::ColorSpace(gfx::ColorSpace::PrimaryID::SMPTE170M,
                      gfx::ColorSpace::TransferID::SMPTE170M,
                      gfx::ColorSpace::MatrixID::SMPTE170M,
                      gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(
      GetReadbackYuvColorSpace(gfx::ColorSpace(
          gfx::ColorSpace::PrimaryID::SMPTE240M,
          gfx::ColorSpace::TransferID::SMPTE240M,
          gfx::ColorSpace::MatrixID::RGB, gfx::ColorSpace::RangeID::FULL)),
      gfx::ColorSpace(gfx::ColorSpace::PrimaryID::SMPTE240M,
                      gfx::ColorSpace::TransferID::SMPTE240M,
                      gfx::ColorSpace::MatrixID::SMPTE240M,
                      gfx::ColorSpace::RangeID::LIMITED));

  EXPECT_EQ(GetReadbackYuvColorSpace(gfx::ColorSpace::CreateHDR10()),
            gfx::ColorSpace(gfx::ColorSpace::PrimaryID::BT2020,
                            gfx::ColorSpace::TransferID::PQ,
                            gfx::ColorSpace::MatrixID::BT2020_NCL,
                            gfx::ColorSpace::RangeID::LIMITED));

  const gfx::ColorSpace yuv_color_space(
      gfx::ColorSpace::PrimaryID::BT709, gfx::ColorSpace::TransferID::BT709,
      gfx::ColorSpace::MatrixID::SMPTE170M, gfx::ColorSpace::RangeID::FULL);
  EXPECT_EQ(
      GetReadbackYuvColorSpace(yuv_color_space),
      yuv_color_space.GetWithMatrixAndRange(yuv_color_space.GetMatrixID(),
                                            gfx::ColorSpace::RangeID::LIMITED));
}

VideoEncoderConfig* CreateConfig() {
  auto* config = MakeGarbageCollected<VideoEncoderConfig>();
  config->setCodec("vp8");
  config->setWidth(kEncodeSize.width());
  config->setHeight(kEncodeSize.height());
  return config;
}

VideoEncoder* CreateEncoder(ScriptState* script_state,
                            const VideoEncoderInit* init,
                            ExceptionState& exception_state) {
  return MakeGarbageCollected<VideoEncoder>(script_state, init,
                                            exception_state);
}

MockVideoEncoder* CreateMockEncoder(ScriptState* script_state,
                                    VideoEncoderInit* init,
                                    ExceptionState& exception_state) {
  return MakeGarbageCollected<MockVideoEncoder>(script_state, init,
                                                exception_state);
}

VideoEncoderInit* CreateInit(ScriptState* script_state,
                             ScriptFunction* output_callback,
                             ScriptFunction* error_callback) {
  auto* init = MakeGarbageCollected<VideoEncoderInit>();
  init->setOutput(V8EncodedVideoChunkOutputCallback::Create(
      output_callback->ToV8Function(script_state)));
  init->setError(V8WebCodecsErrorCallback::Create(
      error_callback->ToV8Function(script_state)));
  return init;
}

VideoFrame* MakeVideoFrame(
    ScriptState* script_state,
    int width,
    int height,
    int timestamp,
    std::optional<gfx::Rect> visible_rect = std::nullopt) {
  std::vector<uint8_t> data(width * height * 4);
  NotShared<DOMUint8ClampedArray> data_u8(DOMUint8ClampedArray::Create(data));

  ImageData* image_data =
      ImageData::Create(data_u8, width, IGNORE_EXCEPTION_FOR_TESTING);

  if (!image_data)
    return nullptr;

  ImageBitmap* image_bitmap = MakeGarbageCollected<ImageBitmap>(
      image_data, std::nullopt, ImageBitmapOptions::Create());

  VideoFrameInit* video_frame_init = VideoFrameInit::Create();
  video_frame_init->setTimestamp(timestamp);
  if (visible_rect) {
    auto* dom_rect = DOMRectInit::Create();
    dom_rect->setX(visible_rect->x());
    dom_rect->setY(visible_rect->y());
    dom_rect->setWidth(visible_rect->width());
    dom_rect->setHeight(visible_rect->height());
    video_frame_init->setVisibleRect(dom_rect);
  }

  auto* source = MakeGarbageCollected<V8CanvasImageSource>(image_bitmap);

  return VideoFrame::Create(script_state, source, video_frame_init,
                            IGNORE_EXCEPTION_FOR_TESTING);
}

TEST_F(VideoEncoderTest, RejectFlushAfterClose) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);
  auto* init = CreateInit(script_state, mock_function.ExpectNoCall(),
                          mock_function.ExpectNoCall());
  auto* encoder = CreateEncoder(script_state, init, es);
  ASSERT_FALSE(es.HadException());

  auto* config = CreateConfig();
  encoder->configure(config, es);
  ASSERT_FALSE(es.HadException());
  {
    // We need this to make sure that configuration has completed.
    auto promise = encoder->flush(es);
    ScriptPromiseTester tester(script_state, promise);
    tester.WaitUntilSettled();
    ASSERT_TRUE(tester.IsFulfilled());
  }

  encoder->encode(
      MakeVideoFrame(script_state, config->width(), config->height(), 1),
      MakeGarbageCollected<VideoEncoderEncodeOptions>(), es);

  ScriptPromiseTester tester(script_state, encoder->flush(es));
  ASSERT_FALSE(es.HadException());
  ASSERT_FALSE(tester.IsFulfilled());
  ASSERT_FALSE(tester.IsRejected());

  encoder->close(es);

  ThreadState::Current()->CollectAllGarbageForTesting();

  tester.WaitUntilSettled();
  ASSERT_TRUE(tester.IsRejected());
}

TEST_F(VideoEncoderTest, CodecReclamation) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  auto& pressure_manager_provider =
      CodecPressureManagerProvider::From(*v8_scope.GetExecutionContext());

  auto* decoder_pressure_manager =
      pressure_manager_provider.GetDecoderPressureManager();
  auto* encoder_pressure_manager =
      pressure_manager_provider.GetEncoderPressureManager();

  // Create a video encoder.
  auto* init = CreateInit(script_state, mock_function.ExpectNoCall(),
                          mock_function.ExpectNoCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);
  ASSERT_FALSE(es.HadException());

  // Simulate backgrounding to enable reclamation.
  if (!encoder->is_backgrounded_for_testing()) {
    encoder->SimulateLifecycleStateForTesting(
        scheduler::SchedulingLifecycleState::kHidden);
    DCHECK(encoder->is_backgrounded_for_testing());
  }

  // Make sure VideoEncoder doesn't apply pressure by default.
  EXPECT_FALSE(encoder->is_applying_codec_pressure());
  ASSERT_EQ(0u, encoder_pressure_manager->pressure_for_testing());
  ASSERT_EQ(0u, decoder_pressure_manager->pressure_for_testing());

  auto* config = CreateConfig();
  media::MockVideoEncoder* first_mock_media_encoder = nullptr;
  {
    base::RunLoop run_loop;
    auto media_encoder = std::make_unique<media::MockVideoEncoder>();
    first_mock_media_encoder = media_encoder.get();

    EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
        .WillOnce(DoAll(
            [encoder = encoder]() {
              media::VideoEncoderInfo info;
              info.implementation_name = "MockEncoderName";
              info.is_hardware_accelerated = true;
              encoder->CallOnMediaEncoderInfoChanged(info);
            },
            Return(ByMove(std::unique_ptr<media::VideoEncoder>(
                std::move(media_encoder))))));
    EXPECT_CALL(*encoder, CreateVideoEncoderMetricsProvider())
        .WillOnce(Return(ByMove(
            std::make_unique<media::MockVideoEncoderMetricsProvider>())));
    EXPECT_CALL(*first_mock_media_encoder, Initialize(_, _, _, _, _))
        .WillOnce(
            WithArgs<4>([quit_closure = run_loop.QuitWhenIdleClosure()](
                            media::VideoEncoder::EncoderStatusCB done_cb) {
              scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                  FROM_HERE, blink::BindOnce(std::move(done_cb),
                                             media::EncoderStatus::Codes::kOk));
              scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                  FROM_HERE, std::move(quit_closure));
            }));

    encoder->configure(config, es);
    ASSERT_FALSE(es.HadException());
    run_loop.Run();
  }

  // Make sure VideoEncoders apply pressure when configured with a HW encoder.
  EXPECT_TRUE(encoder->is_applying_codec_pressure());
  ASSERT_EQ(1u, encoder_pressure_manager->pressure_for_testing());
  ASSERT_EQ(0u, decoder_pressure_manager->pressure_for_testing());

  // Change codec to avoid a pure reconfigure.
  config->setCodec("avc1.42001E");
  {
    base::RunLoop run_loop;

    auto media_encoder = std::make_unique<media::MockVideoEncoder>();
    media::MockVideoEncoder* second_media_encoder = media_encoder.get();

    EXPECT_CALL(*first_mock_media_encoder, Flush(_))
        .WillOnce([](media::VideoEncoder::EncoderStatusCB done_cb) {
          scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
              FROM_HERE, blink::BindOnce(std::move(done_cb),
                                         media::EncoderStatus::Codes::kOk));
        });
    EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
        .WillOnce(DoAll(
            [encoder = encoder]() {
              media::VideoEncoderInfo info;
              info.implementation_name = "MockEncoderName";
              info.is_hardware_accelerated = false;
              encoder->CallOnMediaEncoderInfoChanged(info);
            },
            Return(ByMove(std::unique_ptr<media::VideoEncoder>(
                std::move(media_encoder))))));
    EXPECT_CALL(*second_media_encoder, Initialize(_, _, _, _, _))
        .WillOnce(
            WithArgs<4>([quit_closure = run_loop.QuitWhenIdleClosure()](
                            media::VideoEncoder::EncoderStatusCB done_cb) {
              scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                  FROM_HERE, blink::BindOnce(std::move(done_cb),
                                             media::EncoderStatus::Codes::kOk));
              scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                  FROM_HERE, std::move(quit_closure));
            }));

    encoder->configure(config, es);
    ASSERT_FALSE(es.HadException());
    run_loop.Run();
  }

  // Make sure the pressure is released when reconfigured with a SW encoder.
  EXPECT_FALSE(encoder->is_applying_codec_pressure());
  ASSERT_EQ(0u, encoder_pressure_manager->pressure_for_testing());
  ASSERT_EQ(0u, decoder_pressure_manager->pressure_for_testing());
}

TEST_F(
    VideoEncoderTest,
    ConfigureAndEncode_CallVideoEncoderMetricsProviderInitializeAndIncrementEncodedFrameCount) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  // Create a video encoder.
  auto* init = CreateInit(script_state, mock_function.ExpectCall(),
                          mock_function.ExpectNoCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);

  auto* config = CreateConfig();
  base::RunLoop run_loop;
  media::VideoEncoder::OutputCB output_cb;
  auto media_encoder = std::make_unique<media::MockVideoEncoder>();
  media::MockVideoEncoder* mock_media_encoder = media_encoder.get();
  auto encoder_metrics_provider =
      std::make_unique<media::MockVideoEncoderMetricsProvider>();
  media::MockVideoEncoderMetricsProvider* mock_encoder_metrics_provider =
      encoder_metrics_provider.get();
  EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
      .WillOnce(DoAll(
          [encoder = encoder]() {
            media::VideoEncoderInfo info;
            info.implementation_name = "MockEncoderName";
            info.is_hardware_accelerated = false;
            encoder->CallOnMediaEncoderInfoChanged(info);
          },
          Return(ByMove(std::unique_ptr<media::VideoEncoder>(
              std::move(media_encoder))))));
  EXPECT_CALL(*encoder, CreateVideoEncoderMetricsProvider())
      .WillOnce(Return(ByMove(std::move(encoder_metrics_provider))));
  EXPECT_CALL(
      *mock_encoder_metrics_provider,
      MockInitialize(media::VideoCodecProfile::VP8PROFILE_ANY, kEncodeSize,
                     false, media::SVCScalabilityMode::kL1T1));
  EXPECT_CALL(*mock_media_encoder, Initialize(_, _, _, _, _))
      .WillOnce(DoAll(
          SaveArg<3>(&output_cb),
          WithArgs<4>([quit_closure = run_loop.QuitWhenIdleClosure()](
                          media::VideoEncoder::EncoderStatusCB done_cb) {
            scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                FROM_HERE, blink::BindOnce(std::move(done_cb),
                                           media::EncoderStatus::Codes::kOk));
          })));
  encoder->configure(config, es);
  EXPECT_CALL(*mock_media_encoder, Encode(_, _, _))
      .WillOnce(WithArgs<2>([output_cb = &output_cb](
                                media::VideoEncoder::EncoderStatusCB done_cb) {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, blink::BindOnce(std::move(done_cb),
                                       media::EncoderStatus::Codes::kOk));
        media::VideoEncoderOutput out;
        out.data = base::HeapArray<uint8_t>::Uninit(100);
        out.key_frame = true;
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE,
            blink::BindOnce(*output_cb, std::move(out), std::nullopt));
      }));

  EXPECT_CALL(*mock_encoder_metrics_provider, MockIncrementEncodedFrameCount())
      .WillOnce([quit_closure = run_loop.QuitWhenIdleClosure()] {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, std::move(quit_closure));
      });
  encoder->encode(
      MakeVideoFrame(script_state, config->width(), config->height(), 1),
      MakeGarbageCollected<VideoEncoderEncodeOptions>(), es);
  run_loop.Run();
}

TEST_F(VideoEncoderTest,
       ConfigureTwice_CallVideoEncoderMetricsProviderInitializeTwice) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  // Create a video encoder.
  auto* init = CreateInit(script_state, mock_function.ExpectNoCall(),
                          mock_function.ExpectNoCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);

  auto* config = CreateConfig();
  base::RunLoop run_loop;
  media::VideoEncoder::OutputCB output_cb;
  auto media_encoder = std::make_unique<media::MockVideoEncoder>();
  media::MockVideoEncoder* mock_media_encoder = media_encoder.get();
  auto encoder_metrics_provider =
      std::make_unique<media::MockVideoEncoderMetricsProvider>();
  media::MockVideoEncoderMetricsProvider* mock_encoder_metrics_provider =
      encoder_metrics_provider.get();
  EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
      .WillOnce(DoAll(
          [encoder = encoder]() {
            media::VideoEncoderInfo info;
            info.implementation_name = "MockEncoderName";
            info.is_hardware_accelerated = false;
            encoder->CallOnMediaEncoderInfoChanged(info);
          },
          Return(ByMove(std::unique_ptr<media::VideoEncoder>(
              std::move(media_encoder))))));
  EXPECT_CALL(*encoder, CreateVideoEncoderMetricsProvider())
      .WillOnce(Return(ByMove(std::move(encoder_metrics_provider))));
  EXPECT_CALL(
      *mock_encoder_metrics_provider,
      MockInitialize(media::VideoCodecProfile::VP8PROFILE_ANY, kEncodeSize,
                     false, media::SVCScalabilityMode::kL1T1));
  EXPECT_CALL(*mock_media_encoder, Initialize(_, _, _, _, _))
      .WillOnce(DoAll(
          SaveArg<3>(&output_cb),
          WithArgs<4>([](media::VideoEncoder::EncoderStatusCB done_cb) {
            scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                FROM_HERE, blink::BindOnce(std::move(done_cb),
                                           media::EncoderStatus::Codes::kOk));
          })));
  encoder->configure(config, es);
  EXPECT_CALL(*mock_media_encoder, Flush(_))
      .WillOnce([](media::VideoEncoder::EncoderStatusCB done_cb) {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, blink::BindOnce(std::move(done_cb),
                                       media::EncoderStatus::Codes::kOk));
      });
  EXPECT_CALL(
      *mock_encoder_metrics_provider,
      MockInitialize(media::VideoCodecProfile::VP8PROFILE_ANY, kEncodeSize,
                     false, media::SVCScalabilityMode::kL1T1));
  EXPECT_CALL(*mock_media_encoder, ChangeOptions(_, _, _))
      .WillOnce(WithArgs<2>([quit_closure = run_loop.QuitWhenIdleClosure()](
                                media::VideoEncoder::EncoderStatusCB done_cb) {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, blink::BindOnce(std::move(done_cb),
                                       media::EncoderStatus::Codes::kOk));
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, std::move(quit_closure));
      }));
  encoder->configure(config, es);
  run_loop.Run();
}

TEST_F(VideoEncoderTest,
       InitializeFailure_CallVideoEncoderMetricsProviderSetError) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  // Create a video encoder.
  auto* init = CreateInit(script_state, mock_function.ExpectNoCall(),
                          mock_function.ExpectCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);

  auto* config = CreateConfig();
  base::RunLoop run_loop;
  media::VideoEncoder::OutputCB output_cb;
  auto media_encoder = std::make_unique<media::MockVideoEncoder>();
  media::MockVideoEncoder* mock_media_encoder = media_encoder.get();
  auto encoder_metrics_provider =
      std::make_unique<media::MockVideoEncoderMetricsProvider>();
  media::MockVideoEncoderMetricsProvider* mock_encoder_metrics_provider =
      encoder_metrics_provider.get();
  EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
      .WillOnce(DoAll(
          [encoder = encoder]() {
            media::VideoEncoderInfo info;
            info.implementation_name = "MockEncoderName";
            info.is_hardware_accelerated = false;
            encoder->CallOnMediaEncoderInfoChanged(info);
          },
          Return(ByMove(std::unique_ptr<media::VideoEncoder>(
              std::move(media_encoder))))));
  EXPECT_CALL(*encoder, CreateVideoEncoderMetricsProvider())
      .WillOnce(Return(ByMove(std::move(encoder_metrics_provider))));
  EXPECT_CALL(
      *mock_encoder_metrics_provider,
      MockInitialize(media::VideoCodecProfile::VP8PROFILE_ANY, kEncodeSize,
                     false, media::SVCScalabilityMode::kL1T1));
  EXPECT_CALL(*mock_media_encoder, Initialize(_, _, _, _, _))
      .WillOnce(WithArgs<4>([quit_closure = run_loop.QuitWhenIdleClosure()](
                                media::VideoEncoder::EncoderStatusCB done_cb) {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE,
            blink::BindOnce(
                std::move(done_cb),
                media::EncoderStatus::Codes::kEncoderUnsupportedConfig));
      }));
  EXPECT_CALL(*mock_encoder_metrics_provider, MockSetError(_))
      .WillOnce(RunClosure(run_loop.QuitWhenIdleClosure()));
  encoder->configure(config, es);
  run_loop.Run();
}

TEST_F(VideoEncoderTest, NoAvailableMediaVideoEncoder) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  // Create a video encoder.
  auto* init = CreateInit(script_state, mock_function.ExpectNoCall(),
                          mock_function.ExpectCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);
  auto* config = CreateConfig();
  EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
      .WillOnce(Return(media::EncoderStatus(
          media::EncoderStatus::Codes::kEncoderUnsupportedProfile)));
  encoder->configure(config, es);
}

TEST_F(VideoEncoderTest, EncodePreservesVisibleRect) {
  V8TestingScope v8_scope;
  auto& es = v8_scope.GetExceptionState();
  auto* script_state = v8_scope.GetScriptState();

  MockFunctionScope mock_function(script_state);

  auto* init = CreateInit(script_state, mock_function.ExpectCall(),
                          mock_function.ExpectNoCall());
  auto* encoder = CreateMockEncoder(script_state, init, es);

  const gfx::Rect kVisibleRect(10, 10, 60, 40);
  auto* config = CreateConfig();
  config->setWidth(kVisibleRect.width());
  config->setHeight(kVisibleRect.height());

  base::RunLoop run_loop;
  media::VideoEncoder::OutputCB output_cb;
  auto media_encoder = std::make_unique<media::MockVideoEncoder>();
  media::MockVideoEncoder* mock_media_encoder = media_encoder.get();
  auto encoder_metrics_provider =
      std::make_unique<media::MockVideoEncoderMetricsProvider>();

  EXPECT_CALL(*encoder, CreateMediaVideoEncoder(_, _, _))
      .WillOnce(DoAll(
          [encoder = encoder]() {
            media::VideoEncoderInfo info;
            info.implementation_name = "MockEncoderName";
            info.is_hardware_accelerated = false;
            encoder->CallOnMediaEncoderInfoChanged(info);
          },
          Return(ByMove(std::unique_ptr<media::VideoEncoder>(
              std::move(media_encoder))))));
  EXPECT_CALL(*encoder, CreateVideoEncoderMetricsProvider())
      .WillOnce(Return(ByMove(std::move(encoder_metrics_provider))));
  EXPECT_CALL(*mock_media_encoder, Initialize(_, _, _, _, _))
      .WillOnce(DoAll(
          SaveArg<3>(&output_cb),
          WithArgs<4>([](media::VideoEncoder::EncoderStatusCB done_cb) {
            scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                FROM_HERE, blink::BindOnce(std::move(done_cb),
                                           media::EncoderStatus::Codes::kOk));
          })));
  encoder->configure(config, es);

  EXPECT_CALL(*mock_media_encoder, Encode(_, _, _))
      .WillOnce(
          WithArgs<0, 2>([&](scoped_refptr<media::VideoFrame> frame,
                             media::VideoEncoder::EncoderStatusCB done_cb) {
            EXPECT_EQ(frame->visible_rect(), kVisibleRect);
            EXPECT_EQ(frame->natural_size(), kVisibleRect.size());
            scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                FROM_HERE, blink::BindOnce(std::move(done_cb),
                                           media::EncoderStatus::Codes::kOk));
            media::VideoEncoderOutput out;
            out.data = base::HeapArray<uint8_t>::Uninit(100);
            out.key_frame = true;
            scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
                FROM_HERE,
                blink::BindOnce(output_cb, std::move(out), std::nullopt));
          }));

  EXPECT_CALL(*mock_media_encoder, Flush(_))
      .WillOnce([](media::VideoEncoder::EncoderStatusCB done_cb) {
        scheduler::GetSequencedTaskRunnerForTesting()->PostTask(
            FROM_HERE, blink::BindOnce(std::move(done_cb),
                                       media::EncoderStatus::Codes::kOk));
      });

  encoder->encode(MakeVideoFrame(script_state, kEncodeSize.width(),
                                 kEncodeSize.height(), 1, kVisibleRect),
                  MakeGarbageCollected<VideoEncoderEncodeOptions>(), es);

  ScriptPromiseTester tester(script_state, encoder->flush(es));
  tester.WaitUntilSettled();
  EXPECT_TRUE(tester.IsFulfilled());
}
}  // namespace

}  // namespace blink
