// Copyright 2020 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/sync/engine/commit_processor.h"

#include <memory>

#include "components/sync/engine/commit_contribution.h"
#include "components/sync/engine/commit_contributor.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace syncer {
namespace {

constexpr int kMaxEntries = 17;

using testing::_;
using testing::IsEmpty;
using testing::Pair;
using testing::UnorderedElementsAre;

MATCHER_P(HasNumEntries, num_entries, "") {
  return static_cast<int>(arg->GetNumEntries()) == num_entries;
}

// Simple implementation of CommitContribution that only implements
// GetNumEntries().
class FakeCommitContribution : public CommitContribution {
 public:
  explicit FakeCommitContribution(size_t num_entries)
      : num_entries_(num_entries) {}

  ~FakeCommitContribution() override = default;

  // CommitContribution implementation.
  void AddToCommitMessage(sync_pb::ClientToServerMessage* msg) override {}
  SyncerError ProcessCommitResponse(
      const sync_pb::ClientToServerResponse& response,
      StatusController* status) override {
    return SyncerError();
  }
  void ProcessCommitFailure(SyncCommitError commit_error) override {}
  size_t GetNumEntries() const override { return num_entries_; }

 private:
  const size_t num_entries_;
};

ACTION_P(ReturnContributionWithEntries, num_entries) {
  return std::make_unique<FakeCommitContribution>(num_entries);
}

class MockCommitContributor : public CommitContributor {
 public:
  MockCommitContributor() = default;
  ~MockCommitContributor() override = default;
  MOCK_METHOD(std::unique_ptr<CommitContribution>,
              GetContribution,
              (size_t max_entries),
              (override));
};

class CommitProcessorTest : public testing::Test {
 protected:
  CommitProcessorTest()
      : contributor_map_{{NIGORI, &nigori_contributor_},
                         {SHARING_MESSAGE, &sharing_message_contributor_},
                         {BOOKMARKS, &bookmark_contributor_},
                         {PREFERENCES, &preference_contributor_},
                         {HISTORY, &history_contributor_}},
        processor_(
            /*commit_types=*/ModelTypeSet{NIGORI, SHARING_MESSAGE, BOOKMARKS,
                                          PREFERENCES, HISTORY},
            &contributor_map_) {
    EXPECT_TRUE(HighPriorityUserTypes().Has(SHARING_MESSAGE));
    EXPECT_FALSE(HighPriorityUserTypes().Has(BOOKMARKS));
    EXPECT_FALSE(HighPriorityUserTypes().Has(PREFERENCES));
    EXPECT_TRUE(LowPriorityUserTypes().Has(HISTORY));
  }

  testing::NiceMock<MockCommitContributor> nigori_contributor_;

  // A high-priority user type.
  testing::NiceMock<MockCommitContributor> sharing_message_contributor_;

  // Regular user types.
  testing::NiceMock<MockCommitContributor> bookmark_contributor_;
  testing::NiceMock<MockCommitContributor> preference_contributor_;

  // A low-priority user type.
  testing::NiceMock<MockCommitContributor> history_contributor_;

  CommitContributorMap contributor_map_;
  CommitProcessor processor_;
};

TEST_F(CommitProcessorTest, ShouldGatherNigoriOnlyContribution) {
  EXPECT_CALL(nigori_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(/*num_entries=*/1));

  // No user types should be gathered and combined with NIGORI.
  EXPECT_CALL(sharing_message_contributor_, GetContribution).Times(0);
  EXPECT_CALL(bookmark_contributor_, GetContribution).Times(0);
  EXPECT_CALL(preference_contributor_, GetContribution).Times(0);
  EXPECT_CALL(history_contributor_, GetContribution).Times(0);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(Pair(NIGORI, HasNumEntries(1))));
}

TEST_F(CommitProcessorTest, ShouldGatherHighPriorityUserTypesOnlyContribution) {
  const int kNumReturnedEntries = 3;

  testing::Sequence s1, s2, s3;

  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries))
      .InSequence(s1, s2, s3)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedEntries));

  // Non-priority user types should be gathered after the high-priority types,
  // but the relative ordering between these is unspecified.
  EXPECT_CALL(bookmark_contributor_, GetContribution).InSequence(s1);
  EXPECT_CALL(preference_contributor_, GetContribution).InSequence(s2);
  EXPECT_CALL(history_contributor_, GetContribution).InSequence(s3);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(
                  Pair(SHARING_MESSAGE, HasNumEntries(kNumReturnedEntries))));
}

