// 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.

#include "media/gpu/windows/d3d12_video_encode_delegate_unittest.h"

#include "base/containers/flat_map.h"
#include "base/no_destructor.h"
#include "base/rand_util.h"
#include "media/base/win/d3d12_mocks.h"
#include "media/base/win/d3d12_video_mocks.h"
#include "media/gpu/h264_dpb.h"
#include "media/gpu/windows/format_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/microsoft_dxheaders/src/include/directx/d3dx12_core.h"

using testing::_;
using testing::Mock;
using testing::NiceMock;
using testing::Return;

namespace media {

namespace {

class MockD3D12VideoEncodeDelegate : public D3D12VideoEncodeDelegate {
 public:
  explicit MockD3D12VideoEncodeDelegate(
      Microsoft::WRL::ComPtr<ID3D12VideoDevice3> video_device,
      const gpu::GpuDriverBugWorkarounds& gpu_workarounds)
      : D3D12VideoEncodeDelegate(std::move(video_device), gpu_workarounds) {}
  ~MockD3D12VideoEncodeDelegate() override = default;

  size_t GetMaxNumOfRefFrames() const override { return 8; }
  size_t GetMaxNumOfManualRefBuffers() const override { return 4; }
  bool SupportsRateControlReconfiguration() const override { return false; }
  EncoderStatus EncodeImpl(ID3D12Resource*,
                           UINT,
                           const VideoEncoder::EncodeOptions&,
                           const gfx::ColorSpace&,
                           const gfx::HDRMetadata&) override {
    return EncoderStatus::Codes::kOk;
  }

