// Copyright 2014 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/dom_distiller/core/distilled_content_store.h"

#include <utility>

#include "base/bind.h"
#include "base/run_loop.h"
#include "base/test/scoped_task_environment.h"
#include "components/dom_distiller/core/article_entry.h"
#include "components/dom_distiller/core/proto/distilled_article.pb.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace dom_distiller {

namespace {

ArticleEntry CreateEntry(std::string entry_id,
                         std::string page_url1,
                         std::string page_url2,
                         std::string page_url3) {
  ArticleEntry entry;
  entry.set_entry_id(entry_id);
  if (!page_url1.empty()) {
    ArticleEntryPage* page = entry.add_pages();
    page->set_url(page_url1);
  }
  if (!page_url2.empty()) {
    ArticleEntryPage* page = entry.add_pages();
    page->set_url(page_url2);
  }
  if (!page_url3.empty()) {
    ArticleEntryPage* page = entry.add_pages();
    page->set_url(page_url3);
  }
  return entry;
}

DistilledArticleProto CreateDistilledArticleForEntry(
    const ArticleEntry& entry) {
  DistilledArticleProto article;
  for (int i = 0; i < entry.pages_size(); ++i) {
    DistilledPageProto* page = article.add_pages();
    page->set_url(entry.pages(i).url());
    page->set_html("<div>" + entry.pages(i).url() + "</div>");
  }
  return article;
}

}  // namespace

class InMemoryContentStoreTest : public testing::Test {
 public:
  void OnLoadCallback(bool success,
                      std::unique_ptr<DistilledArticleProto> proto) {
    load_success_ = success;
    loaded_proto_ = std::move(proto);
  }

  void OnSaveCallback(bool success) { save_success_ = success; }

 protected:
  // testing::Test implementation:
  void SetUp() override {
    store_.reset(new InMemoryContentStore(kDefaultMaxNumCachedEntries));
    save_success_ = false;
    load_success_ = false;
    loaded_proto_.reset();
  }

  std::unique_ptr<InMemoryContentStore> store_;
  bool save_success_;
  bool load_success_;
  std::unique_ptr<DistilledArticleProto> loaded_proto_;
};

