// Copyright 2012 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/syncer.h"

#include <stddef.h>

#include <algorithm>
#include <limits>
#include <list>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <utility>
#include <vector>

#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/sync/base/client_tag_hash.h"
#include "components/sync/base/extensions_activity.h"
#include "components/sync/base/features.h"
#include "components/sync/base/time.h"
#include "components/sync/base/unique_position.h"
#include "components/sync/engine/active_devices_invalidation_info.h"
#include "components/sync/engine/backoff_delay_provider.h"
#include "components/sync/engine/cancelation_signal.h"
#include "components/sync/engine/cycle/sync_cycle_context.h"
#include "components/sync/engine/data_type_activation_response.h"
#include "components/sync/engine/forwarding_data_type_processor.h"
#include "components/sync/engine/keystore_keys_handler.h"
#include "components/sync/engine/net/server_connection_manager.h"
#include "components/sync/engine/sync_scheduler_impl.h"
#include "components/sync/engine/syncer_proto_util.h"
#include "components/sync/protocol/bookmark_specifics.pb.h"
#include "components/sync/protocol/client_commands.pb.h"
#include "components/sync/protocol/entity_specifics.pb.h"
#include "components/sync/protocol/preference_specifics.pb.h"
#include "components/sync/protocol/sync.pb.h"
#include "components/sync/protocol/sync_enums.pb.h"
#include "components/sync/test/fake_connection_manager.h"
#include "components/sync/test/fake_sync_encryption_handler.h"
#include "components/sync/test/mock_data_type_processor.h"
#include "components/sync/test/mock_debug_info_getter.h"
#include "components/sync/test/mock_nudge_handler.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace syncer {

namespace {

using testing::ElementsAre;
using testing::IsEmpty;
using testing::SizeIs;
using testing::UnorderedElementsAre;

sync_pb::EntitySpecifics MakeSpecifics(DataType data_type) {
  sync_pb::EntitySpecifics specifics;
  AddDefaultFieldValue(data_type, &specifics);
  return specifics;
}

sync_pb::EntitySpecifics MakeBookmarkSpecificsToCommit() {
  sync_pb::EntitySpecifics specifics = MakeSpecifics(BOOKMARKS);
  // The worker DCHECKs for the validity of the `type` and `unique_position`
  // fields for outgoing commits.
  specifics.mutable_bookmark()->set_type(sync_pb::BookmarkSpecifics::URL);
  *specifics.mutable_bookmark()->mutable_unique_position() =
      UniquePosition::InitialPosition(UniquePosition::RandomSuffix()).ToProto();
  return specifics;
}

}  // namespace

