// 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 "components/persistent_cache/persistent_cache_collection.h"

#include <algorithm>
#include <optional>
#include <string>
#include <vector>

#include "base/containers/span.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/gtest_util.h"
#include "base/test/task_environment.h"
#include "base/threading/platform_thread.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "components/persistent_cache/backend.h"
#include "components/persistent_cache/client.h"
#include "components/persistent_cache/mock/mock_backend.h"
#include "components/persistent_cache/mock/mock_backend_storage_delegate.h"
#include "components/persistent_cache/pending_backend.h"
#include "components/persistent_cache/persistent_cache.h"
#include "components/persistent_cache/sqlite/sqlite_backend_impl.h"
#include "components/persistent_cache/test_utils.h"
#include "components/sqlite_vfs/constants.h"
#include "components/sqlite_vfs/sqlite_database_vfs_file_set.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

std::vector<base::FilePath> GetPathsInDir(const base::FilePath& directory) {
  std::vector<base::FilePath> paths;
  base::FileEnumerator(directory, /*recursive=*/false,
                       base::FileEnumerator::FILES)
      .ForEach([&paths](const base::FilePath& file_path) {
        paths.push_back(file_path);
      });
  return paths;
}

}  // namespace

namespace persistent_cache {

using ::base::test::ErrorIs;
using ::base::test::HasValue;
using ::base::test::ValueIs;
using ::testing::_;
using ::testing::AnyNumber;
using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::IsTrue;
using ::testing::Ne;
using ::testing::Optional;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::Return;
using ::testing::StrEq;
using ::testing::UnorderedElementsAre;

class PersistentCacheCollectionTest : public testing::Test {
 protected:
  void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }

  static constexpr int64_t kTargetFootprint = 20000;
  base::ScopedTempDir temp_dir_;
  base::test::SingleThreadTaskEnvironment task_environment_;
};

// Two operations with the same cache_id operate on the same database.
TEST_F(PersistentCacheCollectionTest, CreateAndUse) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kTargetFootprint,
                                       Client::kTest);

  std::string cache_id("cache_id");
  auto key = base::byte_span_from_cstring("key");
  EXPECT_THAT(collection.Insert(cache_id, key, key), HasValue());
  ASSERT_THAT(FindEntry(collection, cache_id, key),
              ValueIs(Optional(ContentEq(key))));
}

TEST_F(PersistentCacheCollectionTest, DeleteAllFiles) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kTargetFootprint,
                                       Client::kTest);

  std::string cache_id("cache_id");
  auto key = base::byte_span_from_cstring("key");
  EXPECT_THAT(collection.Insert(cache_id, key, key), HasValue());

  // Inserting an entry should have created at least one file.
  EXPECT_FALSE(base::IsDirectoryEmpty(temp_dir_.GetPath()));

  collection.DeleteAllFiles();
  EXPECT_TRUE(base::IsDirectoryEmpty(temp_dir_.GetPath()));
}

static constexpr int64_t kOneHundredMiB = 100 * 1024 * 1024;

TEST_F(PersistentCacheCollectionTest, Retrieval) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  constexpr char first_cache_id[] = "first_cache_id";
  constexpr char second_cache_id[] = "second_cache_id";

  const auto first_key = base::byte_span_from_cstring("first_key");
  const auto second_key = base::byte_span_from_cstring("second_key");

  const auto first_content = base::byte_span_from_cstring("first_content");

  // At first there is nothing in the collection.
  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ValueIs(Eq(std::nullopt)));
  EXPECT_THAT(FindEntry(collection, first_cache_id, second_key),
              ValueIs(Eq(std::nullopt)));
  EXPECT_THAT(FindEntry(collection, second_cache_id, first_key),
              ValueIs(Eq(std::nullopt)));
  EXPECT_THAT(FindEntry(collection, second_cache_id, second_key),
              ValueIs(Eq(std::nullopt)));

  // Inserting for a certain cache id allows retrieval for this id and this id
  // only.
  EXPECT_THAT(collection.Insert(first_cache_id, first_key, first_content),
              HasValue());
  ASSERT_THAT(FindEntry(collection, first_cache_id, first_key),
              ValueIs(Optional(ContentEq(first_content))));

  EXPECT_THAT(FindEntry(collection, second_cache_id, first_key),
              ValueIs(Eq(std::nullopt)));
}

