// 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.

// Unit tests for the SyncApi. Note that a lot of the underlying
// functionality is provided by the Syncable layer, which has its own
// unit tests. We'll test SyncApi specific things in this harness.

#include <map>

#include "base/basictypes.h"
#include "base/format_macros.h"
#include "base/memory/scoped_ptr.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/message_loop.h"
#include "base/string_number_conversions.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/sync/engine/http_post_provider_factory.h"
#include "chrome/browser/sync/engine/http_post_provider_interface.h"
#include "chrome/browser/sync/engine/model_safe_worker.h"
#include "chrome/browser/sync/engine/syncapi.h"
#include "chrome/browser/sync/js_arg_list.h"
#include "chrome/browser/sync/js_backend.h"
#include "chrome/browser/sync/js_event_handler.h"
#include "chrome/browser/sync/js_event_router.h"
#include "chrome/browser/sync/js_test_util.h"
#include "chrome/browser/sync/notifier/sync_notifier.h"
#include "chrome/browser/sync/notifier/sync_notifier_observer.h"
#include "chrome/browser/sync/protocol/password_specifics.pb.h"
#include "chrome/browser/sync/protocol/proto_value_conversions.h"
#include "chrome/browser/sync/sessions/sync_session.h"
#include "chrome/browser/sync/syncable/directory_manager.h"
#include "chrome/browser/sync/syncable/nigori_util.h"
#include "chrome/browser/sync/syncable/syncable.h"
#include "chrome/browser/sync/syncable/syncable_id.h"
#include "chrome/browser/sync/util/cryptographer.h"
#include "chrome/test/sync/engine/test_user_share.h"
#include "chrome/test/values_test_util.h"
#include "content/browser/browser_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using browser_sync::Cryptographer;
using browser_sync::HasArgsAsList;
using browser_sync::KeyParams;
using browser_sync::JsArgList;
using browser_sync::MockJsEventHandler;
using browser_sync::MockJsEventRouter;
using browser_sync::ModelSafeRoutingInfo;
using browser_sync::ModelSafeWorker;
using browser_sync::ModelSafeWorkerRegistrar;
using browser_sync::sessions::SyncSessionSnapshot;
using syncable::ModelType;
using syncable::ModelTypeSet;
using test::ExpectDictionaryValue;
using test::ExpectStringValue;
using testing::_;
using testing::AtLeast;
using testing::Invoke;
using testing::SaveArg;
using testing::StrictMock;

namespace sync_api {

namespace {

void ExpectInt64Value(int64 expected_value,
                      const DictionaryValue& value, const std::string& key) {
  std::string int64_str;
  EXPECT_TRUE(value.GetString(key, &int64_str));
  int64 val = 0;
  EXPECT_TRUE(base::StringToInt64(int64_str, &val));
  EXPECT_EQ(expected_value, val);
}

// Makes a non-folder child of the root node.  Returns the id of the
// newly-created node.
int64 MakeNode(UserShare* share,
               ModelType model_type,
               const std::string& client_tag) {
  WriteTransaction trans(share);
  ReadNode root_node(&trans);
  root_node.InitByRootLookup();
  WriteNode node(&trans);
  EXPECT_TRUE(node.InitUniqueByCreation(model_type, root_node, client_tag));
  node.SetIsFolder(false);
  return node.GetId();
}

// Make a folder as a child of the root node. Returns the id of the
// newly-created node.
int64 MakeFolder(UserShare* share,
                 syncable::ModelType model_type,
                 const std::string& client_tag) {
  WriteTransaction trans(share);
  ReadNode root_node(&trans);
  root_node.InitByRootLookup();
  WriteNode node(&trans);
  EXPECT_TRUE(node.InitUniqueByCreation(model_type, root_node, client_tag));
  node.SetIsFolder(true);
  return node.GetId();
}

// Makes a non-folder child of a non-root node. Returns the id of the
// newly-created node.
int64 MakeNodeWithParent(UserShare* share,
                         ModelType model_type,
                         const std::string& client_tag,
                         int64 parent_id) {
  WriteTransaction trans(share);
  ReadNode parent_node(&trans);
  parent_node.InitByIdLookup(parent_id);
  WriteNode node(&trans);
  EXPECT_TRUE(node.InitUniqueByCreation(model_type, parent_node, client_tag));
  node.SetIsFolder(false);
  return node.GetId();
}

// Makes a folder child of a non-root node. Returns the id of the
// newly-created node.
int64 MakeFolderWithParent(UserShare* share,
                           ModelType model_type,
                           int64 parent_id,
                           BaseNode* predecessor) {
  WriteTransaction trans(share);
  ReadNode parent_node(&trans);
  parent_node.InitByIdLookup(parent_id);
  WriteNode node(&trans);
  EXPECT_TRUE(node.InitByCreation(model_type, parent_node, predecessor));
  node.SetIsFolder(true);
  return node.GetId();
}

// Creates the "synced" root node for a particular datatype. We use the syncable
// methods here so that the syncer treats these nodes as if they were already
// received from the server.
int64 MakeServerNodeForType(UserShare* share,
                            ModelType model_type) {
  sync_pb::EntitySpecifics specifics;
  syncable::AddDefaultExtensionValue(model_type, &specifics);
  syncable::ScopedDirLookup dir(share->dir_manager.get(), share->name);
  EXPECT_TRUE(dir.good());
  syncable::WriteTransaction trans(dir, syncable::UNITTEST, __FILE__, __LINE__);
  // Attempt to lookup by nigori tag.
  std::string type_tag = syncable::ModelTypeToRootTag(model_type);
  syncable::Id node_id = syncable::Id::CreateFromServerId(type_tag);
  syncable::MutableEntry entry(&trans, syncable::CREATE_NEW_UPDATE_ITEM,
                               node_id);
  EXPECT_TRUE(entry.good());
  entry.Put(syncable::BASE_VERSION, 1);
  entry.Put(syncable::SERVER_VERSION, 1);
  entry.Put(syncable::IS_UNAPPLIED_UPDATE, false);
  entry.Put(syncable::SERVER_PARENT_ID, syncable::kNullId);
  entry.Put(syncable::SERVER_IS_DIR, true);
  entry.Put(syncable::IS_DIR, true);
  entry.Put(syncable::SERVER_SPECIFICS, specifics);
  entry.Put(syncable::UNIQUE_SERVER_TAG, type_tag);
  entry.Put(syncable::NON_UNIQUE_NAME, type_tag);
  entry.Put(syncable::IS_DEL, false);
  entry.Put(syncable::SPECIFICS, specifics);
  return entry.Get(syncable::META_HANDLE);
}

}  // namespace

class SyncApiTest : public testing::Test {
 public:
  virtual void SetUp() {
    test_user_share_.SetUp();
  }

