// Copyright 2017 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/feature_engagement/internal/persistent_availability_store.h"

#include <stdint.h>

#include <map>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/feature_engagement/internal/proto/availability.pb.h"
#include "components/feature_engagement/public/feature_list.h"
#include "components/leveldb_proto/public/proto_database.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace feature_engagement {

namespace {
BASE_FEATURE(kPersistentTestFeatureFoo,
             "test_foo",
             base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kPersistentTestFeatureBar,
             "test_bar",
             base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kPersistentTestFeatureQux,
             "test_qux",
             base::FEATURE_DISABLED_BY_DEFAULT);
BASE_FEATURE(kPersistentTestFeatureNop,
             "test_nop",
             base::FEATURE_DISABLED_BY_DEFAULT);

Availability CreateAvailability(const base::Feature& feature, uint32_t day) {
  Availability availability;
  availability.set_feature_name(feature.name);
  availability.set_day(day);
  return availability;
}

class PersistentAvailabilityStoreTest : public testing::Test {
 public:
  PersistentAvailabilityStoreTest()
      : db_(nullptr),
        storage_dir_(FILE_PATH_LITERAL("/persistent/store/lalala")) {
    load_callback_ = base::BindOnce(
        &PersistentAvailabilityStoreTest::LoadCallback, base::Unretained(this));
  }

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

  ~PersistentAvailabilityStoreTest() override = default;

  // Creates a DB and stores off a pointer to it as a member.
  std::unique_ptr<leveldb_proto::test::FakeDB<Availability>> CreateDB() {
    auto db = std::make_unique<leveldb_proto::test::FakeDB<Availability>>(
        &db_availabilities_);
    db_ = db.get();
    return db;
  }

  void LoadCallback(
      bool success,
      std::unique_ptr<std::map<std::string, uint32_t>> availabilities) {
    load_successful_ = success;
    load_results_ = std::move(availabilities);
    // This callback only gets called once and the DB, created in CreateDB(),
    // will be freed before the callstack unwinds. Clear the cached pointer to
    // avoid a dangling pointer error later.
    db_ = nullptr;
  }

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;

  // The end result of the store pipeline.
  PersistentAvailabilityStore::OnLoadedCallback load_callback_;

  // Callback results.
  std::optional<bool> load_successful_;
  std::unique_ptr<std::map<std::string, uint32_t>> load_results_;

  // |db_availabilities_| is used during creation of the FakeDB in CreateDB(),
  // to simplify what the DB has stored.
  std::map<std::string, Availability> db_availabilities_;

  // The database that is in use.
  raw_ptr<leveldb_proto::test::FakeDB<Availability>> db_;

  // Constant test data.
  base::FilePath storage_dir_;
};

}  // namespace

TEST_F(PersistentAvailabilityStoreTest, InitFail) {
  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_),
      14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kError);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_FALSE(load_successful_.value());
  EXPECT_EQ(0u, load_results_->size());
  EXPECT_EQ(0u, db_availabilities_.size());
}

TEST_F(PersistentAvailabilityStoreTest, LoadFail) {
  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_),
      14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(false);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_FALSE(load_successful_.value());
  EXPECT_EQ(0u, load_results_->size());
  EXPECT_EQ(0u, db_availabilities_.size());
}

TEST_F(PersistentAvailabilityStoreTest, EmptyDBEmptyFeatureFilterUpdateFailed) {
  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_),
      14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(true);
  EXPECT_FALSE(load_successful_.has_value());

  db_->UpdateCallback(false);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_FALSE(load_successful_.value());
  EXPECT_EQ(0u, load_results_->size());
  EXPECT_EQ(0u, db_availabilities_.size());
}

TEST_F(PersistentAvailabilityStoreTest, EmptyDBEmptyFeatureFilterUpdateOK) {
  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_),
      14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(true);
  EXPECT_FALSE(load_successful_.has_value());

  db_->UpdateCallback(true);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_TRUE(load_successful_.value());
  EXPECT_EQ(0u, load_results_->size());
  EXPECT_EQ(0u, db_availabilities_.size());
}

