// Copyright (c) 2012 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 "sync/sessions/sync_session.h"

#include "base/compiler_specific.h"
#include "base/location.h"
#include "base/memory/ref_counted.h"
#include "base/message_loop.h"
#include "sync/engine/conflict_resolver.h"
#include "sync/engine/syncer_types.h"
#include "sync/engine/throttled_data_type_tracker.h"
#include "sync/internal_api/public/base/model_type.h"
#include "sync/internal_api/public/base/model_type_state_map_test_util.h"
#include "sync/sessions/session_state.h"
#include "sync/sessions/status_controller.h"
#include "sync/syncable/syncable_id.h"
#include "sync/syncable/write_transaction.h"
#include "sync/test/engine/fake_model_worker.h"
#include "sync/test/engine/test_directory_setter_upper.h"
#include "sync/test/fake_extensions_activity_monitor.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace syncer {

using syncable::WriteTransaction;

namespace sessions {
namespace {

class SyncSessionTest : public testing::Test,
                        public SyncSession::Delegate {
 public:
  SyncSessionTest() : controller_invocations_allowed_(false) {}

  SyncSession* MakeSession() {
    std::vector<ModelSafeWorker*> workers;
    GetWorkers(&workers);
    return new SyncSession(context_.get(), this, SyncSourceInfo(),
                           routes_, workers);
  }

  virtual void SetUp() {
    routes_.clear();
    routes_[BOOKMARKS] = GROUP_UI;
    routes_[AUTOFILL] = GROUP_DB;
    scoped_refptr<ModelSafeWorker> passive_worker(
        new FakeModelWorker(GROUP_PASSIVE));
    scoped_refptr<ModelSafeWorker> ui_worker(
        new FakeModelWorker(GROUP_UI));
    scoped_refptr<ModelSafeWorker> db_worker(
        new FakeModelWorker(GROUP_DB));
    workers_.clear();
    workers_.push_back(passive_worker);
    workers_.push_back(ui_worker);
    workers_.push_back(db_worker);

    std::vector<ModelSafeWorker*> workers;
    GetWorkers(&workers);

    context_.reset(
        new SyncSessionContext(
            NULL,
            NULL,
            workers,
            &extensions_activity_monitor_,
            throttled_data_type_tracker_.get(),
            std::vector<SyncEngineEventListener*>(),
            NULL,
            NULL,
            true  /* enable keystore encryption */));
    context_->set_routing_info(routes_);

    session_.reset(MakeSession());
    throttled_data_type_tracker_.reset(new ThrottledDataTypeTracker(NULL));
  }
  virtual void TearDown() {
    session_.reset();
    context_.reset();
  }

  virtual void OnSilencedUntil(const base::TimeTicks& silenced_until) OVERRIDE {
    FailControllerInvocationIfDisabled("OnSilencedUntil");
  }
  virtual bool IsSyncingCurrentlySilenced() OVERRIDE {
    FailControllerInvocationIfDisabled("IsSyncingCurrentlySilenced");
    return false;
  }
  virtual void OnReceivedLongPollIntervalUpdate(
      const base::TimeDelta& new_interval) OVERRIDE {
    FailControllerInvocationIfDisabled("OnReceivedLongPollIntervalUpdate");
  }
  virtual void OnReceivedShortPollIntervalUpdate(
      const base::TimeDelta& new_interval) OVERRIDE {
    FailControllerInvocationIfDisabled("OnReceivedShortPollIntervalUpdate");
  }
  virtual void OnReceivedSessionsCommitDelay(
      const base::TimeDelta& new_delay) OVERRIDE {
    FailControllerInvocationIfDisabled("OnReceivedSessionsCommitDelay");
  }
  virtual void OnShouldStopSyncingPermanently() OVERRIDE {
    FailControllerInvocationIfDisabled("OnShouldStopSyncingPermanently");
  }
  virtual void OnSyncProtocolError(
      const sessions::SyncSessionSnapshot& snapshot) {
    FailControllerInvocationIfDisabled("SyncProtocolError");
  }

  void GetWorkers(std::vector<ModelSafeWorker*>* out) const {
    out->clear();
    for (std::vector<scoped_refptr<ModelSafeWorker> >::const_iterator it =
             workers_.begin(); it != workers_.end(); ++it) {
      out->push_back(it->get());
    }
  }
  void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) const {
    *out = routes_;
  }