  virtual void TearDown() {
    test_user_share_.TearDown();
  }

 protected:
  browser_sync::TestUserShare test_user_share_;
};

TEST_F(SyncApiTest, SanityCheckTest) {
  {
    ReadTransaction trans(test_user_share_.user_share());
    EXPECT_TRUE(trans.GetWrappedTrans() != NULL);
  }
  {
    WriteTransaction trans(test_user_share_.user_share());
    EXPECT_TRUE(trans.GetWrappedTrans() != NULL);
  }
  {
    // No entries but root should exist
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    // Metahandle 1 can be root, sanity check 2
    EXPECT_FALSE(node.InitByIdLookup(2));
  }
}

TEST_F(SyncApiTest, BasicTagWrite) {
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode root_node(&trans);
    root_node.InitByRootLookup();
    EXPECT_EQ(root_node.GetFirstChildId(), 0);
  }

  ignore_result(MakeNode(test_user_share_.user_share(),
                         syncable::BOOKMARKS, "testtag"));

  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    EXPECT_TRUE(node.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));

    ReadNode root_node(&trans);
    root_node.InitByRootLookup();
    EXPECT_NE(node.GetId(), 0);
    EXPECT_EQ(node.GetId(), root_node.GetFirstChildId());
  }
}

TEST_F(SyncApiTest, GenerateSyncableHash) {
  EXPECT_EQ("OyaXV5mEzrPS4wbogmtKvRfekAI=",
      BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag1"));
  EXPECT_EQ("iNFQtRFQb+IZcn1kKUJEZDDkLs4=",
      BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag1"));
  EXPECT_EQ("gO1cPZQXaM73sHOvSA+tKCKFs58=",
      BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag1"));

  EXPECT_EQ("A0eYIHXM1/jVwKDDp12Up20IkKY=",
      BaseNode::GenerateSyncableHash(syncable::BOOKMARKS, "tag2"));
  EXPECT_EQ("XYxkF7bhS4eItStFgiOIAU23swI=",
      BaseNode::GenerateSyncableHash(syncable::PREFERENCES, "tag2"));
  EXPECT_EQ("GFiWzo5NGhjLlN+OyCfhy28DJTQ=",
      BaseNode::GenerateSyncableHash(syncable::AUTOFILL, "tag2"));
}

TEST_F(SyncApiTest, ModelTypesSiloed) {
  {
    WriteTransaction trans(test_user_share_.user_share());
    ReadNode root_node(&trans);
    root_node.InitByRootLookup();
    EXPECT_EQ(root_node.GetFirstChildId(), 0);
  }

  ignore_result(MakeNode(test_user_share_.user_share(),
                         syncable::BOOKMARKS, "collideme"));
  ignore_result(MakeNode(test_user_share_.user_share(),
                         syncable::PREFERENCES, "collideme"));
  ignore_result(MakeNode(test_user_share_.user_share(),
                         syncable::AUTOFILL, "collideme"));

  {
    ReadTransaction trans(test_user_share_.user_share());

    ReadNode bookmarknode(&trans);
    EXPECT_TRUE(bookmarknode.InitByClientTagLookup(syncable::BOOKMARKS,
        "collideme"));

    ReadNode prefnode(&trans);
    EXPECT_TRUE(prefnode.InitByClientTagLookup(syncable::PREFERENCES,
        "collideme"));

    ReadNode autofillnode(&trans);
    EXPECT_TRUE(autofillnode.InitByClientTagLookup(syncable::AUTOFILL,
        "collideme"));

    EXPECT_NE(bookmarknode.GetId(), prefnode.GetId());
    EXPECT_NE(autofillnode.GetId(), prefnode.GetId());
    EXPECT_NE(bookmarknode.GetId(), autofillnode.GetId());
  }
}

TEST_F(SyncApiTest, ReadMissingTagsFails) {
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));
  }
  {
    WriteTransaction trans(test_user_share_.user_share());
    WriteNode node(&trans);
    EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));
  }
}

