// Copyright 2016 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/services/storage/dom_storage/storage_area_impl.h"

#include <list>
#include <memory>
#include <string>
#include <string_view>
#include <tuple>
#include <vector>

#include "base/atomic_ref_count.h"
#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/thread.h"
#include "base/token.h"
#include "base/trace_event/memory_allocator_dump_guid.h"
#include "base/uuid.h"
#include "components/services/storage/dom_storage/async_dom_storage_database.h"
#include "components/services/storage/dom_storage/db_status.h"
#include "components/services/storage/dom_storage/dom_storage_database.h"
#include "components/services/storage/dom_storage/features.h"
#include "components/services/storage/dom_storage/test_support/dom_storage_database_testing.h"
#include "components/services/storage/dom_storage/test_support/storage_area_test_util.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"

namespace storage {

namespace {

using test::MakeGetAllCallback;
using test::MakeSuccessCallback;
using CacheMode = StorageAreaImpl::CacheMode;

constexpr base::Token kTestStorageAreaId(1, 2);
const size_t kTestSizeLimit = 512;

constexpr const char kFirstSessionId[] = "ce8c7dc5_73b4_4320_a506_ce1f4fd3356f";
constexpr const char kSecondSessionId[] =
    "36356e0b_1627_4492_a474_db76a8996bed";
constexpr const char kThirdSessionId[] = "5fe0e896_c6d8_4d2b_8b3c_d26f47832125";
constexpr const char kFourthSessionId[] =
    "b5675eaf_30eb_462d_8d82_c6ba8e6bee4c";
constexpr const char kFifthSessionId[] = "fe95d538_fe75_4d3f_8dc4_ed223f2671eb";

constexpr const char kFakeUrlString[] = "https://a-fake.test/";
constexpr const char kTestPageUrlString[] = "https://example.url";

constexpr int64_t kFirstMapId = 1;
constexpr int64_t kSecondMapId = 2;
constexpr int64_t kThirdMapId = 3;
constexpr int64_t kFourthMapId = 4;
constexpr int64_t kFifthMapId = 5;

std::string ToString(const std::vector<uint8_t>& input) {
  return std::string(input.begin(), input.end());
}

std::vector<uint8_t> ToBytes(std::string_view input) {
  return std::vector<uint8_t>(input.begin(), input.end());
}

std::string ToString(CacheMode cache_mode) {
  switch (cache_mode) {
    case CacheMode::KEYS_AND_VALUES:
      return "KeysAndValues";
    case CacheMode::KEYS_ONLY_WHEN_POSSIBLE:
      return "KeysOnlyWhenPossible";
  }
  NOTREACHED();
}

class BarrierBuilder {
 public:
  class ContinuationRef : public base::RefCountedThreadSafe<ContinuationRef> {
   public:
    explicit ContinuationRef(base::OnceClosure continuation)
        : continuation_(std::move(continuation)) {}

   private:
    friend class base::RefCountedThreadSafe<ContinuationRef>;
    ~ContinuationRef() = default;

    base::ScopedClosureRunner continuation_;
  };

  explicit BarrierBuilder(base::OnceClosure continuation)
      : continuation_(
            base::MakeRefCounted<ContinuationRef>(std::move(continuation))) {}

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

  base::OnceClosure AddClosure() {
    return base::DoNothingWithBoundArgs(continuation_);
  }

 private:
  const scoped_refptr<ContinuationRef> continuation_;
};

class MockDelegate : public StorageAreaImpl::Delegate {
 public:
  MockDelegate() = default;
  ~MockDelegate() override = default;

  void OnNoBindings() override {}
  void DidCommit(DbStatus status) override {
    if (!status.ok())
      LOG(ERROR) << "error committing: " << status.ToString();
    if (committed_)
      std::move(committed_).Run();
  }
  void OnMapLoaded() override { ++map_load_count_; }

  int map_load_count() const { return map_load_count_; }

  void SetDidCommitCallback(base::OnceClosure committed) {
    committed_ = std::move(committed);
  }

 private:
  int map_load_count_ = 0;
  base::OnceClosure committed_;
};

StorageAreaImpl::Options GetDefaultTestingOptions(CacheMode cache_mode) {
  StorageAreaImpl::Options options;
  options.max_size = kTestSizeLimit;
  options.default_commit_delay = base::Seconds(5);
  options.max_bytes_per_hour = 10 * 1024 * 1024;
  options.max_commits_per_hour = 60;
  options.cache_mode = cache_mode;
  return options;
}

}  // namespace

// Base test fixture for `StorageAreaImpl` tests. Provides common setup
// including an in-memory database, a default StorageAreaImpl instance, and an
// observer infrastructure for tracking storage mutations. Subclasses can
// parameterize tests to run on SQLite or LevelDB using `is_sqlite_enabled` when
// constructing `StorageAreaImplTestBase`.
class StorageAreaImplTestBase : public testing::Test,
                                public blink::mojom::StorageAreaObserver {
 public:
  struct Observation {
    enum { kChange, kChangeFailed, kDelete, kDeleteAll, kSendOldValue } type;
    std::string key;
    std::optional<std::string> old_value;
    std::string new_value;
    blink::mojom::StorageAreaSourcePtr source;
    bool should_send_old_value;
  };

  explicit StorageAreaImplTestBase(bool is_sqlite_enabled) {
    // `kDomStorageSqlite` enables SQLite for all databases (on-disk and
    // in-memory). Also explicitly control `kDomStorageSqliteInMemory` so that
    // LevelDB tests don't accidentally use the SQLite in-memory backend.
    feature_list_.InitWithFeatureStates(
        {{kDomStorageSqlite, is_sqlite_enabled},
         {kDomStorageSqliteInMemory, is_sqlite_enabled}});

    // Create an in-memory database.
    base::RunLoop loop;
    db_ = AsyncDomStorageDatabase::Open(
        StorageType::kSessionStorage,
        /*database_path=*/base::FilePath(),
        /*memory_dump_id=*/std::nullopt,
        /*dir_to_destroy=*/base::FilePath(),
        base::BindLambdaForTesting(
            [&](AsyncDomStorageDatabase::OpenOutcome) { loop.Quit(); }));
    loop.Run();

    StorageAreaImpl::Options options =
        GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
    storage_area_ = std::make_unique<StorageAreaImpl>(
        db_.get(), test_map_locator_, &delegate_, options);

    SetDatabaseEntry(*test_map_locator_, test_key1_, test_value1_);
    SetDatabaseEntry(*test_map_locator_, test_key2_, test_value2_);
    SetDatabaseEntry(*test_other_map_locator_, "123", "baddata");

    storage_area_->Bind(storage_area_remote_.BindNewPipeAndPassReceiver());
    storage_area_remote_->AddObserver(
        observer_receiver_.BindNewPipeAndPassRemote());
  }

  ~StorageAreaImplTestBase() override { task_environment_.RunUntilIdle(); }

  void SetDatabaseEntry(const DomStorageDatabase::MapLocator& map_locator,
                        const std::vector<uint8_t>& key,
                        const std::vector<uint8_t>& value) {
    FakeCommitter committer(db_.get(), map_locator.Clone());
    committer.PutMapKeyValueSync(key, value);
  }

  void SetDatabaseEntry(const DomStorageDatabase::MapLocator& map_locator,
                        std::string_view key,
                        std::string_view value) {
    SetDatabaseEntry(map_locator, ToBytes(key), ToBytes(value));
  }

  std::string GetDatabaseEntry(
      const DomStorageDatabase::MapLocator& map_locator,
      std::string_view key) {
    std::map<DomStorageDatabase::Key, DomStorageDatabase::Value> map_key_values;
    ReadMapKeyValuesSync(*db_, map_locator.Clone(), &map_key_values);

    auto it = map_key_values.find(ToBytes(key));
    if (it == map_key_values.end()) {
      return std::string();
    }
    return ToString(it->second);
  }

  bool HasDatabaseEntry(const DomStorageDatabase::MapLocator& map_locator,
                        std::string_view key) {
    std::map<DomStorageDatabase::Key, DomStorageDatabase::Value> map_key_values;
    ReadMapKeyValuesSync(*db_, map_locator.Clone(), &map_key_values);

    auto it = map_key_values.find(ToBytes(key));
    return it != map_key_values.end();
  }

  // Use `AsyncDomStorageDatabase::DeleteSessions()` to delete all key/value
  // pairs and metadata from the database.
  void ClearDatabase() {
    // Enumerate all of the sessions and maps to delete.
    DomStorageDatabase::Metadata all_metadata;
    ASSERT_NO_FATAL_FAILURE(ReadAllMetadataSync(*db_, &all_metadata));

    std::vector<std::string> session_ids_to_delete;
    std::vector<DomStorageDatabase::MapLocator> maps_to_delete;

    for (const DomStorageDatabase::MapMetadata& map_metadata :
         all_metadata.map_metadata) {
      DomStorageDatabase::MapLocator map_to_delete =
          map_metadata.map_locator.Clone();

      for (const std::string& session_id :
           map_metadata.map_locator.session_ids()) {
        session_ids_to_delete.push_back(session_id);
        map_to_delete.RemoveSession(session_id);
      }

      maps_to_delete.push_back(std::move(map_to_delete));
    }

    // Delete all of the sessions and maps.
    ASSERT_NO_FATAL_FAILURE(DeleteSessionsSync(
        *db_, std::move(session_ids_to_delete), std::move(maps_to_delete)));

    // Verify that no maps key/values or metadata exists in the database.
    DomStorageDatabase::Metadata empty_metadata;
    ReadAllMetadataSync(*db_, &empty_metadata);
    EXPECT_EQ(empty_metadata.map_metadata.size(), 0u);

    for (const DomStorageDatabase::MapMetadata& map_metadata :
         all_metadata.map_metadata) {
      std::map<DomStorageDatabase::Key, DomStorageDatabase::Value>
          empty_entries;
      ASSERT_NO_FATAL_FAILURE(ReadMapKeyValuesSync(
          *db_, map_metadata.map_locator.Clone(), &empty_entries));
      EXPECT_EQ(empty_entries.size(), 0u);
    }
  }

  blink::mojom::StorageArea* storage_area() {
    return storage_area_remote_.get();
  }
  StorageAreaImpl* storage_area_impl() { return storage_area_.get(); }

  void FlushAreaBinding() { storage_area_remote_.FlushForTesting(); }

  void DeleteSync(blink::mojom::StorageArea* area,
                  const std::vector<uint8_t>& key,
                  const std::optional<std::vector<uint8_t>>& client_old_value) {
    test::DeleteSync(
        area, key, client_old_value,
        test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId));
  }

