// 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 "content/browser/indexed_db/instance/backing_store.h"

#include <memory>
#include <string>

#include "base/files/file.h"
#include "base/files/file_util.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "content/browser/indexed_db/indexed_db_value.h"
#include "content/browser/indexed_db/instance/backing_store_test_base.h"
#include "content/browser/indexed_db/instance/backing_store_util.h"
#include "content/browser/indexed_db/instance/bucket_context.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_key_path.h"
#include "third_party/blink/public/common/indexeddb/indexeddb_metadata.h"

using blink::IndexedDBIndexMetadata;
using blink::IndexedDBKey;
using blink::IndexedDBKeyPath;
using blink::IndexedDBKeyRange;
using blink::IndexedDBObjectStoreMetadata;

namespace {

enum class ExternalObjectTestType {
  kOnlyBlobs,
  kOnlyFileSystemAccessHandles,
  kBlobsAndFileSystemAccessHandles
};

}  // namespace

namespace content::indexed_db {

// Tests the backend-agnostic behavior of the backing store. Currently, these
// tests verify the correct behavior for both SQLite and LevelDB backing store
// implementations.
class BackingStoreTest : public testing::WithParamInterface<bool>,
                         public BackingStoreTestBase {
 public:
  BackingStoreTest() : BackingStoreTestBase(IsSqliteBackingStoreEnabled()) {}

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

  bool IsSqliteBackingStoreEnabled() { return GetParam(); }
};

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    BackingStoreTest,
    testing::Bool(),
    [](const testing::TestParamInfo<BackingStoreTest::ParamType>& info) {
      return info.param ? "Sqlite" : "LevelDb";
    });

TEST_P(BackingStoreTest, PutGetConsistency) {
  const IndexedDBKey& key = key1_;
  IndexedDBValue& value = value1_;

  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  {
    std::unique_ptr<BackingStore::Transaction> transaction1 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction1->Begin(CreateDummyLock());
    EXPECT_TRUE(transaction1->PutRecord(1, key, value.Clone()).has_value());
    CommitTransactionAndVerify(std::move(transaction1));
  }

  {
    std::unique_ptr<BackingStore::Transaction> transaction2 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction2->Begin(CreateDummyLock());
    auto result = transaction2->GetRecord(1, key);
    EXPECT_TRUE(result.has_value());
    CommitTransactionAndVerify(std::move(transaction2));
    EXPECT_EQ(base::span(value.bits), base::span(result->bits));
  }
}

// Tests what happens when a blob returns an error when being read.
TEST_P(BackingStoreTest, PutBrokenBlob) {
  const IndexedDBKey& key = key1_;
  IndexedDBValue& value = value1_;

  // Make a `FakeBlob` with no body (not an empty body), which will return an
  // error when read.
  value.external_objects.emplace_back(
      CreateBlobInfo(u"text/plain", std::nullopt));

  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  std::unique_ptr<BackingStore::Transaction> transaction =
      db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                           blink::mojom::IDBTransactionMode::ReadWrite);

  transaction->Begin(CreateDummyLock());
  EXPECT_TRUE(transaction->PutRecord(1, key, value.Clone()).has_value());
  EXPECT_FALSE(CommitTransactionPhaseOneAndVerify(*transaction));
  transaction->Rollback();
}