// TODO(chron): Hook this all up to the server and write full integration tests
//              for update->undelete behavior.
TEST_F(SyncApiTest, TestDeleteBehavior) {
  int64 node_id;
  int64 folder_id;
  std::wstring test_title(L"test1");

  {
    WriteTransaction trans(test_user_share_.user_share());
    ReadNode root_node(&trans);
    root_node.InitByRootLookup();

    // we'll use this spare folder later
    WriteNode folder_node(&trans);
    EXPECT_TRUE(folder_node.InitByCreation(syncable::BOOKMARKS,
        root_node, NULL));
    folder_id = folder_node.GetId();

    WriteNode wnode(&trans);
    EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS,
        root_node, "testtag"));
    wnode.SetIsFolder(false);
    wnode.SetTitle(test_title);

    node_id = wnode.GetId();
  }

  // Ensure we can delete something with a tag.
  {
    WriteTransaction trans(test_user_share_.user_share());
    WriteNode wnode(&trans);
    EXPECT_TRUE(wnode.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));
    EXPECT_FALSE(wnode.GetIsFolder());
    EXPECT_EQ(wnode.GetTitle(), test_title);

    wnode.Remove();
  }

  // Lookup of a node which was deleted should return failure,
  // but have found some data about the node.
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    EXPECT_FALSE(node.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));
    // Note that for proper function of this API this doesn't need to be
    // filled, we're checking just to make sure the DB worked in this test.
    EXPECT_EQ(node.GetTitle(), test_title);
  }

  {
    WriteTransaction trans(test_user_share_.user_share());
    ReadNode folder_node(&trans);
    EXPECT_TRUE(folder_node.InitByIdLookup(folder_id));

    WriteNode wnode(&trans);
    // This will undelete the tag.
    EXPECT_TRUE(wnode.InitUniqueByCreation(syncable::BOOKMARKS,
        folder_node, "testtag"));
    EXPECT_EQ(wnode.GetIsFolder(), false);
    EXPECT_EQ(wnode.GetParentId(), folder_node.GetId());
    EXPECT_EQ(wnode.GetId(), node_id);
    EXPECT_NE(wnode.GetTitle(), test_title);  // Title should be cleared
    wnode.SetTitle(test_title);
  }

  // Now look up should work.
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    EXPECT_TRUE(node.InitByClientTagLookup(syncable::BOOKMARKS,
        "testtag"));
    EXPECT_EQ(node.GetTitle(), test_title);
    EXPECT_EQ(node.GetModelType(), syncable::BOOKMARKS);
  }
}

TEST_F(SyncApiTest, WriteAndReadPassword) {
  KeyParams params = {"localhost", "username", "passphrase"};
  {
    ReadTransaction trans(test_user_share_.user_share());
    trans.GetCryptographer()->AddKey(params);
  }
  {
    WriteTransaction trans(test_user_share_.user_share());
    ReadNode root_node(&trans);
    root_node.InitByRootLookup();

    WriteNode password_node(&trans);
    EXPECT_TRUE(password_node.InitUniqueByCreation(syncable::PASSWORDS,
                                                   root_node, "foo"));
    sync_pb::PasswordSpecificsData data;
    data.set_password_value("secret");
    password_node.SetPasswordSpecifics(data);
  }
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode root_node(&trans);
    root_node.InitByRootLookup();

    ReadNode password_node(&trans);
    EXPECT_TRUE(password_node.InitByClientTagLookup(syncable::PASSWORDS,
                                                    "foo"));
    const sync_pb::PasswordSpecificsData& data =
        password_node.GetPasswordSpecifics();
    EXPECT_EQ("secret", data.password_value());
  }
}

namespace {

void CheckNodeValue(const BaseNode& node, const DictionaryValue& value) {
  ExpectInt64Value(node.GetId(), value, "id");
  ExpectInt64Value(node.GetModificationTime(), value, "modificationTime");
  ExpectInt64Value(node.GetParentId(), value, "parentId");
  {
    bool is_folder = false;
    EXPECT_TRUE(value.GetBoolean("isFolder", &is_folder));
    EXPECT_EQ(node.GetIsFolder(), is_folder);
  }
  ExpectStringValue(WideToUTF8(node.GetTitle()), value, "title");
  {
    ModelType expected_model_type = node.GetModelType();
    std::string type_str;
    EXPECT_TRUE(value.GetString("type", &type_str));
    if (expected_model_type >= syncable::FIRST_REAL_MODEL_TYPE) {
      ModelType model_type =
          syncable::ModelTypeFromString(type_str);
      EXPECT_EQ(expected_model_type, model_type);
    } else if (expected_model_type == syncable::TOP_LEVEL_FOLDER) {
      EXPECT_EQ("Top-level folder", type_str);
    } else if (expected_model_type == syncable::UNSPECIFIED) {
      EXPECT_EQ("Unspecified", type_str);
    } else {
      ADD_FAILURE();
    }
  }
  ExpectInt64Value(node.GetExternalId(), value, "externalId");
  ExpectInt64Value(node.GetPredecessorId(), value, "predecessorId");
  ExpectInt64Value(node.GetSuccessorId(), value, "successorId");
  ExpectInt64Value(node.GetFirstChildId(), value, "firstChildId");
  {
    scoped_ptr<DictionaryValue> expected_entry(node.GetEntry()->ToValue());
    Value* entry = NULL;
    EXPECT_TRUE(value.Get("entry", &entry));
    EXPECT_TRUE(Value::Equals(entry, expected_entry.get()));
  }
  EXPECT_EQ(11u, value.size());
}

}  // namespace

