// 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/reading_list/core/reading_list_model.h"

#include "base/bind.h"
#include "base/test/simple_test_clock.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/reading_list/core/reading_list_model_storage.h"
#include "components/reading_list/core/reading_list_store_delegate.h"
#include "components/sync/model/metadata_change_list.h"
#include "components/sync/model/model_error.h"
#include "components/sync/model_impl/client_tag_based_model_type_processor.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

const GURL callback_url("http://example.com");
const std::string callback_title("test title");

base::Time AdvanceAndGetTime(base::SimpleTestClock* clock) {
  clock->Advance(base::TimeDelta::FromMilliseconds(10));
  return clock->Now();
}

class TestReadingListStorageObserver {
 public:
  virtual void ReadingListDidSaveEntry() = 0;
  virtual void ReadingListDidRemoveEntry() = 0;
};

class TestReadingListStorage : public ReadingListModelStorage {
 public:
  TestReadingListStorage(TestReadingListStorageObserver* observer,
                         base::SimpleTestClock* clock)
      : ReadingListModelStorage(
            // TODO(crbug.com/823380): Consider using mock/fake here.
            std::make_unique<syncer::ClientTagBasedModelTypeProcessor>(
                syncer::READING_LIST,
                /*dump_stack=*/base::RepeatingClosure())),
        entries_(new ReadingListStoreDelegate::ReadingListEntries()),
        observer_(observer),
        clock_(clock) {}