 private:
  EncoderStatus InitializeVideoEncoder(
      const VideoEncodeAccelerator::Config&) override {
    video_encoder_wrapper_ =
        video_encoder_wrapper_factory_.Run({}, {}, {}, {}, {}, {}, {});
    return EncoderStatus::Codes::kOk;
  }
};

}  // namespace

MockD3D12VideoProcessorWrapper::MockD3D12VideoProcessorWrapper(
    Microsoft::WRL::ComPtr<ID3D12VideoDevice> device)
    : D3D12VideoProcessorWrapper(std::move(device)) {}

MockD3D12VideoProcessorWrapper::~MockD3D12VideoProcessorWrapper() = default;

MockD3D12VideoEncoderWrapper::MockD3D12VideoEncoderWrapper()
    : D3D12VideoEncoderWrapper(nullptr, nullptr) {}

MockD3D12VideoEncoderWrapper::~MockD3D12VideoEncoderWrapper() = default;

D3D12VideoEncodeDelegateTestBase::D3D12VideoEncodeDelegateTestBase() = default;

D3D12VideoEncodeDelegateTestBase::~D3D12VideoEncodeDelegateTestBase() = default;

std::unique_ptr<D3D12VideoProcessorWrapper>
D3D12VideoEncodeDelegateTestBase::CreateVideoProcessorWrapper(
    Microsoft::WRL::ComPtr<ID3D12VideoDevice>&& video_device) {
  auto video_processor_wrapper =
      std::make_unique<NiceMock<MockD3D12VideoProcessorWrapper>>(video_device);
  // The lambda captures the fence ComPtr by value, which ensures the fence
  // outlives the lambda stored in ON_CALL.
  auto fence = MakeComPtr<NiceMock<D3D12FenceMock>>();
  ON_CALL(*video_processor_wrapper, Init).WillByDefault(Return(true));
  ON_CALL(*video_processor_wrapper, CheckVideoProcessorSupport)
      .WillByDefault(Return(true));
  ON_CALL(*video_processor_wrapper, ProcessFrames)
      .WillByDefault([fence](ID3D12Resource*, UINT, const gfx::ColorSpace&,
                             const gfx::Rect&, ID3D12Resource*, UINT,
                             const gfx::ColorSpace&, const gfx::Rect&) {
        return D3D12FenceAndValue{fence.Get(), 0};
      });
  return std::move(video_processor_wrapper);
}

std::unique_ptr<D3D12VideoEncoderWrapper>
D3D12VideoEncodeDelegateTestBase::CreateVideoEncoderWrapper(
    ID3D12VideoDevice*,
    D3D12_VIDEO_ENCODER_CODEC,
    const D3D12_VIDEO_ENCODER_PROFILE_DESC&,
    const D3D12_VIDEO_ENCODER_LEVEL_SETTING&,
    DXGI_FORMAT,
    const D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION&,
    const D3D12_VIDEO_ENCODER_PICTURE_RESOLUTION_DESC&) {
  auto video_encoder_wrapper =
      std::make_unique<NiceMock<MockD3D12VideoEncoderWrapper>>();
  ON_CALL(*video_encoder_wrapper, Initialize).WillByDefault(Return(true));
  ON_CALL(*video_encoder_wrapper, Wait).WillByDefault(Return(true));
  ON_CALL(*video_encoder_wrapper, Encode)
      .WillByDefault(Return(EncoderStatus::Codes::kOk));
  ON_CALL(*video_encoder_wrapper, ReadbackBitstream)
      .WillByDefault(Return(EncoderStatus::Codes::kOk));
  return std::move(video_encoder_wrapper);
}

MockD3D12VideoProcessorWrapper*
D3D12VideoEncodeDelegateTestBase::GetVideoProcessorWrapper() const {
  return static_cast<MockD3D12VideoProcessorWrapper*>(
      encoder_delegate_->GetVideoProcessorWrapperForTesting());
}

MockD3D12VideoEncoderWrapper*
D3D12VideoEncodeDelegateTestBase::GetVideoEncoderWrapper() const {
  return static_cast<MockD3D12VideoEncoderWrapper*>(
      encoder_delegate_->GetVideoEncoderWrapperForTesting());
}

VideoEncodeAccelerator::Config
D3D12VideoEncodeDelegateTestBase::GetDefaultH264Config() const {
  VideoEncodeAccelerator::Config config;
  config.output_profile = H264PROFILE_BASELINE;
  config.input_format = PIXEL_FORMAT_NV12;
  config.input_visible_size = gfx::Size(1280, 720);
  config.bitrate = Bitrate::ConstantBitrate(300000u);
  config.framerate = 30;
  config.gop_length = 3000;
  return config;
}

ScopedD3D12ResourceMap
D3D12VideoEncodeDelegateTestBase::GetEncoderOutputMetadataResourceMap(
    size_t bitstream_size) const {
  Microsoft::WRL::ComPtr<D3D12ResourceMock> resource =
      MakeComPtr<NiceMock<D3D12ResourceMock>>();
  ON_CALL(*resource.Get(), GetDesc())
      .WillByDefault(Return(CD3DX12_RESOURCE_DESC::Buffer(bitstream_size)));
  static base::NoDestructor<
      base::flat_map<D3D12ResourceMock*, std::unique_ptr<uint8_t[]>>>
      mapped_metadata;
  ON_CALL(*resource.Get(), Map(0, _, _))
      .WillByDefault([&](UINT, const D3D12_RANGE*, void** data) {
        // Allocate a buffer large enough for the metadata structure or the
        // bitstream size, whichever is larger.
        auto buffer = std::make_unique<uint8_t[]>(std::max(
            bitstream_size, sizeof(D3D12_VIDEO_ENCODER_OUTPUT_METADATA)));
        auto* metadata = reinterpret_cast<D3D12_VIDEO_ENCODER_OUTPUT_METADATA*>(
            buffer.get());
        *metadata = D3D12_VIDEO_ENCODER_OUTPUT_METADATA{
            .EncodedBitstreamWrittenBytesCount = bitstream_size,
            .WrittenSubregionsCount = 1,
        };
        *data = buffer.get();
        (*mapped_metadata)[resource.Get()] = std::move(buffer);
        return S_OK;
      });
  ScopedD3D12ResourceMap metadata_buffer;
  EXPECT_TRUE(metadata_buffer.Map(resource.Get(), 0, nullptr));
  ON_CALL(*resource.Get(), Unmap(0, _))
      .WillByDefault(
          [resource = std::move(resource)](UINT, const D3D12_RANGE*) mutable {
            mapped_metadata->erase(resource.Get());
            // gtest complains that the mock isn't freed if we don't drop the
            // reference to it here.
            resource.Reset();
          });
  return metadata_buffer;
}

Microsoft::WRL::ComPtr<ID3D12Resource>
D3D12VideoEncodeDelegateTestBase::CreateResource(
    const gfx::Size& size,
    VideoPixelFormat format) const {
  auto input_frame = MakeComPtr<NiceMock<D3D12ResourceMock>>();
  ON_CALL(*input_frame.Get(), GetDesc())
      .WillByDefault(Return(D3D12_RESOURCE_DESC{
          .Width = static_cast<UINT64>(size.width()),
          .Height = static_cast<UINT>(size.height()),
          .Format = VideoPixelFormatToDxgiFormat(format),
      }));
  return input_frame;
}

void D3D12VideoEncodeDelegateTestBase::EnableFeature(
    const base::Feature& feature) {
  scoped_feature_list_.emplace();
  scoped_feature_list_->InitAndEnableFeature(feature);
}

void D3D12VideoEncodeDelegateTestBase::DisableFeature(
    const base::Feature& feature) {
  scoped_feature_list_.emplace();
  scoped_feature_list_->InitAndDisableFeature(feature);
}

class D3D12VideoEncodeDelegateTest : public D3D12VideoEncodeDelegateTestBase {
 protected:
  void SetUp() override {
    device_ = MakeComPtr<NiceMock<D3D12DeviceMock>>();
    video_device3_ = MakeComPtr<NiceMock<D3D12VideoDevice3Mock>>();
    ON_CALL(*video_device3_.Get(), QueryInterface(IID_ID3D12Device, _))
        .WillByDefault(SetComPointeeAndReturnOk<1>(device_.Get()));
    ON_CALL(*video_device3_.Get(), QueryInterface(IID_ID3D12VideoDevice1, _))
        .WillByDefault(SetComPointeeAndReturnOk<1>(video_device3_.Get()));
    gpu::GpuDriverBugWorkarounds gpu_workarounds{};
    encoder_delegate_ = std::make_unique<MockD3D12VideoEncodeDelegate>(
        video_device3_, gpu_workarounds);
    encoder_delegate_->SetFactoriesForTesting(
        base::BindRepeating(&CreateVideoEncoderWrapper),
        base::BindRepeating(&CreateVideoProcessorWrapper));
  }