TEST_F(SyncApiTest, BaseNodeToValue) {
  ReadTransaction trans(test_user_share_.user_share());
  ReadNode node(&trans);
  node.InitByRootLookup();
  scoped_ptr<DictionaryValue> value(node.ToValue());
  if (value.get()) {
    CheckNodeValue(node, *value);
  } else {
    ADD_FAILURE();
  }
}

namespace {

void ExpectChangeRecordActionValue(SyncManager::ChangeRecord::Action
                                       expected_value,
                                   const DictionaryValue& value,
                                   const std::string& key) {
  std::string str_value;
  EXPECT_TRUE(value.GetString(key, &str_value));
  switch (expected_value) {
    case SyncManager::ChangeRecord::ACTION_ADD:
      EXPECT_EQ("Add", str_value);
      break;
    case SyncManager::ChangeRecord::ACTION_UPDATE:
      EXPECT_EQ("Update", str_value);
      break;
    case SyncManager::ChangeRecord::ACTION_DELETE:
      EXPECT_EQ("Delete", str_value);
      break;
    default:
      NOTREACHED();
      break;
  }
}

void CheckNonDeleteChangeRecordValue(const SyncManager::ChangeRecord& record,
                                     const DictionaryValue& value,
                                     BaseTransaction* trans) {
  EXPECT_NE(SyncManager::ChangeRecord::ACTION_DELETE, record.action);
  ExpectChangeRecordActionValue(record.action, value, "action");
  {
    ReadNode node(trans);
    EXPECT_TRUE(node.InitByIdLookup(record.id));
    scoped_ptr<DictionaryValue> expected_node_value(node.ToValue());
    ExpectDictionaryValue(*expected_node_value, value, "node");
  }
}

void CheckDeleteChangeRecordValue(const SyncManager::ChangeRecord& record,
                                  const DictionaryValue& value) {
  EXPECT_EQ(SyncManager::ChangeRecord::ACTION_DELETE, record.action);
  ExpectChangeRecordActionValue(record.action, value, "action");
  DictionaryValue* node_value = NULL;
  EXPECT_TRUE(value.GetDictionary("node", &node_value));
  if (node_value) {
    ExpectInt64Value(record.id, *node_value, "id");
    scoped_ptr<DictionaryValue> expected_specifics_value(
        browser_sync::EntitySpecificsToValue(record.specifics));
    ExpectDictionaryValue(*expected_specifics_value,
                          *node_value, "specifics");
    scoped_ptr<DictionaryValue> expected_extra_value;
    if (record.extra.get()) {
      expected_extra_value.reset(record.extra->ToValue());
    }
    Value* extra_value = NULL;
    EXPECT_EQ(record.extra.get() != NULL,
              node_value->Get("extra", &extra_value));
    EXPECT_TRUE(Value::Equals(extra_value, expected_extra_value.get()));
  }
}

class MockExtraChangeRecordData
    : public SyncManager::ExtraPasswordChangeRecordData {
 public:
  MOCK_CONST_METHOD0(ToValue, DictionaryValue*());
};

}  // namespace

TEST_F(SyncApiTest, ChangeRecordToValue) {
  int64 child_id = MakeNode(test_user_share_.user_share(),
                            syncable::BOOKMARKS, "testtag");
  sync_pb::EntitySpecifics child_specifics;
  {
    ReadTransaction trans(test_user_share_.user_share());
    ReadNode node(&trans);
    EXPECT_TRUE(node.InitByIdLookup(child_id));
    child_specifics = node.GetEntry()->Get(syncable::SPECIFICS);
  }

  // Add
  {
    ReadTransaction trans(test_user_share_.user_share());
    SyncManager::ChangeRecord record;
    record.action = SyncManager::ChangeRecord::ACTION_ADD;
    record.id = 1;
    record.specifics = child_specifics;
    record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
    scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
    CheckNonDeleteChangeRecordValue(record, *value, &trans);
  }

  // Update
  {
    ReadTransaction trans(test_user_share_.user_share());
    SyncManager::ChangeRecord record;
    record.action = SyncManager::ChangeRecord::ACTION_UPDATE;
    record.id = child_id;
    record.specifics = child_specifics;
    record.extra.reset(new StrictMock<MockExtraChangeRecordData>());
    scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
    CheckNonDeleteChangeRecordValue(record, *value, &trans);
  }

  // Delete (no extra)
  {
    ReadTransaction trans(test_user_share_.user_share());
    SyncManager::ChangeRecord record;
    record.action = SyncManager::ChangeRecord::ACTION_DELETE;
    record.id = child_id + 1;
    record.specifics = child_specifics;
    scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
    CheckDeleteChangeRecordValue(record, *value);
  }

  // Delete (with extra)
  {
    ReadTransaction trans(test_user_share_.user_share());
    SyncManager::ChangeRecord record;
    record.action = SyncManager::ChangeRecord::ACTION_DELETE;
    record.id = child_id + 1;
    record.specifics = child_specifics;

    DictionaryValue extra_value;
    extra_value.SetString("foo", "bar");
    scoped_ptr<StrictMock<MockExtraChangeRecordData> > extra(
        new StrictMock<MockExtraChangeRecordData>());
    EXPECT_CALL(*extra, ToValue()).Times(2).WillRepeatedly(
        Invoke(&extra_value, &DictionaryValue::DeepCopy));

    record.extra.reset(extra.release());
    scoped_ptr<DictionaryValue> value(record.ToValue(&trans));
    CheckDeleteChangeRecordValue(record, *value);
  }
}