  void AddSampleEntries() {
    // Adds timer and interlace read/unread entry creation to avoid having two
    // entries with the same creation timestamp.
    ReadingListEntry unread_a(GURL("http://unread_a.com"), "unread_a",
                              AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://unread_a.com"), std::move(unread_a)));

    ReadingListEntry read_a(GURL("http://read_a.com"), "read_a",
                            AdvanceAndGetTime(clock_));
    read_a.SetRead(true, AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://read_a.com"), std::move(read_a)));

    ReadingListEntry unread_b(GURL("http://unread_b.com"), "unread_b",
                              AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://unread_b.com"), std::move(unread_b)));

    ReadingListEntry read_b(GURL("http://read_b.com"), "read_b",
                            AdvanceAndGetTime(clock_));
    read_b.SetRead(true, AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://read_b.com"), std::move(read_b)));

    ReadingListEntry unread_c(GURL("http://unread_c.com"), "unread_c",
                              AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://unread_c.com"), std::move(unread_c)));

    ReadingListEntry read_c(GURL("http://read_c.com"), "read_c",
                            AdvanceAndGetTime(clock_));
    read_c.SetRead(true, AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://read_c.com"), std::move(read_c)));

    ReadingListEntry unread_d(GURL("http://unread_d.com"), "unread_d",
                              AdvanceAndGetTime(clock_));
    entries_->insert(
        std::make_pair(GURL("http://unread_d.com"), std::move(unread_d)));
  }

  void SetReadingListModel(ReadingListModel* model,
                           ReadingListStoreDelegate* delegate,
                           base::Clock* clock) override {
    delegate->StoreLoaded(std::move(entries_));
    clock_ = static_cast<base::SimpleTestClock*>(clock);
  }

  // Saves or updates an entry. If the entry is not yet in the database, it is
  // created.
  void SaveEntry(const ReadingListEntry& entry) override {
    observer_->ReadingListDidSaveEntry();
  }

  // Removes an entry from the storage.
  void RemoveEntry(const ReadingListEntry& entry) override {
    observer_->ReadingListDidRemoveEntry();
  }

  std::unique_ptr<ScopedBatchUpdate> EnsureBatchCreated() override {
    return std::unique_ptr<ScopedBatchUpdate>();
  }

  // Syncing is not used in this test class.
  std::unique_ptr<syncer::MetadataChangeList> CreateMetadataChangeList()
      override {
    NOTREACHED();
    return std::unique_ptr<syncer::MetadataChangeList>();
  }

  base::Optional<syncer::ModelError> MergeSyncData(
      std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
      syncer::EntityChangeList entity_data) override {
    NOTREACHED();
    return {};
  }

  base::Optional<syncer::ModelError> ApplySyncChanges(
      std::unique_ptr<syncer::MetadataChangeList> metadata_change_list,
      syncer::EntityChangeList entity_changes) override {
    NOTREACHED();
    return {};
  }

  void GetData(StorageKeyList storage_keys, DataCallback callback) override {
    NOTREACHED();
    return;
  }

  void GetAllDataForDebugging(DataCallback callback) override {
    NOTREACHED();
    return;
  }

  std::string GetClientTag(const syncer::EntityData& entity_data) override {
    NOTREACHED();
    return "";
  }

  std::string GetStorageKey(const syncer::EntityData& entity_data) override {
    NOTREACHED();
    return "";
  }

 private:
  std::unique_ptr<ReadingListStoreDelegate::ReadingListEntries> entries_;
  TestReadingListStorageObserver* observer_;
  base::SimpleTestClock* clock_;
};

class ReadingListModelTest : public ReadingListModelObserver,
                             public TestReadingListStorageObserver,
                             public testing::Test {
 public:
  ReadingListModelTest() : callback_called_(false) {
    model_ = std::make_unique<ReadingListModelImpl>(nullptr, nullptr, &clock_);
    ClearCounts();
    model_->AddObserver(this);
  }
  ~ReadingListModelTest() override {}

  void SetStorage(std::unique_ptr<TestReadingListStorage> storage) {
    model_ = std::make_unique<ReadingListModelImpl>(std::move(storage), nullptr,
                                                    &clock_);
    ClearCounts();
    model_->AddObserver(this);
  }

  void ClearCounts() {
    observer_loaded_ = observer_started_batch_update_ =
        observer_completed_batch_update_ = observer_deleted_ =
            observer_remove_ = observer_move_ = observer_add_ =
                observer_did_add_ = observer_update_ = observer_did_apply_ =
                    storage_saved_ = storage_removed_ = 0;
  }

  void AssertObserverCount(int observer_loaded,
                           int observer_started_batch_update,
                           int observer_completed_batch_update,
                           int observer_deleted,
                           int observer_remove,
                           int observer_move,
                           int observer_add,
                           int observer_update,
                           int observer_did_apply) {
    ASSERT_EQ(observer_loaded, observer_loaded_);
    ASSERT_EQ(observer_started_batch_update, observer_started_batch_update_);
    ASSERT_EQ(observer_completed_batch_update,
              observer_completed_batch_update_);
    ASSERT_EQ(observer_deleted, observer_deleted_);
    ASSERT_EQ(observer_remove, observer_remove_);
    ASSERT_EQ(observer_move, observer_move_);
    // Add and did_add should be the same.
    ASSERT_EQ(observer_add, observer_add_);
    ASSERT_EQ(observer_add, observer_did_add_);
    ASSERT_EQ(observer_update, observer_update_);
    ASSERT_EQ(observer_did_apply, observer_did_apply_);
  }

  void AssertStorageCount(int storage_saved, int storage_removed) {
    ASSERT_EQ(storage_saved, storage_saved_);
    ASSERT_EQ(storage_removed, storage_removed_);
  }

  // ReadingListModelObserver
  void ReadingListModelLoaded(const ReadingListModel* model) override {
    observer_loaded_ += 1;
  }
  void ReadingListModelBeganBatchUpdates(
      const ReadingListModel* model) override {
    observer_started_batch_update_ += 1;
  }
  void ReadingListModelCompletedBatchUpdates(
      const ReadingListModel* model) override {
    observer_completed_batch_update_ += 1;
  }
  void ReadingListModelBeingDeleted(const ReadingListModel* model) override {
    observer_deleted_ += 1;
  }
  void ReadingListWillRemoveEntry(const ReadingListModel* model,
                                  const GURL& url) override {
    observer_remove_ += 1;
  }
  void ReadingListWillMoveEntry(const ReadingListModel* model,
                                const GURL& url) override {
    observer_move_ += 1;
  }
  void ReadingListWillAddEntry(const ReadingListModel* model,
                               const ReadingListEntry& entry) override {
    observer_add_ += 1;
  }
  void ReadingListDidAddEntry(const ReadingListModel* model,
                              const GURL& url,
                              reading_list::EntrySource entry_source) override {
    observer_did_add_ += 1;
  }
  void ReadingListWillUpdateEntry(const ReadingListModel* model,
                                  const GURL& url) override {
    observer_update_ += 1;
  }
  void ReadingListDidApplyChanges(ReadingListModel* model) override {
    observer_did_apply_ += 1;
  }

  void ReadingListDidSaveEntry() override { storage_saved_ += 1; }
  void ReadingListDidRemoveEntry() override { storage_removed_ += 1; }

  size_t UnreadSize() {
    size_t size = 0;
    for (const auto& url : model_->Keys()) {
      const ReadingListEntry* entry = model_->GetEntryByURL(url);
      if (!entry->IsRead()) {
        size++;
      }
    }
    DCHECK_EQ(size, model_->unread_size());
    return size;
  }

  size_t ReadSize() {
    size_t size = 0;
    for (const auto& url : model_->Keys()) {
      const ReadingListEntry* entry = model_->GetEntryByURL(url);
      if (entry->IsRead()) {
        size++;
      }
    }
    return size;
  }

  void Callback(const ReadingListEntry& entry) {
    EXPECT_EQ(callback_url, entry.URL());
    EXPECT_EQ(callback_title, entry.Title());
    callback_called_ = true;
  }

  bool CallbackCalled() { return callback_called_; }

 protected:
  int observer_loaded_;
  int observer_started_batch_update_;
  int observer_completed_batch_update_;
  int observer_deleted_;
  int observer_remove_;
  int observer_move_;
  int observer_add_;
  int observer_did_add_;
  int observer_update_;
  int observer_did_apply_;
  int storage_saved_;
  int storage_removed_;
  bool callback_called_;

  std::unique_ptr<ReadingListModelImpl> model_;
  base::SimpleTestClock clock_;
};

// Tests creating an empty model.
TEST_F(ReadingListModelTest, EmptyLoaded) {
  EXPECT_TRUE(model_->loaded());
  AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  model_->Shutdown();
  EXPECT_FALSE(model_->loaded());
  // Shutdown() does not delete the model observer. Verify that deleting the
  // model will delete the model observer.
  model_.reset();
  AssertObserverCount(1, 0, 0, 1, 0, 0, 0, 0, 0);
}

// Tests load model.
TEST_F(ReadingListModelTest, ModelLoaded) {
  ClearCounts();
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  storage->AddSampleEntries();
  SetStorage(std::move(storage));

  AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0);
  std::map<GURL, std::string> loaded_entries;
  int size = 0;
  for (const auto& url : model_->Keys()) {
    size++;
    const ReadingListEntry* entry = model_->GetEntryByURL(url);
    loaded_entries[url] = entry->Title();
  }
  EXPECT_EQ(size, 7);
  EXPECT_EQ(loaded_entries[GURL("http://unread_a.com")], "unread_a");
  EXPECT_EQ(loaded_entries[GURL("http://unread_b.com")], "unread_b");
  EXPECT_EQ(loaded_entries[GURL("http://unread_c.com")], "unread_c");
  EXPECT_EQ(loaded_entries[GURL("http://unread_d.com")], "unread_d");
  EXPECT_EQ(loaded_entries[GURL("http://read_a.com")], "read_a");
  EXPECT_EQ(loaded_entries[GURL("http://read_b.com")], "read_b");
  EXPECT_EQ(loaded_entries[GURL("http://read_c.com")], "read_c");
}

// Tests adding entry.
TEST_F(ReadingListModelTest, AddEntry) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  ClearCounts();

  const ReadingListEntry& entry =
      model_->AddEntry(GURL("http://example.com"), "\n  \tsample Test ",
                       reading_list::ADDED_VIA_CURRENT_APP);
  EXPECT_EQ(GURL("http://example.com"), entry.URL());
  EXPECT_EQ("sample Test", entry.Title());

  AssertObserverCount(0, 0, 0, 0, 0, 0, 1, 0, 1);
  AssertStorageCount(1, 0);
  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_TRUE(model_->GetLocalUnseenFlag());

  const ReadingListEntry* other_entry =
      model_->GetEntryByURL(GURL("http://example.com"));
  EXPECT_NE(other_entry, nullptr);
  EXPECT_FALSE(other_entry->IsRead());
  EXPECT_EQ(GURL("http://example.com"), other_entry->URL());
  EXPECT_EQ("sample Test", other_entry->Title());
}

// Tests adding an entry that already exists.
TEST_F(ReadingListModelTest, AddExistingEntry) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  GURL url = GURL("http://example.com");
  std::string title = "\n  \tsample Test ";
  model_->AddEntry(url, title, reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();

  const ReadingListEntry& entry =
      model_->AddEntry(url, title, reading_list::ADDED_VIA_CURRENT_APP);
  EXPECT_EQ(GURL("http://example.com"), entry.URL());
  EXPECT_EQ("sample Test", entry.Title());

  AssertObserverCount(0, 1, 1, 0, 1, 0, 1, 0, 2);
  AssertStorageCount(1, 1);
  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_TRUE(model_->GetLocalUnseenFlag());

  const ReadingListEntry* other_entry =
      model_->GetEntryByURL(GURL("http://example.com"));
  EXPECT_NE(other_entry, nullptr);
  EXPECT_FALSE(other_entry->IsRead());
  EXPECT_EQ(GURL("http://example.com"), other_entry->URL());
  EXPECT_EQ("sample Test", other_entry->Title());
}

// Tests addin entry from sync.
TEST_F(ReadingListModelTest, SyncAddEntry) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  auto entry = std::make_unique<ReadingListEntry>(
      GURL("http://example.com"), "sample", AdvanceAndGetTime(&clock_));
  entry->SetRead(true, AdvanceAndGetTime(&clock_));
  ClearCounts();

  model_->SyncAddEntry(std::move(entry));
  AssertObserverCount(0, 0, 0, 0, 0, 0, 1, 0, 1);
  AssertStorageCount(0, 0);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  ClearCounts();
}

// Tests updating entry from sync.
TEST_F(ReadingListModelTest, SyncMergeEntry) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  const base::FilePath distilled_path(FILE_PATH_LITERAL("distilled/page.html"));
  const GURL distilled_url("http://example.com/distilled");
  int64_t size = 50;
  int64_t time = 100;
  model_->SetEntryDistilledInfo(GURL("http://example.com"), distilled_path,
                                distilled_url, size,
                                base::Time::FromTimeT(time));
  const ReadingListEntry* local_entry =
      model_->GetEntryByURL(GURL("http://example.com"));
  int64_t local_update_time = local_entry->UpdateTime();

  auto sync_entry = std::make_unique<ReadingListEntry>(
      GURL("http://example.com"), "sample", AdvanceAndGetTime(&clock_));
  sync_entry->SetRead(true, AdvanceAndGetTime(&clock_));
  ASSERT_GT(sync_entry->UpdateTime(), local_update_time);
  int64_t sync_update_time = sync_entry->UpdateTime();
  EXPECT_TRUE(sync_entry->DistilledPath().empty());

  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());

  ReadingListEntry* merged_entry =
      model_->SyncMergeEntry(std::move(sync_entry));
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  EXPECT_EQ(merged_entry->DistilledPath(),
            base::FilePath(FILE_PATH_LITERAL("distilled/page.html")));
  EXPECT_EQ(merged_entry->UpdateTime(), sync_update_time);
  EXPECT_EQ(size, merged_entry->DistillationSize());
  EXPECT_EQ(time * base::Time::kMicrosecondsPerSecond,
            merged_entry->DistillationTime());
}

// Tests deleting entry.
TEST_F(ReadingListModelTest, RemoveEntryByUrl) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();
  EXPECT_NE(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  model_->RemoveEntryByURL(GURL("http://example.com"));
  AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 0, 1);
  AssertStorageCount(0, 1);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_EQ(model_->GetEntryByURL(GURL("http://example.com")), nullptr);

  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(GURL("http://example.com"), true);
  ClearCounts();
  EXPECT_NE(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  model_->RemoveEntryByURL(GURL("http://example.com"));
  AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 0, 1);
  AssertStorageCount(0, 1);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_EQ(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
}

// Tests deleting entry from sync.
TEST_F(ReadingListModelTest, RemoveSyncEntryByUrl) {
  auto storage = std::make_unique<TestReadingListStorage>(this, &clock_);
  SetStorage(std::move(storage));
  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();
  EXPECT_NE(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  model_->SyncRemoveEntry(GURL("http://example.com"));
  AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 0, 1);
  AssertStorageCount(0, 0);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_EQ(model_->GetEntryByURL(GURL("http://example.com")), nullptr);

  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(GURL("http://example.com"), true);
  ClearCounts();
  EXPECT_NE(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  model_->SyncRemoveEntry(GURL("http://example.com"));
  AssertObserverCount(0, 0, 0, 0, 1, 0, 0, 0, 1);
  AssertStorageCount(0, 0);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_EQ(model_->GetEntryByURL(GURL("http://example.com")), nullptr);
}

// Tests marking entry read.
TEST_F(ReadingListModelTest, ReadEntry) {
  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);

  ClearCounts();
  model_->SetReadStatus(GURL("http://example.com"), true);
  AssertObserverCount(0, 0, 0, 0, 0, 1, 0, 0, 1);
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  EXPECT_EQ(0ul, model_->unseen_size());

  const ReadingListEntry* other_entry =
      model_->GetEntryByURL(GURL("http://example.com"));
  EXPECT_NE(other_entry, nullptr);
  EXPECT_TRUE(other_entry->IsRead());
  EXPECT_EQ(GURL("http://example.com"), other_entry->URL());
  EXPECT_EQ("sample", other_entry->Title());
}

// Tests accessing existing entry.
TEST_F(ReadingListModelTest, EntryFromURL) {
  GURL url1("http://example.com");
  GURL url2("http://example2.com");
  std::string entry1_title = "foo bar qux";
  model_->AddEntry(url1, entry1_title, reading_list::ADDED_VIA_CURRENT_APP);

  // Check call with nullptr |read| parameter.
  const ReadingListEntry* entry1 = model_->GetEntryByURL(url1);
  EXPECT_NE(nullptr, entry1);
  EXPECT_EQ(entry1_title, entry1->Title());

  entry1 = model_->GetEntryByURL(url1);
  EXPECT_NE(nullptr, entry1);
  EXPECT_EQ(entry1_title, entry1->Title());
  EXPECT_EQ(entry1->IsRead(), false);
  model_->SetReadStatus(url1, true);
  entry1 = model_->GetEntryByURL(url1);
  EXPECT_NE(nullptr, entry1);
  EXPECT_EQ(entry1_title, entry1->Title());
  EXPECT_EQ(entry1->IsRead(), true);

  const ReadingListEntry* entry2 = model_->GetEntryByURL(url2);
  EXPECT_EQ(nullptr, entry2);
}

// Tests mark entry unread.
TEST_F(ReadingListModelTest, UnreadEntry) {
  // Setup.
  model_->AddEntry(GURL("http://example.com"), "sample",
                   reading_list::ADDED_VIA_CURRENT_APP);
  EXPECT_TRUE(model_->GetLocalUnseenFlag());
  model_->SetReadStatus(GURL("http://example.com"), true);
  ClearCounts();
  EXPECT_EQ(0ul, UnreadSize());
  EXPECT_EQ(1ul, ReadSize());
  EXPECT_FALSE(model_->GetLocalUnseenFlag());

  // Action.
  model_->SetReadStatus(GURL("http://example.com"), false);

  // Tests.
  AssertObserverCount(0, 0, 0, 0, 0, 1, 0, 0, 1);
  EXPECT_EQ(1ul, UnreadSize());
  EXPECT_EQ(0ul, ReadSize());
  EXPECT_FALSE(model_->GetLocalUnseenFlag());

  const ReadingListEntry* other_entry =
      model_->GetEntryByURL(GURL("http://example.com"));
  EXPECT_NE(other_entry, nullptr);
  EXPECT_FALSE(other_entry->IsRead());
  EXPECT_EQ(GURL("http://example.com"), other_entry->URL());
  EXPECT_EQ("sample", other_entry->Title());
}

// Tests batch updates observers are called.
TEST_F(ReadingListModelTest, BatchUpdates) {
  auto token = model_->BeginBatchUpdates();
  AssertObserverCount(1, 1, 0, 0, 0, 0, 0, 0, 0);
  EXPECT_TRUE(model_->IsPerformingBatchUpdates());

  delete token.release();
  AssertObserverCount(1, 1, 1, 0, 0, 0, 0, 0, 0);
  EXPECT_FALSE(model_->IsPerformingBatchUpdates());
}

// Tests batch updates are reentrant.
TEST_F(ReadingListModelTest, BatchUpdatesReentrant) {
  // When two updates happen at the same time, the notification is only sent
  // for beginning of first update and completion of last update.
  EXPECT_FALSE(model_->IsPerformingBatchUpdates());

  auto token = model_->BeginBatchUpdates();
  AssertObserverCount(1, 1, 0, 0, 0, 0, 0, 0, 0);
  EXPECT_TRUE(model_->IsPerformingBatchUpdates());

  auto second_token = model_->BeginBatchUpdates();
  AssertObserverCount(1, 1, 0, 0, 0, 0, 0, 0, 0);
  EXPECT_TRUE(model_->IsPerformingBatchUpdates());

  delete token.release();
  AssertObserverCount(1, 1, 0, 0, 0, 0, 0, 0, 0);
  EXPECT_TRUE(model_->IsPerformingBatchUpdates());

  delete second_token.release();
  AssertObserverCount(1, 1, 1, 0, 0, 0, 0, 0, 0);
  EXPECT_FALSE(model_->IsPerformingBatchUpdates());

  // Consequent updates send notifications.
  auto third_token = model_->BeginBatchUpdates();
  AssertObserverCount(1, 2, 1, 0, 0, 0, 0, 0, 0);
  EXPECT_TRUE(model_->IsPerformingBatchUpdates());

  delete third_token.release();
  AssertObserverCount(1, 2, 2, 0, 0, 0, 0, 0, 0);
  EXPECT_FALSE(model_->IsPerformingBatchUpdates());
}

// Tests setting title on unread entry.
TEST_F(ReadingListModelTest, UpdateEntryTitle) {
  const GURL gurl("http://example.com");
  const ReadingListEntry& entry =
      model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();

  model_->SetEntryTitle(gurl, "ping");
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ("ping", entry.Title());
}
// Tests setting distillation state on unread entry.
TEST_F(ReadingListModelTest, UpdateEntryDistilledState) {
  const GURL gurl("http://example.com");
  const ReadingListEntry& entry =
      model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();

  model_->SetEntryDistilledState(gurl, ReadingListEntry::PROCESSING);
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ(ReadingListEntry::PROCESSING, entry.DistilledState());
}

// Tests setting distillation info on unread entry.
TEST_F(ReadingListModelTest, UpdateDistilledInfo) {
  const GURL gurl("http://example.com");
  const ReadingListEntry& entry =
      model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  ClearCounts();

  const base::FilePath distilled_path(FILE_PATH_LITERAL("distilled/page.html"));
  const GURL distilled_url("http://example.com/distilled");
  int64_t size = 50;
  int64_t time = 100;
  model_->SetEntryDistilledInfo(GURL("http://example.com"), distilled_path,
                                distilled_url, size,
                                base::Time::FromTimeT(time));
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ(ReadingListEntry::PROCESSED, entry.DistilledState());
  EXPECT_EQ(distilled_path, entry.DistilledPath());
  EXPECT_EQ(distilled_url, entry.DistilledURL());
  EXPECT_EQ(size, entry.DistillationSize());
  EXPECT_EQ(time * base::Time::kMicrosecondsPerSecond,
            entry.DistillationTime());
}

// Tests setting title on read entry.
TEST_F(ReadingListModelTest, UpdateReadEntryTitle) {
  const GURL gurl("http://example.com");
  model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(gurl, true);
  const ReadingListEntry* entry = model_->GetEntryByURL(gurl);
  ClearCounts();

  model_->SetEntryTitle(gurl, "ping");
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ("ping", entry->Title());
}

// Tests setting distillation state on read entry.
TEST_F(ReadingListModelTest, UpdateReadEntryState) {
  const GURL gurl("http://example.com");
  model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(gurl, true);
  const ReadingListEntry* entry = model_->GetEntryByURL(gurl);
  ClearCounts();

  model_->SetEntryDistilledState(gurl, ReadingListEntry::PROCESSING);
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ(ReadingListEntry::PROCESSING, entry->DistilledState());
}

// Tests setting distillation info on read entry.
TEST_F(ReadingListModelTest, UpdateReadDistilledInfo) {
  const GURL gurl("http://example.com");
  model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(gurl, true);
  const ReadingListEntry* entry = model_->GetEntryByURL(gurl);
  ClearCounts();

  const base::FilePath distilled_path(FILE_PATH_LITERAL("distilled/page.html"));
  const GURL distilled_url("http://example.com/distilled");
  int64_t size = 50;
  int64_t time = 100;
  model_->SetEntryDistilledInfo(GURL("http://example.com"), distilled_path,
                                distilled_url, size,
                                base::Time::FromTimeT(time));
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ(ReadingListEntry::PROCESSED, entry->DistilledState());
  EXPECT_EQ(distilled_path, entry->DistilledPath());
  EXPECT_EQ(distilled_url, entry->DistilledURL());
  EXPECT_EQ(size, entry->DistillationSize());
  EXPECT_EQ(time * base::Time::kMicrosecondsPerSecond,
            entry->DistillationTime());
}

// Tests setting ContentSuggestionsExtra info on entry.
TEST_F(ReadingListModelTest, UpdateContentSuggestionsExtra) {
  const GURL gurl("http://example.com");
  model_->AddEntry(gurl, "sample", reading_list::ADDED_VIA_CURRENT_APP);
  const ReadingListEntry* entry = model_->GetEntryByURL(gurl);
  ClearCounts();

  reading_list::ContentSuggestionsExtra extra;
  extra.dismissed = true;

  model_->SetContentSuggestionsExtra(gurl, extra);
  AssertObserverCount(0, 0, 0, 0, 0, 0, 0, 1, 1);
  EXPECT_EQ(extra.dismissed, entry->ContentSuggestionsExtra()->dismissed);
}

// Tests that ReadingListModel calls CallbackModelBeingDeleted when destroyed.
TEST_F(ReadingListModelTest, CallbackModelBeingDeleted) {
  AssertObserverCount(1, 0, 0, 0, 0, 0, 0, 0, 0);
  model_.reset();
  AssertObserverCount(1, 0, 0, 1, 0, 0, 0, 0, 0);
}

// Tests that new line characters and spaces are collapsed in title.
TEST_F(ReadingListModelTest, TestTrimmingTitle) {
  const GURL gurl("http://example.com");
  std::string title = "\n  This\ttitle \n contains new     line \n characters ";
  model_->AddEntry(gurl, title, reading_list::ADDED_VIA_CURRENT_APP);
  model_->SetReadStatus(gurl, true);
  const ReadingListEntry* entry = model_->GetEntryByURL(gurl);
  EXPECT_EQ(entry->Title(), "This title contains new line characters");
  model_->SetEntryTitle(gurl, "test");
  EXPECT_EQ(entry->Title(), "test");
  model_->SetEntryTitle(gurl, title);
  EXPECT_EQ(entry->Title(), "This title contains new line characters");
}

}  // namespace