  void DeleteAllSync(blink::mojom::StorageArea* area) {
    test::DeleteAllSync(
        area, test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId));
  }

  std::optional<std::vector<uint8_t>> GetSync(const std::vector<uint8_t>& key) {
    return test::GetSync(storage_area(), key);
  }

  bool PutSync(const std::vector<uint8_t>& key,
               const std::vector<uint8_t>& value,
               const std::optional<std::vector<uint8_t>>& client_old_value,
               blink::mojom::StorageAreaSourcePtr source =
                   test::MakeStorageAreaSource(GURL(kTestPageUrlString),
                                               kTestStorageAreaId)) {
    return test::PutSync(storage_area(), key, value, client_old_value,
                         std::move(source));
  }

  void DeleteSync(const std::vector<uint8_t>& key,
                  const std::optional<std::vector<uint8_t>>& client_old_value) {
    DeleteSync(storage_area(), key, client_old_value);
  }

  void DeleteAllSync() { DeleteAllSync(storage_area()); }

  std::string GetSyncStrUsingGetAll(StorageAreaImpl* area_impl,
                                    const std::string& key) {
    std::vector<blink::mojom::KeyValuePtr> data = test::GetAllSync(area_impl);

    for (const auto& key_value : data) {
      if (key_value->key == ToBytes(key)) {
        return ToString(key_value->value);
      }
    }
    return "";
  }

  bool HasChangesToCommit(StorageAreaImpl* area) {
    return area->commit_batch_.get();
  }

  bool BlockingCommit() {
    return BlockingCommit(&delegate_, storage_area_.get());
  }

  bool BlockingCommit(MockDelegate* delegate, StorageAreaImpl* area) {
    bool did_something = false;
    while (area->has_pending_load_tasks() || HasChangesToCommit(area)) {
      base::RunLoop loop;
      delegate->SetDidCommitCallback(loop.QuitClosure());
      area->ScheduleImmediateCommit();
      loop.Run();
      did_something = true;
    }
    return did_something;
  }

  const std::vector<Observation>& observations() { return observations_; }

  MockDelegate* delegate() { return &delegate_; }
  AsyncDomStorageDatabase* database() { return db_.get(); }

  void should_record_send_old_value_observations(bool value) {
    should_record_send_old_value_observations_ = value;
  }

 protected:
  // Enables or disables SQLite.
  base::test::ScopedFeatureList feature_list_;

  const std::string test_key1_ = "def";
  const std::string test_key2_ = "123";
  const std::string test_value1_ = "defdata";
  const std::string test_value2_ = "123data";


  const std::vector<uint8_t> test_key1_bytes_ = ToBytes(test_key1_);
  const std::vector<uint8_t> test_key2_bytes_ = ToBytes(test_key2_);
  const std::vector<uint8_t> test_value1_bytes_ = ToBytes(test_value1_);
  const std::vector<uint8_t> test_value2_bytes_ = ToBytes(test_value2_);

  const blink::StorageKey test_storage_key_ =
      blink::StorageKey::CreateFromStringForTesting(kFakeUrlString);

  const GURL test_page_url_ = GURL(kTestPageUrlString);

  scoped_refptr<DomStorageDatabase::SharedMapLocator> test_map_locator_ =
      base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
          DomStorageDatabase::MapLocator(kFirstSessionId,
                                         test_storage_key_,
                                         kFirstMapId));

  scoped_refptr<DomStorageDatabase::SharedMapLocator> cloned_map_locator1_ =
      base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
          DomStorageDatabase::MapLocator(kSecondSessionId,
                                         test_storage_key_,
                                         kSecondMapId));

  scoped_refptr<DomStorageDatabase::SharedMapLocator> cloned_map_locator2_ =
      base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
          DomStorageDatabase::MapLocator(kThirdSessionId,
                                         test_storage_key_,
                                         kThirdMapId));

  scoped_refptr<DomStorageDatabase::SharedMapLocator> cloned_map_locator3_ =
      base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
          DomStorageDatabase::MapLocator(kFourthSessionId,
                                         test_storage_key_,
                                         kFourthMapId));

  scoped_refptr<DomStorageDatabase::SharedMapLocator> test_other_map_locator_ =
      base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
          DomStorageDatabase::MapLocator(kFifthSessionId,
                                         test_storage_key_,
                                         kFifthMapId));

  scoped_refptr<DomStorageDatabase::SharedMapLocator> GenerateMapLocator(
      int64_t map_id) {
    std::string session_id = base::Uuid::GenerateRandomV4().AsLowercaseString();
    return base::MakeRefCounted<DomStorageDatabase::SharedMapLocator>(
        DomStorageDatabase::MapLocator(std::move(session_id), test_storage_key_,
                                       map_id));
  }

 private:
  // blink::mojom::StorageAreaObserver:
  void KeyChanged(const std::vector<uint8_t>& key,
                  const std::vector<uint8_t>& new_value,
                  const std::optional<std::vector<uint8_t>>& old_value,
                  blink::mojom::StorageAreaSourcePtr source) override {
    std::optional<std::string> optional_old_value;
    if (old_value)
      optional_old_value = ToString(*old_value);
    observations_.push_back({Observation::kChange, ToString(key),
                             optional_old_value, ToString(new_value),
                             std::move(source), false});
  }
  void KeyChangeFailed(const std::vector<uint8_t>& key,
                       blink::mojom::StorageAreaSourcePtr source) override {
    observations_.push_back({Observation::kChangeFailed, ToString(key), "", "",
                             std::move(source), false});
  }
  void KeyDeleted(const std::vector<uint8_t>& key,
                  const std::optional<std::vector<uint8_t>>& old_value,
                  blink::mojom::StorageAreaSourcePtr source) override {
    std::optional<std::string> optional_old_value;
    if (old_value)
      optional_old_value = ToString(*old_value);
    observations_.push_back({Observation::kDelete, ToString(key),
                             optional_old_value, "", std::move(source), false});
  }
  void AllDeleted(bool was_nonempty,
                  blink::mojom::StorageAreaSourcePtr source) override {
    observations_.push_back(
        {Observation::kDeleteAll, "", "", "", std::move(source), false});
  }
  void ShouldSendOldValueOnMutations(bool value) override {
    if (should_record_send_old_value_observations_) {
      observations_.push_back(
          {Observation::kSendOldValue, "", "", "", nullptr, value});
    }
  }

  base::test::TaskEnvironment task_environment_;
  std::unique_ptr<AsyncDomStorageDatabase> db_;
  MockDelegate delegate_;
  std::unique_ptr<StorageAreaImpl> storage_area_;
  mojo::Remote<blink::mojom::StorageArea> storage_area_remote_;
  mojo::Receiver<blink::mojom::StorageAreaObserver> observer_receiver_{this};
  std::vector<Observation> observations_;
  bool should_record_send_old_value_observations_ = false;
};