namespace {

class TestHttpPostProviderFactory : public HttpPostProviderFactory {
 public:
  virtual ~TestHttpPostProviderFactory() {}
  virtual HttpPostProviderInterface* Create() {
    NOTREACHED();
    return NULL;
  }
  virtual void Destroy(HttpPostProviderInterface* http) {
    NOTREACHED();
  }
};

class SyncManagerObserverMock : public SyncManager::Observer {
 public:
  MOCK_METHOD4(OnChangesApplied,
               void(ModelType,
                    const BaseTransaction*,
                    const SyncManager::ChangeRecord*,
                    int));  // NOLINT
  MOCK_METHOD1(OnChangesComplete, void(ModelType));  // NOLINT
  MOCK_METHOD1(OnSyncCycleCompleted,
               void(const SyncSessionSnapshot*));  // NOLINT
  MOCK_METHOD0(OnInitializationComplete, void());  // NOLINT
  MOCK_METHOD1(OnAuthError, void(const GoogleServiceAuthError&));  // NOLINT
  MOCK_METHOD1(OnPassphraseRequired, void(bool));  // NOLINT
  MOCK_METHOD0(OnPassphraseFailed, void());  // NOLINT
  MOCK_METHOD1(OnPassphraseAccepted, void(const std::string&));  // NOLINT
  MOCK_METHOD0(OnStopSyncingPermanently, void());  // NOLINT
  MOCK_METHOD1(OnUpdatedToken, void(const std::string&));  // NOLINT
  MOCK_METHOD1(OnMigrationNeededForTypes, void(const ModelTypeSet&));
  MOCK_METHOD0(OnClearServerDataFailed, void());  // NOLINT
  MOCK_METHOD0(OnClearServerDataSucceeded, void());  // NOLINT
  MOCK_METHOD1(OnEncryptionComplete, void(const ModelTypeSet&));  // NOLINT
};

class SyncNotifierMock : public sync_notifier::SyncNotifier {
 public:
  MOCK_METHOD1(AddObserver, void(sync_notifier::SyncNotifierObserver*));
  MOCK_METHOD1(RemoveObserver, void(sync_notifier::SyncNotifierObserver*));
  MOCK_METHOD1(SetState, void(const std::string&));
  MOCK_METHOD2(UpdateCredentials,
               void(const std::string&, const std::string&));
  MOCK_METHOD1(UpdateEnabledTypes,
               void(const syncable::ModelTypeSet&));
  MOCK_METHOD0(SendNotification, void());
};

class SyncManagerTest : public testing::Test,
                        public ModelSafeWorkerRegistrar {
 protected:
  SyncManagerTest()
      : ui_thread_(BrowserThread::UI, &ui_loop_),
        sync_notifier_observer_(NULL),
        update_enabled_types_call_count_(0) {}

  // Test implementation.
  void SetUp() {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    SyncCredentials credentials;
    credentials.email = "foo@bar.com";
    credentials.sync_token = "sometoken";

    sync_notifier_mock_.reset(new StrictMock<SyncNotifierMock>());
    EXPECT_CALL(*sync_notifier_mock_, AddObserver(_)).
        WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierAddObserver));
    EXPECT_CALL(*sync_notifier_mock_, SetState(""));
    EXPECT_CALL(*sync_notifier_mock_,
                UpdateCredentials(credentials.email, credentials.sync_token));
    EXPECT_CALL(*sync_notifier_mock_, UpdateEnabledTypes(_)).
        Times(AtLeast(1)).
        WillRepeatedly(
            Invoke(this, &SyncManagerTest::SyncNotifierUpdateEnabledTypes));
    EXPECT_CALL(*sync_notifier_mock_, RemoveObserver(_)).
        WillOnce(Invoke(this, &SyncManagerTest::SyncNotifierRemoveObserver));

    EXPECT_FALSE(sync_notifier_observer_);

    sync_manager_.Init(temp_dir_.path(), "bogus", 0, false,
                       new TestHttpPostProviderFactory(), this, "bogus",
                       credentials, sync_notifier_mock_.get(), "",
                       true /* setup_for_test_mode */);

    EXPECT_TRUE(sync_notifier_observer_);
    sync_manager_.AddObserver(&observer_);

    EXPECT_EQ(1, update_enabled_types_call_count_);

