// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/offline_pages/offline_page_storage_manager.h"

#include <stdint.h>
#include <map>
#include <vector>

#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/test/simple_test_clock.h"
#include "base/time/time.h"
#include "components/offline_pages/archive_manager.h"
#include "components/offline_pages/client_namespace_constants.h"
#include "components/offline_pages/client_policy_controller.h"
#include "components/offline_pages/offline_page_item.h"
#include "components/offline_pages/offline_page_model_impl.h"
#include "components/offline_pages/offline_page_types.h"
#include "testing/gtest/include/gtest/gtest.h"

using LifetimePolicy = offline_pages::LifetimePolicy;
using ClearStorageResult =
    offline_pages::OfflinePageStorageManager::ClearStorageResult;
using StorageStats = offline_pages::ArchiveManager::StorageStats;

namespace offline_pages {

namespace {
const GURL kTestUrl("http://example.com");
const base::FilePath::CharType kFilePath[] = FILE_PATH_LITERAL("/data");
const int64_t kTestFileSize = 1 << 19;  // Make a page 512KB.
const int64_t kFreeSpaceNormal = 100 * (1 << 20);

enum TestOptions {
  DEFAULT = 1 << 0,
  EXPIRE_FAILURE = 1 << 1,
  DELETE_FAILURE = 1 << 2,
  EXPIRE_AND_DELETE_FAILURES = EXPIRE_FAILURE | DELETE_FAILURE,
};

struct PageSettings {
  std::string name_space;
  int fresh_pages_count;
  int expired_pages_count;
};
}  // namespace

class OfflinePageTestModel : public OfflinePageModelImpl {
 public:
  OfflinePageTestModel(std::vector<PageSettings> page_settings,
                       base::SimpleTestClock* clock,
                       TestOptions options = TestOptions::DEFAULT)
      : policy_controller_(new ClientPolicyController()),
        clock_(clock),
        options_(options),
        next_offline_id_(0) {
    for (const auto& setting : page_settings)
      AddPages(setting);
  }

  ~OfflinePageTestModel() override;

  void GetAllPages(const MultipleOfflinePageItemCallback& callback) override {
    MultipleOfflinePageItemResult pages;
    for (const auto& id_page_pair : pages_)
      pages.push_back(id_page_pair.second);
    callback.Run(pages);
  }

  void DeletePagesByOfflineId(const std::vector<int64_t>& offline_ids,
                              const DeletePageCallback& callback) override;

  void ExpirePages(const std::vector<int64_t>& offline_ids,
                   const base::Time& expiration_time,
                   const base::Callback<void(bool)>& callback) override;

  void AddPages(const PageSettings& setting);

  const std::vector<OfflinePageItem>& GetRemovedPages() {
    return removed_pages_;
  }

  int64_t GetTotalSize() const;

  base::SimpleTestClock* clock() { return clock_; }

 private:
  std::map<int64_t, OfflinePageItem> pages_;

  std::vector<OfflinePageItem> removed_pages_;

  std::unique_ptr<ClientPolicyController> policy_controller_;

  base::SimpleTestClock* clock_;

  TestOptions options_;

  int64_t next_offline_id_;
};

void OfflinePageTestModel::ExpirePages(
    const std::vector<int64_t>& offline_ids,
    const base::Time& expiration_time,
    const base::Callback<void(bool)>& callback) {
  for (const auto id : offline_ids)
    pages_.at(id).expiration_time = expiration_time;
  if (options_ & TestOptions::EXPIRE_FAILURE) {
    callback.Run(false);
    return;
  }
  callback.Run(true);
}

void OfflinePageTestModel::DeletePagesByOfflineId(
    const std::vector<int64_t>& offline_ids,
    const DeletePageCallback& callback) {
  if (options_ & TestOptions::DELETE_FAILURE) {
    callback.Run(DeletePageResult::STORE_FAILURE);
    return;
  }
  for (const auto id : offline_ids) {
    removed_pages_.push_back(pages_.at(id));
    pages_.erase(id);
  }
  callback.Run(DeletePageResult::SUCCESS);
}

int64_t OfflinePageTestModel::GetTotalSize() const {
  int64_t res = 0;
  for (const auto& id_page_pair : pages_) {
    if (!id_page_pair.second.IsExpired())
      res += id_page_pair.second.file_size;
  }
  return res;
}

OfflinePageTestModel::~OfflinePageTestModel() {}

void OfflinePageTestModel::AddPages(const PageSettings& setting) {
  std::string name_space = setting.name_space;
  int fresh_pages_count = setting.fresh_pages_count;
  int expired_pages_count = setting.expired_pages_count;
  base::Time now = clock()->Now();
  // Fresh pages.
  for (int i = 0; i < fresh_pages_count; i++) {
    OfflinePageItem page =
        OfflinePageItem(kTestUrl, next_offline_id_,
                        ClientId(name_space, std::to_string(next_offline_id_)),
                        base::FilePath(kFilePath), kTestFileSize);
    page.last_access_time = now;
    pages_[next_offline_id_] = page;
    next_offline_id_++;
  }
  // Expired pages.
  for (int i = 0; i < expired_pages_count; i++) {
    OfflinePageItem page =
        OfflinePageItem(kTestUrl, next_offline_id_,
                        ClientId(name_space, std::to_string(next_offline_id_)),
                        base::FilePath(kFilePath), kTestFileSize);
    page.last_access_time = now -
                            policy_controller_->GetPolicy(name_space)
                                .lifetime_policy.expiration_period;
    pages_[next_offline_id_] = page;
    next_offline_id_++;
  }
}

class TestArchiveManager : public ArchiveManager {
 public:
  explicit TestArchiveManager(StorageStats stats) : stats_(stats) {}