TEST_F(PersistentCacheCollectionTest, RetrievalAfterClear) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  std::string first_cache_id = "first_cache_id";
  const auto first_key = base::byte_span_from_cstring("first_key");
  const auto first_content = base::byte_span_from_cstring("first_content");

  // Test basic retrieval.
  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ValueIs(Eq(std::nullopt)));

  EXPECT_THAT(collection.Insert(first_cache_id, first_key, first_content),
              HasValue());

  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ValueIs(Ne(std::nullopt)));

  collection.Clear();

  // Retrieval still works after clear because data persistence is unaffected by
  // lifetime of PersistentCache instances.
  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ValueIs(Ne(std::nullopt)));
}

TEST_F(PersistentCacheCollectionTest, ContinuousFootPrintReduction) {
  constexpr int64_t kSmallFootprint = 128;

  PersistentCacheCollection collection(temp_dir_.GetPath(), kSmallFootprint,
                                       Client::kTest);

  int i = 0;
  int64_t added_footprint = 0;

  // Add things right up to the limit where files start to be deleted.
  while (added_footprint < kSmallFootprint) {
    std::string number = base::NumberToString(i);

    // Account for size of key and value.
    int64_t footprint_after_insertion = added_footprint + (number.length() * 2);

    if (footprint_after_insertion < kSmallFootprint) {
      int64_t directory_size_before =
          base::ComputeDirectorySize(temp_dir_.GetPath());

      EXPECT_THAT(collection.Insert(number, base::as_byte_span(number),
                                    base::as_byte_span(number)),
                  HasValue());

      int64_t directory_size_after =
          base::ComputeDirectorySize(temp_dir_.GetPath());

      // If there's no footprint reduction and the new values are being stored
      // then directory size is just going up.
      EXPECT_GT(directory_size_after, directory_size_before);
    }

    added_footprint = footprint_after_insertion;
    ++i;
  }

  // If `kSmallFootprint` is not large enough to trigger at least two successful
  // insertions into the cache the test does not provide sufficient coverage.
  ASSERT_GT(i, 2);

  int64_t directory_size_before =
      base::ComputeDirectorySize(temp_dir_.GetPath());

  // Since no footprint reduction should have been triggered all values added
  // should still be available.
  for (int j = 0; j < i - 1; ++j) {
    std::string number = base::NumberToString(j);
    EXPECT_THAT(FindEntry(collection, number, base::as_byte_span(number)),
                ValueIs(Ne(std::nullopt)));
  }

  // Add one more item which should bring things over the limit.
  std::string number = base::NumberToString(i + 1);
  EXPECT_THAT(collection.Insert(number, base::as_byte_span(number),
                                base::as_byte_span(number)),
              HasValue());

  int64_t directory_size_after =
      base::ComputeDirectorySize(temp_dir_.GetPath());

  // Footprint reduction happened automatically. Note that's it's not possible
  // to specifically know what the current footprint is since the last insert
  // took place after the footprint reduction.
  EXPECT_LT(directory_size_after, directory_size_before);
}

TEST_F(PersistentCacheCollectionTest, BaseNameFromCacheId) {
  // Invalid tokens results in empty string and not a crash.
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("\x01"),
            base::FilePath());
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("\x80"),
            base::FilePath());

  // Verify '%' is mapped to '`p'.
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("%"),
            base::FilePath::FromASCII("`p"));

  // Verify '{' is mapped to '`l'.
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("{"),
            base::FilePath::FromASCII("`l"));

  // Verify '}' is mapped to '`r'.
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("}"),
            base::FilePath::FromASCII("`r"));

  // Verify '`' is mapped to '`g'.
  EXPECT_EQ(PersistentCacheCollection::BaseNameFromCacheId("`"),
            base::FilePath::FromASCII("`g"));

  // Verify file name is obfuscated.
  std::string cache_id("devs_first_db");
  base::FilePath base_name(
      PersistentCacheCollection::BaseNameFromCacheId(cache_id));
  EXPECT_EQ(base_name.value().find(base::FilePath::FromASCII(cache_id).value()),
            std::string::npos);
  EXPECT_EQ(base_name.value().find(FILE_PATH_LITERAL("devs")),
            std::string::npos);
  EXPECT_EQ(base_name.value().find(FILE_PATH_LITERAL("first")),
            std::string::npos);
}

TEST_F(PersistentCacheCollectionTest, FullAllowedCharacterSetHandled) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  std::string all_chars_key =
      PersistentCacheCollection::GetAllAllowedCharactersInCacheIds();
  const auto number = base::byte_span_from_cstring("number");

  EXPECT_THAT(collection.Insert(all_chars_key, number, number), HasValue());
  EXPECT_THAT(FindEntry(collection, all_chars_key, number),
              ValueIs(Ne(std::nullopt)));
}

