// Copyright 2020 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/feed/core/v2/stream_model.h"

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

#include "base/optional.h"
#include "base/strings/string_number_conversions.h"
#include "components/feed/core/proto/v2/store.pb.h"
#include "components/feed/core/proto/v2/wire/content_id.pb.h"
#include "components/feed/core/v2/stream_model_update_request.h"
#include "components/feed/core/v2/test/stream_builder.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace feed {
namespace {
using StoreUpdate = StreamModel::StoreUpdate;
using UiUpdate = StreamModel::UiUpdate;

std::vector<std::string> GetContentFrames(const StreamModel& model) {
  std::vector<std::string> frames;
  for (ContentRevision rev : model.GetContentList()) {
    const feedstore::Content* content = model.FindContent(rev);
    if (content) {
      frames.push_back(content->frame());
    } else {
      frames.push_back("<null>");
    }
  }
  return frames;
}

class TestObserver : public StreamModel::Observer {
 public:
  explicit TestObserver(StreamModel* model) { model->SetObserver(this); }

  // StreamModel::Observer.
  void OnUiUpdate(const UiUpdate& update) override { update_ = update; }
  const base::Optional<UiUpdate>& GetUiUpdate() const { return update_; }
  bool ContentListChanged() const {
    return update_ && update_->content_list_changed;
  }

  void Clear() { update_ = base::nullopt; }

 private:
  base::Optional<UiUpdate> update_;
};

class TestStoreObserver : public StreamModel::StoreObserver {
 public:
  explicit TestStoreObserver(StreamModel* model) {
    model->SetStoreObserver(this);
  }

  // StreamModel::StoreObserver.
  void OnStoreChange(const StoreUpdate& records) override { update_ = records; }

  const base::Optional<StoreUpdate>& GetUpdate() const { return update_; }

  void Clear() { update_ = base::nullopt; }

 private:
  base::Optional<StoreUpdate> update_;
};

TEST(StreamModelTest, ConstructEmptyModel) {
  StreamModel model;
  TestObserver observer(&model);

  EXPECT_EQ(0UL, model.GetContentList().size());
}

TEST(StreamModelTest, ExecuteOperationsTypicalStream) {
  StreamModel model;
  TestObserver observer(&model);
  TestStoreObserver store_observer(&model);

  model.ExecuteOperations(MakeTypicalStreamOperations());
  EXPECT_TRUE(observer.ContentListChanged());
  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
  ASSERT_TRUE(store_observer.GetUpdate());
  ASSERT_EQ(MakeTypicalStreamOperations().size(),
            store_observer.GetUpdate()->operations.size());
}

TEST(StreamModelTest, AddContentWithoutRoot) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations{
      MakeOperation(MakeCluster(0, MakeRootId())),
      MakeOperation(MakeContentNode(0, MakeClusterId(0))),
      MakeOperation(MakeContent(0)),
  };
  model.ExecuteOperations(operations);

  // Without a root, no content is visible.
  EXPECT_EQ(std::vector<std::string>({}), GetContentFrames(model));
}

// Verify Stream -> Content works.
TEST(StreamModelTest, AddStreamContent) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations{
      MakeOperation(MakeStream()),
      MakeOperation(MakeContentNode(0, MakeRootId())),
      MakeOperation(MakeContent(0)),
  };
  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:0"}), GetContentFrames(model));
}

TEST(StreamModelTest, AddRootAsChild) {
  // When the root is added as a child, it's no longer considered a root.
  StreamModel model;
  TestObserver observer(&model);
  feedstore::StreamStructure stream_with_parent = MakeStream();
  *stream_with_parent.mutable_parent_id() = MakeContentContentId(0);
  std::vector<feedstore::DataOperation> operations{
      MakeOperation(MakeStream()),
      MakeOperation(MakeContentNode(0, MakeRootId())),
      MakeOperation(MakeContent(0)),
      MakeOperation(stream_with_parent),
  };

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({}), GetContentFrames(model));
}