// Syncer unit tests. Unfortunately a lot of these tests
// are outdated and need to be reworked and updated.
class SyncerTestBase : public SyncCycle::Delegate,
                       public SyncEngineEventListener {
 public:
  explicit SyncerTestBase(bool use_propagated_access_token) {
    feature_list_.InitWithFeatureState(kSyncUsePropagatedAccessToken,
                                       use_propagated_access_token);

    mock_server_ = std::make_unique<FakeConnectionManager>();
    debug_info_getter_ = std::make_unique<MockDebugInfoGetter>();
    std::vector<SyncEngineEventListener*> listeners;
    listeners.push_back(this);

    data_type_registry_ = std::make_unique<DataTypeRegistry>(
        &mock_nudge_handler_, &cancelation_signal_, &encryption_handler_);

    EnableDatatype(BOOKMARKS);
    EnableDatatype(EXTENSIONS);
    EnableDatatype(NIGORI);
    EnableDatatype(PREFERENCES);

    context_ = std::make_unique<SyncCycleContext>(
        mock_server_.get(), extensions_activity_.get(), listeners,
        debug_info_getter_.get(), data_type_registry_.get(), local_cache_guid(),
        mock_server_->store_birthday(), "fake_bag_of_chips",
        /*poll_interval=*/base::Minutes(30),
        /*account_email=*/"test@example.com",
        /*sync_access_token_fetcher=*/nullptr);
    auto syncer = std::make_unique<Syncer>(&cancelation_signal_);
    // The syncer is destroyed with the scheduler that owns it.
    syncer_ = syncer.get();
    scheduler_ = std::make_unique<SyncSchedulerImpl>(
        "TestSyncScheduler", BackoffDelayProvider::FromDefaults(),
        context_.get(), std::move(syncer), false);

    mock_server_->SetKeystoreKey("encryption_key");
  }

  ~SyncerTestBase() override {
    mock_server_.reset();
    scheduler_.reset();
  }

  SyncerTestBase(const SyncerTestBase&) = delete;
  SyncerTestBase& operator=(const SyncerTestBase&) = delete;

  // SyncCycle::Delegate implementation.
  void OnThrottled(const base::TimeDelta& throttle_duration) override {
    FAIL() << "Should not get silenced.";
  }

  void OnTypesThrottled(DataTypeSet types,
                        const base::TimeDelta& throttle_duration) override {
    scheduler_->OnTypesThrottled(types, throttle_duration);
  }

  void OnTypesBackedOff(DataTypeSet types) override {
    scheduler_->OnTypesBackedOff(types);
  }

  bool IsAnyThrottleOrBackoff() override { return false; }

  void OnReceivedPollIntervalUpdate(
      const base::TimeDelta& new_interval) override {
    last_poll_interval_received_ = new_interval;
  }

  void OnReceivedCustomNudgeDelays(
      const std::map<DataType, base::TimeDelta>& delay_map) override {
    auto iter = delay_map.find(BOOKMARKS);
    if (iter != delay_map.end() && iter->second.is_positive()) {
      last_bookmarks_commit_delay_ = iter->second;
    }
  }

  void OnReceivedMigrationRequest(DataTypeSet types) override {}
  void OnReceivedQuotaParamsForExtensionTypes(
      std::optional<int> max_tokens,
      std::optional<base::TimeDelta> refill_interval,
      std::optional<base::TimeDelta> depleted_quota_nudge_delay) override {}
  void OnProtocolEvent(const ProtocolEvent& event) override {}
  void OnSyncProtocolError(const SyncProtocolError& error) override {}

  void OnSyncCycleEvent(const SyncCycleEvent& event) override {
    DVLOG(1) << "HandleSyncEngineEvent in unittest " << event.what_happened;
  }

  void OnActionableProtocolError(const SyncProtocolError& error) override {}
  void OnRetryTimeChanged(base::Time retry_time) override {}
  void OnThrottledTypesChanged(DataTypeSet throttled_types) override {}
  void OnBackedOffTypesChanged(DataTypeSet backed_off_types) override {}
  void OnMigrationRequested(DataTypeSet types) override {}

  // Resets the sync cycle. When `kSyncUsePropagatedAccessToken` is enabled,
  // SyncerProtoUtil forwards `cycle->access_token_info()` to the network layer
  // (ServerConnectionManager) instead of relying on credentials cached inside
  // FakeConnectionManager. In real execution, SyncSchedulerImpl fetches and
  // attaches a token before creating the cycle. A valid token is supplied here
  // so Syncer's normal sync and configuration cycles succeed under the feature.
  void ResetCycle() {
    signin::AccessTokenInfo access_token_info;
    if (base::FeatureList::IsEnabled(kSyncUsePropagatedAccessToken)) {
      access_token_info.token = "AccessToken";
      access_token_info.expiration_time = base::Time::Now() + base::Hours(1);
    }
    cycle_ =
        std::make_unique<SyncCycle>(context_.get(), this, access_token_info);
  }

  bool SyncShareNudge() {
    ResetCycle();

    // Pretend we've seen a local change, to make the nudge_tracker look normal.
    nudge_tracker_.RecordLocalChange(BOOKMARKS, false);

    return syncer_->NormalSyncShare(context_->GetConnectedTypes(),
                                    &nudge_tracker_, cycle_.get());
  }

  bool SyncShareConfigure() {
    return SyncShareConfigureTypes(context_->GetConnectedTypes());
  }

  bool SyncShareConfigureTypes(DataTypeSet types) {
    ResetCycle();
    return syncer_->ConfigureSyncShare(
        types, sync_pb::SyncEnums::RECONFIGURATION, cycle_.get());
  }

  const std::string local_cache_guid() { return "lD16ebCGCZh+zkiZ68gWDw=="; }

  const std::string foreign_cache_guid() { return "kqyg7097kro6GSUod+GSg=="; }

  MockDataTypeProcessor* GetProcessor(DataType data_type) {
    return &mock_data_type_processors_[data_type];
  }

  std::unique_ptr<DataTypeActivationResponse> MakeFakeActivationResponse(
      DataType data_type) {
    auto response = std::make_unique<DataTypeActivationResponse>();
    response->data_type_state.set_initial_sync_state(
        sync_pb::DataTypeState_InitialSyncState_INITIAL_SYNC_DONE);
    response->data_type_state.mutable_progress_marker()->set_data_type_id(
        GetSpecificsFieldNumberFromDataType(data_type));
    response->type_processor =
        std::make_unique<ForwardingDataTypeProcessor>(GetProcessor(data_type));
    return response;
  }

  void EnableDatatype(DataType data_type) {
    enabled_datatypes_.Put(data_type);
    data_type_registry_->ConnectDataType(data_type,
                                         MakeFakeActivationResponse(data_type));
    mock_server_->ExpectGetUpdatesRequestTypes(enabled_datatypes_);
  }

  void DisableDatatype(DataType data_type) {
    enabled_datatypes_.Remove(data_type);
    data_type_registry_->DisconnectDataType(data_type);
    mock_server_->ExpectGetUpdatesRequestTypes(enabled_datatypes_);
  }

  // Configures SyncCycleContext and NudgeTracker so Syncer won't call
  // GetUpdates prior to Commit. This method can be used to ensure a Commit is
  // not preceeded by GetUpdates.
  void ConfigureNoGetUpdatesRequired() {
    nudge_tracker_.OnInvalidationsEnabled();
    nudge_tracker_.RecordSuccessfulSyncCycleIfNotBlocked(DataTypeSet::All());

    ASSERT_FALSE(nudge_tracker_.IsGetUpdatesRequired(DataTypeSet::All()));
  }

 protected:
  base::test::ScopedFeatureList feature_list_;
  base::test::SingleThreadTaskEnvironment task_environment_;

  FakeSyncEncryptionHandler encryption_handler_;
  scoped_refptr<ExtensionsActivity> extensions_activity_ =
      new ExtensionsActivity;
  std::unique_ptr<FakeConnectionManager> mock_server_;
  CancelationSignal cancelation_signal_;
  std::map<DataType, MockDataTypeProcessor> mock_data_type_processors_;

  raw_ptr<Syncer, DanglingUntriaged> syncer_ = nullptr;

  std::unique_ptr<SyncCycle> cycle_;
  MockNudgeHandler mock_nudge_handler_;
  std::unique_ptr<DataTypeRegistry> data_type_registry_;
  std::unique_ptr<SyncSchedulerImpl> scheduler_;
  std::unique_ptr<SyncCycleContext> context_;
  base::TimeDelta last_poll_interval_received_;
  base::TimeDelta last_bookmarks_commit_delay_;
  int last_client_invalidation_hint_buffer_size_ = 10;

  DataTypeSet enabled_datatypes_;
  NudgeTracker nudge_tracker_;
  std::unique_ptr<MockDebugInfoGetter> debug_info_getter_;
};

// Parameterized on `bool` (whether `kSyncUsePropagatedAccessToken` is enabled)
// to verify that Syncer correctly performs sync cycles (GetUpdates, Commits,
// Configuration) both with legacy cached auth and with access tokens propagated
// through SyncCycle.
class SyncerTest : public testing::TestWithParam<bool>, public SyncerTestBase {
 public:
  SyncerTest() : SyncerTestBase(GetParam()) {}
  ~SyncerTest() override = default;
};

TEST_P(SyncerTest, CommitFiltersThrottledEntries) {
  const DataTypeSet throttled_types = {BOOKMARKS};

  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  // Sync without enabling bookmarks.
  mock_server_->ExpectGetUpdatesRequestTypes(
      Difference(context_->GetConnectedTypes(), throttled_types));
  ResetCycle();
  syncer_->NormalSyncShare(
      Difference(context_->GetConnectedTypes(), throttled_types),
      &nudge_tracker_, cycle_.get());

  // Nothing should have been committed as bookmarks is throttled.
  EXPECT_EQ(0, GetProcessor(BOOKMARKS)->GetLocalChangesCallCount());

  // Sync again with bookmarks enabled.
  mock_server_->ExpectGetUpdatesRequestTypes(context_->GetConnectedTypes());
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, GetProcessor(BOOKMARKS)->GetLocalChangesCallCount());
}