// Runs each test twice: once with SQLite and once with LevelDB.
class StorageAreaImplTest
    : public testing::WithParamInterface</*is_sqlite_enabled=*/bool>,
      public StorageAreaImplTestBase {
 public:
  StorageAreaImplTest() : StorageAreaImplTestBase(GetParam()) {}
  ~StorageAreaImplTest() override = default;
};

INSTANTIATE_TEST_SUITE_P(
    /*no prefix*/,
    StorageAreaImplTest,
    testing::Bool(),
    /*name_generator=*/
    [](const testing::TestParamInfo<StorageAreaImplTest::ParamType>& info) {
      return info.param ? "SQLite" : "LevelDB";
    });

// Runs tests with each `CacheMode` and each storage backend.
class StorageAreaImplCacheModeTest
    : public testing::WithParamInterface<
          std::tuple<CacheMode, /*is_sqlite_enabled=*/bool>>,
      public StorageAreaImplTestBase {
 public:
  StorageAreaImplCacheModeTest()
      : StorageAreaImplTestBase(std::get<1>(GetParam())) {}
  ~StorageAreaImplCacheModeTest() override = default;

  CacheMode GetCacheMode() const { return std::get<0>(GetParam()); }
};

INSTANTIATE_TEST_SUITE_P(
    /*no prefix*/,
    StorageAreaImplCacheModeTest,
    testing::Combine(testing::Values(CacheMode::KEYS_ONLY_WHEN_POSSIBLE,
                                     CacheMode::KEYS_AND_VALUES),
                     testing::Bool()),
    /*name_generator=*/
    [](const testing::TestParamInfo<StorageAreaImplCacheModeTest::ParamType>&
           info) {
      CacheMode cache_mode;
      bool is_sqlite_enabled;
      std::tie(cache_mode, is_sqlite_enabled) = info.param;

      std::string description = ToString(cache_mode);
      if (is_sqlite_enabled) {
        description += "_SQLite";
      } else {
        description += "_LevelDB";
      }
      return description;
    });

TEST_P(StorageAreaImplTest, GetLoadedFromMap) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::optional<std::vector<uint8_t>> result = GetSync(test_key2_bytes_);
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(test_value2_bytes_, *result);

  EXPECT_FALSE(GetSync(ToBytes("x")).has_value());
}

TEST_P(StorageAreaImplTest, NoDataCallsOnMapLoaded) {
  StorageAreaImpl::Options options =
      GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  // Load an area that has no data inside, so the result will be empty and the
  // migration code is triggered.
  auto area = std::make_unique<StorageAreaImpl>(
      database(), cloned_map_locator2_, delegate(), options);
  std::vector<blink::mojom::KeyValuePtr> data = test::GetAllSync(area.get());
  EXPECT_TRUE(data.empty());
  EXPECT_EQ(1, delegate()->map_load_count());
}

TEST_P(StorageAreaImplTest, GetFromPutOverwrite) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");

  base::RunLoop loop;
  bool put_success = false;
  std::vector<blink::mojom::KeyValuePtr> key_value_pairs;
  {
    BarrierBuilder barrier(loop.QuitClosure());
    storage_area()->Put(
        key, value, test_value2_bytes_, test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success));
    storage_area()->GetAll(
        mojo::NullRemote(),
        MakeGetAllCallback(barrier.AddClosure(), &key_value_pairs));
  }

  loop.Run();
  EXPECT_TRUE(put_success);
  std::vector<uint8_t> result;
  bool found = false;
  for (const auto& kv : key_value_pairs) {
    if (kv->key == key) {
      result = kv->value;
      found = true;
      break;
    }
  }
  EXPECT_TRUE(found);
  EXPECT_EQ(value, result);
}

TEST_P(StorageAreaImplTest, GetFromPutNewKey) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::vector<uint8_t> key = ToBytes("newkey");
  std::vector<uint8_t> value = ToBytes("foo");

  EXPECT_TRUE(PutSync(key, value, std::nullopt));

  std::optional<std::vector<uint8_t>> result = GetSync(key);
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(value, *result);
}

TEST_P(StorageAreaImplTest, PutLoadsValuesAfterCacheModeUpgrade) {
  std::vector<uint8_t> key = ToBytes("newkey");
  std::vector<uint8_t> value1 = ToBytes("foo");
  std::vector<uint8_t> value2 = ToBytes("bar");

  ASSERT_EQ(CacheMode::KEYS_ONLY_WHEN_POSSIBLE,
            storage_area_impl()->cache_mode());

  // Do a put to load the key-only cache.
  EXPECT_TRUE(PutSync(key, value1, std::nullopt));
  BlockingCommit();
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);

  // Change cache mode.
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  // Loading new map isn't necessary yet.
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);

  // Do another put and check that the map has been upgraded
  EXPECT_TRUE(PutSync(key, value2, value1));
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_AND_VALUES,
            storage_area_impl()->map_state_);
}

