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

#include "base/memory/ref_counted.h"
#include "chrome/browser/sync/engine/conflict_resolver.h"
#include "chrome/browser/sync/engine/mock_model_safe_workers.h"
#include "chrome/browser/sync/engine/syncer_types.h"
#include "chrome/browser/sync/engine/syncer_util.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/syncable/model_type.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/test/sync/engine/test_directory_setter_upper.h"
#include "testing/gtest/include/gtest/gtest.h"

using syncable::WriteTransaction;

namespace browser_sync {
namespace sessions {
namespace {

class SyncSessionTest : public testing::Test,
                        public SyncSession::Delegate,
                        public ModelSafeWorkerRegistrar {
 public:
  SyncSessionTest() : controller_invocations_allowed_(false) {
    GetModelSafeRoutingInfo(&routes_);
  }

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

  virtual void SetUp() {
    context_.reset(new SyncSessionContext(NULL, NULL, this,
        std::vector<SyncEngineEventListener*>()));
    routes_.clear();
    routes_[syncable::BOOKMARKS] = GROUP_UI;
    routes_[syncable::AUTOFILL] = GROUP_UI;
    session_.reset(MakeSession());
  }
  virtual void TearDown() {
    session_.reset();
    context_.reset();
  }

  virtual void OnSilencedUntil(const base::TimeTicks& silenced_until) {
    FailControllerInvocationIfDisabled("OnSilencedUntil");
  }
  virtual bool IsSyncingCurrentlySilenced() {
    FailControllerInvocationIfDisabled("IsSyncingCurrentlySilenced");
    return false;
  }
  virtual void OnReceivedLongPollIntervalUpdate(
      const base::TimeDelta& new_interval) {
    FailControllerInvocationIfDisabled("OnReceivedLongPollIntervalUpdate");
  }
  virtual void OnReceivedShortPollIntervalUpdate(
      const base::TimeDelta& new_interval) {
    FailControllerInvocationIfDisabled("OnReceivedShortPollIntervalUpdate");
  }
  virtual void OnShouldStopSyncingPermanently() {
    FailControllerInvocationIfDisabled("OnShouldStopSyncingPermanently");
  }

  // ModelSafeWorkerRegistrar implementation.
  virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) {}
  virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) {
    out->swap(routes_);
  }

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

  syncable::ModelTypeBitSet ParamsMeaningAllEnabledTypes() {
    syncable::ModelTypeBitSet request_params;
    request_params[syncable::BOOKMARKS] = true;
    request_params[syncable::AUTOFILL] = true;
    return request_params;
  }

  syncable::ModelTypeBitSet ParamsMeaningJustOneEnabledType() {
    syncable::ModelTypeBitSet request_params;
    request_params[syncable::AUTOFILL] = true;
    return request_params;
  }

  bool controller_invocations_allowed_;
  scoped_ptr<SyncSession> session_;
  scoped_ptr<SyncSessionContext> context_;
  ModelSafeRoutingInfo routes_;
};

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 db;
  db.SetUp();
  session_.reset();
  context_.reset(new SyncSessionContext(NULL, db.manager(), this,
      std::vector<SyncEngineEventListener*>()));
  session_.reset(MakeSession());
  context_->set_account_name(db.name());
  syncable::ScopedDirLookup dir(context_->directory_manager(),
                                context_->account_name());
  ASSERT_TRUE(dir.good());

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

TEST_F(SyncSessionTest, MoreToSyncIfUnsyncedGreaterThanCommitted) {
  // If any forward progress was made during the session, and the number of
  // unsynced handles still exceeds the number of commit ids we added, there is
  // more to sync. For example, this occurs if we had more commit ids
  // than could fit in a single commit batch.
  EXPECT_FALSE(session_->HasMoreToSync());
  OrderedCommitSet commit_set(routes_);
  commit_set.AddCommitItem(0, syncable::Id(), syncable::BOOKMARKS);
  status()->set_commit_set(commit_set);
  EXPECT_FALSE(session_->HasMoreToSync());

  std::vector<int64> unsynced_handles;
  unsynced_handles.push_back(1);
  unsynced_handles.push_back(2);
  status()->set_unsynced_handles(unsynced_handles);
  EXPECT_FALSE(session_->HasMoreToSync());
  status()->increment_num_successful_commits();
  EXPECT_TRUE(session_->HasMoreToSync());
}

TEST_F(SyncSessionTest, MoreToSyncIfConflictSetsBuilt) {
  // If we built conflict sets, then we need to loop back and try
  // to get updates & commit again.
  status()->update_conflict_sets_built(true);
  EXPECT_TRUE(session_->HasMoreToSync());
}

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