  void GetStorageStats(const base::Callback<
                       void(const ArchiveManager::StorageStats& storage_stats)>&
                           callback) const override {
    callback.Run(stats_);
  }

  void SetValues(ArchiveManager::StorageStats stats) { stats_ = stats; }

 private:
  StorageStats stats_;
};

class OfflinePageStorageManagerTest : public testing::Test {
 public:
  OfflinePageStorageManagerTest();
  OfflinePageStorageManager* manager() { return manager_.get(); }
  OfflinePageTestModel* model() { return model_.get(); }
  ClientPolicyController* policy_controller() {
    return policy_controller_.get();
  }
  TestArchiveManager* test_archive_manager() { return archive_manager_.get(); }
  void OnPagesCleared(size_t pages_cleared_count, ClearStorageResult result);
  void Initialize(const std::vector<PageSettings>& settings,
                  StorageStats stats = {kFreeSpaceNormal, 0},
                  TestOptions options = TestOptions::DEFAULT);
  void TryClearPages();

  // testing::Test
  void TearDown() override;

  base::SimpleTestClock* clock() { return clock_; }
  int last_cleared_page_count() const {
    return static_cast<int>(last_cleared_page_count_);
  }
  int total_cleared_times() const { return total_cleared_times_; }
  ClearStorageResult last_clear_storage_result() const {
    return last_clear_storage_result_;
  }

 private:
  std::unique_ptr<OfflinePageStorageManager> manager_;
  std::unique_ptr<OfflinePageTestModel> model_;
  std::unique_ptr<ClientPolicyController> policy_controller_;
  std::unique_ptr<TestArchiveManager> archive_manager_;

  base::SimpleTestClock* clock_;