TEST_F(PersistentCacheCollectionTest, InstancesAbandonnedOnLRUEviction) {
  const auto key = base::byte_span_from_cstring("key");

  static constexpr size_t kLruCacheCapacity = 5;
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest, kLruCacheCapacity);

  std::vector<std::unique_ptr<PersistentCache>> caches;
  size_t cache_id = 0;
  auto get_increasing_cache_id = [&cache_id]() {
    return base::NumberToString(cache_id++);
  };

  // Creates caches exactly up to capacity.
  for (size_t i = 0; i < kLruCacheCapacity; ++i) {
    ASSERT_OK_AND_ASSIGN(
        auto pending_backend,
        collection.ShareReadWriteConnection(get_increasing_cache_id()));
    ASSERT_OK_AND_ASSIGN(
        auto cache,
        PersistentCache::Bind(Client::kTest, std::move(pending_backend)));
    caches.push_back(std::move(cache));
  }
  ASSERT_NE(caches.front(), nullptr);

  // Find succeeds since the instance is not evicted yet.
  EXPECT_THAT(FindEntry(*caches.front(), key), HasValue());

  // Create one more cache which goes over the limit.
  ASSERT_OK_AND_ASSIGN(
      auto pending_backend,
      collection.ShareReadWriteConnection(get_increasing_cache_id()));
  ASSERT_OK_AND_ASSIGN(
      auto cache,
      PersistentCache::Bind(Client::kTest, std::move(pending_backend)));
  caches.emplace_back(std::move(cache));

  // The first cache has now been evicted and is abandoned.
  EXPECT_THAT(FindEntry(*caches.front(), key),
              ErrorIs(TransactionError::kConnectionError));
}

TEST_F(PersistentCacheCollectionTest, InstancesAbandonnedOnClear) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  std::string key("key");
  ASSERT_OK_AND_ASSIGN(auto pending_backend,
                       collection.ShareReadWriteConnection(key));
  ASSERT_OK_AND_ASSIGN(
      auto cache,
      PersistentCache::Bind(Client::kTest, std::move(pending_backend)));

  collection.Clear();
  EXPECT_THAT(FindEntry(*cache, base::as_byte_span(key)),
              ErrorIs(TransactionError::kConnectionError));
}

TEST_F(PersistentCacheCollectionTest, AbandonnedErrorsDoNotCauseDeletions) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  std::string first_cache_id = "first_cache_id";
  const auto first_key = base::byte_span_from_cstring("first_key");
  const auto first_content = base::byte_span_from_cstring("first_content");

  EXPECT_THAT(collection.Insert(first_cache_id, first_key, first_content),
              HasValue());
  EXPECT_THAT(
      GetPathsInDir(temp_dir_.GetPath()),
      UnorderedElementsAre(Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kDbFileExtension)),
                           Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kJournalFileExtension))));

  ASSERT_OK_AND_ASSIGN(auto pending_backend,
                       collection.ShareReadWriteConnection(first_cache_id));
  ASSERT_OK_AND_ASSIGN(
      auto cache,
      PersistentCache::Bind(Client::kTest, std::move(pending_backend)));
  EXPECT_EQ(cache->Abandon(), LockState::kNotHeld);

  EXPECT_THAT(FindEntry(*cache, first_key),
              ErrorIs(TransactionError::kConnectionError));
  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ErrorIs(TransactionError::kConnectionError));

  // Files are still there.
  EXPECT_THAT(
      GetPathsInDir(temp_dir_.GetPath()),
      UnorderedElementsAre(Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kDbFileExtension)),
                           Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kJournalFileExtension))));
}

TEST_F(PersistentCacheCollectionTest, EvictWhileLockedDeletesFiles) {
  auto mock_delegate =
      std::make_unique<testing::NiceMock<MockBackendStorageDelegate>>();
  auto backend = std::make_unique<testing::NiceMock<MockBackend>>();

  // Backend default behavior.
  ON_CALL(*backend, IsReadOnly()).WillByDefault(Return(false));

  // Simulates the fact that readers are left over on abandonment.
  EXPECT_CALL(*backend, Abandon()).WillOnce(Return(LockState::kReading));

  // Return the mock backend from the BackendStorage::Delegate when requested,
  // and remember the cache base name.
  base::FilePath saved_base_name;
  EXPECT_CALL(*mock_delegate,
              MakeBackend(Client::kTest, temp_dir_.GetPath(), _, false, false))
      .WillOnce([&](Client client, const base::FilePath& directory,
                    const base::FilePath& base_name, bool single_connection,
                    bool journal_mode_wal) {
        saved_base_name = base_name;
        return base::ok(std::move(backend));
      });

  // This call only takes place as a reaction to the reader being left over
  // after abandonment.
  EXPECT_CALL(
      *mock_delegate,
      DeleteFiles(Client::kTest, temp_dir_.GetPath(),
                  ResultOf(
                      [&saved_base_name](const base::FilePath& base_name) {
                        return base_name == saved_base_name;
                      },
                      IsTrue())));

  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       std::move(mock_delegate), Client::kTest);
  std::string first_cache_id = "first_cache_id";
  // `ExportReadWriteBackendParams` called to force the collection to create a
  // `PersistentCache`.
  collection.ShareReadWriteConnection(first_cache_id);
  collection.Clear();
}