// Tests what happens when a transaction is being committed and a blob is being
// written (asynchronously) when Rollback() is invoked.
TEST_P(BackingStoreTest, RollbackDuringBlobWrite) {
  const IndexedDBKey& key = key1_;
  IndexedDBValue& value = value1_;
  value.external_objects.emplace_back(
      CreateBlobInfo(u"text/plain", "contents"));

  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  ASSERT_TRUE(db.get());

  std::unique_ptr<BackingStore::Transaction> transaction =
      db->CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                            blink::mojom::IDBTransactionMode::ReadWrite);

  transaction->Begin(CreateDummyLock());
  EXPECT_TRUE(transaction->PutRecord(1, key, value.Clone()).has_value());

  bool blob_write_callback_lives = false;
  ASSERT_OK_AND_ASSIGN(
      bool async_work_to_be_done,
      transaction->CommitPhaseOne(
          /*blob_write_callback=*/
          base::IgnoreArgs<Status>(base::BindOnce(
              [](base::AutoReset<bool> auto_reset) { ADD_FAILURE(); },
              base::AutoReset(&blob_write_callback_lives, true))),
          /*serialize_fsa_handle=*/base::DoNothing()));
  EXPECT_TRUE(async_work_to_be_done);
  EXPECT_TRUE(blob_write_callback_lives);
  transaction->Rollback();

  // Make sure the blob write callback was dropped without being called. If
  // called, it will cause the test to fail with ADD_FAILURE().
  EXPECT_FALSE(blob_write_callback_lives);

  // Make sure there are no other errors as the backing store potentially
  // attempts to write blobs in the background. In particular, the LevelDB
  // store has to explicitly handle the Rollback case to prevent a crash, and
  // spinning a runloop is necessary to give it a chance to not crash.
  base::RunLoop().RunUntilIdle();
  base::RunLoop().RunUntilIdle();
}

TEST_P(BackingStoreTest, Snapshots) {
  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  ASSERT_OK_AND_ASSIGN(base::DictValue empty_snapshot, SnapshotDatabase(db));

  CreateObjectStore(db);

  int total_record_count = 0;
  auto add_records = [&](size_t num_records) {
    std::unique_ptr<BackingStore::Transaction> transaction =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction->Begin(CreateDummyLock());

    for (size_t i = 0; i < num_records; ++i) {
      IndexedDBKey key(i + total_record_count,
                       blink::mojom::IDBKeyType::Number);
      EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key, value1_.Clone())
                      .has_value());
    }
    total_record_count += num_records;
    CommitTransactionAndVerify(std::move(transaction));
  };
  add_records(100);

  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot, SnapshotDatabase(db));

  // Adding a record changes the snapshot.
  add_records(3);
  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot2, SnapshotDatabase(db));
  EXPECT_NE(snapshot, snapshot2);

  // Updating a value changes the snapshot.
  {
    std::unique_ptr<BackingStore::Transaction> transaction =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction->Begin(CreateDummyLock());

    IndexedDBKey key(15, blink::mojom::IDBKeyType::Number);
    EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key, value2_.Clone())
                    .has_value());
    CommitTransactionAndVerify(std::move(transaction));
  };

  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot3, SnapshotDatabase(db));
  EXPECT_NE(snapshot2, snapshot3);

  // Changing the updated value back to the original, snapshot should revert
  // too.
  {
    std::unique_ptr<BackingStore::Transaction> transaction =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction->Begin(CreateDummyLock());

    IndexedDBKey key(15, blink::mojom::IDBKeyType::Number);
    EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key, value1_.Clone())
                    .has_value());
    CommitTransactionAndVerify(std::move(transaction));
  };
  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot4, SnapshotDatabase(db));
  EXPECT_EQ(snapshot2, snapshot4);

  // Exercise the whole-store hashing code.
  add_records(1000);
  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot5, SnapshotDatabase(db));
  EXPECT_LT(snapshot5.DebugString().size(), snapshot4.DebugString().size());

  add_records(2);
  ASSERT_OK_AND_ASSIGN(base::DictValue snapshot6, SnapshotDatabase(db));
  EXPECT_NE(snapshot5, snapshot6);
  // Size should not have changed since the row would change the digest but not
  // the size of the digest. Note that this sort of cheats because the digest is
  // actually omitted from the debug string due to being a binary, but even if
  // we were to encode it as a string (e.g. with base64), this check would pass.
  EXPECT_EQ(snapshot6.DebugString().size(), snapshot5.DebugString().size());

  // Delete all records and verify the snapshot works, and is distinct from the
  // one for a database that lacks object stores/indices.
  {
    std::unique_ptr<BackingStore::Transaction> transaction =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction->Begin(CreateDummyLock());

    EXPECT_TRUE(
        transaction->DeleteRange(kObjectStoreId1, blink::IndexedDBKeyRange())
            .ok());
    CommitTransactionAndVerify(std::move(transaction));
  };
  ASSERT_OK_AND_ASSIGN(base::DictValue no_record_snapshot,
                       SnapshotDatabase(db));
  EXPECT_NE(empty_snapshot, no_record_snapshot);
}