TEST_P(StorageAreaImplTest, PendingLoadTasks) {
  // `StorageAreaImpl` starts unloaded.
  EXPECT_FALSE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_FALSE(storage_area_impl()->has_pending_load_read_write_tasks());

  // `GetAll()` enqueues a readonly pending load task.
  storage_area_impl()->GetAll(/*new_observer=*/mojo::NullRemote(),
                              base::DoNothing());

  EXPECT_TRUE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_FALSE(storage_area_impl()->has_pending_load_read_write_tasks());

  // `ScheduleImmediateCommit()` must do nothing when no map key/value
  // modifications exist.
  storage_area_impl()->ScheduleImmediateCommit();

  EXPECT_TRUE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_FALSE(storage_area_impl()->has_pending_load_read_write_tasks());

  // `Put()` enqueues a readwrite pending load task.
  storage_area_impl()->Put(test_key1_bytes_, test_value1_bytes_,
                           test_value2_bytes_, test::MakeStorageAreaSource(),
                           base::DoNothing());

  EXPECT_TRUE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_TRUE(storage_area_impl()->has_pending_load_read_write_tasks());

  // `ScheduleImmediateCommit()` now enqueues readwrite pending load task.
  storage_area_impl()->ScheduleImmediateCommit();

  EXPECT_TRUE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_TRUE(storage_area_impl()->has_pending_load_read_write_tasks());

  // Use `GetSync()` to wait for operations above to complete.
  ASSERT_OK_AND_ASSIGN(std::vector<uint8_t> actual_value1,
                       GetSync(test_key1_bytes_));
  EXPECT_EQ(actual_value1, test_value1_bytes_);

  // The map must be loaded.
  EXPECT_FALSE(storage_area_impl()->has_pending_load_tasks());
  EXPECT_FALSE(storage_area_impl()->has_pending_load_read_write_tasks());

  // The `Put()` must be committed.
  EXPECT_FALSE(HasChangesToCommit(storage_area_impl()));
}

TEST_P(StorageAreaImplCacheModeTest, GetAll) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());

  std::vector<blink::mojom::KeyValuePtr> data =
      test::GetAllSync(storage_area());
  EXPECT_EQ(2u, data.size());
}

TEST_P(StorageAreaImplCacheModeTest, CommitPutToDB) {
  base::HistogramTester histograms;

  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key1 = test_key2_;
  std::string value1 = "foo";
  std::string key2 = "fake_key_for_CommitPutToDB";
  std::string value2 = "data abc";

  base::RunLoop loop;
  bool put_success1 = false;
  bool put_success2 = false;
  bool put_success3 = false;
  {
    BarrierBuilder barrier(loop.QuitClosure());

    storage_area()->Put(
        ToBytes(key1), ToBytes(value1), test_value2_bytes_,
        test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success1));
    storage_area()->Put(
        ToBytes(key2), ToBytes("old value"), std::nullopt,
        test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success2));
    storage_area()->Put(
        ToBytes(key2), ToBytes(value2), ToBytes("old value"),
        test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success3));
  }

  loop.Run();
  EXPECT_TRUE(put_success1);
  EXPECT_TRUE(put_success2);
  EXPECT_TRUE(put_success3);
  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, key2));

  BlockingCommit();

  EXPECT_EQ(value1, GetDatabaseEntry(*test_map_locator_, key1));
  EXPECT_EQ(value2, GetDatabaseEntry(*test_map_locator_, key2));
}

TEST_P(StorageAreaImplCacheModeTest, PutObservations) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key = "new_key";
  std::string value1 = "foo";
  std::string value2 = "data abc";
  constexpr base::Token source1(1, 1);
  constexpr base::Token source2(2, 2);

  EXPECT_TRUE(PutSync(ToBytes(key), ToBytes(value1), std::nullopt,
                      test::MakeStorageAreaSource(test_page_url_, source1)));
  ASSERT_EQ(1u, observations().size());
  EXPECT_EQ(Observation::kChange, observations()[0].type);
  EXPECT_EQ(key, observations()[0].key);
  EXPECT_EQ(value1, observations()[0].new_value);
  EXPECT_EQ(std::nullopt, observations()[0].old_value);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, source1),
            observations()[0].source);

  EXPECT_TRUE(PutSync(ToBytes(key), ToBytes(value2), ToBytes(value1),
                      test::MakeStorageAreaSource(test_page_url_, source2)));
  ASSERT_EQ(2u, observations().size());
  EXPECT_EQ(Observation::kChange, observations()[1].type);
  EXPECT_EQ(key, observations()[1].key);
  EXPECT_EQ(value1, *observations()[1].old_value);
  EXPECT_EQ(value2, observations()[1].new_value);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, source2),
            observations()[1].source);

  // Same put should cause another observation.
  EXPECT_TRUE(PutSync(ToBytes(key), ToBytes(value2), ToBytes(value2),
                      test::MakeStorageAreaSource(test_page_url_, source2)));
  ASSERT_EQ(3u, observations().size());
  EXPECT_EQ(Observation::kChange, observations()[2].type);
  EXPECT_EQ(key, observations()[2].key);
  EXPECT_EQ(value2, *observations()[2].old_value);
  EXPECT_EQ(value2, observations()[2].new_value);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, source2),
            observations()[2].source);
}

TEST_P(StorageAreaImplCacheModeTest, DeleteNonExistingKey) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  DeleteSync(ToBytes("doesn't exist"), std::vector<uint8_t>());
  EXPECT_EQ(1u, observations().size());
}

TEST_P(StorageAreaImplCacheModeTest, DeleteExistingKey) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key = "newkey";
  std::string value = "foo";
  SetDatabaseEntry(*test_map_locator_, key, value);

  DeleteSync(ToBytes(key), ToBytes(value));
  ASSERT_EQ(1u, observations().size());
  EXPECT_EQ(Observation::kDelete, observations()[0].type);
  EXPECT_EQ(key, observations()[0].key);
  EXPECT_EQ(value, *observations()[0].old_value);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[0].source);

  EXPECT_TRUE(HasDatabaseEntry(*test_map_locator_, key));

  BlockingCommit();
  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, key));
}

TEST_P(StorageAreaImplCacheModeTest, DeleteAllWithoutLoadedMap) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key = "newkey";
  std::string value = "foo";
  std::string dummy_key = "foobar";
  SetDatabaseEntry(*test_other_map_locator_, dummy_key, value);
  SetDatabaseEntry(*test_map_locator_, key, value);

  DeleteAllSync();
  ASSERT_EQ(1u, observations().size());
  EXPECT_EQ(Observation::kDeleteAll, observations()[0].type);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[0].source);

  EXPECT_TRUE(HasDatabaseEntry(*test_map_locator_, key));
  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));

  BlockingCommit();
  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, key));
  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));

  // Deleting all again should still work, and still cause an observation.
  DeleteAllSync();
  ASSERT_EQ(2u, observations().size());
  EXPECT_EQ(Observation::kDeleteAll, observations()[1].type);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[1].source);

  // And now we've deleted all, writing something the quota size should work.
  EXPECT_TRUE(PutSync(std::vector<uint8_t>(kTestSizeLimit, 'b'),
                      std::vector<uint8_t>(), std::nullopt));
}

TEST_P(StorageAreaImplCacheModeTest, DeleteAllWithLoadedMap) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key = "newkey";
  std::string value = "foo";
  std::string dummy_key = "foobar";
  SetDatabaseEntry(*test_other_map_locator_, dummy_key, value);

  EXPECT_TRUE(PutSync(ToBytes(key), ToBytes(value), std::nullopt));

  DeleteAllSync();
  ASSERT_EQ(2u, observations().size());
  EXPECT_EQ(Observation::kDeleteAll, observations()[1].type);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[1].source);

  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));

  BlockingCommit();
  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, key));
  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));
}

TEST_P(StorageAreaImplCacheModeTest, DeleteAllWithPendingMapLoad) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::string key = "newkey";
  std::string value = "foo";
  std::string dummy_key = "foobar";
  SetDatabaseEntry(*test_other_map_locator_, dummy_key, value);

  storage_area()->Put(ToBytes(key), ToBytes(value), std::nullopt,
                      test::MakeStorageAreaSource(), base::DoNothing());

  DeleteAllSync();
  ASSERT_EQ(2u, observations().size());
  EXPECT_EQ(Observation::kDeleteAll, observations()[1].type);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[1].source);

  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));

  BlockingCommit();
  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, key));
  EXPECT_TRUE(HasDatabaseEntry(*test_other_map_locator_, dummy_key));
}