TEST_P(SyncerTest, GetUpdatesPartialThrottled) {
  const sync_pb::EntitySpecifics bookmark = MakeSpecifics(BOOKMARKS);
  const sync_pb::EntitySpecifics pref = MakeSpecifics(PREFERENCES);

  // Normal sync, all the data types should get synced.
  mock_server_->AddUpdateSpecifics("1", "0", "A", 10, 10, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "B", 10, 10, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "C", 10, 10, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "D", 10, 10, false, pref);

  EXPECT_TRUE(SyncShareNudge());
  // Initial state. Everything is normal.
  ASSERT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
  ASSERT_EQ(3U, GetProcessor(BOOKMARKS)->GetNthUpdateResponse(0).size());
  ASSERT_EQ(1U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  ASSERT_EQ(1U, GetProcessor(PREFERENCES)->GetNthUpdateResponse(0).size());

  // Set BOOKMARKS throttled but PREFERENCES not,
  // then BOOKMARKS should not get synced but PREFERENCES should.
  DataTypeSet throttled_types = {BOOKMARKS};
  mock_server_->set_throttling(true);
  mock_server_->SetPartialFailureTypes(throttled_types);

  mock_server_->AddUpdateSpecifics("1", "0", "E", 20, 20, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "F", 20, 20, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "G", 20, 20, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "H", 20, 20, false, pref);
  EXPECT_TRUE(SyncShareNudge());

  // PREFERENCES continues to work normally (not throttled).
  ASSERT_EQ(2U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  // BOOKMARKS throttled.
  EXPECT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());

  // Unthrottled BOOKMARKS, then BOOKMARKS should get synced now.
  mock_server_->set_throttling(false);

  mock_server_->AddUpdateSpecifics("1", "0", "E", 30, 30, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "F", 30, 30, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "G", 30, 30, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "H", 30, 30, false, pref);
  EXPECT_TRUE(SyncShareNudge());
  // BOOKMARKS unthrottled.
  EXPECT_EQ(2U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
}

TEST_P(SyncerTest, GetUpdatesPartialFailure) {
  const sync_pb::EntitySpecifics bookmark = MakeSpecifics(BOOKMARKS);
  const sync_pb::EntitySpecifics pref = MakeSpecifics(PREFERENCES);

  // Normal sync, all the data types should get synced.
  mock_server_->AddUpdateSpecifics("1", "0", "A", 10, 10, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "B", 10, 10, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "C", 10, 10, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "D", 10, 10, false, pref);

  EXPECT_TRUE(SyncShareNudge());
  // Initial state. Everything is normal.
  ASSERT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
  ASSERT_EQ(3U, GetProcessor(BOOKMARKS)->GetNthUpdateResponse(0).size());
  ASSERT_EQ(1U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  ASSERT_EQ(1U, GetProcessor(PREFERENCES)->GetNthUpdateResponse(0).size());

  // Set BOOKMARKS failure but PREFERENCES not,
  // then BOOKMARKS should not get synced but PREFERENCES should.
  DataTypeSet failed_types = {BOOKMARKS};
  mock_server_->set_partial_failure(true);
  mock_server_->SetPartialFailureTypes(failed_types);

  mock_server_->AddUpdateSpecifics("1", "0", "E", 20, 20, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "F", 20, 20, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "G", 20, 20, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "H", 20, 20, false, pref);
  EXPECT_TRUE(SyncShareNudge());

  // PREFERENCES continues to work normally (not throttled).
  ASSERT_EQ(2U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  // BOOKMARKS failed.
  EXPECT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());

  // Set BOOKMARKS not partial failed, then BOOKMARKS should get synced now.
  mock_server_->set_partial_failure(false);

  mock_server_->AddUpdateSpecifics("1", "0", "E", 30, 30, true, bookmark,
                                   foreign_cache_guid(), "-1");
  mock_server_->AddUpdateSpecifics("2", "1", "F", 30, 30, false, bookmark,
                                   foreign_cache_guid(), "-2");
  mock_server_->AddUpdateSpecifics("3", "1", "G", 30, 30, false, bookmark,
                                   foreign_cache_guid(), "-3");
  mock_server_->AddUpdateSpecifics("4", "0", "H", 30, 30, false, pref);
  EXPECT_TRUE(SyncShareNudge());
  // BOOKMARKS not failed.
  EXPECT_EQ(2U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
}

TEST_P(SyncerTest, TestSimpleCommit) {
  const std::string kSyncId1 = "id1";
  const std::string kSyncId2 = "id2";

  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), kSyncId1);
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag2"),
                            MakeSpecifics(PREFERENCES), kSyncId2);

  EXPECT_TRUE(SyncShareNudge());
  EXPECT_THAT(mock_server_->committed_ids(),
              UnorderedElementsAre(kSyncId1, kSyncId2));
}

TEST_P(SyncerTest, TestSimpleGetUpdates) {
  std::string id = "some_id";
  std::string parent_id = "0";
  std::string name = "in_root";
  int64_t version = 10;
  int64_t timestamp = 10;
  mock_server_->AddUpdateDirectory(id, parent_id, name, version, timestamp,
                                   foreign_cache_guid(), "-1");

  EXPECT_TRUE(SyncShareNudge());

  ASSERT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
  std::vector<const UpdateResponseData*> updates_list =
      GetProcessor(BOOKMARKS)->GetNthUpdateResponse(0);
  EXPECT_EQ(1U, updates_list.size());

  const UpdateResponseData& update = *updates_list.back();
  const EntityData& entity = update.entity;

  EXPECT_EQ(id, entity.id);
  EXPECT_EQ(version, update.response_version);
  // Creation time hardcoded in MockConnectionManager::AddUpdateMeta().
  EXPECT_EQ(ProtoTimeToTime(1), entity.creation_time);
  EXPECT_EQ(ProtoTimeToTime(timestamp), entity.modification_time);
  EXPECT_EQ(name, entity.name);
  EXPECT_FALSE(entity.is_deleted());
}

// Committing more than kDefaultMaxCommitBatchSize items requires that
// we post more than one commit command to the server.  This test makes
// sure that scenario works as expected.
TEST_P(SyncerTest, CommitManyItemsInOneGo_Success) {
  int num_batches = 3;
  int items_to_commit = kDefaultMaxCommitBatchSize * num_batches;

  for (int i = 0; i < items_to_commit; i++) {
    GetProcessor(PREFERENCES)
        ->AppendCommitRequest(
            ClientTagHash::FromHashed(base::StringPrintf("tag%d", i)),
            MakeSpecifics(PREFERENCES));
  }

  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(static_cast<size_t>(num_batches),
            mock_server_->commit_messages().size());

  ASSERT_EQ(static_cast<size_t>(num_batches),
            GetProcessor(PREFERENCES)->GetNumCommitResponses());
  EXPECT_EQ(static_cast<size_t>(kDefaultMaxCommitBatchSize),
            GetProcessor(PREFERENCES)->GetNthCommitResponse(0).size());
  EXPECT_EQ(static_cast<size_t>(kDefaultMaxCommitBatchSize),
            GetProcessor(PREFERENCES)->GetNthCommitResponse(1).size());
  EXPECT_EQ(static_cast<size_t>(kDefaultMaxCommitBatchSize),
            GetProcessor(PREFERENCES)->GetNthCommitResponse(2).size());
}

// Test that a single failure to contact the server will cause us to exit the
// commit loop immediately.
TEST_P(SyncerTest, CommitManyItemsInOneGo_PostBufferFail) {
  int num_batches = 3;
  int items_to_commit = kDefaultMaxCommitBatchSize * num_batches;

  for (int i = 0; i < items_to_commit; i++) {
    GetProcessor(PREFERENCES)
        ->AppendCommitRequest(
            ClientTagHash::FromHashed(base::StringPrintf("tag%d", i)),
            MakeSpecifics(PREFERENCES));
  }

  // The second commit should fail.  It will be preceded by one successful
  // GetUpdate and one succesful commit.
  mock_server_->FailNthPostBufferToPathCall(3);
  base::HistogramTester histogram_tester;
  EXPECT_FALSE(SyncShareNudge());

  EXPECT_EQ(1U, mock_server_->commit_messages().size());
  ASSERT_EQ(
      cycle_->status_controller().model_neutral_state().commit_result.type(),
      SyncerError::Type::kHttpError);

  // Since the second batch fails, the third one should not even be gathered.
  EXPECT_EQ(2, GetProcessor(PREFERENCES)->GetLocalChangesCallCount());

  histogram_tester.ExpectBucketCount("Sync.CommitResponse.PREFERENCE",
                                     SyncerErrorValueForUma::kHttpError,
                                     /*expected_count=*/1);
  histogram_tester.ExpectBucketCount("Sync.CommitResponse",
                                     SyncerErrorValueForUma::kHttpError,
                                     /*expected_count=*/1);

  // Latency is not recorded for failed commits (only 1 commit succeeded).
  histogram_tester.ExpectBucketCount("Sync.CommitResponse",
                                     SyncerErrorValueForUma::kSyncerOk,
                                     /*expected_count=*/1);
  histogram_tester.ExpectTotalCount("Sync.CommitLatency", /*expected_count=*/1);
  histogram_tester.ExpectTotalCount("Sync.CommitLatency.PREFERENCE",
                                    /*expected_count=*/1);
}

// Test that a single conflict response from the server will cause us to exit
// the commit loop immediately.
TEST_P(SyncerTest, CommitManyItemsInOneGo_CommitConflict) {
  int num_batches = 2;
  int items_to_commit = kDefaultMaxCommitBatchSize * num_batches;

  for (int i = 0; i < items_to_commit; i++) {
    GetProcessor(PREFERENCES)
        ->AppendCommitRequest(
            ClientTagHash::FromHashed(base::StringPrintf("tag%d", i)),
            MakeSpecifics(PREFERENCES));
  }

  // Return a CONFLICT response for the first item.
  mock_server_->set_conflict_n_commits(1);
  EXPECT_FALSE(SyncShareNudge());

  // We should stop looping at the first sign of trouble.
  EXPECT_EQ(1U, mock_server_->commit_messages().size());
  EXPECT_EQ(1, GetProcessor(PREFERENCES)->GetLocalChangesCallCount());
}

// Tests that sending debug info events works.
TEST_P(SyncerTest, SendDebugInfoEventsOnGetUpdates_HappyCase) {
  debug_info_getter_->AddDebugEvent();
  debug_info_getter_->AddDebugEvent();

  EXPECT_TRUE(SyncShareNudge());

  // Verify we received one GetUpdates request with two debug info events.
  EXPECT_EQ(1U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(2, mock_server_->last_request().debug_info().events_size());

  EXPECT_TRUE(SyncShareNudge());

  // See that we received another GetUpdates request, but that it contains no
  // debug info events.
  EXPECT_EQ(2U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(0, mock_server_->last_request().debug_info().events_size());

  debug_info_getter_->AddDebugEvent();

  EXPECT_TRUE(SyncShareNudge());

  // See that we received another GetUpdates request and it contains one debug
  // info event.
  EXPECT_EQ(3U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(1, mock_server_->last_request().debug_info().events_size());
}

// Tests that debug info events are dropped on server error.
TEST_P(SyncerTest, SendDebugInfoEventsOnGetUpdates_PostFailsDontDrop) {
  debug_info_getter_->AddDebugEvent();
  debug_info_getter_->AddDebugEvent();

  mock_server_->FailNextPostBufferToPathCall();
  EXPECT_FALSE(SyncShareNudge());

  // Verify we attempted to send one GetUpdates request with two debug info
  // events.
  EXPECT_EQ(1U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(2, mock_server_->last_request().debug_info().events_size());

  EXPECT_TRUE(SyncShareNudge());

  // See that the client resent the two debug info events.
  EXPECT_EQ(2U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(2, mock_server_->last_request().debug_info().events_size());

  // The previous send was successful so this next one shouldn't generate any
  // debug info events.
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(3U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_get_updates());
  EXPECT_EQ(0, mock_server_->last_request().debug_info().events_size());
}

// Tests that commit failure with conflict will trigger GetUpdates for next
// cycle of sync
TEST_P(SyncerTest, CommitFailureWithConflict) {
  ConfigureNoGetUpdatesRequired();

  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");

  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired(DataTypeSet::All()));

  EXPECT_TRUE(SyncShareNudge());
  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired(DataTypeSet::All()));

  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");

  mock_server_->set_conflict_n_commits(1);
  EXPECT_FALSE(SyncShareNudge());
  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired(DataTypeSet::All()));

  nudge_tracker_.RecordSuccessfulSyncCycleIfNotBlocked(DataTypeSet::All());
  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired(DataTypeSet::All()));
}

// Tests that sending debug info events on Commit works.
TEST_P(SyncerTest, SendDebugInfoEventsOnCommit_HappyCase) {
  // Make sure GetUpdate isn't call as it would "steal" debug info events before
  // Commit has a chance to send them.
  ConfigureNoGetUpdatesRequired();

  // Generate a debug info event and trigger a commit.
  debug_info_getter_->AddDebugEvent();
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");
  EXPECT_TRUE(SyncShareNudge());

  // Verify that the last request received is a Commit and that it contains a
  // debug info event.
  EXPECT_EQ(1U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_EQ(1, mock_server_->last_request().debug_info().events_size());

  // Generate another commit, but no debug info event.
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag2"),
                            MakeSpecifics(PREFERENCES), "id2");
  EXPECT_TRUE(SyncShareNudge());

  // See that it was received and contains no debug info events.
  EXPECT_EQ(2U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_EQ(0, mock_server_->last_request().debug_info().events_size());
}

// Tests that debug info events are not dropped on server error.
TEST_P(SyncerTest, SendDebugInfoEventsOnCommit_PostFailsDontDrop) {
  // Make sure GetUpdate isn't call as it would "steal" debug info events before
  // Commit has a chance to send them.
  ConfigureNoGetUpdatesRequired();

  mock_server_->FailNextPostBufferToPathCall();

  // Generate a debug info event and trigger a commit.
  debug_info_getter_->AddDebugEvent();
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");
  EXPECT_FALSE(SyncShareNudge());

  // Verify that the last request sent is a Commit and that it contains a debug
  // info event.
  EXPECT_EQ(1U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_EQ(1, mock_server_->last_request().debug_info().events_size());

  // Try again. Because of how MockDataTypeProcessor works, commit data needs
  // to be provided again.
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");
  EXPECT_TRUE(SyncShareNudge());

  // Verify that we've received another Commit and that it contains a debug info
  // event (just like the previous one).
  EXPECT_EQ(2U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_EQ(1, mock_server_->last_request().debug_info().events_size());

  // Generate another commit and try again.
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag2"),
                            MakeSpecifics(PREFERENCES), "id2");
  EXPECT_TRUE(SyncShareNudge());

  // See that it was received and contains no debug info events.
  EXPECT_EQ(3U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_EQ(0, mock_server_->last_request().debug_info().events_size());
}

TEST_P(SyncerTest, TestClientCommandDuringUpdate) {
  using sync_pb::ClientCommand;

  auto command = std::make_unique<ClientCommand>();
  command->set_set_sync_poll_interval(8);
  command->set_set_sync_long_poll_interval(800);
  sync_pb::CustomNudgeDelay* bookmark_delay =
      command->add_custom_nudge_delays();
  bookmark_delay->set_datatype_id(
      GetSpecificsFieldNumberFromDataType(BOOKMARKS));
  bookmark_delay->set_delay_ms(950);
  mock_server_->AddUpdateDirectory("1", "0", "in_root", 1, 1,
                                   foreign_cache_guid(), "-1");
  mock_server_->SetGUClientCommand(std::move(command));
  EXPECT_TRUE(SyncShareNudge());

  EXPECT_EQ(base::Seconds(8), last_poll_interval_received_);
  EXPECT_EQ(base::Milliseconds(950), last_bookmarks_commit_delay_);

  command = std::make_unique<ClientCommand>();
  command->set_set_sync_poll_interval(180);
  command->set_set_sync_long_poll_interval(190);
  bookmark_delay = command->add_custom_nudge_delays();
  bookmark_delay->set_datatype_id(
      GetSpecificsFieldNumberFromDataType(BOOKMARKS));
  bookmark_delay->set_delay_ms(1050);
  mock_server_->AddUpdateDirectory("1", "0", "in_root", 1, 1,
                                   foreign_cache_guid(), "-1");
  mock_server_->SetGUClientCommand(std::move(command));
  EXPECT_TRUE(SyncShareNudge());

  EXPECT_EQ(base::Seconds(180), last_poll_interval_received_);
  EXPECT_EQ(base::Milliseconds(1050), last_bookmarks_commit_delay_);
}

TEST_P(SyncerTest, TestClientCommandDuringCommit) {
  using sync_pb::ClientCommand;

  auto command = std::make_unique<ClientCommand>();
  command->set_set_sync_poll_interval(8);
  command->set_set_sync_long_poll_interval(800);
  sync_pb::CustomNudgeDelay* bookmark_delay =
      command->add_custom_nudge_delays();
  bookmark_delay->set_datatype_id(
      GetSpecificsFieldNumberFromDataType(BOOKMARKS));
  bookmark_delay->set_delay_ms(950);
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");
  mock_server_->SetCommitClientCommand(std::move(command));
  EXPECT_TRUE(SyncShareNudge());

  EXPECT_EQ(base::Seconds(8), last_poll_interval_received_);
  EXPECT_EQ(base::Milliseconds(950), last_bookmarks_commit_delay_);

  command = std::make_unique<ClientCommand>();
  command->set_set_sync_poll_interval(180);
  command->set_set_sync_long_poll_interval(190);
  bookmark_delay = command->add_custom_nudge_delays();
  bookmark_delay->set_datatype_id(
      GetSpecificsFieldNumberFromDataType(BOOKMARKS));
  bookmark_delay->set_delay_ms(1050);
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag2"), MakeBookmarkSpecificsToCommit(),
      "id2");
  mock_server_->SetCommitClientCommand(std::move(command));
  EXPECT_TRUE(SyncShareNudge());

  EXPECT_EQ(base::Seconds(180), last_poll_interval_received_);
  EXPECT_EQ(base::Milliseconds(1050), last_bookmarks_commit_delay_);
}

TEST_P(SyncerTest, ShouldPopulateSingleClientFlag) {
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  // No other devices are interested in bookmarks.
  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          /*all_fcm_registration_tokens=*/{},
          /*all_interested_data_types=*/{PREFERENCES},
          /*fcm_token_and_interested_data_types=*/{},
          /*old_invalidations_interested_data_types=*/{}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_TRUE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_TRUE(mock_server_->last_request()
                  .commit()
                  .config_params()
                  .single_client_with_standalone_invalidations());
  EXPECT_TRUE(mock_server_->last_request()
                  .commit()
                  .config_params()
                  .single_client_with_old_invalidations());
}

TEST_P(SyncerTest,
       ShouldPopulateSingleClientFlagForStandaloneInvalidationsOnly) {
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  // No other devices with standalone invalidations are interested in bookmarks.
  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          /*all_fcm_registration_tokens=*/{"token_1"},
          /*all_interested_data_types=*/{BOOKMARKS, PREFERENCES},
          /*fcm_token_and_interested_data_types=*/
          {{"token_1", {PREFERENCES}}},
          /*old_invalidations_interested_data_types=*/{BOOKMARKS}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_TRUE(mock_server_->last_request()
                  .commit()
                  .config_params()
                  .single_client_with_standalone_invalidations());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_old_invalidations());
}

TEST_P(SyncerTest, ShouldPopulateSingleClientForOldInvalidations) {
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  // No other devices without standalone invalidations are interested in
  // bookmarks.
  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          /*all_fcm_registration_tokens=*/{"token_1"},
          /*all_interested_data_types=*/{BOOKMARKS, PREFERENCES},
          /*fcm_token_and_interested_data_types=*/
          {{"token_1", {BOOKMARKS, PREFERENCES}}},
          /*old_invalidations_interested_data_types=*/{PREFERENCES}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_standalone_invalidations());
  EXPECT_TRUE(mock_server_->last_request()
                  .commit()
                  .config_params()
                  .single_client_with_old_invalidations());
}

TEST_P(SyncerTest, ShouldPopulateFcmRegistrationTokens) {
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          {"token"}, /*all_interested_data_types=*/{BOOKMARKS},
          /*fcm_token_and_interested_data_types=*/{{"token", {BOOKMARKS}}},
          /*old_invalidations_interested_data_types=*/{}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_standalone_invalidations());
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .devices_fcm_registration_tokens(),
              ElementsAre("token"));
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .fcm_registration_tokens_for_interested_clients(),
              ElementsAre("token"));
}

