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

#include "ui/base/resource/resource_bundle.h"

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <array>
#include <atomic>
#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <vector>

#include "base/base_paths.h"
#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/i18n/language_tag.h"
#include "base/memory/ref_counted_memory.h"
#include "base/numerics/byte_conversions.h"
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "skia/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/resource/data_pack.h"
#include "ui/base/resource/data_pack_literal.h"
#include "ui/base/resource/mock_resource_bundle_delegate.h"
#include "ui/base/resource/resource_scale_factor.h"
#include "ui/gfx/codec/png_codec.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"

#if BUILDFLAG(IS_WIN)
#include "ui/display/win/dpi.h"
#endif

namespace ui {
namespace {

using ::base::i18n::GetKnownLanguageTag;
using ::testing::_;
using ::testing::Between;
using ::testing::DoAll;
using ::testing::Property;
using ::testing::Return;
using ::testing::ReturnArg;
using ::testing::SetArgPointee;

const unsigned char kPngMagic[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 };
const size_t kPngChunkMetadataSize = 12;
const unsigned char kPngIHDRChunkType[4] = { 'I', 'H', 'D', 'R' };

// Custom chunk that GRIT adds to PNG to indicate that it could not find a
// bitmap at the requested scale factor and fell back to 1x.
const unsigned char kPngScaleChunk[12] = { 0x00, 0x00, 0x00, 0x00,
                                           'c', 's', 'C', 'l',
                                           0xc1, 0x30, 0x60, 0x4d };

#if BUILDFLAG(SKIA_SUPPORT_SKOTTIE) && BUILDFLAG(USE_BLINK)
// The width and height attributes values in the lottie asset.
constexpr int kLottieWidth = 200;
constexpr int kLottieHeight = 200;
// A string with the "LOTTIE" prefix that GRIT adds to Lottie assets.
constexpr std::string_view kLottieData =
    R"(LOTTIE{
    "v": "5.5.2",
    "fr": 1,
    "ip": 0,
    "op": 1,
    "w": 200,
    "h": 200,
    "ddd": 0,
    "assets": [],
    "layers": [
        {
        "ty": 1,
        "ip": 0,
        "op": 1,
        "st": 0,
        "ks": {},
        "sc": "#ff0000",
        "sh": 200,
        "sw": 200
        }
    ]
    })";
// The contents after the prefix has been removed.
constexpr std::string_view kLottieExpected =
    R"({
    "v": "5.5.2",
    "fr": 1,
    "ip": 0,
    "op": 1,
    "w": 200,
    "h": 200,
    "ddd": 0,
    "assets": [],
    "layers": [
        {
        "ty": 1,
        "ip": 0,
        "op": 1,
        "st": 0,
        "ks": {},
        "sc": "#ff0000",
        "sh": 200,
        "sw": 200
        }
    ]
    })";
#endif  // BUILDFLAG(SKIA_SUPPORT_SKOTTIE) && BUILDFLAG(USE_BLINK)

// Returns |bitmap_data| with |custom_chunk| inserted after the IHDR chunk.
void AddCustomChunk(std::string_view custom_chunk,
                    std::vector<unsigned char>* bitmap_data) {
  size_t chunk_offset = 0u;

  // Expect the magic signature first.
  auto magic_span =
      base::as_byte_span(*bitmap_data).first<std::size(kPngMagic)>();
  EXPECT_TRUE(magic_span == kPngMagic);
  chunk_offset += magic_span.size();

  // Expect an IHDR chunk next. It starts with a length.
  auto ihdr_chunk = base::as_byte_span(*bitmap_data).subspan(chunk_offset);
  uint32_t ihdr_chunk_length =
      base::U32FromBigEndian(ihdr_chunk.first<sizeof(uint32_t)>());
  auto ihdr_type =
      ihdr_chunk.subspan<sizeof(uint32_t), std::size(kPngIHDRChunkType)>();
  EXPECT_TRUE(ihdr_type == kPngIHDRChunkType);
  chunk_offset += ihdr_chunk_length;

  // Expect a PNG Metadata chunk next.
  chunk_offset += kPngChunkMetadataSize;

  // Then insert custom chunk.
  ASSERT_LE(chunk_offset, bitmap_data->size());
  bitmap_data->insert(bitmap_data->begin() + chunk_offset, custom_chunk.begin(),
                      custom_chunk.end());
}