TEST_P(StorageAreaImplCacheModeTest, DeleteAllWithoutLoadedEmptyMap) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  ASSERT_NO_FATAL_FAILURE(ClearDatabase());

  DeleteAllSync();
  ASSERT_EQ(1u, observations().size());
  EXPECT_EQ(Observation::kDeleteAll, observations()[0].type);
  EXPECT_EQ(test::MakeStorageAreaSource(test_page_url_, kTestStorageAreaId),
            observations()[0].source);
}

TEST_P(StorageAreaImplCacheModeTest, PutOverQuotaLargeValue) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::vector<uint8_t> key = ToBytes("newkey");
  std::vector<uint8_t> value(kTestSizeLimit, 4);

  EXPECT_FALSE(PutSync(key, value, std::nullopt));

  value.resize(kTestSizeLimit / 2);
  EXPECT_TRUE(PutSync(key, value, std::nullopt));
}

TEST_P(StorageAreaImplCacheModeTest, PutOverQuotaLargeKey) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::vector<uint8_t> key(kTestSizeLimit, 'a');
  std::vector<uint8_t> value = ToBytes("newvalue");

  EXPECT_FALSE(PutSync(key, value, std::nullopt));

  key.resize(kTestSizeLimit / 2);
  EXPECT_TRUE(PutSync(key, value, std::nullopt));
}

TEST_P(StorageAreaImplCacheModeTest, PutWhenAlreadyOverQuota) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::string key = "largedata";
  std::vector<uint8_t> value(kTestSizeLimit, 4);
  std::vector<uint8_t> old_value = value;

  SetDatabaseEntry(*test_map_locator_, key, ToString(value));

  // Put with same data should succeed.
  EXPECT_TRUE(PutSync(ToBytes(key), value, std::nullopt));

  // Put with same data size should succeed.
  value[1] = 13;
  EXPECT_TRUE(PutSync(ToBytes(key), value, old_value));

  // Adding a new key when already over quota should not succeed.
  EXPECT_FALSE(PutSync(ToBytes("newkey"), {1, 2, 3}, std::nullopt));

  // Reducing size should also succeed.
  old_value = value;
  value.resize(kTestSizeLimit / 2);
  EXPECT_TRUE(PutSync(ToBytes(key), value, old_value));

  // Increasing size again should succeed, as still under the limit.
  old_value = value;
  value.resize(value.size() + 1);
  EXPECT_TRUE(PutSync(ToBytes(key), value, old_value));

  // But increasing back to original size should fail.
  old_value = value;
  value.resize(kTestSizeLimit);
  EXPECT_FALSE(PutSync(ToBytes(key), value, old_value));
}

TEST_P(StorageAreaImplCacheModeTest, PutWhenAlreadyOverQuotaBecauseOfLargeKey) {
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  std::vector<uint8_t> key(kTestSizeLimit, 'x');
  std::vector<uint8_t> value = ToBytes("value");
  std::vector<uint8_t> old_value = value;

  SetDatabaseEntry(*test_map_locator_, ToString(key), ToString(value));

  // Put with same data size should succeed.
  value[0] = 'X';
  EXPECT_TRUE(PutSync(key, value, old_value));

  // Reducing size should also succeed.
  old_value = value;
  value.clear();
  EXPECT_TRUE(PutSync(key, value, old_value));

  // Increasing size should fail.
  old_value = value;
  value.resize(1, 'a');
  EXPECT_FALSE(PutSync(key, value, old_value));
}

TEST_P(StorageAreaImplCacheModeTest, PutAfterPurgeMemory) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::vector<uint8_t> result;
  const auto key = test_key2_bytes_;
  const auto value = test_value2_bytes_;
  EXPECT_TRUE(PutSync(key, value, value));
  EXPECT_EQ(delegate()->map_load_count(), 1);

  // Adding again doesn't load map again.
  EXPECT_TRUE(PutSync(key, value, value));
  EXPECT_EQ(delegate()->map_load_count(), 1);

  storage_area_impl()->PurgeMemory();

  // Now adding should still work, and load map again.
  EXPECT_TRUE(PutSync(key, value, value));
  EXPECT_EQ(delegate()->map_load_count(), 2);
}

TEST_P(StorageAreaImplCacheModeTest, PurgeMemoryWithPendingChanges) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  EXPECT_TRUE(PutSync(key, value, test_value2_bytes_));
  EXPECT_EQ(delegate()->map_load_count(), 1);

  // Purge memory, and read. Should not actually have purged, so should not have
  // triggered a load.
  storage_area_impl()->PurgeMemory();

  EXPECT_TRUE(PutSync(key, value, value));
  EXPECT_EQ(delegate()->map_load_count(), 1);
}

TEST_P(StorageAreaImplTest, SetOnlyKeysWithoutDatabase) {
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  MockDelegate delegate;
  StorageAreaImpl storage_area(
      nullptr, test_map_locator_, &delegate,
      GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE));
  mojo::Remote<blink::mojom::StorageArea> storage_area_remote;
  storage_area.Bind(storage_area_remote.BindNewPipeAndPassReceiver());
  // Setting only keys mode is noop.
  storage_area.SetCacheModeForTesting(CacheMode::KEYS_ONLY_WHEN_POSSIBLE);

  EXPECT_FALSE(storage_area.initialized());
  EXPECT_EQ(CacheMode::KEYS_AND_VALUES, storage_area.cache_mode());

  // Put and Get can work synchronously without reload.
  bool put_callback_called = false;
  storage_area.Put(key, value, std::nullopt, test::MakeStorageAreaSource(),
                   base::BindOnce(
                       [](bool* put_callback_called, bool success) {
                         EXPECT_TRUE(success);
                         *put_callback_called = true;
                       },
                       &put_callback_called));
  EXPECT_TRUE(put_callback_called);

  std::optional<std::vector<uint8_t>> expected_value =
      test::GetSync(&storage_area, key);
  ASSERT_TRUE(expected_value.has_value());
  EXPECT_EQ(*expected_value, value);
}