TEST_F(PersistentCacheCollectionTest,
       BackendStorageCreationAfterDeleteSucceedsWithHeldFiles) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);
  std::string first_cache_id = "first_cache_id";

  // Files exists after creating a params since `ExportReadWriteBackendParams`
  // forces the creation of a `PersistentCache`.
  ASSERT_THAT(collection.ShareReadWriteConnection(first_cache_id),
              Ne(std::nullopt));
  EXPECT_THAT(
      GetPathsInDir(temp_dir_.GetPath()),
      UnorderedElementsAre(Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kDbFileExtension)),
                           Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kJournalFileExtension))));

  // No more files after delete.
  collection.DeleteAllFiles();
  EXPECT_THAT(GetPathsInDir(temp_dir_.GetPath()), IsEmpty());

  // It's possible to recreate params/files with the same cache_id.
  ASSERT_OK_AND_ASSIGN(auto other_pending_backend,
                       collection.ShareReadWriteConnection(first_cache_id));
  EXPECT_THAT(
      GetPathsInDir(temp_dir_.GetPath()),
      UnorderedElementsAre(Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kDbFileExtension)),
                           Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kJournalFileExtension))));
}

TEST_F(PersistentCacheCollectionTest, PermanentErrorCausesDeletion) {
  PersistentCacheCollection collection(temp_dir_.GetPath(), kOneHundredMiB,
                                       Client::kTest);

  std::string first_cache_id = "first_cache_id";
  const auto first_key = base::byte_span_from_cstring("first_key");
  const auto first_content = base::byte_span_from_cstring("first_content");

  EXPECT_THAT(collection.Insert(first_cache_id, first_key, first_content),
              HasValue());
  EXPECT_THAT(
      GetPathsInDir(temp_dir_.GetPath()),
      UnorderedElementsAre(Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kDbFileExtension)),
                           Property(&base::FilePath::Extension,
                                    StrEq(sqlite_vfs::kJournalFileExtension))));

  // TODO(https://crbug.com/377475540): Instead of triggering an error in a
  // backend specific way PersistentCacheCollection should have a way to inject
  // mock BackendStorage::Delegate.
  base::FileEnumerator(temp_dir_.GetPath(), /*recursive=*/false,
                       base::FileEnumerator::FILES)
      .ForEach([](const base::FilePath& file_path) {
        base::File file;
        file.Initialize(file_path, base::File::FLAG_WRITE |
                                       base::File::FLAG_OPEN |
                                       base::File::FLAG_CAN_DELETE_ON_CLOSE |
                                       base::File::FLAG_WIN_SHARE_DELETE);

        // Truncate which will cause future requests to start failing.
        CHECK(file.IsValid());
        file.SetLength(0);
      });

  // Permanent error because there are no more valid files.
  EXPECT_THAT(FindEntry(collection, first_cache_id, first_key),
              ErrorIs(TransactionError::kPermanent));

  // TODO(https://crbug.com/377475540): As in previous item once we use mocking
  // to trigger failures we should validate that transient errors are handled
  // properly in a backend agnostic way.

  // Files got deleted on permanent error.
  EXPECT_THAT(GetPathsInDir(temp_dir_.GetPath()), IsEmpty());
}

// Tests that trying to operate on a cache in a collection does not crash if an
// invalid cache_id is used.
TEST_F(PersistentCacheCollectionTest, BadKeysAreIgnored) {
  EXPECT_THAT(
      PersistentCacheCollection(temp_dir_.GetPath(), kOneHundredMiB,
                                Client::kTest)
          .Insert(std::string("BADKEY"), base::byte_span_from_cstring("key"),
                  base::byte_span_from_cstring("value")),
      ErrorIs(TransactionError::kPermanent));
}

}  // namespace persistent_cache