// Creates datapack at |path| with a single bitmap at resource ID 3
// which is |edge_size|x|edge_size| pixels.
// If |custom_chunk| is non empty, adds it after the IHDR chunk
// in the encoded bitmap data.
void CreateDataPackWithSingleBitmap(const base::FilePath& path,
                                    int edge_size,
                                    std::string_view custom_chunk) {
  SkBitmap bitmap;
  bitmap.allocN32Pixels(edge_size, edge_size);
  bitmap.eraseColor(SK_ColorWHITE);
  std::optional<std::vector<uint8_t>> bitmap_data =
      gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, /*discard_transparency=*/false);
  ASSERT_TRUE(bitmap_data);

  if (custom_chunk.size() > 0) {
    AddCustomChunk(custom_chunk, &bitmap_data.value());
  }

  std::map<uint16_t, std::string_view> resources;
  resources[3u] = base::as_string_view(bitmap_data.value());
  DataPack::WritePack(path, resources, ui::DataPack::BINARY);
}

}  // namespace

class ResourceBundleTest : public testing::Test {
 public:
  ResourceBundleTest() : resource_bundle_(nullptr) {}

  ResourceBundleTest(const ResourceBundleTest&) = delete;
  ResourceBundleTest& operator=(const ResourceBundleTest&) = delete;

  ~ResourceBundleTest() override {}

  // Overridden from testing::Test:
  void TearDown() override {
    resource_bundle_.reset();
    if (temp_dir_.IsValid())
      ASSERT_TRUE(temp_dir_.Delete());
  }

  // Returns new ResoureBundle with the specified |delegate|. The
  // ResourceBundleTest class manages the lifetime of the returned
  // ResourceBundle.
  ResourceBundle* CreateResourceBundle(ResourceBundle::Delegate* delegate) {
    DCHECK(!resource_bundle_);
    resource_bundle_ = std::make_unique<ResourceBundle>(delegate);
    return resource_bundle_.get();
  }

 protected:
  base::ScopedTempDir temp_dir_;
  MockResourceBundleDelegate delegate_;
  std::unique_ptr<ResourceBundle> resource_bundle_;
};

TEST_F(ResourceBundleTest, DelegateGetPathForResourcePack) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);
  base::FilePath pack_path(FILE_PATH_LITERAL("/path/to/test_path.pak"));
  ResourceScaleFactor pack_scale_factor = ui::k200Percent;

  EXPECT_CALL(delegate_, GetPathForResourcePack(Property(&base::FilePath::value,
                                                         pack_path.value()),
                                                pack_scale_factor))
      .Times(1)
      .WillOnce(Return(pack_path));

  resource_bundle->AddDataPackFromPath(pack_path, pack_scale_factor);
}

TEST_F(ResourceBundleTest, DelegateGetImageNamed) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);
  gfx::Image empty_image = resource_bundle->GetEmptyImage();
  int resource_id = 5;

  EXPECT_CALL(delegate_, GetImageNamed(resource_id))
      .Times(1)
      .WillOnce(Return(empty_image));

  gfx::Image result = resource_bundle->GetImageNamed(resource_id);
  EXPECT_EQ(empty_image.ToSkBitmap(), result.ToSkBitmap());
}

TEST_F(ResourceBundleTest, DelegateGetNativeImageNamed) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);

  gfx::Image empty_image = resource_bundle->GetEmptyImage();
  int resource_id = 5;

  // Some platforms delegate GetNativeImageNamed calls to GetImageNamed.
  EXPECT_CALL(delegate_, GetImageNamed(resource_id))
      .Times(Between(0, 1))
      .WillOnce(Return(empty_image));
  EXPECT_CALL(delegate_, GetNativeImageNamed(resource_id))
      .Times(Between(0, 1))
      .WillOnce(Return(empty_image));

  gfx::Image result = resource_bundle->GetNativeImageNamed(resource_id);
  EXPECT_EQ(empty_image.ToSkBitmap(), result.ToSkBitmap());
}

TEST_F(ResourceBundleTest, DelegateHasDataResource) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);

  int resource_id = 5;

  EXPECT_CALL(delegate_, HasDataResource(resource_id))
      .Times(1)
      .WillOnce(Return(true));

  bool result = resource_bundle->HasDataResource(resource_id);
  EXPECT_EQ(result, true);
}