// Tests whether saving and then loading a single article works as expected.
TEST_F(InMemoryContentStoreTest, SaveAndLoadSingleArticle) {
  base::test::ScopedTaskEnvironment task_environment;
  const ArticleEntry entry = CreateEntry("test-id", "url1", "url2", "url3");
  const DistilledArticleProto stored_proto =
      CreateDistilledArticleForEntry(entry);
  store_->SaveContent(entry,
                      stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  store_->LoadContent(entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  EXPECT_EQ(stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());
}

// Tests that loading articles which have never been stored, yields a callback
// where success is false.
TEST_F(InMemoryContentStoreTest, LoadNonExistentArticle) {
  base::test::ScopedTaskEnvironment task_environment;
  const ArticleEntry entry = CreateEntry("bogus-id", "url1", "url2", "url3");
  store_->LoadContent(entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(load_success_);
}

// Verifies that content store can store multiple articles, and that ordering
// of save and store does not matter when the total number of articles does not
// exceed |kDefaultMaxNumCachedEntries|.
TEST_F(InMemoryContentStoreTest, SaveAndLoadMultipleArticles) {
  base::test::ScopedTaskEnvironment task_environment;
  // Store first article.
  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
  const DistilledArticleProto first_stored_proto =
      CreateDistilledArticleForEntry(first_entry);
  store_->SaveContent(first_entry,
                      first_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Store second article.
  const ArticleEntry second_entry =
      CreateEntry("second", "url4", "url5", "url6");
  const DistilledArticleProto second_stored_proto =
      CreateDistilledArticleForEntry(second_entry);
  store_->SaveContent(second_entry,
                      second_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Load second article.
  store_->LoadContent(second_entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  load_success_ = false;
  EXPECT_EQ(second_stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());
  loaded_proto_.reset();

  // Load first article.
  store_->LoadContent(first_entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  EXPECT_EQ(first_stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());
}

// Verifies that the content store does not store unlimited number of articles,
// but expires the oldest ones when the limit for number of articles is reached.
TEST_F(InMemoryContentStoreTest, SaveAndLoadMoreThanMaxArticles) {
  base::test::ScopedTaskEnvironment task_environment;

  // Create a new store with only |kMaxNumArticles| articles as the limit.
  const int kMaxNumArticles = 3;
  store_.reset(new InMemoryContentStore(kMaxNumArticles));

  // Store first article.
  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
  const DistilledArticleProto first_stored_proto =
      CreateDistilledArticleForEntry(first_entry);
  store_->SaveContent(first_entry,
                      first_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Store second article.
  const ArticleEntry second_entry =
      CreateEntry("second", "url4", "url5", "url6");
  const DistilledArticleProto second_stored_proto =
      CreateDistilledArticleForEntry(second_entry);
  store_->SaveContent(second_entry,
                      second_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Store third article.
  const ArticleEntry third_entry = CreateEntry("third", "url7", "url8", "url9");
  const DistilledArticleProto third_stored_proto =
      CreateDistilledArticleForEntry(third_entry);
  store_->SaveContent(third_entry,
                      third_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Load first article. This will make the first article the most recent
  // accessed article.
  store_->LoadContent(first_entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  load_success_ = false;
  EXPECT_EQ(first_stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());
  loaded_proto_.reset();

  // Store fourth article.
  const ArticleEntry fourth_entry =
      CreateEntry("fourth", "url10", "url11", "url12");
  const DistilledArticleProto fourth_stored_proto =
      CreateDistilledArticleForEntry(fourth_entry);
  store_->SaveContent(fourth_entry,
                      fourth_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Load second article, which by now is the oldest accessed article, since
  // the first article has been loaded once.
  store_->LoadContent(second_entry,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  // Since the store can only contain |kMaxNumArticles| entries, this load
  // should fail.
  EXPECT_FALSE(load_success_);
}

// Tests whether saving and then loading a single article works as expected.
TEST_F(InMemoryContentStoreTest, LookupArticleByURL) {
  base::test::ScopedTaskEnvironment task_environment;
  const ArticleEntry entry = CreateEntry("test-id", "url1", "url2", "url3");
  const DistilledArticleProto stored_proto =
      CreateDistilledArticleForEntry(entry);
  store_->SaveContent(entry,
                      stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Create an entry where the entry ID does not match, but the first URL does.
  const ArticleEntry lookup_entry1 = CreateEntry("lookup-id", "url1", "", "");
  store_->LoadContent(lookup_entry1,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  EXPECT_EQ(stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());

  // Create an entry where the entry ID does not match, but the second URL does.
  const ArticleEntry lookup_entry2 =
      CreateEntry("lookup-id", "bogus", "url2", "");
  store_->LoadContent(lookup_entry2,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  EXPECT_EQ(stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());
}

// Verifies that the content store does not store unlimited number of articles,
// but expires the oldest ones when the limit for number of articles is reached.
TEST_F(InMemoryContentStoreTest, LoadArticleByURLAfterExpungedFromCache) {
  base::test::ScopedTaskEnvironment task_environment;

  // Create a new store with only |kMaxNumArticles| articles as the limit.
  const int kMaxNumArticles = 1;
  store_.reset(new InMemoryContentStore(kMaxNumArticles));

  // Store an article.
  const ArticleEntry first_entry = CreateEntry("first", "url1", "url2", "url3");
  const DistilledArticleProto first_stored_proto =
      CreateDistilledArticleForEntry(first_entry);
  store_->SaveContent(first_entry,
                      first_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Looking up the first entry by URL should succeed when it is still in the
  // cache.
  const ArticleEntry first_entry_lookup =
      CreateEntry("lookup-id", "url1", "", "");
  store_->LoadContent(first_entry_lookup,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(load_success_);
  EXPECT_EQ(first_stored_proto.SerializeAsString(),
            loaded_proto_->SerializeAsString());

  // Store second article. This will remove the first article from the cache.
  const ArticleEntry second_entry =
      CreateEntry("second", "url4", "url5", "url6");
  const DistilledArticleProto second_stored_proto =
      CreateDistilledArticleForEntry(second_entry);
  store_->SaveContent(second_entry,
                      second_stored_proto,
                      base::Bind(&InMemoryContentStoreTest::OnSaveCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_TRUE(save_success_);
  save_success_ = false;

  // Looking up the first entry by URL should fail when it is not in the cache.
  store_->LoadContent(first_entry_lookup,
                      base::Bind(&InMemoryContentStoreTest::OnLoadCallback,
                                 base::Unretained(this)));
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(load_success_);
}

}  // namespace dom_distiller