  Microsoft::WRL::ComPtr<D3D12DeviceMock> device_;
  Microsoft::WRL::ComPtr<D3D12VideoDevice3Mock> video_device3_;
};

TEST_F(D3D12VideoEncodeDelegateTest, Initialize) {
  EXPECT_TRUE(encoder_delegate_->Initialize(GetDefaultH264Config()).is_ok());
  EXPECT_EQ(encoder_delegate_->GetFormatForTesting(), DXGI_FORMAT_NV12);
}

TEST_F(D3D12VideoEncodeDelegateTest, P010InputFormatFor10BitProfile) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.input_format = PIXEL_FORMAT_P010LE;
  config.output_profile = H264PROFILE_HIGH10PROFILE;
  EXPECT_TRUE(encoder_delegate_->Initialize(config).is_ok());
  EXPECT_EQ(encoder_delegate_->GetFormatForTesting(), DXGI_FORMAT_P010);
}

TEST_F(D3D12VideoEncodeDelegateTest,
       P010InputFormatForRGBInputWith10BitProfile) {
  // RGB(A) inputs feeding a 10-bit profile (e.g. HDR content delivered via an
  // RGBA shared image) must select P010 as the encoder input format so the
  // video processor converts to 10-bit before encoding.
  for (VideoPixelFormat rgb_format : {PIXEL_FORMAT_ARGB, PIXEL_FORMAT_XRGB,
                                      PIXEL_FORMAT_ABGR, PIXEL_FORMAT_XBGR}) {
    VideoEncodeAccelerator::Config config = GetDefaultH264Config();
    config.input_format = rgb_format;
    config.output_profile = H264PROFILE_HIGH10PROFILE;
    EXPECT_TRUE(encoder_delegate_->Initialize(config).is_ok());
    EXPECT_EQ(encoder_delegate_->GetFormatForTesting(), DXGI_FORMAT_P010)
        << "Unexpected input format for RGB pixel format "
        << VideoPixelFormatToString(rgb_format);
  }
}