TEST_F(ResourceBundleTest, DelegateLoadDataResourceBytes) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);

  // Create the data resource for testing purposes.
  const unsigned char data[] = "My test data";
  auto static_memory = base::MakeRefCounted<base::RefCountedStaticMemory>(data);

  int resource_id = 5;
  ResourceScaleFactor scale_factor = ui::kScaleFactorNone;

  EXPECT_CALL(delegate_, LoadDataResourceBytes(resource_id, scale_factor))
      .Times(1)
      .WillOnce(Return(static_memory.get()));

  scoped_refptr<base::RefCountedMemory> result =
      resource_bundle->LoadDataResourceBytesForScale(resource_id, scale_factor);
  EXPECT_EQ(static_memory, result);
}

TEST_F(ResourceBundleTest, DelegateGetRawDataResource) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);

  // Create the string piece for testing purposes.
  char data[] = "My test data";
  std::string_view string_piece(data);

  int resource_id = 5;

  EXPECT_CALL(delegate_,
              GetRawDataResource(resource_id, ui::kScaleFactorNone, _))
      .Times(1)
      .WillOnce(DoAll(SetArgPointee<2>(string_piece), Return(true)));

  std::string_view result = resource_bundle->GetRawDataResource(resource_id);
  EXPECT_EQ(string_piece.data(), result.data());
}

TEST_F(ResourceBundleTest, IsGzipped) {
  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  base::FilePath data_path =
      temp_dir_.GetPath().Append(FILE_PATH_LITERAL("sample.pak"));
  // Dump contents into a pak file and load it.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));
  ResourceBundle* resource_bundle = CreateResourceBundle(nullptr);
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);

  ASSERT_FALSE(resource_bundle->IsGzipped(1));
  ASSERT_FALSE(resource_bundle->IsGzipped(4));
  ASSERT_FALSE(resource_bundle->IsGzipped(6));
  ASSERT_TRUE(resource_bundle->IsGzipped(8));
  // Ask for a non-existent resource ID.
  ASSERT_FALSE(resource_bundle->IsGzipped(200));
}

TEST_F(ResourceBundleTest, IsBrotli) {
  ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  base::FilePath data_path =
      temp_dir_.GetPath().Append(FILE_PATH_LITERAL("sample.pak"));
  // Dump contents into a pak file and load it.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));
  ResourceBundle* resource_bundle = CreateResourceBundle(nullptr);
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);

  ASSERT_FALSE(resource_bundle->IsBrotli(1));
  ASSERT_FALSE(resource_bundle->IsBrotli(4));
  ASSERT_TRUE(resource_bundle->IsBrotli(6));
  ASSERT_FALSE(resource_bundle->IsGzipped(6));
  ASSERT_FALSE(resource_bundle->IsBrotli(8));
  // Ask for non-existent resource ID.
  ASSERT_FALSE(resource_bundle->IsBrotli(200));
}

TEST_F(ResourceBundleTest, DelegateGetLocalizedString) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);
  std::u16string data = u"My test data";
  int resource_id = 5;

  EXPECT_CALL(delegate_, GetLocalizedString(resource_id, _))
      .Times(1)
      .WillOnce(DoAll(SetArgPointee<1>(data), Return(true)));

  std::u16string result = resource_bundle->GetLocalizedString(resource_id);
  EXPECT_EQ(data, result);
}

TEST_F(ResourceBundleTest, OverrideStringResource) {
  ResourceBundle* resource_bundle = CreateResourceBundle(nullptr);

  std::u16string data = u"My test data";
  int resource_id = 5;

  std::u16string result = resource_bundle->GetLocalizedString(resource_id);
  EXPECT_EQ(std::u16string(), result);

  resource_bundle->OverrideLocaleStringResource(resource_id, data);

  result = resource_bundle->GetLocalizedString(resource_id);
  EXPECT_EQ(data, result);
}

#if DCHECK_IS_ON()
TEST_F(ResourceBundleTest, CanOverrideStringResources) {
  ResourceBundle* resource_bundle = CreateResourceBundle(nullptr);
  std::u16string data = u"My test data";
  int resource_id = 5;

  EXPECT_TRUE(
      resource_bundle->get_can_override_locale_string_resources_for_test());
  resource_bundle->GetLocalizedString(resource_id);
  EXPECT_FALSE(
      resource_bundle->get_can_override_locale_string_resources_for_test());
}
#endif