TEST_P(SyncerTest, ShouldPopulateFcmRegistrationTokensForInterestedTypesOnly) {
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          {"token_1", "token_2"}, /*all_interested_data_types=*/{BOOKMARKS},
          /*fcm_token_and_interested_data_types=*/
          {{"token_1", {BOOKMARKS}}, {"token_2", {PREFERENCES}}},
          /*old_invalidations_interested_data_types=*/{}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_standalone_invalidations());
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .devices_fcm_registration_tokens(),
              ElementsAre("token_1", "token_2"));
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .fcm_registration_tokens_for_interested_clients(),
              ElementsAre("token_1"));
}

TEST_P(SyncerTest, ShouldNotPopulateTooManyFcmRegistrationTokens) {
  std::map<std::string, DataTypeSet> fcm_token_and_interested_data_types;
  for (size_t i = 0; i < 7; ++i) {
    fcm_token_and_interested_data_types["token_" + base::NumberToString(i)] = {
        BOOKMARKS};
  }
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          {}, /*all_interested_data_types=*/{BOOKMARKS},
          std::move(fcm_token_and_interested_data_types),
          /*old_invalidations_interested_data_types=*/{}));
  ASSERT_TRUE(SyncShareNudge());
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_standalone_invalidations());
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .devices_fcm_registration_tokens(),
              IsEmpty());
  EXPECT_THAT(mock_server_->last_sent_commit()
                  .config_params()
                  .fcm_registration_tokens_for_interested_clients(),
              IsEmpty());
}

