// Copyright (c) 2021, Alliance for Open Media. All rights reserved
//
// This source code is subject to the terms of the BSD 2 Clause License and
// the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License
// was not distributed with this source code in the LICENSE file, you can
// obtain it at www.aomedia.org/license/software. 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 www.aomedia.org/license/patent.

#include "avifinfo.h"

#include <algorithm>
#include <fstream>
#include <vector>

#include "gtest/gtest.h"

namespace {

using Data = std::vector<uint8_t>;

Data LoadFile(const char file_name[]) {
  std::ifstream file(file_name, std::ios::binary | std::ios::ate);
  if (!file) return Data();
  const auto file_size = file.tellg();
  Data bytes(file_size * sizeof(char));
  file.seekg(0);  // Rewind.
  return file.read(reinterpret_cast<char*>(bytes.data()), file_size) ? bytes
                                                                     : Data();
}

void WriteBigEndian(uint32_t value, uint32_t num_bytes, uint8_t* input) {
  for (int i = num_bytes - 1; i >= 0; --i) {
    input[i] = value & 0xff;
    value >>= 8;
  }
}

bool SetPrimaryItemIdToGainmapId(Data& input) {
  AvifInfoFeatures f;
  if (AvifInfoGetFeatures(input.data(), input.size(), &f) != kAvifInfoOk) {
    return false;
  }
  WriteBigEndian(f.gainmap_item_id, f.primary_item_id_bytes,
                 &input[f.primary_item_id_location]);
  return true;
}

void ExpectEqual(const AvifInfoFeatures& actual,
                 const AvifInfoFeatures& expected) {
  EXPECT_EQ(actual.width, expected.width);
  EXPECT_EQ(actual.height, expected.height);
  EXPECT_EQ(actual.bit_depth, expected.bit_depth);
  EXPECT_EQ(actual.num_channels, expected.num_channels);
  EXPECT_EQ(actual.has_gainmap, expected.has_gainmap);
  EXPECT_EQ(actual.gainmap_item_id, expected.gainmap_item_id);
  EXPECT_EQ(actual.primary_item_id_location, expected.primary_item_id_location);
  EXPECT_EQ(actual.primary_item_id_bytes, expected.primary_item_id_bytes);
}

//------------------------------------------------------------------------------
// Positive tests

TEST(AvifInfoGetTest, Ok) {
  const Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
  ExpectEqual(f, {.width = 1u,
                  .height = 1u,
                  .bit_depth = 8u,
                  .num_channels = 3u,
                  .has_gainmap = 0u,
                  .primary_item_id_location = 96u,
                  .primary_item_id_bytes = 2u});
}

TEST(AvifInfoGetTest, WithAlpha) {
  const Data input = LoadFile("avifinfo_test_2x2_alpha.avif");
  ASSERT_FALSE(input.empty());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
  ExpectEqual(f, {.width = 2u,
                  .height = 2u,
                  .bit_depth = 8u,
                  .num_channels = 4u,
                  .has_gainmap = 0u,
                  .primary_item_id_location = 96u,
                  .primary_item_id_bytes = 2u});
}

TEST(AvifInfoGetTest, WithGainmap) {
  const Data input = LoadFile("avifinfo_test_20x20_gainmap.avif");
  ASSERT_FALSE(input.empty());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
  ExpectEqual(f, {.width = 20u,
                  .height = 20u,
                  .bit_depth = 8u,
                  .num_channels = 3u,
                  .has_gainmap = 1u,
                  .gainmap_item_id = 2u,
                  .primary_item_id_location = 96u,
                  .primary_item_id_bytes = 2u});

  Data gainmap = input;
  ASSERT_TRUE(SetPrimaryItemIdToGainmapId(gainmap));
  ASSERT_EQ(AvifInfoIdentify(gainmap.data(), gainmap.size()), kAvifInfoOk);
  AvifInfoFeatures gainmap_f;
  ASSERT_EQ(AvifInfoGetFeatures(gainmap.data(), gainmap.size(), &gainmap_f),
            kAvifInfoOk);
  // TODO(maryla-uc): find a small test file with a gainmap that is smaller
  // than the main image.
  ExpectEqual(gainmap_f, {.width = 20u,
                          .height = 20u,
                          .bit_depth = 8u,
                          .num_channels = 1u,  // the gainmap is monochrome
                          .has_gainmap = 1u,
                          .gainmap_item_id = 2u,
                          .primary_item_id_location = 96u,
                          .primary_item_id_bytes = 2u});
}

TEST(AvifInfoGetTest, WithGainmapTmap) {
  for (const char* file_name :
       {"avifinfo_test_12x34_gainmap_tmap.avif",
        "avifinfo_test_12x34_gainmap_tmap_iref_after_iprp.avif"}) {
    SCOPED_TRACE(file_name);
    const Data input = LoadFile(file_name);
    ASSERT_FALSE(input.empty());

    ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
    AvifInfoFeatures f;
    ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
    ExpectEqual(f, {.width = 12u,
                    .height = 34u,
                    .bit_depth = 10u,
                    .num_channels = 4u,
                    .has_gainmap = 1u,
                    .gainmap_item_id = 4u,
                    .primary_item_id_location = 96u,
                    .primary_item_id_bytes = 2u});

    Data gainmap = input;
    ASSERT_TRUE(SetPrimaryItemIdToGainmapId(gainmap));
    ASSERT_EQ(AvifInfoIdentify(gainmap.data(), gainmap.size()), kAvifInfoOk);
    AvifInfoFeatures gainmap_f;
    ASSERT_EQ(AvifInfoGetFeatures(gainmap.data(), gainmap.size(), &gainmap_f),
              kAvifInfoOk);
    // Note that num_channels says 4 even though the alpha plane is associated
    // to the main image and not the gain map, but libavifinfo does not check
    // this.
    ExpectEqual(gainmap_f, {.width = 6u,
                            .height = 17u,
                            .bit_depth = 8u,
                            .num_channels = 4u,
                            .has_gainmap = 1u,
                            .gainmap_item_id = 4u,
                            .primary_item_id_location = 96u,
                            .primary_item_id_bytes = 2u});
  }
}

TEST(AvifInfoGetTest, NoPixi10b) {
  // Same as above but "meta" box size is stored as 64 bits, "av1C" has
  // 'high_bitdepth' set to true, "pixi" was renamed to "pixy" and "mdat" size
  // is 0 (extends to the end of the file).
  const Data input =
      LoadFile("avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif");
  ASSERT_FALSE(input.empty());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
  ExpectEqual(f, {.width = 1u,
                  .height = 1u,
                  .bit_depth = 10u,
                  .num_channels = 3u,
                  .has_gainmap = 0u,
                  .primary_item_id_location = 104u,
                  .primary_item_id_bytes = 2u});
}

TEST(AvifInfoGetTest, EnoughBytes) {
  Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());
  // Truncate 'input' just after the required information (discard AV1 box).
  const uint8_t kMdatTag[] = {'m', 'd', 'a', 't'};
  input.resize(std::search(input.begin(), input.end(), kMdatTag, kMdatTag + 4) -
               input.begin());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk);
  ExpectEqual(f, {.width = 1u,
                  .height = 1u,
                  .bit_depth = 8u,
                  .num_channels = 3u,
                  .has_gainmap = 0u,
                  .primary_item_id_location = 96u,
                  .primary_item_id_bytes = 2u});
}