// Changing the STREAM root to CLUSTER means it is no longer eligible to be
// the root.
TEST(StreamModelTest, ChangeStreamToCluster) {
  StreamModel model;
  TestObserver observer(&model);
  feedstore::StreamStructure stream_as_cluster = MakeStream();
  stream_as_cluster.set_type(feedstore::StreamStructure::CLUSTER);

  std::vector<feedstore::DataOperation> operations{
      MakeOperation(MakeStream()),
      MakeOperation(MakeContentNode(0, MakeRootId())),
      MakeOperation(MakeContent(0)),
      MakeOperation(stream_as_cluster),
  };

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({}), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveCluster) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeClusterId(0))));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveContent) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeContentContentId(0))));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveRoot) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeRootId())));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>(), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveAndAddRoot) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeRootId())));
  operations.push_back(MakeOperation(MakeStream()));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, SwitchStreams) {
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeStream(2)));
  operations.push_back(MakeOperation(MakeContentNode(9, MakeRootId(2))));
  operations.push_back(MakeOperation(MakeContent(9)));

  model.ExecuteOperations(operations);

  // The last stream added becomes the root, so only children of 'root2' are
  // included.
  EXPECT_EQ(std::vector<std::string>({"f:9"}), GetContentFrames(model));

  // Adding the original stream back will re-activate it.
  model.ExecuteOperations({MakeOperation(MakeStream())});

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));

  // Removing 'root' will now make 'root2' active again.
  model.ExecuteOperations({MakeOperation(MakeRemove(MakeRootId()))});
  EXPECT_EQ(std::vector<std::string>({"f:9"}), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveAndUpdateCluster) {
  // Remove a cluster and add it back. Adding it back keeps its original
  // placement.
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeClusterId(0))));
  operations.push_back(MakeOperation(MakeCluster(0, MakeRootId())));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, RemoveAndAppendToNewParent) {
  // Attempt to re-parent a node. This is not allowed, the old parent remains.
  StreamModel model;
  TestObserver observer(&model);

  std::vector<feedstore::DataOperation> operations =
      MakeTypicalStreamOperations();
  operations.push_back(MakeOperation(MakeRemove(MakeClusterId(0))));
  operations.push_back(MakeOperation(MakeCluster(0, MakeClusterId(1))));

  model.ExecuteOperations(operations);

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, EphemeralNewCluster) {
  StreamModel model;
  TestObserver observer(&model);

  model.ExecuteOperations(MakeTypicalStreamOperations());
  observer.Clear();

  model.CreateEphemeralChange({
      MakeOperation(MakeCluster(2, MakeRootId())),
      MakeOperation(MakeContentNode(2, MakeClusterId(2))),
      MakeOperation(MakeContent(2)),
  });

  EXPECT_TRUE(observer.ContentListChanged());
  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1", "f:2"}),
            GetContentFrames(model));
}

TEST(StreamModelTest, CommitEphemeralChange) {
  StreamModel model;
  TestObserver observer(&model);

  model.ExecuteOperations(MakeTypicalStreamOperations());

  EphemeralChangeId change_id = model.CreateEphemeralChange({
      MakeOperation(MakeCluster(2, MakeRootId())),
      MakeOperation(MakeContentNode(2, MakeClusterId(2))),
      MakeOperation(MakeContent(2)),
  });

  observer.Clear();
  TestStoreObserver store_observer(&model);
  EXPECT_TRUE(model.CommitEphemeralChange(change_id));

  // Check that the observer's |OnStoreChange()| was called.
  ASSERT_TRUE(store_observer.GetUpdate());
  StoreUpdate store_update = *store_observer.GetUpdate();
  ASSERT_EQ(3UL, store_update.operations.size());
  EXPECT_EQ(feedstore::StreamStructure::CLUSTER,
            store_update.operations[0].structure().type());
  EXPECT_EQ(feedstore::StreamStructure::CONTENT,
            store_update.operations[1].structure().type());

  // Can't reject after commit.
  EXPECT_FALSE(model.RejectEphemeralChange(change_id));

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1", "f:2"}),
            GetContentFrames(model));
}

TEST(StreamModelTest, RejectEphemeralChange) {
  StreamModel model;
  TestObserver observer(&model);

  model.ExecuteOperations(MakeTypicalStreamOperations());
  EphemeralChangeId change_id = model.CreateEphemeralChange({
      MakeOperation(MakeCluster(2, MakeRootId())),
      MakeOperation(MakeContentNode(2, MakeClusterId(2))),
      MakeOperation(MakeContent(2)),
  });
  observer.Clear();

  EXPECT_TRUE(model.RejectEphemeralChange(change_id));
  EXPECT_TRUE(observer.ContentListChanged());
  // Can't commit after reject.
  EXPECT_FALSE(model.CommitEphemeralChange(change_id));

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
}