    ModelSafeRoutingInfo routes;
    GetModelSafeRoutingInfo(&routes);
    for (ModelSafeRoutingInfo::iterator i = routes.begin(); i != routes.end();
         ++i) {
      EXPECT_CALL(observer_, OnChangesApplied(i->first, _, _, 1))
          .RetiresOnSaturation();
      EXPECT_CALL(observer_, OnChangesComplete(i->first))
          .RetiresOnSaturation();
      type_roots_[i->first] = MakeServerNodeForType(
          sync_manager_.GetUserShare(), i->first);
    }
  }

  void TearDown() {
    sync_manager_.RemoveObserver(&observer_);
    sync_manager_.Shutdown();
    EXPECT_FALSE(sync_notifier_observer_);
  }

  // ModelSafeWorkerRegistrar implementation.
  virtual void GetWorkers(std::vector<ModelSafeWorker*>* out) {
    NOTIMPLEMENTED();
    out->clear();
  }
  virtual void GetModelSafeRoutingInfo(ModelSafeRoutingInfo* out) {
    (*out)[syncable::NIGORI] = browser_sync::GROUP_PASSIVE;
    (*out)[syncable::BOOKMARKS] = browser_sync::GROUP_PASSIVE;
    (*out)[syncable::THEMES] = browser_sync::GROUP_PASSIVE;
    (*out)[syncable::SESSIONS] = browser_sync::GROUP_PASSIVE;
    (*out)[syncable::PASSWORDS] = browser_sync::GROUP_PASSIVE;
  }

  // Helper methods.
  bool SetUpEncryption() {
    // We need to create the nigori node as if it were an applied server update.
    UserShare* share = sync_manager_.GetUserShare();
    int64 nigori_id = GetIdForDataType(syncable::NIGORI);
    if (nigori_id == kInvalidId)
      return false;

    // Set the nigori cryptographer information.
    WriteTransaction trans(share);
    Cryptographer* cryptographer = trans.GetCryptographer();
    if (!cryptographer)
      return false;
    KeyParams params = {"localhost", "dummy", "foobar"};
    cryptographer->AddKey(params);
    sync_pb::NigoriSpecifics nigori;
    cryptographer->GetKeys(nigori.mutable_encrypted());
    WriteNode node(&trans);
    node.InitByIdLookup(nigori_id);
    node.SetNigoriSpecifics(nigori);
    return cryptographer->is_ready();
  }

  int64 GetIdForDataType(ModelType type) {
    if (type_roots_.count(type) == 0)
      return 0;
    return type_roots_[type];
  }

  void SyncNotifierAddObserver(
      sync_notifier::SyncNotifierObserver* sync_notifier_observer) {
    EXPECT_EQ(NULL, sync_notifier_observer_);
    sync_notifier_observer_ = sync_notifier_observer;
  }

  void SyncNotifierRemoveObserver(
      sync_notifier::SyncNotifierObserver* sync_notifier_observer) {
    EXPECT_EQ(sync_notifier_observer_, sync_notifier_observer);
    sync_notifier_observer_ = NULL;
  }

  void SyncNotifierUpdateEnabledTypes(
      const syncable::ModelTypeSet& types) {
    ModelSafeRoutingInfo routes;
    GetModelSafeRoutingInfo(&routes);
    syncable::ModelTypeSet expected_types;
    for (ModelSafeRoutingInfo::const_iterator it = routes.begin();
         it != routes.end(); ++it) {
      expected_types.insert(it->first);
    }
    EXPECT_EQ(expected_types, types);
    ++update_enabled_types_call_count_;
  }

 private:
  // Needed by |ui_thread_|.
  MessageLoopForUI ui_loop_;
  // Needed by |sync_manager_|.
  BrowserThread ui_thread_;
  // Needed by |sync_manager_|.
  ScopedTempDir temp_dir_;
  // Sync Id's for the roots of the enabled datatypes.
  std::map<ModelType, int64> type_roots_;
  scoped_ptr<StrictMock<SyncNotifierMock> > sync_notifier_mock_;

 protected:
  SyncManager sync_manager_;
  StrictMock<SyncManagerObserverMock> observer_;
  sync_notifier::SyncNotifierObserver* sync_notifier_observer_;
  int update_enabled_types_call_count_;
};

TEST_F(SyncManagerTest, UpdateEnabledTypes) {
  EXPECT_EQ(1, update_enabled_types_call_count_);
  // Triggers SyncNotifierUpdateEnabledTypes.
  sync_manager_.UpdateEnabledTypes();
  EXPECT_EQ(2, update_enabled_types_call_count_);
}

TEST_F(SyncManagerTest, ParentJsEventRouter) {
  StrictMock<MockJsEventRouter> event_router;
  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();
  EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
  js_backend->SetParentJsEventRouter(&event_router);
  EXPECT_EQ(&event_router, js_backend->GetParentJsEventRouter());
  js_backend->RemoveParentJsEventRouter();
  EXPECT_EQ(NULL, js_backend->GetParentJsEventRouter());
}

TEST_F(SyncManagerTest, ProcessMessage) {
  const JsArgList kNoArgs;

  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  // Messages sent without any parent router should be dropped.
  {
    StrictMock<MockJsEventHandler> event_handler;
    js_backend->ProcessMessage("unknownMessage",
                               kNoArgs, &event_handler);
    js_backend->ProcessMessage("getNotificationState",
                               kNoArgs, &event_handler);
  }

  {
    StrictMock<MockJsEventHandler> event_handler;
    StrictMock<MockJsEventRouter> event_router;

    ListValue false_args;
    false_args.Append(Value::CreateBooleanValue(false));

    EXPECT_CALL(event_router,
                RouteJsEvent("onGetNotificationStateFinished",
                             HasArgsAsList(false_args), &event_handler));

    js_backend->SetParentJsEventRouter(&event_router);

    // This message should be dropped.
    js_backend->ProcessMessage("unknownMessage",
                                 kNoArgs, &event_handler);

    // This should trigger the reply.
    js_backend->ProcessMessage("getNotificationState",
                                 kNoArgs, &event_handler);

    js_backend->RemoveParentJsEventRouter();
  }

  // Messages sent after a parent router has been removed should be
  // dropped.
  {
    StrictMock<MockJsEventHandler> event_handler;
    js_backend->ProcessMessage("unknownMessage",
                                 kNoArgs, &event_handler);
    js_backend->ProcessMessage("getNotificationState",
                                 kNoArgs, &event_handler);
  }
}