  // 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()->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());

  // When the server returns a timestamp, that means we're up to date.
  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, MoreToDownloadIfGotNoChangesRemainingForSubset) {
  status()->set_updates_request_types(ParamsMeaningJustOneEnabledType());

  // When the server returns a timestamp, that means we're up to date for that
  // type.  But there may still be more to download if there are other
  // datatypes that we didn't request on this go-round.
  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, MoreToDownloadIfGotChangesRemainingAndEntries) {
  status()->set_updates_request_types(ParamsMeaningAllEnabledTypes());
  // The actual entry count should not factor into the HasMoreToSync
  // determination.
  status()->mutable_updates_response()->mutable_get_updates()->add_entries();
  status()->mutable_updates_response()->mutable_get_updates()
      ->set_changes_remaining(1000000L);;
  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, MoreToDownloadIfGotNoChangesRemainingAndEntries) {
  status()->set_updates_request_types(ParamsMeaningAllEnabledTypes());
  // The actual entry count should not factor into the HasMoreToSync
  // determination.
  status()->mutable_updates_response()->mutable_get_updates()->add_entries();
  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_->ResetTransientState();
  EXPECT_FALSE(status()->conflicts_resolved());
  EXPECT_FALSE(session_->HasMoreToSync());
  EXPECT_FALSE(status()->TestAndClearIsDirty());
}

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

  scoped_refptr<MockDBModelWorker> db_worker(new MockDBModelWorker());
  scoped_refptr<MockUIModelWorker> ui_worker(new MockUIModelWorker());
  workers_one.push_back(db_worker);
  workers_two.push_back(db_worker);
  workers_two.push_back(ui_worker);
  routes_one[syncable::AUTOFILL] = GROUP_DB;
  routes_two[syncable::AUTOFILL] = GROUP_DB;
  routes_two[syncable::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);

  one.Coalesce(two);

  EXPECT_EQ(two.source().updates_source, one.source().updates_source);
  EXPECT_EQ(all_types, 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, MakeTypePayloadMapFromBitSet) {
  syncable::ModelTypeBitSet types;
  std::string payload = "test";
  syncable::ModelTypePayloadMap types_with_payloads =
      syncable::ModelTypePayloadMapFromBitSet(types,
                                              payload);
  EXPECT_TRUE(types_with_payloads.empty());

  types[syncable::BOOKMARKS] = true;
  types[syncable::PASSWORDS] = true;
  types[syncable::AUTOFILL] = true;
  payload = "test2";
  types_with_payloads = syncable::ModelTypePayloadMapFromBitSet(types, payload);

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

TEST_F(SyncSessionTest, MakeTypePayloadMapFromRoutingInfo) {
  std::string payload = "test";
  syncable::ModelTypePayloadMap types_with_payloads
      = syncable::ModelTypePayloadMapFromRoutingInfo(routes_, payload);
  ASSERT_EQ(routes_.size(), types_with_payloads.size());
  for (ModelSafeRoutingInfo::iterator iter = routes_.begin();
       iter != routes_.end();
       ++iter) {
    EXPECT_EQ(payload, types_with_payloads[iter->first]);
  }
}

TEST_F(SyncSessionTest, CoalescePayloads) {
  syncable::ModelTypePayloadMap original;
  std::string empty_payload;
  std::string payload1 = "payload1";
  std::string payload2 = "payload2";
  std::string payload3 = "payload3";
  original[syncable::BOOKMARKS] = empty_payload;
  original[syncable::PASSWORDS] = payload1;
  original[syncable::AUTOFILL] = payload2;
  original[syncable::THEMES] = payload3;

  syncable::ModelTypePayloadMap update;
  update[syncable::BOOKMARKS] = empty_payload;  // Same.
  update[syncable::PASSWORDS] = empty_payload;  // Overwrite with empty.
  update[syncable::AUTOFILL] = payload1;        // Overwrite with non-empty.
  update[syncable::SESSIONS] = payload2;        // New.
  // Themes untouched.

  CoalescePayloads(&original, update);
  ASSERT_EQ(5U, original.size());
  EXPECT_EQ(empty_payload, original[syncable::BOOKMARKS]);
  EXPECT_EQ(payload1, original[syncable::PASSWORDS]);
  EXPECT_EQ(payload1, original[syncable::AUTOFILL]);
  EXPECT_EQ(payload2, original[syncable::SESSIONS]);
  EXPECT_EQ(payload3, original[syncable::THEMES]);
}

}  // namespace
}  // namespace sessions
}  // namespace browser_sync