// Deleting an index should delete the index metadata and the index data.
TEST_P(BackingStoreTest, CreateAndDeleteIndex) {
  const int64_t object_store_id = 99;
  const IndexedDBKeyPath object_store_key_path(u"object_store_key");

  const int64_t index_id = 999;
  const IndexedDBKeyPath index_key_path(u"index_key");

  ASSERT_OK_AND_ASSIGN(auto db,
                       backing_store()->CreateOrOpenDatabase(u"database_name"));

  {
    std::unique_ptr<BackingStore::Transaction> transaction =
        CreateAndBeginTransaction(
            *db, blink::mojom::IDBTransactionMode::VersionChange);
    EXPECT_TRUE(transaction
                    ->CreateObjectStore(object_store_id, u"object_store_name",
                                        object_store_key_path,
                                        /*auto_increment=*/true)
                    .ok());

    EXPECT_TRUE(
        transaction
            ->CreateIndex(object_store_id, blink::IndexedDBIndexMetadata(
                                               u"index_name", index_id,
                                               index_key_path, /*unique=*/true,
                                               /*multi_entry=*/true))
            .ok());

    CommitTransactionAndVerify(std::move(transaction));
  }

  EXPECT_EQ(db->GetMetadata().object_stores.size(), 1U);
  auto object_store_it = db->GetMetadata().object_stores.find(object_store_id);
  ASSERT_NE(object_store_it, db->GetMetadata().object_stores.end());
  const IndexedDBObjectStoreMetadata& object_store = object_store_it->second;
  EXPECT_NE(object_store.indexes.end(), object_store.indexes.find(index_id));

  {
    auto transaction = CreateAndBeginTransaction(
        *db, blink::mojom::IDBTransactionMode::VersionChange);

    auto record =
        transaction->PutRecord(object_store_id, key1_, value1_.Clone());
    EXPECT_TRUE(record.has_value());
    EXPECT_TRUE(
        transaction
            ->PutIndexDataForRecord(object_store_id, index_id, key2_, *record)
            .ok());
    auto pk = transaction->GetFirstPrimaryKeyForIndexKey(object_store_id,
                                                         index_id, key2_);
    EXPECT_TRUE(pk.has_value());
    EXPECT_TRUE(pk->IsValid());

    EXPECT_TRUE(transaction->DeleteIndex(object_store_id, index_id).ok());

    // The SQLite backing store CHECKs on invalid inputs, such as id which
    // refers to now-deleted index.
    if (!IsSqliteBackingStoreEnabled()) {
      pk = transaction->GetFirstPrimaryKeyForIndexKey(object_store_id, index_id,
                                                      key2_);
      EXPECT_TRUE(pk.has_value());
      EXPECT_FALSE(pk->IsValid());
    }

    CommitTransactionAndVerify(std::move(transaction));
  }

  EXPECT_EQ(object_store.indexes.end(), object_store.indexes.find(index_id));
}