TEST_F(ResourceBundleTest, DelegateGetLocalizedStringWithOverride) {
  ResourceBundle* resource_bundle = CreateResourceBundle(&delegate_);
  std::u16string delegate_data = u"My delegate data";
  int resource_id = 5;

  EXPECT_CALL(delegate_, GetLocalizedString(resource_id, _))
      .Times(1)
      .WillOnce(DoAll(SetArgPointee<1>(delegate_data), Return(true)));

  std::u16string override_data = u"My override data";

  std::u16string result = resource_bundle->GetLocalizedString(resource_id);
  EXPECT_EQ(delegate_data, result);
}

TEST_F(ResourceBundleTest, LocaleDataPakExists) {
  // Check that ResourceBundle::LocaleDataPakExists returns the correct results.
  EXPECT_TRUE(ResourceBundle::LocaleDataPakExists(
      GetKnownLanguageTag("en-US"), ResourceBundle::Gender::kDefault));
  EXPECT_FALSE(ResourceBundle::LocaleDataPakExists(
      GetKnownLanguageTag("en-JP"), ResourceBundle::Gender::kDefault));
}

class ResourceBundleImageTest : public ResourceBundleTest {
 public:
  ResourceBundleImageTest() {}

  ResourceBundleImageTest(const ResourceBundleImageTest&) = delete;
  ResourceBundleImageTest& operator=(const ResourceBundleImageTest&) = delete;

  ~ResourceBundleImageTest() override {}

  void SetUp() override {
    ResourceBundleTest::SetUp();
    // Create a temporary directory to write test resource bundles to.
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
  }

  // Returns resource bundle which uses an empty data pak for locale data.
  ResourceBundle* CreateResourceBundleWithEmptyLocalePak() {
    // Write an empty data pak for locale data.
    const base::FilePath& locale_path = dir_path().Append(
        FILE_PATH_LITERAL("locale.pak"));
    UNSAFE_TODO(EXPECT_TRUE(
        base::WriteFile(locale_path, {kEmptyPakContents, kEmptyPakSize})));

    ui::ResourceBundle* resource_bundle = CreateResourceBundle(nullptr);

    // Load the empty locale data pak.
    resource_bundle->LoadTestResources(base::FilePath(), locale_path);
    return resource_bundle;
  }

  // Returns the path of temporary directory to write test data packs into.
  const base::FilePath& dir_path() { return temp_dir_.GetPath(); }

  // Returns the number of DataPacks managed by |resource_bundle|.
  size_t NumDataPacksInResourceBundle(ResourceBundle* resource_bundle) {
    DCHECK(resource_bundle);
    return resource_bundle->resource_handles_.size();
  }

 private:
  std::unique_ptr<DataPack> locale_pack_;
};

TEST_F(ResourceBundleImageTest, HasDataResource) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));

  // Dump content into pak file.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));

  // Load pak file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, kScaleFactorNone);

  EXPECT_FALSE(resource_bundle->HasDataResource(1));
  EXPECT_TRUE(resource_bundle->HasDataResource(4));
  EXPECT_TRUE(resource_bundle->HasDataResource(6));
  EXPECT_TRUE(resource_bundle->HasDataResource(8));
  EXPECT_FALSE(resource_bundle->HasDataResource(200));
}

TEST_F(ResourceBundleImageTest, LoadDataResourceBytes) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));

  // Dump contents into the pak files.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));

  // Load pak file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, kScaleFactorNone);

  // Test normal uncompressed data.
  scoped_refptr<base::RefCountedMemory> resource =
      resource_bundle->LoadDataResourceBytes(4);
  EXPECT_EQ("this is id 4", base::as_string_view(*resource));

  // Test the brotli data.
  scoped_refptr<base::RefCountedMemory> brotli_resource =
      resource_bundle->LoadDataResourceBytes(6);
  EXPECT_EQ("this is id 6", base::as_string_view(*brotli_resource));

  // Test the gzipped data.
  scoped_refptr<base::RefCountedMemory> gzip_resource =
      resource_bundle->LoadDataResourceBytes(8);
  EXPECT_EQ("this is id 8", base::as_string_view(*gzip_resource));
}