TEST_P(StorageAreaImplCacheModeTest, CommitOnDifferentCacheModes) {
  storage_area_impl()->SetCacheModeForTesting(GetCacheMode());
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  std::vector<uint8_t> value2 = ToBytes("foo2");
  std::vector<uint8_t> value3 = ToBytes("foobar");

  // The initial map always has values, so a nullopt is fine for the old value.
  ASSERT_TRUE(PutSync(key, value, std::nullopt));
  ASSERT_TRUE(storage_area_impl()->commit_batch_);

  // Area stays in CacheMode::KEYS_AND_VALUES until the first commit has
  // succeeded.
  EXPECT_TRUE(storage_area_impl()->commit_batch_->changed_values.empty());
  auto* changes = &storage_area_impl()->commit_batch_->changed_keys;
  ASSERT_EQ(1u, changes->size());
  EXPECT_EQ(key, *changes->begin());

  BlockingCommit();

  ASSERT_TRUE(PutSync(key, value2, value));
  ASSERT_TRUE(storage_area_impl()->commit_batch_);

  // Commit has occured, so the map type will diverge based on the cache mode.
  if (GetCacheMode() == CacheMode::KEYS_AND_VALUES) {
    EXPECT_TRUE(storage_area_impl()->commit_batch_->changed_values.empty());
    auto* changed_keys = &storage_area_impl()->commit_batch_->changed_keys;
    ASSERT_EQ(1u, changed_keys->size());
    EXPECT_EQ(key, *changed_keys->begin());
  } else {
    EXPECT_TRUE(storage_area_impl()->commit_batch_->changed_keys.empty());
    auto* changed_values = &storage_area_impl()->commit_batch_->changed_values;
    ASSERT_EQ(1u, changed_values->size());
    auto it = changed_values->begin();
    EXPECT_EQ(key, it->first);
    EXPECT_EQ(value2, it->second);
  }

  BlockingCommit();

  EXPECT_EQ("foo2", GetDatabaseEntry(*test_map_locator_, test_key2_));
  if (GetCacheMode() == CacheMode::KEYS_AND_VALUES) {
    EXPECT_EQ(2u, storage_area_impl()->keys_values_map_.size());
  } else {
    EXPECT_EQ(2u, storage_area_impl()->keys_only_map_.size());
  }
  ASSERT_TRUE(PutSync(key, value2, value2));
  EXPECT_FALSE(storage_area_impl()->commit_batch_);
  ASSERT_TRUE(PutSync(key, value3, value2));
  ASSERT_TRUE(storage_area_impl()->commit_batch_);

  if (GetCacheMode() == CacheMode::KEYS_AND_VALUES) {
    auto* changed_keys = &storage_area_impl()->commit_batch_->changed_keys;
    EXPECT_EQ(1u, changed_keys->size());
    auto it = changed_keys->find(key);
    ASSERT_NE(it, changed_keys->end());
  } else {
    auto* changed_values = &storage_area_impl()->commit_batch_->changed_values;
    EXPECT_EQ(1u, changed_values->size());
    auto it = changed_values->find(key);
    ASSERT_NE(it, changed_values->end());
    EXPECT_EQ(value3, it->second);
  }

  ASSERT_NO_FATAL_FAILURE(ClearDatabase());
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));
  BlockingCommit();
  EXPECT_EQ("foobar", GetDatabaseEntry(*test_map_locator_, test_key2_));
  EXPECT_FALSE(HasChangesToCommit(storage_area_impl()));
}

TEST_P(StorageAreaImplTest, GetAllWhenCacheOnlyKeys) {
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  std::vector<uint8_t> value2 = ToBytes("foobar");

  // Go to load state only keys.
  ASSERT_TRUE(PutSync(key, value, std::nullopt));
  BlockingCommit();
  ASSERT_TRUE(PutSync(key, value2, value));
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));

  std::vector<blink::mojom::KeyValuePtr> data;

  base::RunLoop loop;
  bool put_result1 = false;
  bool put_result2 = false;
  {
    BarrierBuilder barrier(loop.QuitClosure());

    storage_area()->Put(
        key, value, value2, test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_result1));

    mojo::PendingRemote<blink::mojom::StorageAreaObserver> unused_observer;
    std::ignore = unused_observer.InitWithNewPipeAndPassReceiver();
    storage_area()->GetAll(std::move(unused_observer),
                           MakeGetAllCallback(barrier.AddClosure(), &data));
    storage_area()->Put(
        key, value2, value, test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_result2));
    FlushAreaBinding();
  }

  // GetAll triggers a commit when it's switching map types.
  EXPECT_TRUE(put_result1);
  EXPECT_EQ("foo", GetDatabaseEntry(*test_map_locator_, test_key2_));

  loop.Run();

  EXPECT_TRUE(put_result1);

  EXPECT_EQ(2u, data.size());
  EXPECT_TRUE(data[1]->Equals(
      blink::mojom::KeyValue(test_key1_bytes_, test_value1_bytes_)))
      << ToString(data[1]->value) << " vs expected " << test_value1_;
  EXPECT_TRUE(data[0]->Equals(blink::mojom::KeyValue(key, value)))
      << ToString(data[0]->value) << " vs expected " << ToString(value);

  // The last "put" isn't committed yet.
  EXPECT_EQ("foo", GetDatabaseEntry(*test_map_locator_, test_key2_));

  ASSERT_TRUE(HasChangesToCommit(storage_area_impl()));
  BlockingCommit();

  EXPECT_EQ("foobar", GetDatabaseEntry(*test_map_locator_, test_key2_));
}

TEST_P(StorageAreaImplTest, GetAllAfterSetCacheMode) {
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  std::vector<uint8_t> value2 = ToBytes("foobar");

  // Go to load state only keys.
  ASSERT_TRUE(PutSync(key, value, std::nullopt));
  BlockingCommit();
  EXPECT_TRUE(storage_area_impl()->map_state_ ==
              StorageAreaImpl::MapState::LOADED_KEYS_ONLY);
  ASSERT_TRUE(PutSync(key, value2, value));
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));

  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);

  // Cache isn't cleared when commit batch exists.
  EXPECT_TRUE(storage_area_impl()->map_state_ ==
              StorageAreaImpl::MapState::LOADED_KEYS_ONLY);

  base::RunLoop loop;

  bool put_success = false;
  std::vector<blink::mojom::KeyValuePtr> data;
  {
    BarrierBuilder barrier(loop.QuitClosure());

    storage_area()->Put(
        key, value, value2, test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success));

    mojo::PendingRemote<blink::mojom::StorageAreaObserver> unused_observer;
    std::ignore = unused_observer.InitWithNewPipeAndPassReceiver();
    storage_area()->GetAll(std::move(unused_observer),
                           MakeGetAllCallback(barrier.AddClosure(), &data));

    // This Delete() should not affect the value returned by GetAll().
    storage_area()->Delete(key, value, test::MakeStorageAreaSource(),
                           barrier.AddClosure());
  }
  loop.Run();

  // This test's third Put() loads the storage area's values into the cache.
  EXPECT_TRUE(storage_area_impl()->map_state_ ==
              StorageAreaImpl::MapState::LOADED_KEYS_AND_VALUES);

  EXPECT_EQ(2u, data.size());
  EXPECT_TRUE(data[1]->Equals(
      blink::mojom::KeyValue(test_key1_bytes_, test_value1_bytes_)))
      << ToString(data[1]->value) << " vs expected " << test_value1_;
  EXPECT_TRUE(data[0]->Equals(blink::mojom::KeyValue(key, value)))
      << ToString(data[0]->value) << " vs expected " << ToString(value2);

  EXPECT_TRUE(put_success);

  // GetAll shouldn't trigger a commit before it runs now because the value
  // map should be loading.
  EXPECT_EQ("foobar", GetDatabaseEntry(*test_map_locator_, test_key2_));

  ASSERT_TRUE(HasChangesToCommit(storage_area_impl()));
  BlockingCommit();

  EXPECT_FALSE(HasDatabaseEntry(*test_map_locator_, test_key2_));
}

TEST_P(StorageAreaImplTest, SetCacheModeConsistent) {
  std::vector<uint8_t> key = test_key2_bytes_;
  std::vector<uint8_t> value = ToBytes("foo");
  std::vector<uint8_t> value2 = ToBytes("foobar");

  EXPECT_FALSE(storage_area_impl()->IsMapLoaded());
  EXPECT_TRUE(storage_area_impl()->cache_mode() ==
              CacheMode::KEYS_ONLY_WHEN_POSSIBLE);

  // Clear the database before the area loads data.
  ASSERT_NO_FATAL_FAILURE(ClearDatabase());

  EXPECT_TRUE(PutSync(key, value, std::nullopt));
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));
  BlockingCommit();

  EXPECT_TRUE(PutSync(key, value2, value));
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));

  // Setting cache mode does not reload the cache till it is required.
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);

  // Put operation should change the mode.
  EXPECT_TRUE(PutSync(key, value, value2));
  EXPECT_TRUE(HasChangesToCommit(storage_area_impl()));
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_AND_VALUES,
            storage_area_impl()->map_state_);
  std::optional<std::vector<uint8_t>> result = GetSync(key);
  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(value, *result);
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_AND_VALUES,
            storage_area_impl()->map_state_);

  BlockingCommit();

  // Test that the map will unload correctly
  EXPECT_TRUE(PutSync(key, value2, value));
  storage_area_impl()->SetCacheModeForTesting(
      CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);
  BlockingCommit();
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);

  // Test the map will unload right away when there are no changes.
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  ASSERT_TRUE(GetSync(key).has_value());
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_AND_VALUES,
            storage_area_impl()->map_state_);
  storage_area_impl()->SetCacheModeForTesting(
      CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  EXPECT_EQ(StorageAreaImpl::MapState::LOADED_KEYS_ONLY,
            storage_area_impl()->map_state_);
}