TEST_F(CommitProcessorTest, ShouldGatherRegularUserTypes) {
  const int kNumReturnedBookmarks = 7;

  // High-priority types should be gathered, but no entries are produced.
  EXPECT_CALL(nigori_contributor_, GetContribution(kMaxEntries));
  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries));

  // Return |kNumReturnedBookmarks| bookmarks.
  EXPECT_CALL(bookmark_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kNumReturnedBookmarks));

  // Preferences should also be gathered, but no entries are produced in this
  // test. The precise argument depends on the iteration order so it's not
  // verified in this test.
  EXPECT_CALL(preference_contributor_, GetContribution);

  // Since the regular types don't exhaust `kMaxEntries`, the low-priority types
  // should also be gathered (but don't have any contributions).
  EXPECT_CALL(history_contributor_, GetContribution);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(
                  Pair(BOOKMARKS, HasNumEntries(kNumReturnedBookmarks))));
}

TEST_F(CommitProcessorTest, ShouldGatherLowPriorityUserTypes) {
  const int kNumReturnedHistory = 7;

  // High-priority types and regular types should be gathered, but no entries
  // are produced.
  EXPECT_CALL(nigori_contributor_, GetContribution(kMaxEntries));
  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries));
  EXPECT_CALL(bookmark_contributor_, GetContribution(kMaxEntries));
  EXPECT_CALL(preference_contributor_, GetContribution(kMaxEntries));

  // Return |kNumReturnedHistory| history entries.
  EXPECT_CALL(history_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kNumReturnedHistory));

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(Pair(HISTORY, HasNumEntries(kNumReturnedHistory))));
}

TEST_F(CommitProcessorTest, ShouldGatherMultipleRegularUserTypes) {
  const int kNumReturnedBookmarks = 7;
  const int kNumReturnedPreferences = 8;
  static_assert(kNumReturnedBookmarks + kNumReturnedPreferences < kMaxEntries);

  // Return |kNumReturnedBookmarks| bookmarks and |kNumReturnedPreferences|
  // preferences.
  EXPECT_CALL(bookmark_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedBookmarks));
  EXPECT_CALL(preference_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedPreferences));

  // Since bookmarks+preferences don't exhaust `kMaxEntries`, the low-priority
  // types should also be gathered (but don't have any contributions).
  EXPECT_CALL(history_contributor_, GetContribution);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(
                  Pair(BOOKMARKS, HasNumEntries(kNumReturnedBookmarks)),
                  Pair(PREFERENCES, HasNumEntries(kNumReturnedPreferences))));
}

TEST_F(CommitProcessorTest, ShouldContinueGatheringHighPriorityContributions) {
  const int kNumReturnedSharingMessages = 3;

  // First, return |kMaxEntries| sharing messages.
  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kMaxEntries));

  // Non-priority user types shouldn't even be gathered.
  EXPECT_CALL(bookmark_contributor_, GetContribution).Times(0);
  EXPECT_CALL(preference_contributor_, GetContribution).Times(0);
  EXPECT_CALL(history_contributor_, GetContribution).Times(0);

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(Pair(SHARING_MESSAGE, HasNumEntries(kMaxEntries))));

  // Now, return only |kNumReturnedSharingMessages| sharing messages (all that's
  // left).
  EXPECT_CALL(sharing_message_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedSharingMessages));
  // At this point, there's capacity left, so the non-priority user types should
  // also be gathered (but they don't have any contributions).
  EXPECT_CALL(bookmark_contributor_, GetContribution);
  EXPECT_CALL(preference_contributor_, GetContribution);
  EXPECT_CALL(history_contributor_, GetContribution);

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(
          Pair(SHARING_MESSAGE, HasNumEntries(kNumReturnedSharingMessages))));

  // There are no contributions left, and the contributors should not even be
  // called again.
  EXPECT_CALL(sharing_message_contributor_, GetContribution).Times(0);
  EXPECT_CALL(bookmark_contributor_, GetContribution).Times(0);
  EXPECT_CALL(preference_contributor_, GetContribution).Times(0);
  EXPECT_CALL(history_contributor_, GetContribution).Times(0);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              IsEmpty());
}

