/*
 * Copyright (c) 2026, Alliance for Open Media. All rights reserved
 *
 * This source code is subject to the terms of the BSD 3-Clause Clear License
 * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear
 * License was not distributed with this source code in the LICENSE file, you
 * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/.  If the
 * Alliance for Open Media Patent License 1.0 was not distributed with this
 * source code in the PATENTS file, you can obtain it at
 * aomedia.org/license/patent-license/.
 */

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>

#include "third_party/googletest/src/googletest/include/gtest/gtest.h"
#include "test/codec_factory.h"
#include "test/encode_test_driver.h"
#include "test/y4m_video_source.h"
#include "test/util.h"
#include "av2/common/enums.h"
#include "av2/common/obu_util.h"

namespace {

// ---------------------------------------------------------------------------
// Pruning decisions are based on OBU header metadata (temporal layer ID,
// embedded layer ID, OBU type)
// ---------------------------------------------------------------------------

// Metadata extracted from a packet's first VCL OBU header.
struct PacketInfo {
  int tlayer_id;      // temporal layer from OBU header
  int mlayer_id;      // embedded (spatial) layer from OBU header
  OBU_TYPE vcl_type;  // OBU type of the first VCL OBU (e.g. OBU_RAS_FRAME)
  bool has_vcl;       // true if the packet contains at least one VCL OBU
};

// Parse the first VCL OBU and populate info.
static PacketInfo GetPacketInfo(const std::vector<uint8_t> &data) {
  PacketInfo info = {};
  info.has_vcl = false;
  const uint8_t *ptr = data.data();
  size_t remaining = data.size();
  while (remaining > 0) {
    ObuHeader hdr;
    size_t payload_size = 0;
    size_t bytes_read = 0;
    if (avm_read_obu_header_and_size(ptr, remaining, &hdr, &payload_size,
                                     &bytes_read) != AVM_CODEC_OK)
      break;
    if (remaining - bytes_read < payload_size) break;
    if (is_single_tile_vcl_obu(hdr.type) || is_multi_tile_vcl_obu(hdr.type)) {
      info.tlayer_id = hdr.obu_tlayer_id;
      info.mlayer_id = hdr.obu_mlayer_id;
      info.vcl_type = hdr.type;
      info.has_vcl = true;
      break;
    }
    ptr += bytes_read + payload_size;
    remaining -= bytes_read + payload_size;
  }
  return info;
}

// Returns true if the OBU type is a decoder reset point (KEY, OLK, or RAS).
static bool IsResetPoint(OBU_TYPE type) {
  return type == OBU_CLOSED_LOOP_KEY || type == OBU_OPEN_LOOP_KEY ||
         type == OBU_RAS_FRAME;
}

// Deep-copy an avm_image_t. Caller owns the returned image and must call
// avm_img_free() on it.
static avm_image_t *CopyImage(const avm_image_t *src) {
  avm_image_t *dst = avm_img_alloc(NULL, src->fmt, src->d_w, src->d_h, 1);
  if (!dst) return NULL;
  dst->bit_depth = src->bit_depth;
  dst->monochrome = src->monochrome;
  const int num_planes = src->monochrome ? 1 : 3;
  for (int plane = 0; plane < num_planes; ++plane) {
    const int w = avm_img_plane_width(src, plane);
    const int h = avm_img_plane_height(src, plane);
    const int bytes_per_sample = (src->fmt & AVM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1;
    const uint8_t *src_row = src->planes[plane];
    uint8_t *dst_row = dst->planes[plane];
    for (int y = 0; y < h; ++y) {
      memcpy(dst_row, src_row, w * bytes_per_sample);
      src_row += src->stride[plane];
      dst_row += dst->stride[plane];
    }
  }
  return dst;
}

// Compare two avm_image_t pixel-by-pixel. Returns true if identical.
static bool ImagesMatch(const avm_image_t *a, const avm_image_t *b) {
  if (a->fmt != b->fmt || a->d_w != b->d_w || a->d_h != b->d_h ||
      a->monochrome != b->monochrome)
    return false;
  const int num_planes = a->monochrome ? 1 : 3;
  for (int plane = 0; plane < num_planes; ++plane) {
    const int w = avm_img_plane_width(a, plane);
    const int h = avm_img_plane_height(a, plane);
    const int bytes_per_sample = (a->fmt & AVM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1;
    const uint8_t *row_a = a->planes[plane];
    const uint8_t *row_b = b->planes[plane];
    for (int y = 0; y < h; ++y) {
      if (memcmp(row_a, row_b, w * bytes_per_sample) != 0) return false;
      row_a += a->stride[plane];
      row_b += b->stride[plane];
    }
  }
  return true;
}

// Decode a sequence of packets and collect decoded images.
// Returns decoded images in display order.
static std::vector<avm_image_t *> DecodePackets(
    const std::vector<std::vector<uint8_t>> &packets) {
  std::vector<avm_image_t *> frames;
  avm_codec_dec_cfg_t dec_cfg;
  memset(&dec_cfg, 0, sizeof(dec_cfg));
  dec_cfg.threads = 1;
  libavm_test::AV2Decoder decoder(dec_cfg);

  for (const auto &pkt : packets) {
    avm_codec_err_t res = decoder.DecodeFrame(pkt.data(), pkt.size());
    EXPECT_EQ(AVM_CODEC_OK, res) << decoder.DecodeError();
    if (res != AVM_CODEC_OK) break;
    libavm_test::DxDataIterator iter = decoder.GetDxData();
    const avm_image_t *img;
    while ((img = iter.Next()) != NULL) {
      frames.push_back(CopyImage(img));
    }
  }
  // Flush.
  decoder.DecodeFrame(NULL, 0);
  libavm_test::DxDataIterator final_iter = decoder.GetDxData();
  const avm_image_t *img;
  while ((img = final_iter.Next()) != NULL) {
    frames.push_back(CopyImage(img));
  }
  return frames;
}

static void FreeImages(std::vector<avm_image_t *> &images) {
  for (auto *img : images) avm_img_free(img);
  images.clear();
}

// Prune for multi-temporal-layer tests: retain TL0 and remove prune
// pictures between first KEY and first RAS.
//
// Keeps a packet if:
//   - It is non-VCL (sequence header, etc.), OR
//   - Its temporal layer is 0 AND it is either a reset point (CLK/OLK/RAS)
//     or appears after the first RAS has been retained.
static std::vector<std::vector<uint8_t>> PruneTL0RetainAfterFirstRAS(
    const std::vector<std::vector<uint8_t>> &packets) {
  std::vector<std::vector<uint8_t>> out;
  bool seen_ras = false;
  for (const auto &pkt : packets) {
    const PacketInfo info = GetPacketInfo(pkt);
    if (!info.has_vcl) {
      out.push_back(pkt);
      continue;
    }
    if (info.tlayer_id != 0) continue;
    if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
    if (IsResetPoint(info.vcl_type) || seen_ras) {
      out.push_back(pkt);
    }
  }
  return out;
}

// Prune for random-access test: Remove pictures between OBU_CLOSED_LOOP_KEY and
// first OBU_RAS_FRAME
static std::vector<std::vector<uint8_t>> PruneFirstGoP(
    const std::vector<std::vector<uint8_t>> &packets) {
  std::vector<std::vector<uint8_t>> out;
  int ras_count = 0;
  for (const auto &pkt : packets) {
    const PacketInfo info = GetPacketInfo(pkt);
    if (!info.has_vcl) {
      out.push_back(pkt);
      continue;
    }
    if (info.vcl_type == OBU_CLOSED_LOOP_KEY) {
      out.push_back(pkt);
      continue;
    }
    if (info.vcl_type == OBU_RAS_FRAME) ++ras_count;
    // Keep everything from the second RAS onward.
    if (ras_count >= 2) {
      out.push_back(pkt);
    }
  }
  return out;
}

// Filter packets to keep only those with the given embedded layer ID.
static std::vector<std::vector<uint8_t>> FilterByMlayer(
    const std::vector<std::vector<uint8_t>> &packets, int keep_mlayer) {
  std::vector<std::vector<uint8_t>> out;
  for (const auto &pkt : packets) {
    const PacketInfo info = GetPacketInfo(pkt);
    if (!info.has_vcl || info.mlayer_id == keep_mlayer) out.push_back(pkt);
  }
  return out;
}

// Filter packets to keep only those with the given temporal layer ID or below.
static std::vector<std::vector<uint8_t>> FilterByMaxTlayer(
    const std::vector<std::vector<uint8_t>> &packets, int max_tlayer) {
  std::vector<std::vector<uint8_t>> out;
  for (const auto &pkt : packets) {
    const PacketInfo info = GetPacketInfo(pkt);
    if (!info.has_vcl || info.tlayer_id <= max_tlayer) out.push_back(pkt);
  }
  return out;
}

// Decode packets with --all-layers behaviour (output all embedded layers).
static std::vector<avm_image_t *> DecodePacketsAllLayers(
    const std::vector<std::vector<uint8_t>> &packets) {
  std::vector<avm_image_t *> frames;
  avm_codec_dec_cfg_t dec_cfg;
  memset(&dec_cfg, 0, sizeof(dec_cfg));
  dec_cfg.threads = 1;
  libavm_test::AV2Decoder decoder(dec_cfg);
  decoder.Control(AV2D_SET_OUTPUT_ALL_LAYERS, 1);

  for (const auto &pkt : packets) {
    avm_codec_err_t res = decoder.DecodeFrame(pkt.data(), pkt.size());
    EXPECT_EQ(AVM_CODEC_OK, res) << decoder.DecodeError();
    if (res != AVM_CODEC_OK) break;
    libavm_test::DxDataIterator iter = decoder.GetDxData();
    const avm_image_t *img;
    while ((img = iter.Next()) != NULL) {
      frames.push_back(CopyImage(img));
    }
  }
  // Flush.
  decoder.DecodeFrame(NULL, 0);
  libavm_test::DxDataIterator final_iter = decoder.GetDxData();
  const avm_image_t *img;
  while ((img = final_iter.Next()) != NULL) {
    frames.push_back(CopyImage(img));
  }
  return frames;
}

// Prune for multi-temporal-layer tests: retain TL0 up to and including the
// first RAS, then retain ALL temporal layers after the first RAS.
//
// Keeps a packet if:
//   - It is non-VCL (sequence header, etc.), OR
//   - Before the first RAS: its temporal layer is 0 AND it is a reset point
//     (KEY/OLK/RAS).
//   - From the first RAS onward: every VCL packet regardless of tlayer_id.
static std::vector<std::vector<uint8_t>> PruneRetainAllAfterFirstRAS(
    const std::vector<std::vector<uint8_t>> &packets) {
  std::vector<std::vector<uint8_t>> out;
  bool seen_ras = false;
  for (const auto &pkt : packets) {
    const PacketInfo info = GetPacketInfo(pkt);
    if (!info.has_vcl) {
      out.push_back(pkt);
      continue;
    }
    if (!seen_ras) {
      if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
      // Before (and including) first RAS: keep only TL0 reset points.
      if (info.tlayer_id == 0 && IsResetPoint(info.vcl_type)) {
        out.push_back(pkt);
      }
    } else {
      // After first RAS: keep everything.
      out.push_back(pkt);
    }
  }
  return out;
}

// ---------------------------------------------------------------------------
// RAS Frame Pruning Test
//
// Encodes 20 frames with 2 temporal layers and RAS frames (sframe_type=1) at
// TL0 distance 4.  With 2TL the TL0 frames are at even frame numbers:
// 0,2,4,6,8,...  sframe_dist=8 places the first RAS at frame 8 (the 5th TL0
// frame, i.e. TL0 index 4 — distance 4 in TL0 from the KEY at index 0).
//
// Pruning based on OBU header metadata (temporal layer and OBU type):
//   1. Remove all non-TL0 packets (tlayer_id != 0).
//   2. Among TL0, remove non-reset-point frames before the first RAS.
//
// Retained: KEY(0), RAS(8), and TL0 frames after RAS: 10, 12, 14, 16, 18.
//
// The test:
//   1. Encodes and stores each packet individually.
//   2. Decodes all packets to get original frames.
//   3. Prunes packets based on tlayer_id and OBU type.
//   4. Decodes pruned packets to get pruned frames.
//   5. Verifies retained frames are pixel-identical.
// ---------------------------------------------------------------------------
class MultiLayer2TemporalDecodeBaseOnlyRASPruningTest
    : public ::libavm_test::CodecTestWithParam<int>,
      public ::libavm_test::EncoderTest {
 protected:
  MultiLayer2TemporalDecodeBaseOnlyRASPruningTest()
      : EncoderTest(GET_PARAM(0)), speed_(GET_PARAM(1)) {}
  ~MultiLayer2TemporalDecodeBaseOnlyRASPruningTest() override {}

  void SetUp() override {
    InitializeConfig();
    passes_ = 1;
    cfg_.rc_end_usage = AVM_Q;
    cfg_.rc_min_quantizer = 210;
    cfg_.rc_max_quantizer = 210;
    cfg_.g_threads = 2;
    cfg_.g_profile = MAIN_420_10_IP2;
    cfg_.g_lag_in_frames = 0;
    cfg_.g_bit_depth = AVM_BITS_8;
    cfg_.enable_ops = 1;
    cfg_.enable_lcr = 1;
    cfg_.enable_sframe = 1;
    cfg_.sframe_dist = 8;
    cfg_.sframe_mode = 0;
    cfg_.sframe_type = 1;
    num_temporal_layers_ = 2;
    num_embedded_layers_ = 1;
    layer_frame_cnt_ = 0;
    temporal_layer_id_ = 0;
  }

  int GetNumEmbeddedLayers() override { return num_embedded_layers_; }

  void PreEncodeFrameHook(::libavm_test::VideoSource *video,
                          ::libavm_test::Encoder *encoder) override {
    (void)video;
    frame_flags_ = 0;
    if (layer_frame_cnt_ == 0) {
      encoder->Control(AVME_SET_CPUUSED, speed_);
      encoder->Control(AVME_SET_NUMBER_MLAYERS, num_embedded_layers_);
      encoder->Control(AVME_SET_NUMBER_TLAYERS, num_temporal_layers_);
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
      encoder->Control(AV2E_SET_ENABLE_EXPLICIT_REF_FRAME_MAP, 1);
    }
    if (layer_frame_cnt_ % 2 == 0) {
      temporal_layer_id_ = 0;
      encoder->Control(AVME_SET_TLAYER_ID, 0);
    } else {
      temporal_layer_id_ = 1;
      encoder->Control(AVME_SET_TLAYER_ID, 1);
    }
    layer_frame_cnt_++;
  }

  bool HandleDecodeResult(const avm_codec_err_t res_dec,
                          libavm_test::Decoder *decoder) override {
    EXPECT_EQ(AVM_CODEC_OK, res_dec) << decoder->DecodeError();
    return AVM_CODEC_OK == res_dec;
  }

  void FramePktHook(const avm_codec_cx_pkt_t *pkt,
                    ::libavm_test::DxDataIterator *dec_iter) override {
    (void)dec_iter;
    if (pkt->kind != AVM_CODEC_CX_FRAME_PKT) return;
    const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
    const size_t sz = pkt->data.frame.sz;
    packets_.emplace_back(buf, buf + sz);
  }

  int speed_;
  int num_temporal_layers_;
  int num_embedded_layers_;
  int temporal_layer_id_;
  int layer_frame_cnt_;
  std::vector<std::vector<uint8_t>> packets_;
};

TEST_P(MultiLayer2TemporalDecodeBaseOnlyRASPruningTest, PruneBetweenKeyAndRAS) {
  ::libavm_test::Y4mVideoSource video("park_joy_90p_8_420.y4m", 0, 20);
  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
  ASSERT_FALSE(packets_.empty()) << "No packets produced";

  // Decode all packets to get original frames.
  std::vector<avm_image_t *> orig_frames = DecodePackets(packets_);
  ASSERT_EQ(static_cast<int>(orig_frames.size()),
            static_cast<int>(packets_.size()))
      << "Original decode frame count mismatch";

  // Prune: keep TL0 only, remove non-reset TL0 frames before first RAS.
  // Retained: KEY(0), RAS(8), and TL0 frames after RAS: 10, 12, 14, 16, 18.
  std::vector<std::vector<uint8_t>> pruned =
      PruneTL0RetainAfterFirstRAS(packets_);

  // Build retained mapping using the same filter logic.
  std::vector<int> retained_indices;
  {
    bool seen_ras = false;
    for (int i = 0; i < static_cast<int>(packets_.size()); ++i) {
      const PacketInfo info = GetPacketInfo(packets_[i]);
      if (!info.has_vcl) continue;  // non-VCL don't produce decoded frames
      if (info.tlayer_id != 0) continue;
      if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
      if (IsResetPoint(info.vcl_type) || seen_ras) {
        retained_indices.push_back(i);
      }
    }
  }

  std::vector<avm_image_t *> pruned_frames = DecodePackets(pruned);
  ASSERT_EQ(pruned_frames.size(), retained_indices.size())
      << "Pruned decode frame count mismatch";

  for (size_t i = 0; i < pruned_frames.size(); ++i) {
    ASSERT_TRUE(ImagesMatch(orig_frames[retained_indices[i]], pruned_frames[i]))
        << "Pixel mismatch at pruned frame " << i << " (original picture "
        << retained_indices[i] << ")";
  }

  FreeImages(orig_frames);
  FreeImages(pruned_frames);
}

AV2_INSTANTIATE_TEST_SUITE(MultiLayer2TemporalDecodeBaseOnlyRASPruningTest,
                           ::testing::Values(5));

// ---------------------------------------------------------------------------
// RAS Frame Pruning Test — 3 Temporal Layers, Decode Base Only
//
// Encodes 28 frames with 3 temporal layers, 1 embedded layer, explicit ref
// frame map, and RAS frames (sframe_type=1) at TL0 distance 4.
//
// With 3TL the temporal layer assignment is:
//   TL0: frames 0, 4, 8, 12, 16, 20, 24  (layer_frame_cnt_ % 4 == 0)
//   TL1: frames 2, 6, 10, 14, 18, 22, 26 (layer_frame_cnt_ % 2 == 0, not %4)
//   TL2: frames 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27 (odd)
//
// sframe_dist=16 places the first RAS at frame 16 (the 5th TL0 frame,
// i.e. TL0 index 4 — distance 4 in TL0 from the KEY at index 0).
//
// Pruning based on OBU header metadata (temporal layer and OBU type):
//   - Remove all non-TL0 packets (tlayer_id != 0).
//   - Among TL0, remove non-reset-point frames before the first RAS.
//
// Retained pictures: 0 (KEY), 16 (RAS), 20, 24 (4 TL0 frames).
// ---------------------------------------------------------------------------
class MultiLayer3TemporalDecodeBaseOnlyRASPruningTest
    : public ::libavm_test::CodecTestWithParam<int>,
      public ::libavm_test::EncoderTest {
 protected:
  MultiLayer3TemporalDecodeBaseOnlyRASPruningTest()
      : EncoderTest(GET_PARAM(0)), speed_(GET_PARAM(1)) {}
  ~MultiLayer3TemporalDecodeBaseOnlyRASPruningTest() override {}

  void SetUp() override {
    InitializeConfig();
    passes_ = 1;
    cfg_.rc_end_usage = AVM_Q;
    cfg_.rc_min_quantizer = 210;
    cfg_.rc_max_quantizer = 210;
    cfg_.g_threads = 2;
    cfg_.g_profile = MAIN_420_10_IP2;
    cfg_.g_lag_in_frames = 0;
    cfg_.g_bit_depth = AVM_BITS_8;
    cfg_.enable_ops = 1;
    cfg_.enable_lcr = 1;
    cfg_.enable_sframe = 1;
    cfg_.sframe_dist = 16;
    cfg_.sframe_mode = 0;
    cfg_.sframe_type = 1;
    num_temporal_layers_ = 3;
    num_embedded_layers_ = 1;
    layer_frame_cnt_ = 0;
    temporal_layer_id_ = 0;
  }

  int GetNumEmbeddedLayers() override { return num_embedded_layers_; }

  void PreEncodeFrameHook(::libavm_test::VideoSource *video,
                          ::libavm_test::Encoder *encoder) override {
    (void)video;
    frame_flags_ = 0;
    if (layer_frame_cnt_ == 0) {
      encoder->Control(AVME_SET_CPUUSED, speed_);
      encoder->Control(AVME_SET_NUMBER_MLAYERS, num_embedded_layers_);
      encoder->Control(AVME_SET_NUMBER_TLAYERS, num_temporal_layers_);
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
      encoder->Control(AV2E_SET_ENABLE_EXPLICIT_REF_FRAME_MAP, 1);
    }
    if (layer_frame_cnt_ % 4 == 0) {
      temporal_layer_id_ = 0;
      encoder->Control(AVME_SET_TLAYER_ID, 0);
    } else if (layer_frame_cnt_ % 2 == 0) {
      temporal_layer_id_ = 1;
      encoder->Control(AVME_SET_TLAYER_ID, 1);
    } else {
      temporal_layer_id_ = 2;
      encoder->Control(AVME_SET_TLAYER_ID, 2);
    }
    layer_frame_cnt_++;
  }

  bool HandleDecodeResult(const avm_codec_err_t res_dec,
                          libavm_test::Decoder *decoder) override {
    EXPECT_EQ(AVM_CODEC_OK, res_dec) << decoder->DecodeError();
    return AVM_CODEC_OK == res_dec;
  }

  void FramePktHook(const avm_codec_cx_pkt_t *pkt,
                    ::libavm_test::DxDataIterator *dec_iter) override {
    (void)dec_iter;
    if (pkt->kind != AVM_CODEC_CX_FRAME_PKT) return;
    const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
    const size_t sz = pkt->data.frame.sz;
    packets_.emplace_back(buf, buf + sz);
  }

  int speed_;
  int num_temporal_layers_;
  int num_embedded_layers_;
  int temporal_layer_id_;
  int layer_frame_cnt_;
  std::vector<std::vector<uint8_t>> packets_;
};

TEST_P(MultiLayer3TemporalDecodeBaseOnlyRASPruningTest, PruneBetweenKeyAndRAS) {
  ::libavm_test::Y4mVideoSource video("park_joy_90p_8_420.y4m", 0, 28);
  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
  ASSERT_FALSE(packets_.empty()) << "No packets produced";

  // Decode all packets to get original frames.
  std::vector<avm_image_t *> orig_frames = DecodePackets(packets_);
  ASSERT_EQ(static_cast<int>(orig_frames.size()),
            static_cast<int>(packets_.size()))
      << "Original decode frame count mismatch";

  // Prune: keep TL0 only, remove non-reset TL0 frames before first RAS.
  // Retained: KEY(0), RAS(16), and TL0 frames after RAS: 20, 24.
  std::vector<std::vector<uint8_t>> pruned =
      PruneTL0RetainAfterFirstRAS(packets_);

  // Build retained mapping using the same filter logic.
  std::vector<int> retained_indices;
  {
    bool seen_ras = false;
    for (int i = 0; i < static_cast<int>(packets_.size()); ++i) {
      const PacketInfo info = GetPacketInfo(packets_[i]);
      if (!info.has_vcl) continue;
      if (info.tlayer_id != 0) continue;
      if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
      if (IsResetPoint(info.vcl_type) || seen_ras) {
        retained_indices.push_back(i);
      }
    }
  }

  std::vector<avm_image_t *> pruned_frames = DecodePackets(pruned);
  ASSERT_EQ(pruned_frames.size(), retained_indices.size())
      << "Pruned decode frame count mismatch";

  for (size_t i = 0; i < pruned_frames.size(); ++i) {
    ASSERT_TRUE(ImagesMatch(orig_frames[retained_indices[i]], pruned_frames[i]))
        << "Pixel mismatch at pruned frame " << i << " (original picture "
        << retained_indices[i] << ")";
  }

  FreeImages(orig_frames);
  FreeImages(pruned_frames);
}

AV2_INSTANTIATE_TEST_SUITE(MultiLayer3TemporalDecodeBaseOnlyRASPruningTest,
                           ::testing::Values(5));

// ---------------------------------------------------------------------------
// Random Access RAS Pruning Test
//
// Encodes 17 frames in single-layer random access configuration with RAS
// frames enabled (sframe_type=1, sframe_dist=4, sframe_mode=2).
//
// The encoded bitstream contains 17 pictures (POC 0-16):
//   POC 0  : KEY (OBU_CLOSED_LOOP_KEY)
//   POC 8  : RAS (OBU_RAS_FRAME)
//   POC 4,2,1,3,6,5,7 : INTER (pyramid within first GoP)
//   POC 16 : RAS (OBU_RAS_FRAME)
//   POC 12,10,9,11,14,13,15 : INTER (pyramid within second GoP)
//
// Pruning based on OBU type: keep KEY, skip the entire first GoP
// (including its leading RAS in decode order), keep everything from
// the second GoP's RAS onward. The filter counts OBU_RAS_FRAME
// occurrences and retains from the second RAS.
// ---------------------------------------------------------------------------
class RandomAccessRASPruningTest
    : public ::libavm_test::CodecTestWithParam<int>,
      public ::libavm_test::EncoderTest {
 protected:
  RandomAccessRASPruningTest()
      : EncoderTest(GET_PARAM(0)), speed_(GET_PARAM(1)) {}
  ~RandomAccessRASPruningTest() override {}

  void SetUp() override {
    InitializeConfig();
    passes_ = 1;
    cfg_.rc_end_usage = AVM_Q;
    cfg_.g_threads = 1;
    cfg_.g_lag_in_frames = 19;
    cfg_.kf_min_dist = 65;
    cfg_.kf_max_dist = 65;
    cfg_.use_fixed_qp_offsets = 1;
    cfg_.enable_sframe = 1;
    cfg_.sframe_dist = 4;
    cfg_.sframe_mode = 2;
    cfg_.sframe_type = 1;
  }

  void PreEncodeFrameHook(::libavm_test::VideoSource *video,
                          ::libavm_test::Encoder *encoder) override {
    if (video->frame() == 0) {
      encoder->Control(AVME_SET_CPUUSED, speed_);
      encoder->Control(AVME_SET_QP, 210);
      encoder->Control(AV2E_SET_ENABLE_KEYFRAME_FILTERING, 0);
      encoder->Control(AV2E_SET_MIN_GF_INTERVAL, 8);
      encoder->Control(AV2E_SET_MAX_GF_INTERVAL, 8);
      encoder->Control(AV2E_SET_GF_MIN_PYRAMID_HEIGHT, 3);
      encoder->Control(AV2E_SET_GF_MAX_PYRAMID_HEIGHT, 3);
      encoder->Control(AVME_SET_ENABLEAUTOALTREF, 1);
      encoder->Control(AV2E_SET_DELTAQ_MODE, 0);
      encoder->Control(AV2E_SET_ENABLE_TPL_MODEL, 0);
      encoder->Control(AV2E_SET_ENABLE_EXPLICIT_REF_FRAME_MAP, 1);
      encoder->SetOption("enable-intrabc-ext", "2");
    }
  }

  bool DoDecode() const override { return false; }

  bool HandleDecodeResult(const avm_codec_err_t res_dec,
                          libavm_test::Decoder *decoder) override {
    EXPECT_EQ(AVM_CODEC_OK, res_dec) << decoder->DecodeError();
    return AVM_CODEC_OK == res_dec;
  }

  void FramePktHook(const avm_codec_cx_pkt_t *pkt,
                    ::libavm_test::DxDataIterator *dec_iter) override {
    (void)dec_iter;
    if (pkt->kind != AVM_CODEC_CX_FRAME_PKT) return;
    const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
    const size_t sz = pkt->data.frame.sz;
    packets_.emplace_back(buf, buf + sz);
  }

  int speed_;
  std::vector<std::vector<uint8_t>> packets_;
};

TEST_P(RandomAccessRASPruningTest, PruneFirstGoP) {
  ::libavm_test::Y4mVideoSource video("park_joy_90p_8_420.y4m", 0, 17);
  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
  ASSERT_FALSE(packets_.empty()) << "No packets produced";

  // Decode all packets to get original frames in display order.
  std::vector<avm_image_t *> orig_frames = DecodePackets(packets_);

  // Prune: keep KEY, skip entire first GoP (including its leading RAS),
  // keep everything from the second GoP's RAS onward.
  std::vector<std::vector<uint8_t>> pruned = PruneFirstGoP(packets_);

  // Decode pruned packets.
  std::vector<avm_image_t *> pruned_frames = DecodePackets(pruned);

  // Verify the pruned bitstream decodes successfully and produces frames.
  ASSERT_FALSE(pruned_frames.empty()) << "Pruned decode produced no frames";

  // The original test used range (0,9): retained pictures 0 and 9-16 in
  // display order. With pyramid reordering, the decoder outputs frames in
  // display order. The pruned decode should produce the KEY frame plus
  // the second GoP frames. Verify the first pruned frame matches the
  // first original frame (both are KEY at POC 0).
  ASSERT_TRUE(ImagesMatch(orig_frames[0], pruned_frames[0]))
      << "KEY frame pixel mismatch";

  // Verify remaining pruned frames match the tail of the original stream.
  // The second GoP in display order occupies the last 8 positions of the
  // original 17-frame output (indices 9-16).
  const int second_gop_start = 9;
  const int second_gop_frames =
      static_cast<int>(orig_frames.size()) - second_gop_start;
  // pruned_frames[1..] should match orig_frames[9..16]
  for (int i = 0; i < second_gop_frames &&
                  (i + 1) < static_cast<int>(pruned_frames.size());
       ++i) {
    ASSERT_TRUE(
        ImagesMatch(orig_frames[second_gop_start + i], pruned_frames[1 + i]))
        << "Pixel mismatch at pruned frame " << (1 + i)
        << " (original display frame " << (second_gop_start + i) << ")";
  }

  FreeImages(orig_frames);
  FreeImages(pruned_frames);
}

AV2_INSTANTIATE_TEST_SUITE(RandomAccessRASPruningTest, ::testing::Values(9));

// ---------------------------------------------------------------------------
// RAS Frame Pruning Test — 3 Embedded + 3 Temporal, Drop SL2
//
// Encodes 20 source frames with 3 embedded layers and 3 temporal layers,
// plus RAS frames (sframe_type=1) at TL0 distance 4.
//
// With 3 embedded layers each source frame produces 3 encoder frames
// (ML0, ML1, ML2), so 20 source frames yield 60 pictures (0-59).
//
// Two-stage pruning based on OBU header metadata:
//   Stage 1: Filter by mlayer_id == 0 to remove ML1+ML2.
//     Result: 20 ML0 pictures.
//
//   Stage 2: From ML0 packets, keep TL0 reset points (KEY/RAS) before
//     the first RAS, then retain ALL frames (all temporal layers) from
//     the first RAS onward.
//     Retained: 0(KEY), 16(RAS), 17, 18, 19 — 5 ML0 frames.
//
// Verification: decode original ML0-only stream and pruned ML0-only
// stream, compare retained frames pixel-by-pixel.
// ---------------------------------------------------------------------------
class MultiLayerTest3Embedded3TemporalDropSL2RASPruningTest
    : public ::libavm_test::CodecTestWithParam<int>,
      public ::libavm_test::EncoderTest {
 protected:
  MultiLayerTest3Embedded3TemporalDropSL2RASPruningTest()
      : EncoderTest(GET_PARAM(0)), speed_(GET_PARAM(1)) {}
  ~MultiLayerTest3Embedded3TemporalDropSL2RASPruningTest() override {}

  void SetUp() override {
    InitializeConfig();
    passes_ = 1;
    cfg_.rc_end_usage = AVM_Q;
    cfg_.rc_min_quantizer = 210;
    cfg_.rc_max_quantizer = 210;
    cfg_.g_threads = 2;
    cfg_.g_profile = MAIN_420_10_IP2;
    cfg_.g_lag_in_frames = 0;
    cfg_.g_bit_depth = AVM_BITS_8;
    cfg_.enable_ops = 1;
    cfg_.enable_lcr = 1;
    cfg_.enable_sframe = 1;
    cfg_.sframe_dist = 48;
    cfg_.sframe_mode = 0;
    cfg_.sframe_type = 1;
    num_temporal_layers_ = 3;
    num_embedded_layers_ = 3;
    layer_frame_cnt_ = 0;
    temporal_layer_id_ = 0;
    embedded_layer_id_ = 0;
  }

  int GetNumEmbeddedLayers() override { return num_embedded_layers_; }

  void PreEncodeFrameHook(::libavm_test::VideoSource *video,
                          ::libavm_test::Encoder *encoder) override {
    (void)video;
    encoder->SetOption("add-sef-for-output", "1");
    frame_flags_ = 0;
    if (layer_frame_cnt_ == 0) {
      encoder->Control(AVME_SET_CPUUSED, speed_);
      encoder->Control(AVME_SET_NUMBER_MLAYERS, num_embedded_layers_);
      encoder->Control(AVME_SET_NUMBER_TLAYERS, num_temporal_layers_);
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
      encoder->Control(AV2E_SET_ENABLE_EXPLICIT_REF_FRAME_MAP, 1);
    }
    embedded_layer_id_ = (layer_frame_cnt_ % 3 == 0)         ? 0
                         : ((layer_frame_cnt_ - 1) % 3 == 0) ? 1
                                                             : 2;
    if (embedded_layer_id_ == 0) {
      struct avm_scaling_mode mode = { AVME_ONEFOUR, AVME_ONEFOUR };
      encoder->Control(AVME_SET_SCALEMODE, &mode);
      encoder->Control(AVME_SET_MLAYER_ID, 0);
    } else if (embedded_layer_id_ == 1) {
      struct avm_scaling_mode mode = { AVME_ONETWO, AVME_ONETWO };
      encoder->Control(AVME_SET_SCALEMODE, &mode);
      encoder->Control(AVME_SET_MLAYER_ID, 1);
    } else {
      struct avm_scaling_mode mode = { AVME_NORMAL, AVME_NORMAL };
      encoder->Control(AVME_SET_SCALEMODE, &mode);
      encoder->Control(AVME_SET_MLAYER_ID, 2);
    }
    if (video->frame() % 4 == 0) {
      temporal_layer_id_ = 0;
      encoder->Control(AVME_SET_TLAYER_ID, 0);
    } else if ((video->frame() - 1) % 2 == 0) {
      temporal_layer_id_ = 2;
      encoder->Control(AVME_SET_TLAYER_ID, 2);
    } else if ((video->frame() - 2) % 4 == 0) {
      temporal_layer_id_ = 1;
      encoder->Control(AVME_SET_TLAYER_ID, 1);
    }
    layer_frame_cnt_++;
  }

  bool DoDecode() const override { return false; }

  bool HandleDecodeResult(const avm_codec_err_t res_dec,
                          libavm_test::Decoder *decoder) override {
    EXPECT_EQ(AVM_CODEC_OK, res_dec) << decoder->DecodeError();
    return AVM_CODEC_OK == res_dec;
  }

  void FramePktHook(const avm_codec_cx_pkt_t *pkt,
                    ::libavm_test::DxDataIterator *dec_iter) override {
    (void)dec_iter;
    if (pkt->kind != AVM_CODEC_CX_FRAME_PKT) return;
    const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
    const size_t sz = pkt->data.frame.sz;
    packets_.emplace_back(buf, buf + sz);
  }

  int speed_;
  int num_temporal_layers_;
  int num_embedded_layers_;
  int temporal_layer_id_;
  int embedded_layer_id_;
  int layer_frame_cnt_;
  std::vector<std::vector<uint8_t>> packets_;
};

TEST_P(MultiLayerTest3Embedded3TemporalDropSL2RASPruningTest,
       PruneBetweenKeyAndRAS) {
  ::libavm_test::Y4mVideoSource video("park_joy_90p_8_420.y4m", 0, 20);
  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
  ASSERT_FALSE(packets_.empty()) << "No packets produced";

  // Stage 1: Remove ML1+ML2 — keep only ML0 packets based on OBU mlayer_id.
  std::vector<std::vector<uint8_t>> ml0_packets = FilterByMlayer(packets_, 0);
  ASSERT_EQ(static_cast<int>(ml0_packets.size()), 20)
      << "ML0-only packet count should be 20";

  // Decode ML0-only packets with all-layers to get original ML0 frames.
  std::vector<avm_image_t *> orig_frames = DecodePacketsAllLayers(ml0_packets);
  ASSERT_EQ(static_cast<int>(orig_frames.size()), 20)
      << "Original ML0 decode frame count mismatch";

  // Stage 2: From ML0-only packets, keep KEY and RAS (TL0 reset points)
  // before the first RAS, then retain ALL frames (including non-TL0) after
  // the first RAS.
  // With 3TL and sframe_dist=48 the first ML0 RAS is at packet index 16.
  // Retained: 0(KEY), 16(RAS), 17, 18, 19 — 5 ML0 frames.
  std::vector<std::vector<uint8_t>> pruned =
      PruneRetainAllAfterFirstRAS(ml0_packets);

  // Build retained mapping over the ML0-only stream using the same logic.
  std::vector<int> retained_indices;
  {
    bool seen_ras = false;
    for (int i = 0; i < static_cast<int>(ml0_packets.size()); ++i) {
      const PacketInfo info = GetPacketInfo(ml0_packets[i]);
      if (!info.has_vcl) continue;
      if (!seen_ras) {
        if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
        if (info.tlayer_id == 0 && IsResetPoint(info.vcl_type)) {
          retained_indices.push_back(i);
        }
      } else {
        retained_indices.push_back(i);
      }
    }
  }

  std::vector<avm_image_t *> pruned_frames = DecodePacketsAllLayers(pruned);
  ASSERT_EQ(pruned_frames.size(), retained_indices.size())
      << "Pruned decode frame count mismatch";

  for (size_t i = 0; i < pruned_frames.size(); ++i) {
    ASSERT_TRUE(ImagesMatch(orig_frames[retained_indices[i]], pruned_frames[i]))
        << "Pixel mismatch at pruned frame " << i << " (original ML0 picture "
        << retained_indices[i] << ")";
  }

  FreeImages(orig_frames);
  FreeImages(pruned_frames);
}

AV2_INSTANTIATE_TEST_SUITE(
    MultiLayerTest3Embedded3TemporalDropSL2RASPruningTest,
    ::testing::Values(5));

// ---------------------------------------------------------------------------
// RAS Frame Pruning Test - 2 Embedded + 2 Temporal, Drop TL1
//
// Based on MultiLayerTest2Embedded2TempDropTL1. Encodes 20 source frames
// with 2 embedded layers and 2 temporal layers, explicit ref frame map,
// and RAS frames (sframe_type=1) at TL0 distance 4.
//
// With 2 embedded layers each source frame produces 2 encoder frames
// (ML0, ML1), so 20 source frames yield 40 pictures.
//
// The temporal layer assignment cycles over groups of 4 encoder frames:
//   layer_frame_cnt_ % 4 == 0 : ML0, TL0
//   layer_frame_cnt_ % 4 == 1 : ML1, TL0
//   layer_frame_cnt_ % 4 == 2 : ML0, TL1
//   layer_frame_cnt_ % 4 == 3 : ML1, TL1
//
// sframe_dist=16 places the first RAS at encoder frame 16 (the 5th TL0
// group, i.e. TL0 distance 4 from the KEY at index 0).
//
// Two-stage pruning based on OBU header metadata:
//   Stage 1: Drop TL1, keep only packets with tlayer_id == 0.
//     Result: 20 TL0 packets (10 ML0 + 10 ML1).
//
//   Stage 2: From TL0 packets, keep KEY and RAS (reset points) before
//     the first RAS, then retain ALL packets from the first RAS onward.
//
// Verification: decode original all-layers stream filtered to TL0 and
// the pruned TL0 stream, compare retained frames pixel-by-pixel.
// ---------------------------------------------------------------------------
class MultiLayerTest2Embedded2TempDropTL1RASPruningTest
    : public ::libavm_test::CodecTestWithParam<int>,
      public ::libavm_test::EncoderTest {
 protected:
  MultiLayerTest2Embedded2TempDropTL1RASPruningTest()
      : EncoderTest(GET_PARAM(0)), speed_(GET_PARAM(1)) {}
  ~MultiLayerTest2Embedded2TempDropTL1RASPruningTest() override {}

  void SetUp() override {
    InitializeConfig();
    passes_ = 1;
    cfg_.rc_end_usage = AVM_Q;
    cfg_.rc_min_quantizer = 210;
    cfg_.rc_max_quantizer = 210;
    cfg_.g_threads = 2;
    cfg_.g_profile = MAIN_420_10_IP2;
    cfg_.g_lag_in_frames = 0;
    cfg_.g_bit_depth = AVM_BITS_8;
    cfg_.enable_ops = 1;
    cfg_.enable_lcr = 1;
    cfg_.enable_sframe = 1;
    cfg_.sframe_dist = 16;
    cfg_.sframe_mode = 0;
    cfg_.sframe_type = 1;
    num_temporal_layers_ = 2;
    num_embedded_layers_ = 2;
    layer_frame_cnt_ = 0;
    temporal_layer_id_ = 0;
    embedded_layer_id_ = 0;
  }

  int GetNumEmbeddedLayers() override { return num_embedded_layers_; }

  void PreEncodeFrameHook(::libavm_test::VideoSource *video,
                          ::libavm_test::Encoder *encoder) override {
    (void)video;
    encoder->SetOption("add-sef-for-output", "1");
    frame_flags_ = 0;
    if (layer_frame_cnt_ == 0) {
      encoder->Control(AVME_SET_CPUUSED, speed_);
      encoder->Control(AVME_SET_NUMBER_MLAYERS, num_embedded_layers_);
      encoder->Control(AVME_SET_NUMBER_TLAYERS, num_temporal_layers_);
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
      encoder->Control(AV2E_SET_ENABLE_EXPLICIT_REF_FRAME_MAP, 1);
    }
    if (layer_frame_cnt_ % 4 == 0) {
      struct avm_scaling_mode mode = { AVME_ONETWO, AVME_ONETWO };
      encoder->Control(AVME_SET_SCALEMODE, &mode);
      embedded_layer_id_ = 0;
      temporal_layer_id_ = 0;
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
    } else if (layer_frame_cnt_ % 2 == 0) {
      struct avm_scaling_mode mode = { AVME_ONETWO, AVME_ONETWO };
      encoder->Control(AVME_SET_SCALEMODE, &mode);
      embedded_layer_id_ = 0;
      temporal_layer_id_ = 1;
      encoder->Control(AVME_SET_MLAYER_ID, 0);
      encoder->Control(AVME_SET_TLAYER_ID, 1);
    } else if ((layer_frame_cnt_ - 1) % 4 == 0) {
      embedded_layer_id_ = 1;
      temporal_layer_id_ = 0;
      encoder->Control(AVME_SET_MLAYER_ID, 1);
      encoder->Control(AVME_SET_TLAYER_ID, 0);
    } else if ((layer_frame_cnt_ - 1) % 2 == 0) {
      embedded_layer_id_ = 1;
      temporal_layer_id_ = 1;
      encoder->Control(AVME_SET_MLAYER_ID, 1);
      encoder->Control(AVME_SET_TLAYER_ID, 1);
    }
    layer_frame_cnt_++;
  }

  bool DoDecode() const override { return false; }

  bool HandleDecodeResult(const avm_codec_err_t res_dec,
                          libavm_test::Decoder *decoder) override {
    EXPECT_EQ(AVM_CODEC_OK, res_dec) << decoder->DecodeError();
    return AVM_CODEC_OK == res_dec;
  }

  void FramePktHook(const avm_codec_cx_pkt_t *pkt,
                    ::libavm_test::DxDataIterator *dec_iter) override {
    (void)dec_iter;
    if (pkt->kind != AVM_CODEC_CX_FRAME_PKT) return;
    const uint8_t *buf = static_cast<const uint8_t *>(pkt->data.frame.buf);
    const size_t sz = pkt->data.frame.sz;
    packets_.emplace_back(buf, buf + sz);
  }

  int speed_;
  int num_temporal_layers_;
  int num_embedded_layers_;
  int temporal_layer_id_;
  int embedded_layer_id_;
  int layer_frame_cnt_;
  std::vector<std::vector<uint8_t>> packets_;
};

TEST_P(MultiLayerTest2Embedded2TempDropTL1RASPruningTest,
       PruneBetweenKeyAndRAS) {
  ::libavm_test::Y4mVideoSource video("park_joy_90p_8_420.y4m", 0, 20);
  ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
  ASSERT_FALSE(packets_.empty()) << "No packets produced";

  // Stage 1: Drop TL1 ΓÇö keep only TL0 packets (both ML0 and ML1).
  std::vector<std::vector<uint8_t>> tl0_packets =
      FilterByMaxTlayer(packets_, 0);

  // Decode TL0-only packets with all-layers to get original TL0 frames.
  std::vector<avm_image_t *> orig_frames = DecodePacketsAllLayers(tl0_packets);

  // Stage 2: From TL0-only packets, keep KEY and RAS (TL0 reset points)
  // before the first RAS, then retain ALL packets from the first RAS onward.
  std::vector<std::vector<uint8_t>> pruned =
      PruneRetainAllAfterFirstRAS(tl0_packets);

  // Build retained mapping over the TL0-only stream using the same logic.
  std::vector<int> retained_indices;
  {
    bool seen_ras = false;
    for (int i = 0; i < static_cast<int>(tl0_packets.size()); ++i) {
      const PacketInfo info = GetPacketInfo(tl0_packets[i]);
      if (!info.has_vcl) continue;
      if (!seen_ras) {
        if (info.vcl_type == OBU_RAS_FRAME) seen_ras = true;
        if (info.tlayer_id == 0 && IsResetPoint(info.vcl_type)) {
          retained_indices.push_back(i);
        }
      } else {
        retained_indices.push_back(i);
      }
    }
  }

  std::vector<avm_image_t *> pruned_frames = DecodePacketsAllLayers(pruned);
  ASSERT_EQ(pruned_frames.size(), retained_indices.size())
      << "Pruned decode frame count mismatch";

  for (size_t i = 0; i < pruned_frames.size(); ++i) {
    ASSERT_TRUE(ImagesMatch(orig_frames[retained_indices[i]], pruned_frames[i]))
        << "Pixel mismatch at pruned frame " << i << " (original TL0 picture "
        << retained_indices[i] << ")";
  }

  FreeImages(orig_frames);
  FreeImages(pruned_frames);
}

AV2_INSTANTIATE_TEST_SUITE(MultiLayerTest2Embedded2TempDropTL1RASPruningTest,
                           ::testing::Values(5));

}  // namespace