TEST(AvifInfoGetTest, Null) {
  const Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());

  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr),
            kAvifInfoOk);
}

//------------------------------------------------------------------------------
// Negative tests

TEST(AvifInfoGetTest, Empty) {
  ASSERT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData);
  ExpectEqual(f, {0});
}

TEST(AvifInfoGetTest, NotEnoughBytes) {
  Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());
  // Truncate 'input' before having all the required information.
  const uint8_t kIpmaTag[] = {'i', 'p', 'm', 'a'};
  input.resize(std::search(input.begin(), input.end(), kIpmaTag, kIpmaTag + 4) -
               input.begin());

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
            kAvifInfoNotEnoughData);
}

TEST(AvifInfoGetTest, Broken) {
  Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());
  // Change "ispe" to "aspe".
  const uint8_t kIspeTag[] = {'i', 's', 'p', 'e'};
  std::search(input.begin(), input.end(), kIspeTag, kIspeTag + 4)[0] = 'a';

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
            kAvifInfoInvalidFile);
  ExpectEqual(f, {0});
}

TEST(AvifInfoGetTest, MetaBoxIsTooBig) {
  Data input = LoadFile("avifinfo_test_1x1.avif");
  ASSERT_FALSE(input.empty());
  // Change "meta" box size to the maximum size 2^64-1.
  const uint8_t kMetaTag[] = {'m', 'e', 't', 'a'};
  auto meta_tag =
      std::search(input.begin(), input.end(), kMetaTag, kMetaTag + 4);
  meta_tag[-4] = meta_tag[-3] = meta_tag[-2] = 0;
  meta_tag[-1] = 1;  // 32-bit "1" then 4-char "meta" then 64-bit size.
  input.insert(meta_tag + 4, {255, 255, 255, 255, 255, 255, 255, 255});

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f),
            kAvifInfoTooComplex);
  ExpectEqual(f, {0});
}

TEST(AvifInfoGetTest, TooManyBoxes) {
  // Create a valid-ish input with too many boxes to parse.
  Data input = {0,   0,   0,   16,  'f', 't', 'y', 'p',
                'a', 'v', 'i', 'f', 0,   0,   0,   0};
  const uint32_t kNumBoxes = 12345;
  input.reserve(input.size() + kNumBoxes * 8);
  for (uint32_t i = 0; i < kNumBoxes; ++i) {
    const uint8_t kBox[] = {0, 0, 0, 8, 'a', 'b', 'c', 'd'};
    input.insert(input.end(), kBox, kBox + kBox[3]);
  }

  ASSERT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()),
                                input.size() * 4, &f),
            kAvifInfoTooComplex);
}

TEST(AvifInfoReadTest, Null) {
  ASSERT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr,
                                   /*skip=*/nullptr),
            kAvifInfoNotEnoughData);
  AvifInfoFeatures f;
  ASSERT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr,
                                      /*skip=*/nullptr, &f),
            kAvifInfoNotEnoughData);
  ExpectEqual(f, {0});
}

//------------------------------------------------------------------------------

}  // namespace