// Verify that we don't crash when trying to load a resource that is not found.
// In some cases, we fail to mmap resources.pak, but try to keep going anyway.
TEST_F(ResourceBundleImageTest, LoadDataResourceBytesNotFound) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));

  // Dump contents into the pak files.
  UNSAFE_TODO(ASSERT_TRUE(
      base::WriteFile(data_path, {kEmptyPakContents, kEmptyPakSize})));

  // Create a resource bundle from the file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);

  const int kUnfoundResourceId = 10000;
  EXPECT_EQ(nullptr,
            resource_bundle->LoadDataResourceBytes(kUnfoundResourceId));

  // Give a .pak file that doesn't exist so we will fail to load it.
  resource_bundle->AddDataPackFromPath(
      base::FilePath(FILE_PATH_LITERAL("non-existant-file.pak")),
      ui::kScaleFactorNone);
  EXPECT_EQ(nullptr,
            resource_bundle->LoadDataResourceBytes(kUnfoundResourceId));
}

TEST_F(ResourceBundleImageTest, LoadDataResourceStringForScale) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));
  base::FilePath data_2x_path =
      dir_path().Append(FILE_PATH_LITERAL("sample_2x.pak"));

  // Dump content into pak files.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_2x_path,
      {kSampleCompressScaledPakContents, kSampleCompressScaledPakSize})));

  // Load pak files.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);
  resource_bundle->AddDataPackFromPath(data_2x_path, k200Percent);

  // Resource ID 6 is brotlied and exists in both 1x and 2x paks, so we expect a
  // different result when requesting the 2x scale.
  EXPECT_EQ("this is id 6",
            resource_bundle->LoadDataResourceStringForScale(6, k100Percent));
  EXPECT_EQ("this is id 6 x2",
            resource_bundle->LoadDataResourceStringForScale(6, k200Percent));
}

TEST_F(ResourceBundleImageTest, LoadLocalizedResourceString) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));
  // Dump content into pak file.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));
  // Load pak file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, kScaleFactorNone);
  resource_bundle->OverrideLocalePakForTest(data_path);

  EXPECT_EQ("this is id 6", resource_bundle->LoadLocalizedResourceString(6));
  EXPECT_EQ("this is id 8", resource_bundle->LoadLocalizedResourceString(8));
}

TEST_F(ResourceBundleImageTest, LoadDataResourceString) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));
  // Dump content into pak file.
  UNSAFE_TODO(ASSERT_TRUE(base::WriteFile(
      data_path, {kSampleCompressPakContentsV5, kSampleCompressPakSizeV5})));
  // Load pak file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, kScaleFactorNone);

  // Resource ID 6 is Brotli compressed, expect it to be uncompressed.
  EXPECT_EQ("this is id 6", resource_bundle->LoadDataResourceString(6));

  // Resource ID 8 is Gzip compressed, expect it to be uncompressed.
  EXPECT_EQ("this is id 8", resource_bundle->LoadDataResourceString(8));

  // Resource ID 4 is plain text (not compressed), expect to return as-is.
  EXPECT_EQ("this is id 4", resource_bundle->LoadDataResourceString(4));
}

TEST_F(ResourceBundleImageTest, GetRawDataResource) {
  base::FilePath data_path = dir_path().Append(FILE_PATH_LITERAL("sample.pak"));
  base::FilePath data_2x_path =
      dir_path().Append(FILE_PATH_LITERAL("sample_2x.pak"));

  // Dump contents into the pak files.
  UNSAFE_TODO(ASSERT_TRUE(
      base::WriteFile(data_path, {kSamplePakContentsV4, kSamplePakSizeV4})));
  UNSAFE_TODO(ASSERT_TRUE(
      base::WriteFile(data_2x_path, {kSamplePakContents2x, kSamplePakSize2x})));

  // Load the regular and 2x pak files.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);
  resource_bundle->AddDataPackFromPath(data_2x_path, k200Percent);

  // Resource ID 4 exists in both 1x and 2x paks, so we expect a different
  // result when requesting the 2x scale.
  EXPECT_EQ("this is id 4",
            resource_bundle->GetRawDataResourceForScale(4, k100Percent));
  EXPECT_EQ("this is id 4 2x",
            resource_bundle->GetRawDataResourceForScale(4, k200Percent));

  // Resource ID 6 only exists in the 1x pak so we expect the same resource
  // for both scale factor requests.
  EXPECT_EQ("this is id 6",
            resource_bundle->GetRawDataResourceForScale(6, k100Percent));
  EXPECT_EQ("this is id 6",
            resource_bundle->GetRawDataResourceForScale(6, k200Percent));
}

