// Copyright 2026 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/sqlite/sqlite_backend_impl.h"

#include <stdint.h>

#include <array>
#include <optional>

#include "base/containers/span.h"
#include "base/files/scoped_temp_dir.h"
#include "base/numerics/byte_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "components/persistent_cache/backend_storage.h"
#include "components/persistent_cache/backend_type.h"
#include "components/persistent_cache/client.h"
#include "components/persistent_cache/persistent_cache.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace persistent_cache {

namespace {

using ::base::test::ErrorIs;
using ::base::test::ValueIs;
using ::testing::Eq;
using ::testing::Ne;
using ::testing::Optional;

// See https://sqlite.org/fileformat2.html#user_version_number
constexpr int64_t kUserVersionOffset = 60;

// Make sure the files are initialized with an invalid version.
// 0 is chosen for two reasons: It's a value this is not equal to
// `kCurrentUserVersion` so validates that mismatches are not tolerated. It's
// also the default value present prior to the use of user_version and real
// files exist in clients with no specific user versions and tables which need
// to be discarded.
constexpr int kInvalidVersion = 0;

}  // namespace

class SQLiteBackendImplTest : public testing::Test {
 protected:
  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    backend_storage_.emplace(Client::kTest, BackendType::kSqlite,
                             temp_dir_.GetPath());
  }

  base::ScopedTempDir temp_dir_;
  std::optional<BackendStorage> backend_storage_;
};

TEST_F(SQLiteBackendImplTest, ReopeningFilesWithSameUserVersionWorks) {
  const base::FilePath db_basename = base::FilePath::FromASCII("Cache");

  ASSERT_OK_AND_ASSIGN(
      auto pending_backend,
      backend_storage_->MakePendingBackend(db_basename, false, false));

  // Initialize the backend and close it.
  ASSERT_THAT(
      SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest),
      ValueIs(Ne(nullptr)));

  ASSERT_OK_AND_ASSIGN(pending_backend, backend_storage_->MakePendingBackend(
                                            db_basename, false, false));

  // NOTE: This does not respect the database locks and so is done when there
  // are no live backends.
  std::array<uint8_t, sizeof(int)> data;
  ASSERT_THAT(
      pending_backend.pending_file_set.db_file.Read(kUserVersionOffset, data),
      Optional(sizeof(int)));
  ASSERT_EQ(base::I32FromBigEndian(data),
            SqliteBackendImpl::kCurrentUserVersion);

  ASSERT_THAT(
      SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest),
      ValueIs(Ne(nullptr)));
}

TEST_F(SQLiteBackendImplTest,
       VersionMismatchLeadsToFailedInitializeWhenReadOnly) {
  const base::FilePath db_path = base::FilePath::FromASCII("Cache");

  ASSERT_OK_AND_ASSIGN(
      auto pending_backend,
      backend_storage_->MakePendingBackend(db_path, false, false));
  base::File db_file = pending_backend.pending_file_set.db_file.Duplicate();

  ASSERT_OK_AND_ASSIGN(
      auto cache,
      PersistentCache::Bind(Client::kTest, std::move(pending_backend)));
  ASSERT_NE(cache, nullptr);

  ASSERT_OK_AND_ASSIGN(
      pending_backend,
      backend_storage_->ShareReadOnlyConnection(db_path, *cache));

  // Manually set the user_version in the db file to an invalid value.
  // NOTE: This does not respect the database locks and so is done when there
  // are no live backends.
  cache.reset();
  ASSERT_THAT(
      db_file.Write(kUserVersionOffset, base::I32ToBigEndian(kInvalidVersion)),
      Optional(sizeof(int)));

  // Mismatched version numbers makes Bind() fail.
  ASSERT_THAT(
      SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest),
      ErrorIs(TransactionError::kConnectionError));
}

TEST_F(SQLiteBackendImplTest,
       VersionMismatchDropsTablesWithReadWriteConnection) {
  const base::FilePath db_basename = base::FilePath::FromASCII("Cache");

  ASSERT_OK_AND_ASSIGN(
      auto pending_backend,
      backend_storage_->MakePendingBackend(db_basename, false, false));

  ASSERT_OK_AND_ASSIGN(
      auto cache,
      PersistentCache::Bind(Client::kTest, std::move(pending_backend)));
  ASSERT_NE(cache, nullptr);

  const base::span<const uint8_t> kKey = base::byte_span_from_cstring("key");
  EXPECT_OK(cache->Insert(kKey, base::byte_span_from_cstring("1")));
  EXPECT_THAT(cache->Find(kKey, [](size_t) { return base::span<uint8_t>(); }),
              ValueIs(Ne(std::nullopt)));

  ASSERT_OK_AND_ASSIGN(
      pending_backend,
      backend_storage_->ShareReadWriteConnection(db_basename, *cache));

  // Manually set the user_version in the db file to an invalid value.
  // NOTE: This does not respect the database locks and so is done when there
  // are no live backends.
  cache.reset();
  ASSERT_THAT(pending_backend.pending_file_set.db_file.Write(
                  kUserVersionOffset, base::I32ToBigEndian(kInvalidVersion)),
              Optional(sizeof(int)));

  // Bind succeeds.
  ASSERT_OK_AND_ASSIGN(
      auto backend,
      SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest));
  ASSERT_NE(backend, nullptr);

  // The files were wiped so the value inserted is no longer present.
  EXPECT_THAT(backend->Find(kKey, [](size_t) { return base::span<uint8_t>(); }),
              ValueIs(Eq(std::nullopt)));
}

TEST_F(SQLiteBackendImplTest, BindFailsWhenDatabaseFileIsInvalid) {
  const base::FilePath db_path = base::FilePath::FromASCII("Cache");

  ASSERT_OK_AND_ASSIGN(
      auto pending_backend,
      backend_storage_->MakePendingBackend(db_path, false, false));

  // Invalidate the database file handle.
  pending_backend.pending_file_set.db_file = base::File();

  // Bind should fail because the database file is invalid.
  // With the fix, we should get kConnectionError (due to SQLITE_CANTOPEN).
  // Without the fix, we would get kPermanent (due to SQLITE_ERROR fallback).
  ASSERT_THAT(
      SqliteBackendImpl::Bind(std::move(pending_backend), Client::kTest),
      ErrorIs(TransactionError::kConnectionError));
}

}  // namespace persistent_cache