TEST_F(D3D12VideoEncodeDelegateTest, EncodeFailsWhenVideoProcessorUnsupported) {
  // The encoder input format and the required conversion are only resolved at
  // Encode() time, when the actual input frame format is known. If the video
  // processor cannot perform the conversion, Encode() should fail with an
  // unsupported-config error before submitting any GPU work.
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  ON_CALL(*GetVideoProcessorWrapper(), CheckVideoProcessorSupport)
      .WillByDefault(Return(false));

  // An ARGB input frame differs from the NV12 encoder input format, so the
  // encoder must run the video processor to convert it.
  auto input_frame =
      CreateResource(config.input_visible_size, PIXEL_FORMAT_ARGB);
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);
  EXPECT_CALL(*GetVideoProcessorWrapper(), ProcessFrames).Times(0);
  auto result_or_error = encoder_delegate_->Encode(
      input_frame, gfx::Rect(config.input_visible_size),
      gfx::ColorSpace::CreateSRGB(), bitstream_buffer,
      VideoEncoder::EncodeOptions());
  ASSERT_FALSE(result_or_error.has_value());
  EXPECT_EQ(std::move(result_or_error).error().code(),
            EncoderStatus::Codes::kEncoderUnsupportedConfig);
}

TEST_F(D3D12VideoEncodeDelegateTest, ExternalRateControl) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.bitrate = Bitrate::ExternalRateControl();
  EXPECT_EQ(encoder_delegate_->Initialize(config).code(),
            EncoderStatus::Codes::kOk);
}

class D3D12VideoEncodeDelegateTestWithProcessFrame
    : public D3D12VideoEncodeDelegateTest,
      public testing::WithParamInterface<bool> {};

TEST_F(D3D12VideoEncodeDelegateTestWithProcessFrame, EncodeFrameWithoutVP) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  gfx::Size input_size = config.input_visible_size;
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(
      base::RandIntInclusive(0, H264DPB::kDPBMaxSize - 1),
      shared_memory.Duplicate(), kPayloadSize);
  EXPECT_CALL(*GetVideoProcessorWrapper(), ProcessFrames).Times(0);
  EXPECT_CALL(*GetVideoEncoderWrapper(), GetEncoderOutputMetadata)
      .WillOnce(Return(GetEncoderOutputMetadataResourceMap(kPayloadSize)));
  auto result_or_error = encoder_delegate_->Encode(
      input_frame, gfx::Rect(input_size), color_space, bitstream_buffer,
      VideoEncoder::EncodeOptions());
  Mock::VerifyAndClearExpectations(GetVideoProcessorWrapper());
  ASSERT_TRUE(result_or_error.has_value())
      << std::move(result_or_error).error().message();

  auto [bitstream_buffer_id, metadata] = std::move(result_or_error).value();
  EXPECT_EQ(bitstream_buffer_id, bitstream_buffer.id());
  EXPECT_EQ(metadata.encoded_color_space, color_space);
  EXPECT_EQ(metadata.payload_size_bytes, kPayloadSize);
}

TEST_F(D3D12VideoEncodeDelegateTestWithProcessFrame, EncodeFrameWithVP) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  // A texture wider than the encoder input size, so the video processor has to
  // run. NV12 requires even dimensions, hence the step of two.
  gfx::Size input_size = config.input_visible_size;
  input_size += {2, 0};
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateSRGB();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(
      base::RandIntInclusive(0, H264DPB::kDPBMaxSize - 1),
      shared_memory.Duplicate(), kPayloadSize);
  auto fence = MakeComPtr<NiceMock<D3D12FenceMock>>();
  EXPECT_CALL(*GetVideoProcessorWrapper(), ProcessFrames)
      .WillOnce([&](ID3D12Resource*, UINT, const gfx::ColorSpace&,
                    const gfx::Rect& input_rectangle, ID3D12Resource*, UINT,
                    const gfx::ColorSpace&, const gfx::Rect& output_rectangle) {
        EXPECT_EQ(input_rectangle.width(), input_size.width());
        EXPECT_EQ(input_rectangle.height(), input_size.height());
        EXPECT_EQ(output_rectangle.width(), config.input_visible_size.width());
        EXPECT_EQ(output_rectangle.height(),
                  config.input_visible_size.height());
        return D3D12FenceAndValue{fence.Get(), 0};
      });

  EXPECT_CALL(*GetVideoEncoderWrapper(), GetEncoderOutputMetadata)
      .WillOnce(Return(GetEncoderOutputMetadataResourceMap(kPayloadSize)));
  auto result_or_error = encoder_delegate_->Encode(
      input_frame, gfx::Rect(input_size), color_space, bitstream_buffer,
      VideoEncoder::EncodeOptions());
  Mock::VerifyAndClearExpectations(GetVideoProcessorWrapper());
  ASSERT_TRUE(result_or_error.has_value())
      << std::move(result_or_error).error().message();

  auto [bitstream_buffer_id, metadata] = std::move(result_or_error).value();
  EXPECT_EQ(bitstream_buffer_id, bitstream_buffer.id());
  const gfx::ColorSpace output_color_space = color_space.GetWithMatrixAndRange(
      gfx::ColorSpace::MatrixID::BT709, gfx::ColorSpace::RangeID::FULL);
  EXPECT_EQ(metadata.encoded_color_space, output_color_space);
  EXPECT_EQ(metadata.payload_size_bytes, kPayloadSize);
}