// Data resources may be looked up on a worker thread while a data pack is
// being added on the main thread. Verify that this is supported.
TEST_F(ResourceBundleImageTest, GetRawDataResourceWhileAddingDataPack) {
  base::FilePath empty_path = dir_path().Append(FILE_PATH_LITERAL("empty.pak"));
  constexpr std::array<uint8_t, 15> kEmptyPakData = {
      0x04, 0x00, 0x00, 0x00,             // header(version
      0x00, 0x00, 0x00, 0x00,             //        no. entries
      0x01,                               //        encoding)
      0x00, 0x00, 0x0f, 0x00, 0x00, 0x00  // extra entry for the size of last
  };
  ASSERT_TRUE(base::WriteFile(empty_path, kEmptyPakData));

  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(empty_path, kScaleFactorNone);

  constexpr int kIterations = 256;
  constexpr int kMissingResourceId = 42;

  std::atomic<bool> done = false;
  base::Thread reader_thread("ResourceReader");
  ASSERT_TRUE(reader_thread.Start());
  reader_thread.task_runner()->PostTask(
      FROM_HERE, base::BindLambdaForTesting([&]() {
        while (!done.load()) {
          EXPECT_FALSE(resource_bundle->HasDataResource(kMissingResourceId));
          EXPECT_TRUE(
              resource_bundle->GetRawDataResource(kMissingResourceId).empty());
        }
      }));

  for (int i = 0; i < kIterations; ++i) {
    resource_bundle->AddDataPackFromPath(empty_path, kScaleFactorNone);
  }
  done.store(true);
  reader_thread.Stop();

  EXPECT_FALSE(resource_bundle->HasDataResource(kMissingResourceId));
}

// Test requesting image reps at various scale factors from the image returned
// via ResourceBundle::GetImageNamed().
TEST_F(ResourceBundleImageTest, GetImageNamed) {
#if BUILDFLAG(IS_WIN)
  display::win::SetDefaultDeviceScaleFactor(2.0);
#endif
  test::ScopedSetSupportedResourceScaleFactors scoped_supported(
      {k100Percent, k200Percent});
  base::FilePath data_1x_path = dir_path().AppendASCII("sample_1x.pak");
  base::FilePath data_2x_path = dir_path().AppendASCII("sample_2x.pak");

  // Create the pak files.
  CreateDataPackWithSingleBitmap(data_1x_path, 10, std::string_view());
  CreateDataPackWithSingleBitmap(data_2x_path, 20, std::string_view());

  // Load the regular and 2x pak files.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_1x_path, k100Percent);
  resource_bundle->AddDataPackFromPath(data_2x_path, k200Percent);

  EXPECT_EQ(k200Percent, resource_bundle->GetMaxResourceScaleFactor());

  gfx::ImageSkia* image_skia = resource_bundle->GetImageSkiaNamed(3);

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_WIN)
  // ChromeOS/Windows load highest scale factor first.
  EXPECT_EQ(ui::k200Percent, GetSupportedResourceScaleFactor(
                                 image_skia->image_reps()[0].scale()));
#else
  EXPECT_EQ(ui::k100Percent, GetSupportedResourceScaleFactor(
                                 image_skia->image_reps()[0].scale()));
#endif

  // Resource ID 3 exists in both 1x and 2x paks. Image reps should be
  // available for both scale factors in |image_skia|.
  gfx::ImageSkiaRep image_rep = image_skia->GetRepresentation(
      GetScaleForResourceScaleFactor(ui::k100Percent));
  EXPECT_EQ(ui::k100Percent,
            GetSupportedResourceScaleFactor(image_rep.scale()));
  image_rep = image_skia->GetRepresentation(
      GetScaleForResourceScaleFactor(ui::k200Percent));
  EXPECT_EQ(ui::k200Percent,
            GetSupportedResourceScaleFactor(image_rep.scale()));

  // Requesting the 1.4x resource should return either the 1x or the 2x
  // resource.
  image_rep = image_skia->GetRepresentation(1.4f);
  ResourceScaleFactor scale_factor =
      GetSupportedResourceScaleFactor(image_rep.scale());
  EXPECT_TRUE(scale_factor == ui::k100Percent ||
              scale_factor == ui::k200Percent);

  // ImageSkia scales image if the one for the requested scale factor is not
  // available.
  EXPECT_EQ(1.4f, image_skia->GetRepresentation(1.4f).scale());
}