TEST_F(CommitProcessorTest, ShouldContinueGatheringRegularContributions) {
  const int kNumReturnedBookmarks = 7;

  // First, return |kMaxEntries| bookmarks.
  EXPECT_CALL(bookmark_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kMaxEntries));

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(Pair(BOOKMARKS, HasNumEntries(kMaxEntries))));

  // Now, return only |kNumReturnedBookmarks| bookmarks (all that's left).
  EXPECT_CALL(bookmark_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedBookmarks));

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(
                  Pair(BOOKMARKS, HasNumEntries(kNumReturnedBookmarks))));

  // There are no contributions left, do not return any further and do not even
  // call the contributor.
  EXPECT_CALL(bookmark_contributor_, GetContribution).Times(0);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              IsEmpty());
}

TEST_F(CommitProcessorTest, ShouldContinueGatheringLowPriorityContributions) {
  const int kNumReturnedHistory = 7;

  // First, return |kMaxEntries| history entries.
  EXPECT_CALL(history_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kMaxEntries));

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(Pair(HISTORY, HasNumEntries(kMaxEntries))));

  // Now, return only |kNumReturnedHistory| entries (all that's left).
  EXPECT_CALL(history_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedHistory));

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(Pair(HISTORY, HasNumEntries(kNumReturnedHistory))));

  // There are no contributions left, do not return any further and do not even
  // call the contributor.
  EXPECT_CALL(history_contributor_, GetContribution).Times(0);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              IsEmpty());
}

TEST_F(CommitProcessorTest,
       ShouldContinueGatheringRegularContributionsIfMatchingMaxEntries) {
  // Return |kMaxEntries| bookmarks.
  EXPECT_CALL(bookmark_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kMaxEntries));

  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(Pair(BOOKMARKS, HasNumEntries(kMaxEntries))));

  // There are no contributions left, do not return any further.
  // GetContribution() should however get called since |processor| cannot tell
  // that there are no left.
  EXPECT_CALL(bookmark_contributor_, GetContribution);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              IsEmpty());
}

TEST_F(CommitProcessorTest, ShouldGatherInPriorityOrder) {
  const int kNumReturnedSharingMessages = 3;
  const int kNumReturnedBookmarks1 = kMaxEntries - kNumReturnedSharingMessages;
  const int kNumReturnedBookmarks2 = 4;
  const int kNumReturnedHistory = 5;

  // A high-priority type, a regular type, and a low-priority type all have
  // non-zero contributions.
  testing::InSequence s;
  // First pass: High-priority, and parts of regular.
  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries))
      .WillOnce(ReturnContributionWithEntries(kNumReturnedSharingMessages))
      .RetiresOnSaturation();
  EXPECT_CALL(bookmark_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedBookmarks1));
  // Second pass: Remaining regular, and low-priority.
  EXPECT_CALL(bookmark_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedBookmarks2));
  EXPECT_CALL(history_contributor_, GetContribution)
      .WillOnce(ReturnContributionWithEntries(kNumReturnedHistory));

  // The first call should return the high-priority types, and as much of the
  // regular-priority types as still fits in the budget.
  EXPECT_THAT(
      processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
      UnorderedElementsAre(
          Pair(SHARING_MESSAGE, HasNumEntries(kNumReturnedSharingMessages)),
          Pair(BOOKMARKS, HasNumEntries(kNumReturnedBookmarks1))));

  // Processor has gathered all contributions for SHARING_MESSAGE previously, no
  // further call should happen.
  EXPECT_CALL(sharing_message_contributor_, GetContribution(kMaxEntries))
      .Times(0);

  // The second call should return the remaining regular types as well as the
  // low-priority types.
  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              UnorderedElementsAre(
                  Pair(BOOKMARKS, HasNumEntries(kNumReturnedBookmarks2)),
                  Pair(HISTORY, HasNumEntries(kNumReturnedHistory))));

  // All contributions were gathered; no further calls should happen.
  EXPECT_CALL(bookmark_contributor_, GetContribution(kMaxEntries)).Times(0);
  EXPECT_CALL(preference_contributor_, GetContribution(kMaxEntries)).Times(0);
  EXPECT_CALL(history_contributor_, GetContribution(kMaxEntries)).Times(0);

  EXPECT_THAT(processor_.GatherCommitContributions(/*max_entries=*/kMaxEntries),
              IsEmpty());
}

}  // namespace

}  // namespace syncer