TEST_P(SyncerTest,
       ShouldNotPopulateOptimizationFlagsIfDeviceInfoRecentlyUpdated) {
  EnableDatatype(DEVICE_INFO);
  mock_server_->AddUpdateSpecifics("id", /*parent_id=*/"", "name",
                                   /*version=*/1, /*sync_ts=*/10,
                                   /*is_dir=*/false, /*specifics=*/
                                   MakeSpecifics(DEVICE_INFO));
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      "id1");

  // No other devices are interested in bookmarks.
  context_->set_active_devices_invalidation_info(
      ActiveDevicesInvalidationInfo::Create(
          {"token"}, /*all_interested_data_types=*/{PREFERENCES},
          /*fcm_token_and_interested_data_types=*/{{"token", {PREFERENCES}}},
          /*old_invalidations_interested_data_types=*/{}));
  ASSERT_TRUE(SyncShareNudge());

  // All invalidation info should be ignored due to DeviceInfo update.
  EXPECT_FALSE(
      mock_server_->last_request().commit().config_params().single_client());
  EXPECT_FALSE(mock_server_->last_request()
                   .commit()
                   .config_params()
                   .single_client_with_standalone_invalidations());
  EXPECT_TRUE(mock_server_->last_sent_commit()
                  .config_params()
                  .devices_fcm_registration_tokens()
                  .empty());
  EXPECT_TRUE(mock_server_->last_sent_commit()
                  .config_params()
                  .fcm_registration_tokens_for_interested_clients()
                  .empty());
}