class D3D12VideoEncodeDelegateVisibleRectTest
    : public D3D12VideoEncodeDelegateTest {
 protected:
  // Encodes one frame and returns the source rectangle the video processor was
  // asked to sample. The delegate must be initialized by the caller.
  gfx::Rect EncodeAndGetSourceRect(const D3D12PictureBuffer& picture_buffer,
                                   const gfx::Rect& input_visible_rect,
                                   gfx::Rect* destination_rect = nullptr) {
    constexpr size_t kPayloadSize = 1024;
    auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
    BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(),
                                     kPayloadSize);
    gfx::Rect source_rect;
    auto fence = MakeComPtr<NiceMock<D3D12FenceMock>>();
    EXPECT_CALL(*GetVideoProcessorWrapper(), ProcessFrames)
        .WillOnce([&](ID3D12Resource*, UINT, const gfx::ColorSpace&,
                      const gfx::Rect& input_rectangle, ID3D12Resource*, UINT,
                      const gfx::ColorSpace&,
                      const gfx::Rect& output_rectangle) {
          source_rect = input_rectangle;
          if (destination_rect) {
            *destination_rect = output_rectangle;
          }
          return D3D12FenceAndValue{fence.Get(), 0};
        });
    EXPECT_CALL(*GetVideoEncoderWrapper(), GetEncoderOutputMetadata)
        .WillOnce(Return(GetEncoderOutputMetadataResourceMap(kPayloadSize)));
    auto result_or_error = encoder_delegate_->Encode(
        picture_buffer, input_visible_rect, gfx::ColorSpace::CreateREC709(),
        bitstream_buffer, VideoEncoder::EncodeOptions());
    Mock::VerifyAndClearExpectations(GetVideoProcessorWrapper());
    EXPECT_TRUE(result_or_error.has_value())
        << std::move(result_or_error).error().message();
    return source_rect;
  }

  // Attempts to encode one frame that the delegate is expected to reject before
  // submitting any video processing work, and returns the resulting error code.
  EncoderStatus::Codes EncodeAndGetRejection(
      const D3D12PictureBuffer& picture_buffer,
      const gfx::Rect& input_visible_rect) {
    constexpr size_t kPayloadSize = 1024;
    auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
    BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(),
                                     kPayloadSize);
    EXPECT_CALL(*GetVideoProcessorWrapper(), ProcessFrames).Times(0);
    auto result_or_error = encoder_delegate_->Encode(
        picture_buffer, input_visible_rect, gfx::ColorSpace::CreateREC709(),
        bitstream_buffer, VideoEncoder::EncodeOptions());
    Mock::VerifyAndClearExpectations(GetVideoProcessorWrapper());
    if (result_or_error.has_value()) {
      ADD_FAILURE() << "Encode() unexpectedly succeeded";
      return EncoderStatus::Codes::kOk;
    }
    return std::move(result_or_error).error().code();
  }
};

TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, CropsToVisibleRect) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.input_visible_size = gfx::Size(1920, 1080);
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1920, 1088), config.input_format));

  gfx::Rect destination_rect;
  EXPECT_EQ(EncodeAndGetSourceRect(picture_buffer, gfx::Rect(0, 0, 1920, 1080),
                                   &destination_rect),
            gfx::Rect(0, 0, 1920, 1080));
  EXPECT_EQ(destination_rect, gfx::Rect(0, 0, 1920, 1080));
}

TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, CropsToVisibleRectWithOffset) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.input_visible_size = gfx::Size(1888, 1064);
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1920, 1080), config.input_format));

  EXPECT_EQ(
      EncodeAndGetSourceRect(picture_buffer, gfx::Rect(16, 8, 1888, 1064)),
      gfx::Rect(16, 8, 1888, 1064));
}

TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, RejectsOddVisibleRectOrigin) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.input_visible_size = gfx::Size(1264, 704);
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1280, 720), config.input_format));

  EXPECT_EQ(EncodeAndGetRejection(picture_buffer, gfx::Rect(15, 7, 1264, 704)),
            EncoderStatus::Codes::kInvalidInputFrame);
}

TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, RejectsOddVisibleRectSize) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.input_visible_size = gfx::Size(1264, 704);
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1280, 720), config.input_format));
  for (const gfx::Rect& visible_rect :
       {gfx::Rect(0, 0, 1265, 704), gfx::Rect(0, 0, 1264, 705),
        gfx::Rect(0, 0, 1265, 705)}) {
    EXPECT_EQ(EncodeAndGetRejection(picture_buffer, visible_rect),
              EncoderStatus::Codes::kInvalidInputFrame)
        << "Unexpectedly accepted visible rectangle "
        << visible_rect.ToString();
  }
}

// A crop that is not already the encoder's input size is scaled to it, the same
// tolerance MF VEA provides via PerformD3DScaling(). The crop is the source and
// the encoder input size is the destination; neither is the texture size.
TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, ScalesCropToEncoderInputSize) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1920, 1088), config.input_format));

  gfx::Rect destination_rect;
  EXPECT_EQ(EncodeAndGetSourceRect(picture_buffer, gfx::Rect(0, 0, 1920, 1080),
                                   &destination_rect),
            gfx::Rect(0, 0, 1920, 1080));
  EXPECT_EQ(destination_rect, gfx::Rect(config.input_visible_size));
}

// The video processor samples only the visible rectangle, so the support query
// has to describe the crop size -- not the input texture, and not the encoder
// input size the crop is scaled to.
TEST_F(D3D12VideoEncodeDelegateVisibleRectTest,
       ChecksVideoProcessorSupportForCropSize) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  // An ARGB input frame differs from the NV12 encoder input format, so the
  // encoder runs the video processor to convert it.
  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1296, 728), PIXEL_FORMAT_ARGB));

  EXPECT_CALL(*GetVideoProcessorWrapper(),
              CheckVideoProcessorSupport(1024u, 576u, _, _, _, _))
      .WillOnce(Return(true));
  EXPECT_EQ(EncodeAndGetSourceRect(picture_buffer, gfx::Rect(16, 8, 1024, 576)),
            gfx::Rect(16, 8, 1024, 576));
}

// The alignment requirement comes from 4:2:0 chroma subsampling, so a source
// that stores chroma at full resolution can be cropped at an odd origin.
TEST_F(D3D12VideoEncodeDelegateVisibleRectTest,
       AllowsUnalignedVisibleRectForNon420Format) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  // An ARGB input frame differs from the NV12 encoder input format, so the
  // encoder runs the video processor to convert it.
  D3D12PictureBuffer picture_buffer(
      CreateResource(gfx::Size(1296, 728), PIXEL_FORMAT_ARGB));

  EXPECT_EQ(EncodeAndGetSourceRect(
                picture_buffer,
                gfx::Rect(gfx::Point(15, 7), config.input_visible_size)),
            gfx::Rect(15, 7, 1280, 720));
}

// Callers must always name the region to encode. There is no "whole texture"
// shorthand, so an empty rectangle is a caller bug rather than a request to
// encode everything.
TEST_F(D3D12VideoEncodeDelegateVisibleRectTest, RejectsEmptyVisibleRect) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(config.input_visible_size, config.input_format));

  for (const gfx::Rect& visible_rect :
       {gfx::Rect(), gfx::Rect(0, 0, 1280, 0), gfx::Rect(0, 0, 0, 720),
        gfx::Rect(16, 8, 0, 0)}) {
    EXPECT_EQ(EncodeAndGetRejection(picture_buffer, visible_rect),
              EncoderStatus::Codes::kInvalidInputFrame)
        << "Unexpectedly accepted empty visible rectangle "
        << visible_rect.ToString();
  }
}

TEST_F(D3D12VideoEncodeDelegateVisibleRectTest,
       RejectsVisibleRectOutsideTexture) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  D3D12PictureBuffer picture_buffer(
      CreateResource(config.input_visible_size, config.input_format));

  EXPECT_EQ(EncodeAndGetRejection(
                picture_buffer,
                gfx::Rect(gfx::Point(0, 2), config.input_visible_size)),
            EncoderStatus::Codes::kInvalidInputFrame);
}

