// 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 "base/file_util.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "base/values.h"
#include "chrome/browser/net/gaia/token_service.h"
#include "chrome/browser/sync/glue/bookmark_data_type_controller.h"
#include "chrome/browser/sync/glue/data_type_controller.h"
#include "chrome/browser/sync/js_arg_list.h"
#include "chrome/browser/sync/js_test_util.h"
#include "chrome/browser/sync/profile_sync_factory_mock.h"
#include "chrome/browser/sync/test_profile_sync_service.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/testing_pref_service.h"
#include "chrome/test/testing_profile.h"
#include "content/browser/browser_thread.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

// TODO(akalin): Add tests here that exercise the whole
// ProfileSyncService/SyncBackendHost stack while mocking out as
// little as possible.

namespace browser_sync {

namespace {

using testing::_;
using testing::AtLeast;
using testing::AtMost;
using testing::Return;
using testing::StrictMock;

class ProfileSyncServiceTest : public testing::Test {
 protected:
  ProfileSyncServiceTest()
      : ui_thread_(BrowserThread::UI, &message_loop_) {
    profile_.reset(new TestingProfile());
  }
  virtual ~ProfileSyncServiceTest() {
    // Kill the service before the profile.
    service_.reset();
    profile_.reset();

    // Ensure that the sync objects destruct to avoid memory leaks.
    MessageLoop::current()->RunAllPending();
  }
  virtual void SetUp() {
    profile_->CreateRequestContext();
  }
  virtual void TearDown() {
    {
      // The request context gets deleted on the I/O thread. To prevent a leak
      // supply one here.
      BrowserThread io_thread(BrowserThread::IO, MessageLoop::current());
      profile_->ResetRequestContext();
    }
    MessageLoop::current()->RunAllPending();
  }

  // TODO(akalin): Refactor the StartSyncService*() functions below.

  void StartSyncService() {
    StartSyncServiceAndSetInitialSyncEnded(true, true, false, true);
  }

  void StartSyncServiceAndSetInitialSyncEnded(
      bool set_initial_sync_ended,
      bool issue_auth_token,
      bool synchronous_sync_configuration,
      bool sync_setup_completed) {
    if (!service_.get()) {
      // Set bootstrap to true and it will provide a logged in user for test
      service_.reset(new TestProfileSyncService(&factory_,
                                                profile_.get(),
                                                "test", true, NULL));
      if (!set_initial_sync_ended)
        service_->dont_set_initial_sync_ended_on_init();
      if (synchronous_sync_configuration)
        service_->set_synchronous_sync_configuration();
      if (!sync_setup_completed)
        profile_->GetPrefs()->SetBoolean(prefs::kSyncHasSetupCompleted, false);

      // Register the bookmark data type.
      EXPECT_CALL(factory_, CreateDataTypeManager(_, _)).
          WillOnce(ReturnNewDataTypeManager());

      if (issue_auth_token) {
        profile_->GetTokenService()->IssueAuthTokenForTest(
            GaiaConstants::kSyncService, "token");
      }
      service_->Initialize();
    }
  }

  // This serves as the "UI loop" on which the ProfileSyncService lives and
  // operates. It is needed because the SyncBackend can post tasks back to
  // the service, meaning it can't be null. It doesn't have to be running,
  // though -- OnInitializationCompleted is the only example (so far) in this
  // test where we need to Run the loop to swallow a task and then quit, to
  // avoid leaking the ProfileSyncService (the PostTask will retain the callee
  // and caller until the task is run).
  MessageLoop message_loop_;
  BrowserThread ui_thread_;

  scoped_ptr<TestProfileSyncService> service_;
  scoped_ptr<TestingProfile> profile_;
  ProfileSyncFactoryMock factory_;
};

TEST_F(ProfileSyncServiceTest, InitialState) {
  service_.reset(new TestProfileSyncService(&factory_, profile_.get(),
                                            "", true, NULL));
  EXPECT_TRUE(
      service_->sync_service_url().spec() ==
        ProfileSyncService::kSyncServerUrl ||
      service_->sync_service_url().spec() ==
        ProfileSyncService::kDevServerUrl);
}

TEST_F(ProfileSyncServiceTest, DisabledByPolicy) {
  profile_->GetTestingPrefService()->SetManagedPref(
      prefs::kSyncManaged,
      Value::CreateBooleanValue(true));
  service_.reset(new TestProfileSyncService(&factory_, profile_.get(),
                                            "", true, NULL));
  service_->Initialize();
  EXPECT_TRUE(service_->IsManaged());
}

TEST_F(ProfileSyncServiceTest, AbortedByShutdown) {
  service_.reset(new TestProfileSyncService(&factory_, profile_.get(),
                                            "test", true, NULL));
  EXPECT_CALL(factory_, CreateDataTypeManager(_, _)).Times(0);
  EXPECT_CALL(factory_, CreateBookmarkSyncComponents(_, _)).Times(0);
  service_->RegisterDataTypeController(
      new BookmarkDataTypeController(&factory_,
                                     profile_.get(),
                                     service_.get()));

  service_->Initialize();
  service_.reset();
}
#if defined(OS_CHROMEOS) && defined(GOOGLE_CHROME_BUILD)
#define MAYBE_JsFrontendHandlersBasic DISABLED_JsFrontendHandlersBasic
#else
#define MAYBE_JsFrontendHandlersBasic JsFrontendHandlersBasic
#endif
TEST_F(ProfileSyncServiceTest, MAYBE_JsFrontendHandlersBasic) {
  StartSyncService();

  StrictMock<MockJsEventHandler> event_handler;

  SyncBackendHostForProfileSyncTest* test_backend =
      service_->GetBackendForTest();

  EXPECT_TRUE(service_->sync_initialized());
  ASSERT_TRUE(test_backend != NULL);
  ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
  EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());