TEST_P(SyncerTest, ClientTagServerCreatedUpdatesWork) {
  mock_server_->AddUpdateDirectory("1", "0", "permitem1", 1, 10,
                                   foreign_cache_guid(), "-1");
  mock_server_->SetLastUpdateClientTag("clienttag");

  EXPECT_TRUE(SyncShareNudge());

  ASSERT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
  std::vector<const UpdateResponseData*> updates_list =
      GetProcessor(BOOKMARKS)->GetNthUpdateResponse(0);
  EXPECT_EQ(1U, updates_list.size());

  const UpdateResponseData& update = *updates_list.back();
  const EntityData& entity = update.entity;

  EXPECT_EQ("permitem1", entity.name);
  EXPECT_EQ(ClientTagHash::FromHashed("clienttag"), entity.client_tag_hash);
  EXPECT_FALSE(entity.is_deleted());
}

TEST_P(SyncerTest, GetUpdatesSetsRequestedTypes) {
  // The expectations of this test happen in the MockConnectionManager's
  // GetUpdates handler.  EnableDatatype sets the expectation value from our
  // set of enabled/disabled datatypes.
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, mock_server_->GetAndClearNumGetUpdatesRequests());

  EnableDatatype(AUTOFILL);
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, mock_server_->GetAndClearNumGetUpdatesRequests());

  DisableDatatype(BOOKMARKS);
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, mock_server_->GetAndClearNumGetUpdatesRequests());

  DisableDatatype(AUTOFILL);
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, mock_server_->GetAndClearNumGetUpdatesRequests());

  DisableDatatype(PREFERENCES);
  EnableDatatype(AUTOFILL);
  EXPECT_TRUE(SyncShareNudge());
  EXPECT_EQ(1, mock_server_->GetAndClearNumGetUpdatesRequests());
}