TEST_F(D3D12VideoEncodeDelegateTest, EncodeWithTooManyReferenceBuffersFails) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  gfx::Size input_size = config.input_visible_size;
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(
      base::RandIntInclusive(0, H264DPB::kDPBMaxSize - 1),
      shared_memory.Duplicate(), kPayloadSize);

  VideoEncoder::EncodeOptions options;
  // Fill reference_buffers with one more than supported to trigger failure.
  const size_t too_many = encoder_delegate_->GetMaxNumOfManualRefBuffers() + 1;
  for (size_t i = 0; i < too_many; ++i) {
    options.reference_buffers.push_back(static_cast<uint8_t>(i));
  }

  auto result_or_error =
      encoder_delegate_->Encode(input_frame, gfx::Rect(input_size), color_space,
                                bitstream_buffer, options);

  // Expect an error indicating too many reference buffers.
  EXPECT_FALSE(result_or_error.has_value());
  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
}

TEST_F(D3D12VideoEncodeDelegateTest,
       EncodeWithOutOfRangeReferenceBufferIndexFails) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  gfx::Size input_size = config.input_visible_size;
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);

  VideoEncoder::EncodeOptions options;
  // Use a single reference buffer with an index >= GetMaxNumOfManualRefBuffers.
  options.reference_buffers.push_back(
      static_cast<uint8_t>(encoder_delegate_->GetMaxNumOfManualRefBuffers()));

  auto result_or_error =
      encoder_delegate_->Encode(input_frame, gfx::Rect(input_size), color_space,
                                bitstream_buffer, options);

  EXPECT_FALSE(result_or_error.has_value());
  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
}

TEST_F(D3D12VideoEncodeDelegateTest,
       EncodeWithOutOfRangeUpdateBufferIndexFails) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  gfx::Size input_size = config.input_visible_size;
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);

  VideoEncoder::EncodeOptions options;
  // Set update_buffer to a value >= GetMaxNumOfRefFrames.
  options.update_buffer =
      static_cast<uint8_t>(encoder_delegate_->GetMaxNumOfRefFrames());

  auto result_or_error =
      encoder_delegate_->Encode(input_frame, gfx::Rect(input_size), color_space,
                                bitstream_buffer, options);

  EXPECT_FALSE(result_or_error.has_value());
  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
}

TEST_F(D3D12VideoEncodeDelegateTest, EncodeWithEmptyRefsOnNonKeyframeFails) {
  VideoEncodeAccelerator::Config config = GetDefaultH264Config();
  config.manual_reference_buffer_control = true;
  ASSERT_TRUE(encoder_delegate_->Initialize(config).is_ok());

  gfx::Size input_size = config.input_visible_size;
  auto input_frame = CreateResource(input_size, config.input_format);
  gfx::ColorSpace color_space = gfx::ColorSpace::CreateREC709();
  constexpr size_t kPayloadSize = 1024;
  auto shared_memory = base::UnsafeSharedMemoryRegion::Create(kPayloadSize);
  BitstreamBuffer bitstream_buffer(0, shared_memory.Duplicate(), kPayloadSize);

  EXPECT_CALL(*GetVideoEncoderWrapper(), GetEncoderOutputMetadata)
      .WillRepeatedly(
          [&] { return GetEncoderOutputMetadataResourceMap(kPayloadSize); });

  // Frame 0: keyframe with empty references — should succeed.
  VideoEncoder::EncodeOptions options;
  options.key_frame = true;
  options.reference_buffers = {};
  options.update_buffer = 0;
  auto result_or_error =
      encoder_delegate_->Encode(input_frame, gfx::Rect(input_size), color_space,
                                bitstream_buffer, options);
  ASSERT_TRUE(result_or_error.has_value());

  // Frame 1: non-keyframe with empty references — should fail.
  options.key_frame = false;
  options.reference_buffers = {};
  options.update_buffer = std::nullopt;
  result_or_error =
      encoder_delegate_->Encode(input_frame, gfx::Rect(input_size), color_space,
                                bitstream_buffer, options);
  EXPECT_FALSE(result_or_error.has_value());
  EXPECT_EQ(result_or_error.code(), EncoderStatus::Codes::kBadReferenceBuffer);
}

}  // namespace media