TEST_F(PersistentAvailabilityStoreTest, AllNewFeatures) {
  scoped_feature_list_.InitWithFeatures(
      {kPersistentTestFeatureFoo, kPersistentTestFeatureBar},
      {kPersistentTestFeatureQux});

  FeatureVector feature_filter;
  feature_filter.push_back(&kPersistentTestFeatureFoo);  // Enabled. Not in DB.
  feature_filter.push_back(&kPersistentTestFeatureBar);  // Enabled. Not in DB.
  feature_filter.push_back(&kPersistentTestFeatureQux);  // Disabled. Not in DB.

  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), feature_filter, std::move(load_callback_), 14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(true);
  EXPECT_FALSE(load_successful_.has_value());

  db_->UpdateCallback(true);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_TRUE(load_successful_.value());
  ASSERT_EQ(2u, load_results_->size());
  ASSERT_EQ(2u, db_availabilities_.size());

  ASSERT_TRUE(load_results_->find(kPersistentTestFeatureFoo.name) !=
              load_results_->end());
  EXPECT_EQ(14u, (*load_results_)[kPersistentTestFeatureFoo.name]);
  ASSERT_TRUE(db_availabilities_.find(kPersistentTestFeatureFoo.name) !=
              db_availabilities_.end());
  EXPECT_EQ(14u, db_availabilities_[kPersistentTestFeatureFoo.name].day());

  ASSERT_TRUE(load_results_->find(kPersistentTestFeatureBar.name) !=
              load_results_->end());
  EXPECT_EQ(14u, (*load_results_)[kPersistentTestFeatureBar.name]);
  ASSERT_TRUE(db_availabilities_.find(kPersistentTestFeatureBar.name) !=
              db_availabilities_.end());
  EXPECT_EQ(14u, db_availabilities_[kPersistentTestFeatureBar.name].day());
}

TEST_F(PersistentAvailabilityStoreTest, TestAllFilterCombinations) {
  scoped_feature_list_.InitWithFeatures(
      {kPersistentTestFeatureFoo, kPersistentTestFeatureBar},
      {kPersistentTestFeatureQux, kPersistentTestFeatureNop});

  FeatureVector feature_filter;
  feature_filter.push_back(&kPersistentTestFeatureFoo);  // Enabled. Not in DB.
  feature_filter.push_back(&kPersistentTestFeatureBar);  // Enabled. In DB.
  feature_filter.push_back(&kPersistentTestFeatureQux);  // Disabled. Not in DB.
  feature_filter.push_back(&kPersistentTestFeatureNop);  // Disabled. In DB.

  db_availabilities_[kPersistentTestFeatureBar.name] =
      CreateAvailability(kPersistentTestFeatureBar, 10u);
  db_availabilities_[kPersistentTestFeatureNop.name] =
      CreateAvailability(kPersistentTestFeatureNop, 8u);

  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), feature_filter, std::move(load_callback_), 14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(true);
  EXPECT_FALSE(load_successful_.has_value());

  db_->UpdateCallback(true);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_TRUE(load_successful_.value());
  ASSERT_EQ(2u, load_results_->size());
  ASSERT_EQ(2u, db_availabilities_.size());

  ASSERT_TRUE(load_results_->find(kPersistentTestFeatureFoo.name) !=
              load_results_->end());
  EXPECT_EQ(14u, (*load_results_)[kPersistentTestFeatureFoo.name]);
  ASSERT_TRUE(db_availabilities_.find(kPersistentTestFeatureFoo.name) !=
              db_availabilities_.end());
  EXPECT_EQ(14u, db_availabilities_[kPersistentTestFeatureFoo.name].day());

  ASSERT_TRUE(load_results_->find(kPersistentTestFeatureBar.name) !=
              load_results_->end());
  EXPECT_EQ(10u, (*load_results_)[kPersistentTestFeatureBar.name]);
  ASSERT_TRUE(db_availabilities_.find(kPersistentTestFeatureBar.name) !=
              db_availabilities_.end());
  EXPECT_EQ(10u, db_availabilities_[kPersistentTestFeatureBar.name].day());
}

TEST_F(PersistentAvailabilityStoreTest, TestAllCombinationsEmptyFilter) {
  scoped_feature_list_.InitWithFeatures(
      {kPersistentTestFeatureFoo, kPersistentTestFeatureBar},
      {kPersistentTestFeatureQux, kPersistentTestFeatureNop});

  // Empty filter, but the following setup:
  // kPersistentTestFeatureFoo: Enabled. Not in DB.
  // kPersistentTestFeatureBar: Enabled. In DB.
  // kPersistentTestFeatureQux: Disabled. Not in DB.
  // kPersistentTestFeatureNop: Disabled. In DB.

  db_availabilities_[kPersistentTestFeatureBar.name] =
      CreateAvailability(kPersistentTestFeatureBar, 10u);
  db_availabilities_[kPersistentTestFeatureNop.name] =
      CreateAvailability(kPersistentTestFeatureNop, 8u);

  PersistentAvailabilityStore::LoadAndUpdateStore(
      storage_dir_, CreateDB(), FeatureVector(), std::move(load_callback_),
      14u);

  db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
  EXPECT_FALSE(load_successful_.has_value());

  db_->LoadCallback(true);
  EXPECT_FALSE(load_successful_.has_value());

  db_->UpdateCallback(true);

  EXPECT_TRUE(load_successful_.has_value());
  EXPECT_TRUE(load_successful_.value());
  EXPECT_EQ(0u, load_results_->size());
  EXPECT_EQ(0u, db_availabilities_.size());
}

}  // namespace feature_engagement