TEST_P(BackingStoreTest, CreateDatabase) {
  const std::u16string database_name(u"db1");
  const int64_t version = 9;

  const int64_t object_store_id = 99;
  const std::u16string object_store_name(u"object_store1");
  const bool auto_increment = true;
  const IndexedDBKeyPath object_store_key_path(u"object_store_key");

  const int64_t index_id = 999;
  const std::u16string index_name(u"index1");
  const bool unique = true;
  const bool multi_entry = true;
  const IndexedDBKeyPath index_key_path(u"index_key");

  {
    ASSERT_OK_AND_ASSIGN(auto db1,
                         backing_store()->CreateOrOpenDatabase(database_name));
    UpdateDatabaseVersion(*db1, version);

    std::unique_ptr<indexed_db::BackingStore::Transaction> transaction =
        db1->CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::VersionChange);
    transaction->Begin(CreateDummyLock());

    Status s =
        transaction->CreateObjectStore(object_store_id, object_store_name,
                                       object_store_key_path, auto_increment);
    EXPECT_TRUE(s.ok());

    const IndexedDBObjectStoreMetadata& object_store =
        db1->GetMetadata().object_stores.find(object_store_id)->second;
    EXPECT_EQ(object_store.id, object_store_id);

    s = transaction->CreateIndex(
        object_store.id,
        blink::IndexedDBIndexMetadata(index_name, index_id, index_key_path,
                                      unique, multi_entry));
    EXPECT_TRUE(s.ok());

    const IndexedDBIndexMetadata& index =
        object_store.indexes.find(index_id)->second;
    EXPECT_EQ(index.id, index_id);
    CommitTransactionAndVerify(std::move(transaction));
  }

  // Wait for the database to be unlocked before reopening it.
  AcquireDatabaseLocks(database_name);

  {
    ASSERT_OK_AND_ASSIGN(auto db1,
                         backing_store()->CreateOrOpenDatabase(database_name));

    const blink::IndexedDBDatabaseMetadata& database = db1->GetMetadata();

    EXPECT_EQ(1UL, database.object_stores.size());
    const IndexedDBObjectStoreMetadata& object_store =
        database.object_stores.find(object_store_id)->second;
    EXPECT_EQ(object_store_name, object_store.name);
    EXPECT_EQ(object_store_key_path, object_store.key_path);
    EXPECT_EQ(auto_increment, object_store.auto_increment);

    EXPECT_EQ(1UL, object_store.indexes.size());
    const IndexedDBIndexMetadata& index =
        object_store.indexes.find(index_id)->second;
    EXPECT_EQ(index_name, index.name);
    EXPECT_EQ(index_key_path, index.key_path);
    EXPECT_EQ(unique, index.unique);
    EXPECT_EQ(multi_entry, index.multi_entry);
  }
}

TEST_P(BackingStoreTest, DatabaseExists) {
  ASSERT_OK_AND_ASSIGN(auto db1, backing_store()->CreateOrOpenDatabase(u"db1"));

  ASSERT_OK_AND_ASSIGN(auto db2, backing_store()->CreateOrOpenDatabase(u"db2"));

  // Only databases with non-default versions should be counted as existing by
  // `DatabaseExists()`.
  UpdateDatabaseVersion(*db1, 1);

  ASSERT_OK_AND_ASSIGN(bool db1_exists,
                       backing_store()->DatabaseExists(u"db1"));
  EXPECT_TRUE(db1_exists);

  ASSERT_OK_AND_ASSIGN(bool db2_exists,
                       backing_store()->DatabaseExists(u"db2"));
  EXPECT_FALSE(db2_exists);
}

TEST_P(BackingStoreTest, DatabaseNamesAreSorted) {
  // Hold on to one of the created databases.
  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db,
                       backing_store()->CreateOrOpenDatabase(u"bb"));
  UpdateDatabaseVersion(*db, 1);

  // Create a couple of other databases but immediately close them.
  UpdateDatabaseVersion(**backing_store()->CreateOrOpenDatabase(u"c"), 1);
  UpdateDatabaseVersion(**backing_store()->CreateOrOpenDatabase(u"aaa"), 1);

  // Database names should be in sorted order.
  ASSERT_OK_AND_ASSIGN(
      std::vector<blink::mojom::IDBNameAndVersionPtr> names_and_versions,
      backing_store()->GetDatabaseNamesAndVersions());
  ASSERT_EQ(names_and_versions.size(), 3U);
  EXPECT_EQ(names_and_versions[0]->name, u"aaa");
  EXPECT_EQ(names_and_versions[1]->name, u"bb");
  EXPECT_EQ(names_and_versions[2]->name, u"c");
}