TEST_P(StorageAreaImplTest, SendOldValueObservations) {
  ASSERT_EQ(0u, observations().size());
  should_record_send_old_value_observations(true);
  storage_area_impl()->SetCacheModeForTesting(CacheMode::KEYS_AND_VALUES);
  // Flush tasks on mojo thread to observe callback.
  DeleteSync(ToBytes("doesn't exist"), std::nullopt);
  storage_area_impl()->SetCacheModeForTesting(
      CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  // Flush tasks on mojo thread to observe callback.
  DeleteSync(ToBytes("doesn't exist"), std::nullopt);

  ASSERT_EQ(4u, observations().size());
  EXPECT_EQ(Observation::kSendOldValue, observations()[0].type);
  EXPECT_FALSE(observations()[0].should_send_old_value);
  EXPECT_EQ(Observation::kSendOldValue, observations()[2].type);
  EXPECT_TRUE(observations()[2].should_send_old_value);
}

TEST_P(StorageAreaImplCacheModeTest, MapForking) {
  std::string value3 = "value3";
  std::string value4 = "value4";
  std::string value5 = "value5";

  // In order to test the interaction between forking and mojo calls where
  // forking can happen in between a request and reply to the area mojo
  // service, all calls are done on the 'impl' object itself.

  // Operations in the same run cycle:
  // Fork 1 created from original
  // Put on fork 1
  // Fork 2 create from fork 1
  // Put on fork 1
  // Put on original
  // Fork 3 created from original
  std::unique_ptr<StorageAreaImpl> fork1;
  MockDelegate fork1_delegate;
  std::unique_ptr<StorageAreaImpl> fork2;
  MockDelegate fork2_delegate;
  std::unique_ptr<StorageAreaImpl> fork3;
  MockDelegate fork3_delegate;

  auto options = GetDefaultTestingOptions(GetCacheMode());
  bool put_success1 = false;
  bool put_success2 = false;
  bool put_success3 = false;
  base::RunLoop loop;
  {
    BarrierBuilder barrier(loop.QuitClosure());

    // Create fork 1.
    fork1 = storage_area_impl()->ForkToNewMap(cloned_map_locator1_,
                                              &fork1_delegate, options);

    // Do a put on fork 1 and create fork 2.
    // Note - these are 'skipping' the mojo layer, which is why the fork isn't
    // scheduled.
    fork1->Put(test_key2_bytes_, ToBytes(value4), test_value2_bytes_,
               test::MakeStorageAreaSource(),
               MakeSuccessCallback(barrier.AddClosure(), &put_success1));
    fork2 = fork1->ForkToNewMap(cloned_map_locator2_, &fork2_delegate, options);
    fork1->Put(test_key2_bytes_, ToBytes(value5), ToBytes(value4),
               test::MakeStorageAreaSource(),
               MakeSuccessCallback(barrier.AddClosure(), &put_success2));

    // Do a put on original and create fork 3, which is key-only.
    storage_area_impl()->Put(
        test_key1_bytes_, ToBytes(value3), test_value1_bytes_,
        test::MakeStorageAreaSource(),
        MakeSuccessCallback(barrier.AddClosure(), &put_success3));
    fork3 = storage_area_impl()->ForkToNewMap(
        cloned_map_locator3_, &fork3_delegate,
        GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE));
  }
  loop.Run();
  EXPECT_TRUE(put_success1);
  EXPECT_TRUE(put_success2);
  EXPECT_TRUE(fork2.get());
  EXPECT_TRUE(fork3.get());

  EXPECT_EQ(value3, GetSyncStrUsingGetAll(storage_area_impl(), test_key1_));
  EXPECT_EQ(test_value1_, GetSyncStrUsingGetAll(fork1.get(), test_key1_));
  EXPECT_EQ(test_value1_, GetSyncStrUsingGetAll(fork2.get(), test_key1_));
  EXPECT_EQ(value3, GetSyncStrUsingGetAll(fork3.get(), test_key1_));

  EXPECT_EQ(test_value2_,
            GetSyncStrUsingGetAll(storage_area_impl(), test_key2_));
  EXPECT_EQ(value5, GetSyncStrUsingGetAll(fork1.get(), test_key2_));
  EXPECT_EQ(value4, GetSyncStrUsingGetAll(fork2.get(), test_key2_));
  EXPECT_EQ(test_value2_, GetSyncStrUsingGetAll(fork3.get(), test_key2_));

  BlockingCommit(delegate(), storage_area_impl());
  BlockingCommit(&fork1_delegate, fork1.get());

  // test_key1_ values.
  EXPECT_EQ(value3, GetDatabaseEntry(*test_map_locator_, test_key1_));
  EXPECT_EQ(test_value1_, GetDatabaseEntry(*cloned_map_locator1_, test_key1_));
  EXPECT_EQ(test_value1_, GetDatabaseEntry(*cloned_map_locator2_, test_key1_));
  EXPECT_EQ(value3, GetDatabaseEntry(*cloned_map_locator3_, test_key1_));

  // test_key2_ values.
  EXPECT_EQ(test_value2_, GetDatabaseEntry(*test_map_locator_, test_key2_));
  EXPECT_EQ(value5, GetDatabaseEntry(*cloned_map_locator1_, test_key2_));
  EXPECT_EQ(value4, GetDatabaseEntry(*cloned_map_locator2_, test_key2_));
  EXPECT_EQ(test_value2_, GetDatabaseEntry(*cloned_map_locator3_, test_key2_));
}

TEST_P(StorageAreaImplCacheModeTest, MapForkAfterLoad) {
  const std::string kValue = "foo";
  const std::vector<uint8_t> kValueVec = ToBytes(kValue);

  // Do a sync put so the map loads.
  EXPECT_TRUE(PutSync(test_key1_bytes_, kValueVec, std::nullopt));

  // Execute the fork.
  MockDelegate fork1_delegate;
  std::unique_ptr<StorageAreaImpl> fork1 = storage_area_impl()->ForkToNewMap(
      cloned_map_locator1_, &fork1_delegate,
      GetDefaultTestingOptions(GetCacheMode()));

  // Check our forked state.
  EXPECT_EQ(kValue, GetSyncStrUsingGetAll(fork1.get(), test_key1_));

  BlockingCommit();

  EXPECT_EQ(kValue, GetDatabaseEntry(*cloned_map_locator1_, test_key1_));
}

namespace {

struct FuzzState {
  std::optional<std::vector<uint8_t>> val1;
  std::optional<std::vector<uint8_t>> val2;
};

}  // namespace