  StatusController* status() { return session_->mutable_status_controller(); }
 protected:
  void FailControllerInvocationIfDisabled(const std::string& msg) {
    if (!controller_invocations_allowed_)
      FAIL() << msg;
  }

  ModelTypeSet ParamsMeaningAllEnabledTypes() {
    ModelTypeSet request_params(BOOKMARKS, AUTOFILL);
    return request_params;
  }

  ModelTypeSet ParamsMeaningJustOneEnabledType() {
    return ModelTypeSet(AUTOFILL);
  }

  MessageLoop message_loop_;
  bool controller_invocations_allowed_;
  scoped_ptr<SyncSession> session_;
  scoped_ptr<SyncSessionContext> context_;
  std::vector<scoped_refptr<ModelSafeWorker> > workers_;
  ModelSafeRoutingInfo routes_;
  FakeExtensionsActivityMonitor extensions_activity_monitor_;
  scoped_ptr<ThrottledDataTypeTracker> throttled_data_type_tracker_;
};

TEST_F(SyncSessionTest, EnabledGroupsEmpty) {
  routes_.clear();
  workers_.clear();
  scoped_ptr<SyncSession> session(MakeSession());
  std::set<ModelSafeGroup> expected_enabled_groups;
  expected_enabled_groups.insert(GROUP_PASSIVE);
  EXPECT_EQ(expected_enabled_groups, session->GetEnabledGroups());
}

TEST_F(SyncSessionTest, EnabledGroups) {
  scoped_ptr<SyncSession> session(MakeSession());
  std::set<ModelSafeGroup> expected_enabled_groups;
  expected_enabled_groups.insert(GROUP_PASSIVE);
  expected_enabled_groups.insert(GROUP_UI);
  expected_enabled_groups.insert(GROUP_DB);
  EXPECT_EQ(expected_enabled_groups, session->GetEnabledGroups());
}

TEST_F(SyncSessionTest, EnabledGroupsWithConflictsEmpty) {
  scoped_ptr<SyncSession> session(MakeSession());
  // Auto-create conflict progress.  This shouldn't put that group in
  // conflict.
  session->mutable_status_controller()->
      GetUnrestrictedMutableConflictProgressForTest(GROUP_PASSIVE);
  EXPECT_TRUE(session->GetEnabledGroupsWithConflicts().empty());
}

TEST_F(SyncSessionTest, EnabledGroupsWithConflicts) {
  scoped_ptr<SyncSession> session(MakeSession());
  // Put GROUP_UI in conflict.
  session->mutable_status_controller()->
      GetUnrestrictedMutableConflictProgressForTest(GROUP_UI)->
      AddSimpleConflictingItemById(syncable::Id());
  std::set<ModelSafeGroup> expected_enabled_groups_with_conflicts;
  expected_enabled_groups_with_conflicts.insert(GROUP_UI);
  EXPECT_EQ(expected_enabled_groups_with_conflicts,
            session->GetEnabledGroupsWithConflicts());
}

TEST_F(SyncSessionTest, ScopedContextHelpers) {
  ConflictResolver resolver;
  EXPECT_FALSE(context_->resolver());
  {
    ScopedSessionContextConflictResolver s_resolver(context_.get(), &resolver);
    EXPECT_EQ(&resolver, context_->resolver());
  }
  EXPECT_FALSE(context_->resolver());
}

TEST_F(SyncSessionTest, SetWriteTransaction) {
  TestDirectorySetterUpper dir_maker;
  dir_maker.SetUp();
  syncable::Directory* directory = dir_maker.directory();

  scoped_ptr<SyncSession> session(MakeSession());
  EXPECT_TRUE(NULL == session->write_transaction());
  {
    WriteTransaction trans(FROM_HERE, syncable::UNITTEST, directory);
    sessions::ScopedSetSessionWriteTransaction set_trans(session.get(), &trans);
    EXPECT_TRUE(&trans == session->write_transaction());
  }
}

TEST_F(SyncSessionTest, MoreToDownloadIfDownloadFailed) {
  status()->set_updates_request_types(ParamsMeaningAllEnabledTypes());

  status()->set_last_download_updates_result(NETWORK_IO_ERROR);

  // When DownloadUpdatesCommand fails, these should be false.
  EXPECT_FALSE(status()->ServerSaysNothingMoreToDownload());
  EXPECT_FALSE(status()->download_updates_succeeded());

  // Download updates has its own loop in the syncer; it shouldn't factor
  // into HasMoreToSync.
  EXPECT_FALSE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, MoreToDownloadIfGotChangesRemaining) {
  status()->set_updates_request_types(ParamsMeaningAllEnabledTypes());

  // When the server returns changes_remaining, that means there's
  // more to download.
  status()->set_last_download_updates_result(SYNCER_OK);
  status()->mutable_updates_response()->mutable_get_updates()
     ->set_changes_remaining(1000L);
  EXPECT_FALSE(status()->ServerSaysNothingMoreToDownload());
  EXPECT_TRUE(status()->download_updates_succeeded());

  // Download updates has its own loop in the syncer; it shouldn't factor
  // into HasMoreToSync.
  EXPECT_FALSE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, MoreToDownloadIfGotNoChangesRemaining) {
  status()->set_updates_request_types(ParamsMeaningAllEnabledTypes());

  status()->set_last_download_updates_result(SYNCER_OK);
  status()->mutable_updates_response()->mutable_get_updates()
      ->set_changes_remaining(0);
  EXPECT_TRUE(status()->ServerSaysNothingMoreToDownload());
  EXPECT_TRUE(status()->download_updates_succeeded());

  // Download updates has its own loop in the syncer; it shouldn't factor
  // into HasMoreToSync.
  EXPECT_FALSE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, MoreToSyncIfConflictsResolved) {
  // Conflict resolution happens after get updates and commit,
  // so we need to loop back and get updates / commit again now
  // that we have made forward progress.
  status()->update_conflicts_resolved(true);
  EXPECT_TRUE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, ResetTransientState) {
  status()->update_conflicts_resolved(true);
  status()->increment_num_successful_commits();
  EXPECT_TRUE(session_->HasMoreToSync());
  session_->PrepareForAnotherSyncCycle();
  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::SYNC_CYCLE_CONTINUATION,
            session_->source().updates_source);
  EXPECT_FALSE(status()->conflicts_resolved());
  EXPECT_FALSE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, Coalesce) {
  std::vector<ModelSafeWorker*> workers_one, workers_two;
  ModelSafeRoutingInfo routes_one, routes_two;
  ModelTypeStateMap one_type =
      ModelTypeSetToStateMap(
          ParamsMeaningJustOneEnabledType(),
          std::string());
  ModelTypeStateMap all_types =
      ModelTypeSetToStateMap(
          ParamsMeaningAllEnabledTypes(),
          std::string());
  SyncSourceInfo source_one(sync_pb::GetUpdatesCallerInfo::PERIODIC, one_type);
  SyncSourceInfo source_two(sync_pb::GetUpdatesCallerInfo::LOCAL, all_types);

  scoped_refptr<ModelSafeWorker> passive_worker(
      new FakeModelWorker(GROUP_PASSIVE));
  scoped_refptr<ModelSafeWorker> db_worker(new FakeModelWorker(GROUP_DB));
  scoped_refptr<ModelSafeWorker> ui_worker(new FakeModelWorker(GROUP_UI));
  workers_one.push_back(passive_worker);
  workers_one.push_back(db_worker);
  workers_two.push_back(passive_worker);
  workers_two.push_back(db_worker);
  workers_two.push_back(ui_worker);
  routes_one[AUTOFILL] = GROUP_DB;
  routes_two[AUTOFILL] = GROUP_DB;
  routes_two[BOOKMARKS] = GROUP_UI;
  SyncSession one(context_.get(), this, source_one, routes_one, workers_one);
  SyncSession two(context_.get(), this, source_two, routes_two, workers_two);

  std::set<ModelSafeGroup> expected_enabled_groups_one;
  expected_enabled_groups_one.insert(GROUP_PASSIVE);
  expected_enabled_groups_one.insert(GROUP_DB);

  std::set<ModelSafeGroup> expected_enabled_groups_two;
  expected_enabled_groups_two.insert(GROUP_PASSIVE);
  expected_enabled_groups_two.insert(GROUP_DB);
  expected_enabled_groups_two.insert(GROUP_UI);

  EXPECT_EQ(expected_enabled_groups_one, one.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups_two, two.GetEnabledGroups());

  one.Coalesce(two);

  EXPECT_EQ(expected_enabled_groups_two, one.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups_two, two.GetEnabledGroups());

  EXPECT_EQ(two.source().updates_source, one.source().updates_source);
  EXPECT_THAT(all_types, Eq(one.source().types));
  std::vector<ModelSafeWorker*>::const_iterator it_db =
      std::find(one.workers().begin(), one.workers().end(), db_worker);
  std::vector<ModelSafeWorker*>::const_iterator it_ui =
      std::find(one.workers().begin(), one.workers().end(), ui_worker);
  EXPECT_NE(it_db, one.workers().end());
  EXPECT_NE(it_ui, one.workers().end());
  EXPECT_EQ(routes_two, one.routing_info());
}