class BackingStoreTestWithExternalObjects
    : public testing::WithParamInterface<
          std::tuple<bool, ExternalObjectTestType>>,
      public BackingStoreWithExternalObjectsTestBase {
 public:
  BackingStoreTestWithExternalObjects()
      : BackingStoreWithExternalObjectsTestBase(IsSqliteBackingStoreEnabled()) {
  }

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

  bool IsSqliteBackingStoreEnabled() { return std::get<0>(GetParam()); }
  ExternalObjectTestType TestType() { return std::get<1>(GetParam()); }

  bool IncludesBlobs() override {
    return TestType() != ExternalObjectTestType::kOnlyFileSystemAccessHandles;
  }

  bool IncludesFileSystemAccessHandles() override {
    return TestType() != ExternalObjectTestType::kOnlyBlobs;
  }
};

INSTANTIATE_TEST_SUITE_P(
    /* no prefix */,
    BackingStoreTestWithExternalObjects,
    testing::Combine(
        testing::Bool(),
        testing::Values(
            ExternalObjectTestType::kOnlyBlobs,
            ExternalObjectTestType::kOnlyFileSystemAccessHandles,
            ExternalObjectTestType::kBlobsAndFileSystemAccessHandles)),
    [](const testing::TestParamInfo<
        BackingStoreTestWithExternalObjects::ParamType>& info) {
      std::string external_object_type;
      switch (std::get<1>(info.param)) {
        case ExternalObjectTestType::kOnlyBlobs:
          external_object_type = "Blobs";
          break;
        case ExternalObjectTestType::kOnlyFileSystemAccessHandles:
          external_object_type = "FileSystemAccessHandles";
          break;
        case ExternalObjectTestType::kBlobsAndFileSystemAccessHandles:
          external_object_type = "BlobsAndFileSystemAccessHandles";
          break;
      }
      return base::StrCat({std::get<0>(info.param) ? "Sqlite" : "LevelDb",
                           external_object_type});
    });

TEST_P(BackingStoreTestWithExternalObjects, PutGetConsistency) {
  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;
  {
    // Initiate transaction1 - writing blobs.
    auto transaction1 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction1->Begin(CreateDummyLock());
    EXPECT_TRUE(transaction1->PutRecord(1, key3_, value3_.Clone()).has_value());
    CommitTransactionAndVerify(std::move(transaction1));
  }

  // Initiate transaction2, reading blobs.
  {
    auto transaction2 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);
    transaction2->Begin(CreateDummyLock());
    auto result = transaction2->GetRecord(1, key3_);
    EXPECT_TRUE(result.has_value());
    IndexedDBValue result_value = std::move(result.value());

    CommitTransactionAndVerify(std::move(transaction2));
    EXPECT_EQ(base::span(value3_.bits), base::span(result_value.bits));

    EXPECT_TRUE(CheckBlobInfoMatches(result_value.external_objects));
  }

  // Initiate transaction3, deleting blobs.
  {
    auto transaction3 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction3->Begin(CreateDummyLock());
    EXPECT_TRUE(
        transaction3
            ->DeleteRange(1, IndexedDBKeyRange(key3_.Clone(), key3_.Clone(),
                                               /*lower_open=*/false,
                                               /*upper_open=*/false))
            .ok());
    CommitTransactionAndVerify(std::move(transaction3));
  }

  // Verify deletes
  {
    auto transaction4 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);

    transaction4->Begin(CreateDummyLock());
    auto result = transaction4->GetRecord(1, key3_);
    EXPECT_TRUE(result.has_value());
    IndexedDBValue result_value = std::move(result.value());

    CommitTransactionAndVerify(std::move(transaction4));
    EXPECT_TRUE(result_value.empty());
  }
}