TEST_P(StorageAreaImplTest, MapForkingPseudoFuzzer) {
  const std::string kKey1 = "key1";
  const std::vector<uint8_t> kKey1Vec = ToBytes(kKey1);
  const std::string kKey2 = "key2";
  const std::vector<uint8_t> kKey2Vec = ToBytes(kKey2);
  const int kTotalAreas = 1000;

  // This tests tries to throw all possible enumerations of operations and
  // forking at areas. The purpose is to hit all edge cases possible to
  // expose any loading bugs.

  std::vector<FuzzState> states(kTotalAreas);
  std::vector<std::unique_ptr<StorageAreaImpl>> areas(kTotalAreas);
  std::vector<MockDelegate> delegates(kTotalAreas);
  std::list<bool> successes;

  // `StorageAreaImplTest` test setup initializes the first map to clone using
  // `kFirstMapId`.
  int next_map_id = kFirstMapId + 1;

  base::RunLoop loop;
  {
    BarrierBuilder barrier(loop.QuitClosure());
    for (int64_t i = 0; i < kTotalAreas; i++) {
      FuzzState& state = states[i];
      if (!areas[i]) {
        areas[i] = storage_area_impl()->ForkToNewMap(
            GenerateMapLocator(next_map_id), &delegates[i],
            GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE));
        ++next_map_id;
      }
      int64_t forks = i;
      if ((i % 5 == 0 || i % 6 == 0) && forks + 1 < kTotalAreas) {
        forks++;
        states[forks] = state;
        areas[forks] = areas[i]->ForkToNewMap(
            GenerateMapLocator(next_map_id), &delegates[forks],
            GetDefaultTestingOptions(CacheMode::KEYS_AND_VALUES));
        ++next_map_id;
      }
      if (i % 13 == 0) {
        FuzzState old_state = state;
        state.val1 = std::nullopt;
        areas[i]->Delete(kKey1Vec, old_state.val1,
                         test::MakeStorageAreaSource(), barrier.AddClosure());
      }
      if (i % 4 == 0) {
        FuzzState old_state = state;
        state.val2 =
            std::make_optional<std::vector<uint8_t>>({static_cast<uint8_t>(i)});
        successes.push_back(false);
        areas[i]->Put(
            kKey2Vec, state.val2.value(), old_state.val2,
            test::MakeStorageAreaSource(),
            MakeSuccessCallback(barrier.AddClosure(), &successes.back()));
      }
      if (i % 3 == 0) {
        FuzzState old_state = state;
        state.val1 = std::make_optional<std::vector<uint8_t>>(
            {static_cast<uint8_t>(i + 5)});
        successes.push_back(false);
        areas[i]->Put(
            kKey1Vec, state.val1.value(), old_state.val1,
            test::MakeStorageAreaSource(),
            MakeSuccessCallback(barrier.AddClosure(), &successes.back()));
      }
      if (i % 11 == 0) {
        state.val1 = std::nullopt;
        state.val2 = std::nullopt;
        areas[i]->DeleteAll(test::MakeStorageAreaSource(), mojo::NullRemote(),
                            barrier.AddClosure());
      }
      if (i % 2 == 0 && forks + 1 < kTotalAreas) {
        CacheMode mode = i % 3 == 0 ? CacheMode::KEYS_AND_VALUES
                                    : CacheMode::KEYS_ONLY_WHEN_POSSIBLE;
        forks++;
        states[forks] = state;
        areas[forks] = areas[i]->ForkToNewMap(GenerateMapLocator(next_map_id),
                                              &delegates[forks],
                                              GetDefaultTestingOptions(mode));
        ++next_map_id;
      }
      if (i % 3 == 0) {
        FuzzState old_state = state;
        state.val1 = std::make_optional<std::vector<uint8_t>>(
            {static_cast<uint8_t>(i + 9)});
        successes.push_back(false);
        areas[i]->Put(
            kKey1Vec, state.val1.value(), old_state.val1,
            test::MakeStorageAreaSource(),
            MakeSuccessCallback(barrier.AddClosure(), &successes.back()));
      }
    }
  }
  loop.Run();

  // This section checks that we get the correct values when we query the
  // areas (which may or may not be maintaining their own cache).
  for (size_t i = 0; i < kTotalAreas; i++) {
    FuzzState& state = states[i];
    std::vector<uint8_t> result;

    // Note: this will cause all keys-only areas to commit.
    std::string result1 = GetSyncStrUsingGetAll(areas[i].get(), kKey1);
    std::string result2 = GetSyncStrUsingGetAll(areas[i].get(), kKey2);
    EXPECT_EQ(!!state.val1, !result1.empty()) << i;
    if (state.val1)
      EXPECT_EQ(state.val1.value(), ToBytes(result1));
    EXPECT_EQ(!!state.val2, !result2.empty()) << i;
    if (state.val2)
      EXPECT_EQ(state.val2.value(), ToBytes(result2)) << i;
  }

  // This section verifies that all areas have committed their changes to
  // the database.
  ASSERT_EQ(areas.size(), delegates.size());

  // Committing just one should commit all.
  for (size_t i = 0; i < areas.size(); i++) {
    if (BlockingCommit(&delegates[i], areas[i].get())) {
      break;
    }
  }
  for (const auto& area : areas) {
    EXPECT_FALSE(HasChangesToCommit(area.get()));
  }

  // This section checks the data in the database itself to verify all areas
  // committed changes correctly.
  for (size_t i = 0; i < kTotalAreas; ++i) {
    FuzzState& state = states[i];

    const DomStorageDatabase::MapLocator& map_locator = *areas[i]->map_locator_;
    EXPECT_EQ(!!state.val1, HasDatabaseEntry(map_locator, kKey1));
    if (state.val1)
      EXPECT_EQ(ToString(state.val1.value()),
                GetDatabaseEntry(map_locator, kKey1));
    EXPECT_EQ(!!state.val2, HasDatabaseEntry(map_locator, kKey2));
    if (state.val2)
      EXPECT_EQ(ToString(state.val2.value()),
                GetDatabaseEntry(map_locator, kKey2));

    EXPECT_FALSE(areas[i]->has_pending_load_tasks()) << i;
  }
}

TEST_P(StorageAreaImplCacheModeTest, EmptyMapIgnoresDisk) {
  const std::string kValue = "foo";
  const std::vector<uint8_t> kValueVec = ToBytes(kValue);

  // Set fake data to ensure that our shortcut doesn't read it.
  SetDatabaseEntry(*cloned_map_locator1_, test_key1_, kValue);

  // Create an empty map that will have no data in it.
  StorageAreaImpl::Options options =
      GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  auto empty_storage_area = std::make_unique<StorageAreaImpl>(
      database(), cloned_map_locator1_, delegate(), options);
  empty_storage_area->InitializeAsEmpty();

  // Check the forked state, which should be empty.
  EXPECT_EQ("", GetSyncStrUsingGetAll(empty_storage_area.get(), test_key1_));
}

TEST_P(StorageAreaImplCacheModeTest, ForkFromEmptyMap) {
  const std::string kValue = "foo";
  const std::vector<uint8_t> kValueVec = ToBytes(kValue);

  // Set fake data to ensure that our shortcut doesn't read it.
  SetDatabaseEntry(*cloned_map_locator1_, test_key1_, kValue);

  // Create an empty map that will have no data in it.
  StorageAreaImpl::Options options =
      GetDefaultTestingOptions(CacheMode::KEYS_ONLY_WHEN_POSSIBLE);
  auto empty_storage_area = std::make_unique<StorageAreaImpl>(
      database(), cloned_map_locator1_, delegate(), options);
  empty_storage_area->InitializeAsEmpty();

  // Execute the fork, which should shortcut disk and just be empty.
  MockDelegate fork1_delegate;
  std::unique_ptr<StorageAreaImpl> fork = empty_storage_area->ForkToNewMap(
      cloned_map_locator1_, &fork1_delegate,
      GetDefaultTestingOptions(GetCacheMode()));

  // Check the forked state, which should be empty.
  EXPECT_EQ("", GetSyncStrUsingGetAll(fork.get(), test_key1_));
}

}  // namespace storage