// A typical scenario: server and client each have one update for the other.
// This is the "happy path" alternative to UpdateFailsThenDontCommit.
TEST_P(SyncerTest, UpdateThenCommit) {
  std::string to_receive = "some_id1";
  std::string to_commit = "some_id2";
  std::string parent_id = "0";
  mock_server_->AddUpdateDirectory(to_receive, parent_id, "x", 1, 10,
                                   foreign_cache_guid(), "-1");
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      to_commit);

  EXPECT_TRUE(SyncShareNudge());

  // The sync cycle should have included a GetUpdate, then a commit.
  EXPECT_TRUE(mock_server_->last_request().has_commit());
  EXPECT_THAT(mock_server_->committed_ids(), UnorderedElementsAre(to_commit));

  // The update should have been received.
  ASSERT_EQ(1U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());
  std::vector<const UpdateResponseData*> updates_list =
      GetProcessor(BOOKMARKS)->GetNthUpdateResponse(0);
  ASSERT_EQ(1U, updates_list.size());
  EXPECT_EQ(to_receive, updates_list[0]->entity.id);
}

// Same as above, but this time we fail to download updates.
// We should not attempt to commit anything unless we successfully downloaded
// updates, otherwise we risk causing a server-side conflict.
TEST_P(SyncerTest, UpdateFailsThenDontCommit) {
  std::string to_receive = "some_id1";
  std::string to_commit = "some_id2";
  std::string parent_id = "0";
  mock_server_->AddUpdateDirectory(to_receive, parent_id, "x", 1, 10,
                                   foreign_cache_guid(), "-1");
  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("tag1"), MakeBookmarkSpecificsToCommit(),
      to_commit);

  mock_server_->FailNextPostBufferToPathCall();
  EXPECT_FALSE(SyncShareNudge());

  // We did not receive this update.
  EXPECT_EQ(0U, GetProcessor(BOOKMARKS)->GetNumUpdateResponses());

  // No commit should have been sent.
  EXPECT_FALSE(mock_server_->last_request().has_commit());
  EXPECT_THAT(mock_server_->committed_ids(), IsEmpty());

  // Inform the Mock we won't be fetching all updates.
  mock_server_->ClearUpdatesQueue();
}

// Downloads two updates successfully.
// This is the "happy path" alternative to ConfigureFailsDontApplyUpdates.
TEST_P(SyncerTest, ConfigureDownloadsTwoBatchesSuccess) {
  // Construct the first GetUpdates response.
  mock_server_->AddUpdatePref("id1", "", "one", 1, 10);
  mock_server_->SetChangesRemaining(1);
  mock_server_->NextUpdateBatch();

  // Construct the second GetUpdates response.
  mock_server_->AddUpdatePref("id2", "", "two", 2, 20);

  ASSERT_EQ(0U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());

  SyncShareConfigure();

  // The type should have received the initial updates.
  EXPECT_EQ(1U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  EXPECT_THAT(mock_server_->requests(), SizeIs(2));
}

// Same as the above case, but this time the second batch fails to download.
TEST_P(SyncerTest, ConfigureFailsDontApplyUpdates) {
  // The scenario: we have two batches of updates with one update each. A
  // normal configure step would download all the updates one batch at a time
  // and apply them. This configure will succeed in downloading the first batch
  // then fail when downloading the second.
  mock_server_->FailNthPostBufferToPathCall(2);

  // Construct the first GetUpdates response.
  mock_server_->AddUpdatePref("id1", "", "one", 1, 10);
  mock_server_->SetChangesRemaining(1);
  mock_server_->NextUpdateBatch();

  // Construct the second GetUpdates response.
  mock_server_->AddUpdatePref("id2", "", "two", 2, 20);

  ASSERT_EQ(0U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());

  SyncShareConfigure();

  // The processor should not have received the initial sync data.
  EXPECT_EQ(0U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());

  // One update remains undownloaded.
  mock_server_->ClearUpdatesQueue();
}

// Tests that if type is not registered with DataTypeRegistry (e.g. because
// type's LoadModels failed), Syncer::ConfigureSyncShare runs without triggering
// DCHECK.
TEST_P(SyncerTest, ConfigureFailedUnregisteredType) {
  // Simulate type being unregistered before configuration by including type
  // that isn't registered with DataTypeRegistry.
  SyncShareConfigureTypes({APPS});

  // No explicit verification, DCHECK shouldn't have been triggered.
}

TEST_P(SyncerTest, GetKeySuccess) {
  KeystoreKeysHandler* keystore_keys_handler =
      data_type_registry_->keystore_keys_handler();
  EXPECT_TRUE(keystore_keys_handler->NeedKeystoreKey());

  SyncShareConfigure();

  EXPECT_FALSE(cycle_->status_controller().last_get_key_failed());
  EXPECT_FALSE(keystore_keys_handler->NeedKeystoreKey());
}

TEST_P(SyncerTest, GetKeyEmpty) {
  KeystoreKeysHandler* keystore_keys_handler =
      data_type_registry_->keystore_keys_handler();
  EXPECT_TRUE(keystore_keys_handler->NeedKeystoreKey());

  mock_server_->SetKeystoreKey(std::string());
  SyncShareConfigure();

  EXPECT_TRUE(cycle_->status_controller().last_get_key_failed());
  EXPECT_TRUE(keystore_keys_handler->NeedKeystoreKey());
}

// Verify that commit only types are never requested in GetUpdates, but still
// make it into the commit messages. Additionally, make sure failing GU types
// are correctly removed before commit.
TEST_P(SyncerTest, CommitOnlyTypes) {
  mock_server_->set_partial_failure(true);
  mock_server_->SetPartialFailureTypes({PREFERENCES});

  EnableDatatype(USER_EVENTS);

  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag1"),
                            MakeSpecifics(PREFERENCES), "id1");
  GetProcessor(EXTENSIONS)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag2"),
                            MakeSpecifics(EXTENSIONS), "id2");
  GetProcessor(USER_EVENTS)
      ->AppendCommitRequest(ClientTagHash::FromHashed("tag3"),
                            MakeSpecifics(USER_EVENTS), "id3");

  EXPECT_TRUE(SyncShareNudge());

  ASSERT_EQ(2U, mock_server_->requests().size());
  ASSERT_TRUE(mock_server_->requests()[0].has_get_updates());
  // MockConnectionManager will ensure USER_EVENTS was not included in the GU.
  EXPECT_EQ(
      4, mock_server_->requests()[0].get_updates().from_progress_marker_size());

  ASSERT_TRUE(mock_server_->requests()[1].has_commit());
  const sync_pb::CommitMessage commit = mock_server_->requests()[1].commit();
  EXPECT_EQ(2, commit.entries_size());
  EXPECT_TRUE(commit.entries(0).specifics().has_extension());
  EXPECT_TRUE(commit.entries(1).specifics().has_user_event());
}