TEST_P(BackingStoreTestWithExternalObjects, DeleteRange) {
  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  const auto keys =
      std::to_array({IndexedDBKey(u"key0"), IndexedDBKey(u"key1"),
                     IndexedDBKey(u"key2"), IndexedDBKey(u"key3")});

  // All of these delete ranges should result in the deletion of key1 and key2.
  const auto ranges = std::to_array({
      IndexedDBKeyRange(keys[1].Clone(), keys[2].Clone(), /*lower_open=*/false,
                        /*upper_open=*/false),
      IndexedDBKeyRange(keys[0].Clone(), keys[2].Clone(), /*lower_open=*/true,
                        /*upper_open=*/false),
      IndexedDBKeyRange(keys[1].Clone(), keys[3].Clone(), /*lower_open=*/false,
                        /*upper_open=*/true),
      IndexedDBKeyRange(keys[0].Clone(), keys[3].Clone(), /*lower_open=*/true,
                        /*upper_open=*/true),
  });

  for (size_t i = 0; i < std::size(ranges); ++i) {
    const int64_t object_store_id = i + 1;
    const IndexedDBKeyRange& range = ranges[i];

    std::vector<IndexedDBExternalObject> external_objects;
    for (size_t j = 0; j < 4; ++j) {
      std::string type = "type " + base::NumberToString(j);
      std::string payload = "payload " + base::NumberToString(j);
      external_objects.push_back(
          CreateBlobInfo(base::UTF8ToUTF16(type), payload));
    }

    // Reset from previous iteration.
    blob_context_->ClearWrites();
    file_system_access_context_->ClearWrites();

    auto values = std::to_array({
        IndexedDBValue("value0", {external_objects[0]}),
        IndexedDBValue("value1", {external_objects[1]}),
        IndexedDBValue("value2", {external_objects[2]}),
        IndexedDBValue("value3", {external_objects[3]}),
    });
    ASSERT_GE(keys.size(), values.size());

    {
      // Initiate transaction1 - write records.
      auto transaction1 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction1->Begin(CreateDummyLock());
      BackingStore::RecordIdentifier record;
      for (size_t j = 0; j < values.size(); ++j) {
        EXPECT_TRUE(
            transaction1->PutRecord(object_store_id, keys[j], values[j].Clone())
                .has_value());
      }

      // Start committing transaction1.
      CommitTransactionAndVerify(std::move(transaction1));
    }

    {
      // Initiate transaction 2 - delete range.
      auto transaction2 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction2->Begin(CreateDummyLock());
      EXPECT_TRUE(transaction2->DeleteRange(object_store_id, range).ok());

      // Start committing transaction2.
      CommitTransactionAndVerify(std::move(transaction2));
    }

    // Verify deletes
    for (size_t j = 0; j < keys.size(); ++j) {
      {
        auto transaction = db.CreateTransaction(
            blink::mojom::IDBTransactionDurability::Relaxed,
            blink::mojom::IDBTransactionMode::ReadWrite);
        transaction->Begin(CreateDummyLock());
        auto result = transaction->GetRecord(object_store_id, keys[j]);
        EXPECT_TRUE(result.has_value());
        IndexedDBValue result_value = std::move(result.value());

        CommitTransactionAndVerify(std::move(transaction));

        if (j == 1 || j == 2) {
          EXPECT_TRUE(result_value.empty());
        } else {
          EXPECT_FALSE(result_value.empty());
          EXPECT_EQ(base::span(values[j].bits), base::span(result_value.bits));
        }
      }
    }
  }
}