  JsFrontend* js_backend = service_->GetJsFrontend();
  js_backend->AddHandler(&event_handler);
  ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
  EXPECT_TRUE(test_backend->GetJsBackend()->GetParentJsEventRouter() != NULL);

  js_backend->RemoveHandler(&event_handler);
  EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());
}

TEST_F(ProfileSyncServiceTest,
       JsFrontendHandlersDelayedBackendInitialization) {
  StartSyncServiceAndSetInitialSyncEnded(true, false, false, true);

  StrictMock<MockJsEventHandler> event_handler;
  EXPECT_CALL(event_handler,
              HandleJsEvent("onSyncServiceStateChanged",
                            HasArgs(JsArgList()))).Times(AtLeast(3));
  // For some reason, these events may or may not fire.
  //
  // TODO(akalin): Figure out exactly why there's non-determinism
  // here, and if possible remove it.
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesApplied", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesComplete", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onSyncNotificationStateChange", _))
      .Times(AtMost(1));

  EXPECT_EQ(NULL, service_->GetBackendForTest());
  EXPECT_FALSE(service_->sync_initialized());

  JsFrontend* js_backend = service_->GetJsFrontend();
  js_backend->AddHandler(&event_handler);
  // Since we're doing synchronous initialization, backend should be
  // initialized by this call.
  profile_->GetTokenService()->IssueAuthTokenForTest(
      GaiaConstants::kSyncService, "token");

  SyncBackendHostForProfileSyncTest* test_backend =
      service_->GetBackendForTest();

  EXPECT_TRUE(service_->sync_initialized());
  ASSERT_TRUE(test_backend != NULL);
  ASSERT_TRUE(test_backend->GetJsBackend() != NULL);
  EXPECT_TRUE(test_backend->GetJsBackend()->GetParentJsEventRouter() != NULL);

  js_backend->RemoveHandler(&event_handler);
  EXPECT_EQ(NULL, test_backend->GetJsBackend()->GetParentJsEventRouter());
}

TEST_F(ProfileSyncServiceTest, JsFrontendProcessMessageBasic) {
  StartSyncService();

  StrictMock<MockJsEventHandler> event_handler;
  // For some reason, these events may or may not fire.
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesApplied", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesComplete", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onSyncNotificationStateChange", _))
      .Times(AtMost(1));

  ListValue arg_list1;
  arg_list1.Append(Value::CreateBooleanValue(true));
  arg_list1.Append(Value::CreateIntegerValue(5));
  JsArgList args1(arg_list1);
  EXPECT_CALL(event_handler, HandleJsEvent("testMessage1", HasArgs(args1)));

  ListValue arg_list2;
  arg_list2.Append(Value::CreateStringValue("test"));
  arg_list2.Append(arg_list1.DeepCopy());
  JsArgList args2(arg_list2);
  EXPECT_CALL(event_handler,
              HandleJsEvent("delayTestMessage2", HasArgs(args2)));

  ListValue arg_list3;
  arg_list3.Append(arg_list1.DeepCopy());
  arg_list3.Append(arg_list2.DeepCopy());
  JsArgList args3(arg_list3);

  JsFrontend* js_backend = service_->GetJsFrontend();

  // Never replied to.
  js_backend->ProcessMessage("notRepliedTo", args3, &event_handler);

  // Replied to later.
  js_backend->ProcessMessage("delayTestMessage2", args2, &event_handler);

  js_backend->AddHandler(&event_handler);

  // Replied to immediately.
  js_backend->ProcessMessage("testMessage1", args1, &event_handler);

  // Fires off reply for delayTestMessage2.
  message_loop_.RunAllPending();

  // Never replied to.
  js_backend->ProcessMessage("delayNotRepliedTo", args3, &event_handler);

  js_backend->RemoveHandler(&event_handler);

  message_loop_.RunAllPending();

  // Never replied to.
  js_backend->ProcessMessage("notRepliedTo", args3, &event_handler);
}