TEST_F(SyncManagerTest, ProcessMessageGetRootNode) {
  const JsArgList kNoArgs;

  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  StrictMock<MockJsEventHandler> event_handler;
  StrictMock<MockJsEventRouter> event_router;

  JsArgList return_args;

  EXPECT_CALL(event_router,
              RouteJsEvent("onGetRootNodeFinished", _, &event_handler)).
      WillOnce(SaveArg<1>(&return_args));

  js_backend->SetParentJsEventRouter(&event_router);

  // Should trigger the reply.
  js_backend->ProcessMessage("getRootNode", kNoArgs, &event_handler);

  EXPECT_EQ(1u, return_args.Get().GetSize());
  DictionaryValue* node_info = NULL;
  EXPECT_TRUE(return_args.Get().GetDictionary(0, &node_info));
  if (node_info) {
    ReadTransaction trans(sync_manager_.GetUserShare());
    ReadNode node(&trans);
    node.InitByRootLookup();
    CheckNodeValue(node, *node_info);
  } else {
    ADD_FAILURE();
  }

  js_backend->RemoveParentJsEventRouter();
}

void CheckGetNodeByIdReturnArgs(const SyncManager& sync_manager,
                                const JsArgList& return_args,
                                int64 id) {
  EXPECT_EQ(1u, return_args.Get().GetSize());
  DictionaryValue* node_info = NULL;
  EXPECT_TRUE(return_args.Get().GetDictionary(0, &node_info));
  if (node_info) {
    ReadTransaction trans(sync_manager.GetUserShare());
    ReadNode node(&trans);
    node.InitByIdLookup(id);
    CheckNodeValue(node, *node_info);
  } else {
    ADD_FAILURE();
  }
}