TEST_P(BackingStoreTestWithExternalObjects, DeleteRangeEmptyRange) {
  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  const auto keys = std::to_array({
      IndexedDBKey(u"key0"),
      IndexedDBKey(u"key1"),
      IndexedDBKey(u"key2"),
      IndexedDBKey(u"key3"),
      IndexedDBKey(u"key4"),
  });
  const auto ranges = std::to_array({
      IndexedDBKeyRange(keys[3].Clone(), keys[4].Clone(), /*lower_open=*/true,
                        /*upper_open=*/false),
      IndexedDBKeyRange(keys[2].Clone(), keys[1].Clone(), /*lower_open=*/false,
                        /*upper_open=*/false),
      IndexedDBKeyRange(keys[2].Clone(), keys[1].Clone(), /*lower_open=*/true,
                        /*upper_open=*/true),
  });

  for (size_t i = 0; i < std::size(ranges); ++i) {
    const int64_t object_store_id = i + 1;
    const IndexedDBKeyRange& range = ranges[i];

    std::vector<IndexedDBExternalObject> external_objects;
    for (size_t j = 0; j < 4; ++j) {
      std::string type = "type " + base::NumberToString(j);
      std::string payload = "payload " + base::NumberToString(j);
      external_objects.push_back(
          CreateBlobInfo(base::UTF8ToUTF16(type), payload));
    }

    // Reset from previous iteration.
    blob_context_->ClearWrites();
    file_system_access_context_->ClearWrites();

    const auto values = std::to_array({
        IndexedDBValue("value0", {external_objects[0]}),
        IndexedDBValue("value1", {external_objects[1]}),
        IndexedDBValue("value2", {external_objects[2]}),
        IndexedDBValue("value3", {external_objects[3]}),
    });
    ASSERT_GE(keys.size(), values.size());

    {
      // Initiate transaction1 - write records.
      auto transaction1 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction1->Begin(CreateDummyLock());

      for (size_t j = 0; j < values.size(); ++j) {
        EXPECT_TRUE(
            transaction1->PutRecord(object_store_id, keys[j], values[j].Clone())
                .has_value());
      }
      // Start committing transaction1.
      CommitTransactionAndVerify(std::move(transaction1));
    }

    // Initiate transaction 2 - delete range.
    {
      auto transaction2 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction2->Begin(CreateDummyLock());
      EXPECT_TRUE(transaction2->DeleteRange(object_store_id, range).ok());

      CommitTransactionAndVerify(std::move(transaction2));
    }

    // Verify that no records were deleted.
    for (size_t j = 0; j < values.size(); ++j) {
      {
        auto transaction3 = db.CreateTransaction(
            blink::mojom::IDBTransactionDurability::Relaxed,
            blink::mojom::IDBTransactionMode::ReadWrite);
        transaction3->Begin(CreateDummyLock());
        auto result = transaction3->GetRecord(object_store_id, keys[j]);
        EXPECT_TRUE(result.has_value());
        IndexedDBValue result_value = std::move(result.value());

        CommitTransactionAndVerify(std::move(transaction3));

        // No records should have been deleted.
        EXPECT_FALSE(result_value.empty());
        EXPECT_EQ(base::span(values[j].bits), base::span(result_value.bits));
      }
    }
  }
}