// Test that GetImageNamed() behaves properly for images which GRIT has
// annotated as having fallen back to 1x.
TEST_F(ResourceBundleImageTest, GetImageNamedFallback1x) {
  test::ScopedSetSupportedResourceScaleFactors scoped_supported(
      {k100Percent, k200Percent});
  base::FilePath data_path = dir_path().AppendASCII("sample.pak");
  base::FilePath data_2x_path = dir_path().AppendASCII("sample_2x.pak");

  // Create the pak files.
  CreateDataPackWithSingleBitmap(data_path, 10, std::string_view());
  // 2x data pack bitmap has custom chunk to indicate that the 2x bitmap is not
  // available and that GRIT fell back to 1x.
  CreateDataPackWithSingleBitmap(
      data_2x_path, 10,
      std::string_view(reinterpret_cast<const char*>(kPngScaleChunk),
                       std::size(kPngScaleChunk)));

  // Load the regular and 2x pak files.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_path, k100Percent);
  resource_bundle->AddDataPackFromPath(data_2x_path, k200Percent);

  gfx::ImageSkia* image_skia = resource_bundle->GetImageSkiaNamed(3);

  // The image rep for 2x should be available. It should be resized to the
  // proper 2x size.
  gfx::ImageSkiaRep image_rep = image_skia->GetRepresentation(
      GetScaleForResourceScaleFactor(ui::k200Percent));
  EXPECT_EQ(ui::k200Percent,
            GetSupportedResourceScaleFactor(image_rep.scale()));
  EXPECT_EQ(20, image_rep.pixel_width());
  EXPECT_EQ(20, image_rep.pixel_height());
}

TEST_F(ResourceBundleImageTest, FallbackToNone) {
  // Presents a consistent set of supported scale factors for all platforms.
  // iOS does not include k100Percent, which breaks the test below.
  test::ScopedSetSupportedResourceScaleFactors scoped_supported(
      {k100Percent, k200Percent, k300Percent});

  base::FilePath data_default_path = dir_path().AppendASCII("sample.pak");

  // Create the pak files.
  CreateDataPackWithSingleBitmap(data_default_path, 10, std::string_view());

  // Load the regular pak files only.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_default_path, kScaleFactorNone);

  gfx::ImageSkia* image_skia = resource_bundle->GetImageSkiaNamed(3);
  EXPECT_EQ(1u, image_skia->image_reps().size());
  EXPECT_TRUE(image_skia->image_reps()[0].unscaled());
  EXPECT_EQ(ui::k100Percent, GetSupportedResourceScaleFactor(
                                 image_skia->image_reps()[0].scale()));
}

#if BUILDFLAG(SKIA_SUPPORT_SKOTTIE) && BUILDFLAG(USE_BLINK)
TEST_F(ResourceBundleImageTest, Lottie) {
  // Create the pak files.
  const base::FilePath data_unscaled_path =
      dir_path().AppendASCII("sample.pak");
  const std::map<uint16_t, std::string_view> resources = {
      std::make_pair(3u, kLottieData)};
  DataPack::WritePack(data_unscaled_path, resources, ui::DataPack::BINARY);

  // Load the unscaled pack file.
  ResourceBundle* resource_bundle = CreateResourceBundleWithEmptyLocalePak();
  resource_bundle->AddDataPackFromPath(data_unscaled_path, kScaleFactorNone);

  std::optional<std::vector<uint8_t>> data = resource_bundle->GetLottieData(3);
  ASSERT_TRUE(data.has_value());
  EXPECT_TRUE(std::ranges::equal(*data, kLottieExpected));

  test::ScopedSetSupportedResourceScaleFactors scoped_supported(
      {k100Percent, k200Percent});

  gfx::ImageSkia* image_skia = resource_bundle->GetImageSkiaNamed(3);

  // Unscaled image should always return scale=1.
  EXPECT_EQ(1.f, image_skia->GetRepresentation(2.f).scale());
  EXPECT_EQ(1.f, image_skia->GetRepresentation(1.f).scale());
  EXPECT_EQ(1.f, image_skia->GetRepresentation(1.4f).scale());

  EXPECT_EQ(kLottieWidth, image_skia->width());
  EXPECT_EQ(kLottieHeight, image_skia->height());

  // Lottie resource should be 'unscaled'.
  EXPECT_TRUE(image_skia->image_reps()[0].unscaled());
}
#endif  // BUILDFLAG(SKIA_SUPPORT_SKOTTIE) && BUILDFLAG(USE_BLINK)

}  // namespace ui