TEST_F(SyncManagerTest, ProcessMessageGetNodeById) {
  int64 child_id =
      MakeNode(sync_manager_.GetUserShare(), syncable::BOOKMARKS, "testtag");

  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  StrictMock<MockJsEventHandler> event_handler;
  StrictMock<MockJsEventRouter> event_router;

  JsArgList return_args;

  EXPECT_CALL(event_router,
              RouteJsEvent("onGetNodeByIdFinished", _, &event_handler))
      .Times(2).WillRepeatedly(SaveArg<1>(&return_args));

  js_backend->SetParentJsEventRouter(&event_router);

  // Should trigger the reply.
  {
    ListValue args;
    args.Append(Value::CreateStringValue("1"));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  CheckGetNodeByIdReturnArgs(sync_manager_, return_args, 1);

  // Should trigger another reply.
  {
    ListValue args;
    args.Append(Value::CreateStringValue(base::Int64ToString(child_id)));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  CheckGetNodeByIdReturnArgs(sync_manager_, return_args, child_id);

  js_backend->RemoveParentJsEventRouter();
}

TEST_F(SyncManagerTest, ProcessMessageGetNodeByIdFailure) {
  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  StrictMock<MockJsEventHandler> event_handler;
  StrictMock<MockJsEventRouter> event_router;

  ListValue null_args;
  null_args.Append(Value::CreateNullValue());

  EXPECT_CALL(event_router,
              RouteJsEvent("onGetNodeByIdFinished",
                           HasArgsAsList(null_args), &event_handler))
      .Times(5);

  js_backend->SetParentJsEventRouter(&event_router);

  {
    ListValue args;
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  {
    ListValue args;
    args.Append(Value::CreateStringValue(""));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  {
    ListValue args;
    args.Append(Value::CreateStringValue("nonsense"));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  {
    ListValue args;
    args.Append(Value::CreateStringValue("nonsense"));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  {
    ListValue args;
    args.Append(Value::CreateStringValue("0"));
    js_backend->ProcessMessage("getNodeById", JsArgList(args), &event_handler);
  }

  // TODO(akalin): Figure out how to test InitByIdLookup() failure.

  js_backend->RemoveParentJsEventRouter();
}

TEST_F(SyncManagerTest, OnNotificationStateChange) {
  StrictMock<MockJsEventRouter> event_router;

  ListValue true_args;
  true_args.Append(Value::CreateBooleanValue(true));
  ListValue false_args;
  false_args.Append(Value::CreateBooleanValue(false));

  EXPECT_CALL(event_router,
              RouteJsEvent("onSyncNotificationStateChange",
                           HasArgsAsList(true_args), NULL));
  EXPECT_CALL(event_router,
              RouteJsEvent("onSyncNotificationStateChange",
                           HasArgsAsList(false_args), NULL));

  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  sync_manager_.TriggerOnNotificationStateChangeForTest(true);
  sync_manager_.TriggerOnNotificationStateChangeForTest(false);

  js_backend->SetParentJsEventRouter(&event_router);
  sync_manager_.TriggerOnNotificationStateChangeForTest(true);
  sync_manager_.TriggerOnNotificationStateChangeForTest(false);
  js_backend->RemoveParentJsEventRouter();

  sync_manager_.TriggerOnNotificationStateChangeForTest(true);
  sync_manager_.TriggerOnNotificationStateChangeForTest(false);
}

TEST_F(SyncManagerTest, OnIncomingNotification) {
  StrictMock<MockJsEventRouter> event_router;

  const syncable::ModelTypeBitSet empty_model_types;
  syncable::ModelTypeBitSet model_types;
  model_types.set(syncable::BOOKMARKS);
  model_types.set(syncable::THEMES);

  // Build expected_args to have a single argument with the string
  // equivalents of model_types.
  ListValue expected_args;
  {
    ListValue* model_type_list = new ListValue();
    expected_args.Append(model_type_list);
    for (int i = syncable::FIRST_REAL_MODEL_TYPE;
         i < syncable::MODEL_TYPE_COUNT; ++i) {
      if (model_types[i]) {
        model_type_list->Append(
            Value::CreateStringValue(
                syncable::ModelTypeToString(
                    syncable::ModelTypeFromInt(i))));
      }
    }
  }

  EXPECT_CALL(event_router,
              RouteJsEvent("onSyncIncomingNotification",
                           HasArgsAsList(expected_args), NULL));

  browser_sync::JsBackend* js_backend = sync_manager_.GetJsBackend();

  sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
  sync_manager_.TriggerOnIncomingNotificationForTest(model_types);

  js_backend->SetParentJsEventRouter(&event_router);
  sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
  js_backend->RemoveParentJsEventRouter();

  sync_manager_.TriggerOnIncomingNotificationForTest(empty_model_types);
  sync_manager_.TriggerOnIncomingNotificationForTest(model_types);
}

TEST_F(SyncManagerTest, EncryptDataTypesWithNoData) {
  EXPECT_TRUE(SetUpEncryption());
  ModelTypeSet encrypted_types;
  encrypted_types.insert(syncable::BOOKMARKS);
  // Even though Passwords isn't marked for encryption, it's enabled, so it
  // should automatically be added to the response of OnEncryptionComplete.
  ModelTypeSet expected_types = encrypted_types;
  expected_types.insert(syncable::PASSWORDS);
  EXPECT_CALL(observer_, OnEncryptionComplete(expected_types));
  sync_manager_.EncryptDataTypes(encrypted_types);
  {
    ReadTransaction trans(sync_manager_.GetUserShare());
    EXPECT_EQ(encrypted_types,
              GetEncryptedDataTypes(trans.GetWrappedTrans()));
  }
}

TEST_F(SyncManagerTest, EncryptDataTypesWithData) {
  size_t batch_size = 5;
  EXPECT_TRUE(SetUpEncryption());

  // Create some unencrypted unsynced data.
  int64 folder = MakeFolderWithParent(sync_manager_.GetUserShare(),
                                      syncable::BOOKMARKS,
                                      GetIdForDataType(syncable::BOOKMARKS),
                                      NULL);
  // First batch_size nodes are children of folder.
  size_t i;
  for (i = 0; i < batch_size; ++i) {
    MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::BOOKMARKS,
                       StringPrintf("%"PRIuS"", i), folder);
  }
  // Next batch_size nodes are a different type and on their own.
  for (; i < 2*batch_size; ++i) {
    MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::SESSIONS,
                       StringPrintf("%"PRIuS"", i),
                       GetIdForDataType(syncable::SESSIONS));
  }
  // Last batch_size nodes are a third type that will not need encryption.
  for (; i < 3*batch_size; ++i) {
    MakeNodeWithParent(sync_manager_.GetUserShare(), syncable::THEMES,
                       StringPrintf("%"PRIuS"", i),
                       GetIdForDataType(syncable::THEMES));
  }

  {
    ReadTransaction trans(sync_manager_.GetUserShare());
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::BOOKMARKS,
                                                   false /* not encrypted */));
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::SESSIONS,
                                                   false /* not encrypted */));
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::THEMES,
                                                   false /* not encrypted */));
  }

  ModelTypeSet encrypted_types;
  encrypted_types.insert(syncable::BOOKMARKS);
  encrypted_types.insert(syncable::SESSIONS);
  encrypted_types.insert(syncable::PASSWORDS);
  EXPECT_CALL(observer_, OnEncryptionComplete(encrypted_types));
  sync_manager_.EncryptDataTypes(encrypted_types);

  {
    ReadTransaction trans(sync_manager_.GetUserShare());
    encrypted_types.erase(syncable::PASSWORDS);  // Not stored in nigori node.
    EXPECT_EQ(encrypted_types,
              GetEncryptedDataTypes(trans.GetWrappedTrans()));
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::BOOKMARKS,
                                                   true /* is encrypted */));
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::SESSIONS,
                                                   true /* is encrypted */));
    EXPECT_TRUE(syncable::VerifyDataTypeEncryption(trans.GetWrappedTrans(),
                                                   syncable::THEMES,
                                                   false /* not encrypted */));
  }
}

}  // namespace

}  // namespace browser_sync