// This tests that external objects are deleted when ClearObjectStore is called.
// See: http://crbug.com/488851
// TODO(enne): we could use more comprehensive testing for ClearObjectStore.
TEST_P(BackingStoreTestWithExternalObjects, ClearObjectStoreObjects) {
  std::vector<IndexedDBExternalObject> external_objects;
  for (size_t j = 0; j < 4; ++j) {
    std::string type = "type " + base::NumberToString(j);
    std::string payload = "payload " + base::NumberToString(j);
    external_objects.push_back(
        CreateBlobInfo(base::UTF8ToUTF16(type), payload));
  }

  const auto keys =
      std::to_array({IndexedDBKey(u"key0"), IndexedDBKey(u"key1"),
                     IndexedDBKey(u"key2"), IndexedDBKey(u"key3")});

  const auto values = std::to_array({
      IndexedDBValue("value0", {external_objects[0]}),
      IndexedDBValue("value1", {external_objects[1]}),
      IndexedDBValue("value2", {external_objects[2]}),
      IndexedDBValue("value3", {external_objects[3]}),
  });
  ASSERT_GE(keys.size(), values.size());

  const int64_t object_store_id = 999;

  ASSERT_OK_AND_ASSIGN(std::unique_ptr<BackingStore::Database> db_ptr,
                       backing_store()->CreateOrOpenDatabase(u"name"));
  BackingStore::Database& db = *db_ptr;

  // Create two object stores, to verify that only one gets deleted.
  for (size_t i = 0; i < 2; ++i) {
    const int64_t write_object_store_id = object_store_id + i;

    {  // Initiate transaction1 - write records.
      auto transaction1 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction1->Begin(CreateDummyLock());
      for (size_t j = 0; j < values.size(); ++j) {
        EXPECT_TRUE(
            transaction1
                ->PutRecord(write_object_store_id, keys[j], values[j].Clone())
                .has_value());
      }

      CommitTransactionAndVerify(std::move(transaction1));
    }
  }

  // Initiate transaction 2 - delete object store
  {
    auto transaction2 =
        db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                             blink::mojom::IDBTransactionMode::ReadWrite);
    transaction2->Begin(CreateDummyLock());
    IndexedDBValue result_value;
    EXPECT_TRUE(transaction2->ClearObjectStore(object_store_id).ok());

    // Start committing transaction2.
    CommitTransactionAndVerify(std::move(transaction2));
  }

  // Verify that all blobs were removed.
  for (size_t j = 0; j < values.size(); ++j) {
    {
      auto transaction3 =
          db.CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                               blink::mojom::IDBTransactionMode::ReadWrite);
      transaction3->Begin(CreateDummyLock());
      auto result = transaction3->GetRecord(object_store_id, keys[j]);
      EXPECT_TRUE(result.has_value());
      IndexedDBValue result_value = std::move(result.value());

      CommitTransactionAndVerify(std::move(transaction3));
      EXPECT_TRUE(result_value.empty());
    }
  }
}

class BackingStoreMigrationTest
    : public BackingStoreWithExternalObjectsTestBase {
 public:
  BackingStoreMigrationTest()
      : BackingStoreWithExternalObjectsTestBase(
            /*use_sqlite=*/false) {}
  ~BackingStoreMigrationTest() override = default;

  bool IncludesBlobs() override { return true; }
  bool IncludesFileSystemAccessHandles() override { return true; }
};

TEST_F(BackingStoreMigrationTest, Migrate) {
  base::HistogramTester histogram_tester;
  blob_context_->SetWriteFilesToDisk(true);
  ASSERT_OK_AND_ASSIGN(auto db,
                       backing_store()->CreateOrOpenDatabase(u"test_db"));
  CreateObjectStore(*db);

  {
    auto transaction =
        db->CreateTransaction(blink::mojom::IDBTransactionDurability::Relaxed,
                              blink::mojom::IDBTransactionMode::ReadWrite);
    transaction->Begin(CreateDummyLock());
    EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key1_, value1_.Clone())
                    .has_value());
    EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key2_, value2_.Clone())
                    .has_value());
    EXPECT_TRUE(transaction->PutRecord(kObjectStoreId1, key3_, value3_.Clone())
                    .has_value());
    CommitTransactionAndVerify(std::move(transaction));
  }
  db.reset();

  // Delete one of the blob files before migration.
  ASSERT_GE(blob_context_->writes().size(), 1u);
  ASSERT_TRUE(base::DeleteFile(blob_context_->writes()[0].path));

  // Migration should still succeed despite the missing file.
  MigrateAndVerifyBackingStore();

  // Verify the histogram records both success and failure.
  EXPECT_EQ(
      histogram_tester.GetBucketCount(
          "IndexedDB.SqliteMigration.RenameBlobResult", -base::File::FILE_OK),
      2);
  EXPECT_EQ(histogram_tester.GetBucketCount(
                "IndexedDB.SqliteMigration.RenameBlobResult",
                -base::File::FILE_ERROR_NOT_FOUND),
            1);
}

}  // namespace content::indexed_db