TEST_F(SyncSessionTest, RebaseRoutingInfoWithLatestRemoveOneType) {
  std::vector<ModelSafeWorker*> workers_one, workers_two;
  ModelSafeRoutingInfo routes_one, routes_two;
  ModelTypeStateMap one_type =
      ModelTypeSetToStateMap(
          ParamsMeaningJustOneEnabledType(),
          std::string());
  ModelTypeStateMap all_types =
      ModelTypeSetToStateMap(
          ParamsMeaningAllEnabledTypes(),
          std::string());
  SyncSourceInfo source_one(sync_pb::GetUpdatesCallerInfo::PERIODIC, one_type);
  SyncSourceInfo source_two(sync_pb::GetUpdatesCallerInfo::LOCAL, all_types);

  scoped_refptr<ModelSafeWorker> passive_worker(
      new FakeModelWorker(GROUP_PASSIVE));
  scoped_refptr<ModelSafeWorker> db_worker(new FakeModelWorker(GROUP_DB));
  scoped_refptr<ModelSafeWorker> ui_worker(new FakeModelWorker(GROUP_UI));
  workers_one.push_back(passive_worker);
  workers_one.push_back(db_worker);
  workers_two.push_back(passive_worker);
  workers_two.push_back(db_worker);
  workers_two.push_back(ui_worker);
  routes_one[AUTOFILL] = GROUP_DB;
  routes_two[AUTOFILL] = GROUP_DB;
  routes_two[BOOKMARKS] = GROUP_UI;
  SyncSession one(context_.get(), this, source_one, routes_one, workers_one);
  SyncSession two(context_.get(), this, source_two, routes_two, workers_two);

  std::set<ModelSafeGroup> expected_enabled_groups_one;
  expected_enabled_groups_one.insert(GROUP_PASSIVE);
  expected_enabled_groups_one.insert(GROUP_DB);

  std::set<ModelSafeGroup> expected_enabled_groups_two;
  expected_enabled_groups_two.insert(GROUP_PASSIVE);
  expected_enabled_groups_two.insert(GROUP_DB);
  expected_enabled_groups_two.insert(GROUP_UI);

  EXPECT_EQ(expected_enabled_groups_one, one.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups_two, two.GetEnabledGroups());

  two.RebaseRoutingInfoWithLatest(one);

  EXPECT_EQ(expected_enabled_groups_one, one.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups_one, two.GetEnabledGroups());

  // Make sure the source has not been touched.
  EXPECT_EQ(two.source().updates_source,
      sync_pb::GetUpdatesCallerInfo::LOCAL);

  // Make sure the payload is reduced to one.
  EXPECT_THAT(one_type, Eq(two.source().types));

  // Make sure the workers are udpated.
  std::vector<ModelSafeWorker*>::const_iterator it_db =
      std::find(two.workers().begin(), two.workers().end(), db_worker);
  std::vector<ModelSafeWorker*>::const_iterator it_ui =
      std::find(two.workers().begin(), two.workers().end(), ui_worker);
  EXPECT_NE(it_db, two.workers().end());
  EXPECT_EQ(it_ui, two.workers().end());
  EXPECT_EQ(two.workers().size(), 2U);

  // Make sure the model safe routing info is reduced to one type.
  ModelSafeRoutingInfo::const_iterator it =
      two.routing_info().find(AUTOFILL);
  // Note that attempting to use EXPECT_NE would fail for an Android build due
  // to seeming incompatibility with gtest and stlport.
  EXPECT_TRUE(it != two.routing_info().end());
  EXPECT_EQ(it->second, GROUP_DB);
  EXPECT_EQ(two.routing_info().size(), 1U);
}