TEST_P(SyncerTest, ShouldEarlyExitDownloadIfRequested) {
  // Construct the first GetUpdates response.
  mock_server_->AddUpdatePref("id1", "", "one", 1, 10);
  mock_server_->SetChangesRemaining(1);
  mock_server_->NextUpdateBatch();

  // Construct the second GetUpdates response.
  mock_server_->AddUpdatePref("id2", "", "two", 2, 20);

  ASSERT_EQ(0U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());

  // Request early exit. The first GetUpdates response should be downloaded, but
  // the second one should be skipped.
  cancelation_signal_.Signal();
  SyncShareConfigure();

  // No updates should be applied to the processor but there should be a single
  // GetUpdates request.
  EXPECT_EQ(0U, GetProcessor(PREFERENCES)->GetNumUpdateResponses());
  EXPECT_THAT(mock_server_->requests(), SizeIs(1));

  // One update is still pending.
  mock_server_->ClearUpdatesQueue();
}

enum {
  TEST_PARAM_BOOKMARK_ENABLE_BIT,
  TEST_PARAM_AUTOFILL_ENABLE_BIT,
  TEST_PARAM_BIT_COUNT
};

class MixedResult : public testing::TestWithParam<std::tuple<bool, int>>,
                    public SyncerTestBase {
 public:
  MixedResult() : SyncerTestBase(std::get<0>(GetParam())) {}
  ~MixedResult() override = default;

 protected:
  bool ShouldFailBookmarkCommit() {
    return (std::get<1>(GetParam()) & (1 << TEST_PARAM_BOOKMARK_ENABLE_BIT)) ==
           0;
  }
  bool ShouldFailAutofillCommit() {
    return (std::get<1>(GetParam()) & (1 << TEST_PARAM_AUTOFILL_ENABLE_BIT)) ==
           0;
  }
};

INSTANTIATE_TEST_SUITE_P(
    ExtensionsActivity,
    MixedResult,
    testing::Combine(testing::Bool(),
                     testing::Range(0, 1 << TEST_PARAM_BIT_COUNT)));

TEST_P(MixedResult, ExtensionsActivity) {
  GetProcessor(PREFERENCES)
      ->AppendCommitRequest(ClientTagHash::FromHashed("pref1"),
                            MakeSpecifics(PREFERENCES), "prefid1");

  GetProcessor(BOOKMARKS)->AppendCommitRequest(
      ClientTagHash::FromHashed("bookmark1"), MakeBookmarkSpecificsToCommit(),
      "bookmarkid2");

  if (ShouldFailBookmarkCommit()) {
    mock_server_->SetTransientErrorId("bookmarkid2");
  }

  if (ShouldFailAutofillCommit()) {
    mock_server_->SetTransientErrorId("prefid1");
  }

  // Put some extensions activity records into the monitor.
  {
    ExtensionsActivity::Records records;
    records["ABC"].extension_id = "ABC";
    records["ABC"].bookmark_write_count = 2049U;
    records["xyz"].extension_id = "xyz";
    records["xyz"].bookmark_write_count = 4U;
    context_->extensions_activity()->PutRecords(records);
  }

  EXPECT_EQ(!ShouldFailBookmarkCommit() && !ShouldFailAutofillCommit(),
            SyncShareNudge());

  ExtensionsActivity::Records final_monitor_records;
  context_->extensions_activity()->GetAndClearRecords(&final_monitor_records);
  if (ShouldFailBookmarkCommit()) {
    ASSERT_EQ(2U, final_monitor_records.size())
        << "Should restore records after unsuccessful bookmark commit.";
    EXPECT_EQ("ABC", final_monitor_records["ABC"].extension_id);
    EXPECT_EQ("xyz", final_monitor_records["xyz"].extension_id);
    EXPECT_EQ(2049U, final_monitor_records["ABC"].bookmark_write_count);
    EXPECT_EQ(4U, final_monitor_records["xyz"].bookmark_write_count);
  } else {
    EXPECT_TRUE(final_monitor_records.empty())
        << "Should not restore records after successful bookmark commit.";
  }
}

INSTANTIATE_TEST_SUITE_P(,
                         SyncerTest,
                         testing::Bool(),
                         [](const testing::TestParamInfo<bool>& info) {
                           return info.param ? "PropagatedAccessToken"
                                             : "CachedAccessToken";
                         });

}  // namespace syncer