TEST_F(ProfileSyncServiceTest,
       JsFrontendProcessMessageBasicDelayedBackendInitialization) {
  StartSyncServiceAndSetInitialSyncEnded(true, false, false, true);

  StrictMock<MockJsEventHandler> event_handler;
  // For some reason, these events may or may not fire.
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesApplied", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onChangesComplete", _))
      .Times(AtMost(1));
  EXPECT_CALL(event_handler, HandleJsEvent("onSyncNotificationStateChange", _))
      .Times(AtMost(1));

  ListValue arg_list1;
  arg_list1.Append(Value::CreateBooleanValue(true));
  arg_list1.Append(Value::CreateIntegerValue(5));
  JsArgList args1(arg_list1);
  EXPECT_CALL(event_handler, HandleJsEvent("testMessage1", HasArgs(args1)));

  ListValue arg_list2;
  arg_list2.Append(Value::CreateStringValue("test"));
  arg_list2.Append(arg_list1.DeepCopy());
  JsArgList args2(arg_list2);
  EXPECT_CALL(event_handler, HandleJsEvent("testMessage2", HasArgs(args2)));

  ListValue arg_list3;
  arg_list3.Append(arg_list1.DeepCopy());
  arg_list3.Append(arg_list2.DeepCopy());
  JsArgList args3(arg_list3);
  EXPECT_CALL(event_handler,
              HandleJsEvent("delayTestMessage3", HasArgs(args3)));

  const JsArgList kNoArgs;

  EXPECT_CALL(event_handler, HandleJsEvent("onSyncServiceStateChanged",
      HasArgs(kNoArgs))).Times(AtLeast(3));

  JsFrontend* js_backend = service_->GetJsFrontend();

  // We expect a reply for this message, even though its sent before
  // |event_handler| is added as a handler.
  js_backend->ProcessMessage("testMessage1", args1, &event_handler);

  js_backend->AddHandler(&event_handler);

  js_backend->ProcessMessage("testMessage2", args2, &event_handler);
  js_backend->ProcessMessage("delayTestMessage3", args3, &event_handler);

  // Fires testMessage1 and testMessage2.
  profile_->GetTokenService()->IssueAuthTokenForTest(
      GaiaConstants::kSyncService, "token");

  // Fires delayTestMessage3.
  message_loop_.RunAllPending();

  js_backend->ProcessMessage("delayNotRepliedTo", kNoArgs, &event_handler);

  js_backend->RemoveHandler(&event_handler);

  message_loop_.RunAllPending();

  js_backend->ProcessMessage("notRepliedTo", kNoArgs, &event_handler);
}

// Make sure that things still work if sync is not enabled, but some old sync
// databases are lingering in the "Sync Data" folder.
TEST_F(ProfileSyncServiceTest, TestStartupWithOldSyncData) {
  const char* nonsense1 = "reginald";
  const char* nonsense2 = "beartato";
  const char* nonsense3 = "harrison";
  FilePath temp_directory = profile_->GetPath().AppendASCII("Sync Data");
  FilePath sync_file1 =
      temp_directory.AppendASCII("BookmarkSyncSettings.sqlite3");
  FilePath sync_file2 = temp_directory.AppendASCII("SyncData.sqlite3");
  FilePath sync_file3 = temp_directory.AppendASCII("nonsense_file");
  ASSERT_TRUE(file_util::CreateDirectory(temp_directory));
  ASSERT_NE(-1,
            file_util::WriteFile(sync_file1, nonsense1, strlen(nonsense1)));
  ASSERT_NE(-1,
            file_util::WriteFile(sync_file2, nonsense2, strlen(nonsense2)));
  ASSERT_NE(-1,
            file_util::WriteFile(sync_file3, nonsense3, strlen(nonsense3)));

  StartSyncServiceAndSetInitialSyncEnded(false, false, true, false);
  EXPECT_FALSE(service_->HasSyncSetupCompleted());

  // Since we're doing synchronous initialization, backend should be
  // initialized by this call.
  profile_->GetTokenService()->IssueAuthTokenForTest(
      GaiaConstants::kSyncService, "token");

  // Stop the service so we can read the new Sync Data files that were
  // created.
  service_.reset();

  // This file should have been deleted when the whole directory was nuked.
  ASSERT_FALSE(file_util::PathExists(sync_file3));
  ASSERT_FALSE(file_util::PathExists(sync_file1));

  // This will still exist, but the text should have changed.
  ASSERT_TRUE(file_util::PathExists(sync_file2));
  std::string file2text;
  ASSERT_TRUE(file_util::ReadFileToString(sync_file2, &file2text));
  ASSERT_NE(file2text.compare(nonsense2), 0);
}

}  // namespace

}  // namespace browser_sync