TEST_F(SyncSessionTest, RebaseRoutingInfoWithLatestWithSameType) {
  std::vector<ModelSafeWorker*> workers_first, workers_second;
  ModelSafeRoutingInfo routes_first, routes_second;
  ModelTypeStateMap all_types =
      ModelTypeSetToStateMap(
          ParamsMeaningAllEnabledTypes(),
          std::string());
  SyncSourceInfo source_first(sync_pb::GetUpdatesCallerInfo::PERIODIC,
      all_types);
  SyncSourceInfo source_second(sync_pb::GetUpdatesCallerInfo::LOCAL,
      all_types);

  scoped_refptr<ModelSafeWorker> passive_worker(
      new FakeModelWorker(GROUP_PASSIVE));
  scoped_refptr<FakeModelWorker> db_worker(new FakeModelWorker(GROUP_DB));
  scoped_refptr<FakeModelWorker> ui_worker(new FakeModelWorker(GROUP_UI));
  workers_first.push_back(passive_worker);
  workers_first.push_back(db_worker);
  workers_first.push_back(ui_worker);
  workers_second.push_back(passive_worker);
  workers_second.push_back(db_worker);
  workers_second.push_back(ui_worker);
  routes_first[AUTOFILL] = GROUP_DB;
  routes_first[BOOKMARKS] = GROUP_UI;
  routes_second[AUTOFILL] = GROUP_DB;
  routes_second[BOOKMARKS] = GROUP_UI;
  SyncSession first(context_.get(), this, source_first, routes_first,
      workers_first);
  SyncSession second(context_.get(), this, source_second, routes_second,
      workers_second);

  std::set<ModelSafeGroup> expected_enabled_groups;
  expected_enabled_groups.insert(GROUP_PASSIVE);
  expected_enabled_groups.insert(GROUP_DB);
  expected_enabled_groups.insert(GROUP_UI);

  EXPECT_EQ(expected_enabled_groups, first.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups, second.GetEnabledGroups());

  second.RebaseRoutingInfoWithLatest(first);

  EXPECT_EQ(expected_enabled_groups, first.GetEnabledGroups());
  EXPECT_EQ(expected_enabled_groups, second.GetEnabledGroups());

  // Make sure the source has not been touched.
  EXPECT_EQ(second.source().updates_source,
      sync_pb::GetUpdatesCallerInfo::LOCAL);

  // Make sure our payload is still the same.
  EXPECT_THAT(all_types, Eq(second.source().types));

  // Make sure the workers are still the same.
  std::vector<ModelSafeWorker*>::const_iterator it_passive =
      std::find(second.workers().begin(), second.workers().end(),
                passive_worker);
  std::vector<ModelSafeWorker*>::const_iterator it_db =
      std::find(second.workers().begin(), second.workers().end(), db_worker);
  std::vector<ModelSafeWorker*>::const_iterator it_ui =
      std::find(second.workers().begin(), second.workers().end(), ui_worker);
  EXPECT_NE(it_passive, second.workers().end());
  EXPECT_NE(it_db, second.workers().end());
  EXPECT_NE(it_ui, second.workers().end());
  EXPECT_EQ(second.workers().size(), 3U);

  // Make sure the model safe routing info is reduced to first type.
  ModelSafeRoutingInfo::const_iterator it1 =
      second.routing_info().find(AUTOFILL);
  ModelSafeRoutingInfo::const_iterator it2 =
      second.routing_info().find(BOOKMARKS);

  // Note that attempting to use EXPECT_NE would fail for an Android build due
  // to seeming incompatibility with gtest and stlport.
  EXPECT_TRUE(it1 != second.routing_info().end());
  EXPECT_EQ(it1->second, GROUP_DB);

  // Note that attempting to use EXPECT_NE would fail for an Android build due
  // to seeming incompatibility with gtest and stlport.
  EXPECT_TRUE(it2 != second.routing_info().end());
  EXPECT_EQ(it2->second, GROUP_UI);
  EXPECT_EQ(second.routing_info().size(), 2U);
}


TEST_F(SyncSessionTest, MakeTypeStateMapFromBitSet) {
  ModelTypeSet types;
  std::string payload = "test";
  ModelTypeStateMap type_state_map =
      ModelTypeSetToStateMap(types, payload);
  EXPECT_TRUE(type_state_map.empty());

  types.Put(BOOKMARKS);
  types.Put(PASSWORDS);
  types.Put(AUTOFILL);
  payload = "test2";
  type_state_map = ModelTypeSetToStateMap(types, payload);

  ASSERT_EQ(3U, type_state_map.size());
  EXPECT_EQ(type_state_map[BOOKMARKS].payload, payload);
  EXPECT_EQ(type_state_map[PASSWORDS].payload, payload);
  EXPECT_EQ(type_state_map[AUTOFILL].payload, payload);
}

}  // namespace
}  // namespace sessions
}  // namespace syncer