  size_t last_cleared_page_count_;
  int total_cleared_times_;
  ClearStorageResult last_clear_storage_result_;
};

OfflinePageStorageManagerTest::OfflinePageStorageManagerTest()
    : policy_controller_(new ClientPolicyController()),
      last_cleared_page_count_(0),
      total_cleared_times_(0),
      last_clear_storage_result_(ClearStorageResult::SUCCESS) {}

void OfflinePageStorageManagerTest::Initialize(
    const std::vector<PageSettings>& page_settings,
    StorageStats stats,
    TestOptions options) {
  std::unique_ptr<base::SimpleTestClock> clock(new base::SimpleTestClock());
  clock_ = clock.get();
  clock_->SetNow(base::Time::Now());
  model_.reset(new OfflinePageTestModel(page_settings, clock_, options));

  if (stats.free_disk_space == 0)
    stats.free_disk_space = kFreeSpaceNormal;
  if (stats.total_archives_size == 0) {
    stats.total_archives_size = model_->GetTotalSize();
  }
  archive_manager_.reset(new TestArchiveManager(stats));
  manager_.reset(new OfflinePageStorageManager(
      model_.get(), policy_controller(), archive_manager_.get()));
  manager_->SetClockForTesting(std::move(clock));
}

void OfflinePageStorageManagerTest::TryClearPages() {
  manager()->ClearPagesIfNeeded(base::Bind(
      &OfflinePageStorageManagerTest::OnPagesCleared, base::Unretained(this)));
}

void OfflinePageStorageManagerTest::TearDown() {
  manager_.reset();
  model_.reset();
}

void OfflinePageStorageManagerTest::OnPagesCleared(size_t pages_cleared_count,
                                                   ClearStorageResult result) {
  last_cleared_page_count_ = pages_cleared_count;
  total_cleared_times_++;
  last_clear_storage_result_ = result;
}

TEST_F(OfflinePageStorageManagerTest, TestClearPagesLessThanLimit) {
  Initialize(std::vector<PageSettings>(
      {{kBookmarkNamespace, 1, 1}, {kLastNNamespace, 1, 1}}));
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(2, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestClearPagesMoreThanLimit) {
  Initialize(std::vector<PageSettings>(
      {{kBookmarkNamespace, 10, 15}, {kLastNNamespace, 5, 30}}));
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(45, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestClearPagesMoreFreshPages) {
  Initialize(std::vector<PageSettings>(
                 {{kBookmarkNamespace, 30, 0}, {kLastNNamespace, 100, 1}}),
             {1000 * (1 << 20), 0});
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(1, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestDeleteAsyncPages) {
  Initialize(std::vector<PageSettings>({{kAsyncNamespace, 20, 0}}));
  clock()->Advance(base::TimeDelta::FromDays(367));
  TryClearPages();
  EXPECT_EQ(0, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestDeletionFailed) {
  Initialize(std::vector<PageSettings>(
                 {{kBookmarkNamespace, 10, 10}, {kLastNNamespace, 10, 10}}),
             {kFreeSpaceNormal, 0}, TestOptions::DELETE_FAILURE);
  TryClearPages();
  EXPECT_EQ(20, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::DELETE_FAILURE, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestRemoveFromStoreFailure) {
  Initialize(std::vector<PageSettings>({{kBookmarkNamespace, 10, 10}}), {0, 0},
             TestOptions::EXPIRE_FAILURE);
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(10, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::EXPIRE_FAILURE, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestBothFailure) {
  Initialize(std::vector<PageSettings>({{kBookmarkNamespace, 10, 10}}), {0, 0},
             TestOptions::EXPIRE_AND_DELETE_FAILURES);
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(ClearStorageResult::EXPIRE_AND_DELETE_FAILURES,
            last_clear_storage_result());
}

TEST_F(OfflinePageStorageManagerTest, TestStorageTimeInterval) {
  Initialize(std::vector<PageSettings>(
      {{kBookmarkNamespace, 10, 10}, {kLastNNamespace, 10, 10}}));
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(20, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  // Advance clock so we go over the gap, but no expired pages.
  clock()->Advance(constants::kClearStorageInterval +
                   base::TimeDelta::FromMinutes(1));
  TryClearPages();
  EXPECT_EQ(0, last_cleared_page_count());
  EXPECT_EQ(2, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  // Advance clock so we are still in the gap, should be unnecessary.
  clock()->Advance(constants::kClearStorageInterval -
                   base::TimeDelta::FromMinutes(1));
  TryClearPages();
  EXPECT_EQ(0, last_cleared_page_count());
  EXPECT_EQ(3, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::UNNECESSARY, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestTwoStepExpiration) {
  Initialize(std::vector<PageSettings>({{kBookmarkNamespace, 10, 10}}));
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  TryClearPages();
  EXPECT_EQ(10, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  clock()->Advance(constants::kRemovePageItemInterval +
                   base::TimeDelta::FromDays(1));
  TryClearPages();
  EXPECT_EQ(10, last_cleared_page_count());
  EXPECT_EQ(2, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(10, static_cast<int>(model()->GetRemovedPages().size()));
}

TEST_F(OfflinePageStorageManagerTest, TestClearMultipleTimes) {
  Initialize(std::vector<PageSettings>({{kBookmarkNamespace, 30, 0},
                                        {kLastNNamespace, 100, 1},
                                        {kAsyncNamespace, 40, 0}}),
             {1000 * (1 << 20), 0});
  clock()->Advance(base::TimeDelta::FromMinutes(30));
  LifetimePolicy policy =
      policy_controller()->GetPolicy(kLastNNamespace).lifetime_policy;

  TryClearPages();
  EXPECT_EQ(1, last_cleared_page_count());
  EXPECT_EQ(1, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  // Advance the clock by expiration period of last_n namespace, should be
  // expiring all pages left in the namespace.
  clock()->Advance(policy.expiration_period);
  TryClearPages();
  EXPECT_EQ(100, last_cleared_page_count());
  EXPECT_EQ(2, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  // Only 1 ms passes and no changes in pages, so no need to clear page.
  clock()->Advance(base::TimeDelta::FromMilliseconds(1));
  TryClearPages();
  EXPECT_EQ(0, last_cleared_page_count());
  EXPECT_EQ(3, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::UNNECESSARY, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));

  // Adding more fresh pages to make it go over limit.
  clock()->Advance(base::TimeDelta::FromMinutes(5));
  model()->AddPages({kBookmarkNamespace, 400, 0});
  int64_t total_size_before = model()->GetTotalSize();
  int64_t available_space = 300 * (1 << 20);  // 300 MB
  test_archive_manager()->SetValues({available_space, total_size_before});
  EXPECT_GE(total_size_before, constants::kOfflinePageStorageLimit *
                                   (available_space + total_size_before));
  TryClearPages();
  EXPECT_LE(total_size_before * constants::kOfflinePageStorageClearThreshold,
            model()->GetTotalSize());
  EXPECT_EQ(4, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  EXPECT_EQ(0, static_cast<int>(model()->GetRemovedPages().size()));
  int expired_page_count = last_cleared_page_count();

  // After more days, all pages should be expired and .
  clock()->Advance(constants::kRemovePageItemInterval +
                   base::TimeDelta::FromDays(1));
  TryClearPages();
  EXPECT_EQ(0, model()->GetTotalSize());
  EXPECT_EQ(5, total_cleared_times());
  EXPECT_EQ(ClearStorageResult::SUCCESS, last_clear_storage_result());
  // Number of removed pages should be the ones expired above and all the pages
  // initially created for last_n namespace.
  EXPECT_EQ(expired_page_count + 101,
            static_cast<int>(model()->GetRemovedPages().size()));
}

}  // namespace offline_pages