TEST(StreamModelTest, RejectFirstEphemeralChange) {
  StreamModel model;
  TestObserver observer(&model);

  model.ExecuteOperations(MakeTypicalStreamOperations());
  EphemeralChangeId change_id1 = model.CreateEphemeralChange({
      MakeOperation(MakeCluster(2, MakeRootId())),
      MakeOperation(MakeContentNode(2, MakeClusterId(2))),
      MakeOperation(MakeContent(2)),
  });

  model.CreateEphemeralChange({
      MakeOperation(MakeCluster(3, MakeRootId())),
      MakeOperation(MakeContentNode(3, MakeClusterId(3))),
      MakeOperation(MakeContent(3)),
  });
  observer.Clear();

  EXPECT_TRUE(model.RejectEphemeralChange(change_id1));
  EXPECT_TRUE(observer.ContentListChanged());
  // Can't commit after reject.
  EXPECT_FALSE(model.CommitEphemeralChange(change_id1));

  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1", "f:3"}),
            GetContentFrames(model));
}

TEST(StreamModelTest, InitialLoad) {
  StreamModel model;
  TestObserver observer(&model);
  TestStoreObserver store_observer(&model);
  model.Update(MakeTypicalInitialModelState());

  // Check that content was added and the store doesn't receive its own update.
  EXPECT_TRUE(observer.ContentListChanged());
  EXPECT_EQ(std::vector<std::string>({"f:0", "f:1"}), GetContentFrames(model));
  ASSERT_EQ(1UL, observer.GetUiUpdate()->shared_states.size());
  EXPECT_NE("", observer.GetUiUpdate()->shared_states[0].shared_state_id);
  const std::string* shared_state_data = model.FindSharedStateData(
      observer.GetUiUpdate()->shared_states[0].shared_state_id);
  ASSERT_TRUE(shared_state_data);
  EXPECT_EQ("ss:0", *shared_state_data);
  EXPECT_FALSE(store_observer.GetUpdate());
}

TEST(StreamModelTest, StoreObserverReceivesIncreasingSequenceNumbers) {
  StreamModel model;
  TestObserver observer(&model);
  TestStoreObserver store_observer(&model);

  // Initialize the model starting at sequence number 5.
  {
    std::unique_ptr<StreamModelUpdateRequest> initial_state =
        MakeTypicalInitialModelState();
    initial_state->max_structure_sequence_number = 5;
    model.Update(std::move(initial_state));
  }

  model.ExecuteOperations({MakeOperation(MakeRemove(MakeContentContentId(0)))});

  ASSERT_TRUE(store_observer.GetUpdate());
  EXPECT_EQ(6, store_observer.GetUpdate()->sequence_number);

  store_observer.Clear();
  model.ExecuteOperations({MakeOperation(MakeRemove(MakeContentContentId(0)))});

  ASSERT_TRUE(store_observer.GetUpdate());
  EXPECT_EQ(7, store_observer.GetUpdate()->sequence_number);
}

TEST(StreamModelTest, SharedStateCanBeAddedOnlyOnce) {
  StreamModel model;
  TestObserver observer(&model);
  TestStoreObserver store_observer(&model);

  // Update the model twice with this request. The shared state should not
  // be added the second time.
  StreamModelUpdateRequest update_request;
  update_request.source =
      StreamModelUpdateRequest::Source::kInitialLoadFromStore;
  update_request.content.push_back(MakeContent(0));
  update_request.stream_structures = {MakeStream(),
                                      MakeCluster(0, MakeRootId()),
                                      MakeContentNode(0, MakeClusterId(0))};
  update_request.shared_states.push_back(MakeSharedState(0));

  model.Update(std::make_unique<StreamModelUpdateRequest>(update_request));
  observer.Clear();
  model.Update(std::make_unique<StreamModelUpdateRequest>(update_request));
  ASSERT_EQ(1UL, observer.GetUiUpdate()->shared_states.size());
  EXPECT_FALSE(observer.GetUiUpdate()->shared_states[0].updated);
}

TEST(StreamModelTest, ClearAllErasesSharedStates) {
  StreamModel model;
  TestObserver observer(&model);
  TestStoreObserver store_observer(&model);
  // CLEAR_ALL is the first operation in the typical initial model state.
  // The second Update() will therefore need to remove and add the shared
  // state.
  model.Update(MakeTypicalInitialModelState());
  observer.Clear();
  model.Update(MakeTypicalInitialModelState());

  ASSERT_EQ(1UL, observer.GetUiUpdate()->shared_states.size());
  EXPECT_TRUE(observer.GetUiUpdate()->shared_states[0].updated);
}

}  // namespace
}  // namespace feed
