// 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 <map>
#include <string>

#include "base/memory/scoped_ptr.h"
#include "base/memory/scoped_temp_dir.h"
#include "base/message_loop.h"
#include "base/stl_util-inl.h"
#include "base/task.h"
#include "chrome/browser/sessions/session_service.h"
#include "chrome/browser/sessions/session_service_test_helper.h"
#include "chrome/browser/sync/abstract_profile_sync_service_test.h"
#include "chrome/browser/sync/engine/syncapi.h"
#include "chrome/browser/sync/glue/session_change_processor.h"
#include "chrome/browser/sync/glue/session_data_type_controller.h"
#include "chrome/browser/sync/glue/session_model_associator.h"
#include "chrome/browser/sync/glue/sync_backend_host.h"
#include "chrome/browser/sync/profile_sync_factory_mock.h"
#include "chrome/browser/sync/profile_sync_test_util.h"
#include "chrome/browser/sync/protocol/session_specifics.pb.h"
#include "chrome/browser/sync/protocol/sync.pb.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/browser/sync/test_profile_sync_service.h"
#include "chrome/common/net/gaia/gaia_constants.h"
#include "chrome/test/browser_with_test_window_test.h"
#include "chrome/test/file_test_utils.h"
#include "chrome/test/profile_mock.h"
#include "chrome/test/sync/engine/test_id_factory.h"
#include "chrome/test/testing_profile.h"
#include "content/common/notification_observer.h"
#include "content/common/notification_registrar.h"
#include "content/common/notification_service.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using browser_sync::SessionChangeProcessor;
using browser_sync::SessionDataTypeController;
using browser_sync::SessionModelAssociator;
using browser_sync::SyncBackendHost;
using sync_api::SyncManager;
using testing::_;
using testing::Return;
using browser_sync::TestIdFactory;

namespace browser_sync {

class ProfileSyncServiceSessionTest
    : public BrowserWithTestWindowTest,
      public NotificationObserver {
 public:
  ProfileSyncServiceSessionTest()
      : window_bounds_(0, 1, 2, 3),
        notified_of_update_(false) {}
  ProfileSyncService* sync_service() { return sync_service_.get(); }

  TestIdFactory* ids() { return sync_service_->id_factory(); }

 protected:
  SessionService* service() { return helper_.service(); }

  virtual void SetUp() {
    // BrowserWithTestWindowTest implementation.
    BrowserWithTestWindowTest::SetUp();
    profile()->CreateRequestContext();
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    SessionService* session_service = new SessionService(temp_dir_.path());
    helper_.set_service(session_service);
    service()->SetWindowType(window_id_, Browser::TYPE_NORMAL);
    service()->SetWindowBounds(window_id_, window_bounds_, false);
    registrar_.Add(this, NotificationType::FOREIGN_SESSION_UPDATED,
        NotificationService::AllSources());
  }

  void Observe(NotificationType type,
      const NotificationSource& source,
      const NotificationDetails& details) {
    switch (type.value) {
      case NotificationType::FOREIGN_SESSION_UPDATED:
        notified_of_update_ = true;
        break;
      default:
        NOTREACHED();
        break;
    }
  }

  virtual void TearDown() {
    helper_.set_service(NULL);
    profile()->set_session_service(NULL);
    sync_service_.reset();
    {
      // 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();
  }

  bool StartSyncService(Task* task, bool will_fail_association) {
    if (sync_service_.get())
      return false;
    sync_service_.reset(new TestProfileSyncService(
        &factory_, profile(), "test user", false, task));
    profile()->set_session_service(helper_.service());

    // Register the session data type.
    model_associator_ =
        new SessionModelAssociator(sync_service_.get(),
                                   true /* setup_for_test */);
    change_processor_ = new SessionChangeProcessor(
        sync_service_.get(), model_associator_,
        true /* setup_for_test */);
    EXPECT_CALL(factory_, CreateSessionSyncComponents(_, _)).
        WillOnce(Return(ProfileSyncFactory::SyncComponents(
            model_associator_, change_processor_)));
    EXPECT_CALL(factory_, CreateDataTypeManager(_, _)).
        WillOnce(ReturnNewDataTypeManager());
    sync_service_->RegisterDataTypeController(
        new SessionDataTypeController(&factory_,
                                      profile(),
                                      sync_service_.get()));
    profile()->GetTokenService()->IssueAuthTokenForTest(
        GaiaConstants::kSyncService, "token");
    sync_service_->Initialize();
    MessageLoop::current()->Run();
    return true;
  }

  // Path used in testing.
  ScopedTempDir temp_dir_;
  SessionServiceTestHelper helper_;
  SessionModelAssociator* model_associator_;
  SessionChangeProcessor* change_processor_;
  SessionID window_id_;
  ProfileSyncFactoryMock factory_;
  scoped_ptr<TestProfileSyncService> sync_service_;
  const gfx::Rect window_bounds_;
  bool notified_of_update_;
  NotificationRegistrar registrar_;
};

class CreateRootTask : public Task {
 public:
  explicit CreateRootTask(ProfileSyncServiceSessionTest* test)
      : test_(test), success_(false) {
  }

  virtual ~CreateRootTask() {}
  virtual void Run() {
    success_ = ProfileSyncServiceTestHelper::CreateRoot(
        syncable::SESSIONS,
        test_->sync_service()->GetUserShare(),
        test_->ids());
  }

  bool success() { return success_; }

 private:
  ProfileSyncServiceSessionTest* test_;
  bool success_;
};

// Test that we can write this machine's session to a node and retrieve it.
TEST_F(ProfileSyncServiceSessionTest, WriteSessionToNode) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());
  ASSERT_EQ(model_associator_->GetSessionService(), helper_.service());

  // Check that the DataTypeController associated the models.
  bool has_nodes;
  ASSERT_TRUE(model_associator_->SyncModelHasUserCreatedNodes(&has_nodes));
  ASSERT_TRUE(has_nodes);
  std::string machine_tag = model_associator_->GetCurrentMachineTag();
  int64 sync_id = model_associator_->GetSyncIdFromSessionTag(machine_tag);
  ASSERT_NE(sync_api::kInvalidId, sync_id);

  // Check that we can get the correct session specifics back from the node.
  sync_api::ReadTransaction trans(sync_service_->GetUserShare());
  sync_api::ReadNode node(&trans);
  ASSERT_TRUE(node.InitByClientTagLookup(syncable::SESSIONS,
      machine_tag));
  const sync_pb::SessionSpecifics& specifics(node.GetSessionSpecifics());
  ASSERT_EQ(machine_tag, specifics.session_tag());
  ASSERT_TRUE(specifics.has_header());
  const sync_pb::SessionHeader& header_s = specifics.header();
  ASSERT_EQ(0, header_s.window_size());
}

// Test that we can fill this machine's session, write it to a node,
// and then retrieve it.
TEST_F(ProfileSyncServiceSessionTest, WriteFilledSessionToNode) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  // Check that the DataTypeController associated the models.
  bool has_nodes;
  ASSERT_TRUE(model_associator_->SyncModelHasUserCreatedNodes(&has_nodes));
  ASSERT_TRUE(has_nodes);
  AddTab(browser(), GURL("http://foo/1"));
  NavigateAndCommitActiveTab(GURL("http://foo/2"));
  AddTab(browser(), GURL("http://bar/1"));
  NavigateAndCommitActiveTab(GURL("http://bar/2"));

  ASSERT_TRUE(model_associator_->SyncModelHasUserCreatedNodes(&has_nodes));
  ASSERT_TRUE(has_nodes);
  std::string machine_tag = model_associator_->GetCurrentMachineTag();
  int64 sync_id = model_associator_->GetSyncIdFromSessionTag(machine_tag);
  ASSERT_NE(sync_api::kInvalidId, sync_id);

  // Check that this machine's data is not included in the foreign windows.
  std::vector<const ForeignSession*> foreign_sessions;
  model_associator_->GetAllForeignSessions(&foreign_sessions);
  ASSERT_EQ(foreign_sessions.size(), 0U);

  // Get the tabs for this machine from the node and check that they were
  // filled.
  SessionModelAssociator::TabLinksMap tab_map = model_associator_->tab_map_;
  ASSERT_EQ(2U, tab_map.size());
  // Tabs are ordered by sessionid in tab_map, so should be able to traverse
  // the tree based on order of tabs created
  SessionModelAssociator::TabLinksMap::iterator iter = tab_map.begin();
  ASSERT_EQ(2, iter->second.tab()->controller().entry_count());
  ASSERT_EQ(GURL("http://foo/1"), iter->second.tab()->controller().
          GetEntryAtIndex(0)->virtual_url());
  ASSERT_EQ(GURL("http://foo/2"), iter->second.tab()->controller().
          GetEntryAtIndex(1)->virtual_url());
  iter++;
  ASSERT_EQ(2, iter->second.tab()->controller().entry_count());
  ASSERT_EQ(GURL("http://bar/1"), iter->second.tab()->controller().
      GetEntryAtIndex(0)->virtual_url());
  ASSERT_EQ(GURL("http://bar/2"), iter->second.tab()->controller().
      GetEntryAtIndex(1)->virtual_url());
}

// Test that we fail on a failed model association.
TEST_F(ProfileSyncServiceSessionTest, FailModelAssociation) {
  ASSERT_TRUE(StartSyncService(NULL, true));
  ASSERT_TRUE(sync_service_->unrecoverable_error_detected());
}

// Write a foreign session to a node, and then retrieve it.
TEST_F(ProfileSyncServiceSessionTest, WriteForeignSessionToNode) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  // Check that the DataTypeController associated the models.
  bool has_nodes;
  ASSERT_TRUE(model_associator_->SyncModelHasUserCreatedNodes(&has_nodes));
  ASSERT_TRUE(has_nodes);

  // Fill an instance of session specifics with a foreign session's data.
  sync_pb::SessionSpecifics meta_specifics;
  std::string machine_tag = "session_sync123";
  meta_specifics.set_session_tag(machine_tag);
  sync_pb::SessionHeader* header_s = meta_specifics.mutable_header();
  sync_pb::SessionWindow* window_s = header_s->add_window();
  window_s->add_tab(0);
  window_s->set_browser_type(sync_pb::SessionWindow_BrowserType_TYPE_NORMAL);
  window_s->set_selected_tab_index(1);

  sync_pb::SessionSpecifics tab_specifics;
  tab_specifics.set_session_tag(machine_tag);
  sync_pb::SessionTab* tab = tab_specifics.mutable_tab();
  tab->set_tab_visual_index(13);
  tab->set_current_navigation_index(3);
  tab->set_pinned(true);
  tab->set_extension_app_id("app_id");
  sync_pb::TabNavigation* navigation = tab->add_navigation();
  navigation->set_index(12);
  navigation->set_virtual_url("http://foo/1");
  navigation->set_referrer("referrer");
  navigation->set_title("title");
  navigation->set_page_transition(sync_pb::TabNavigation_PageTransition_TYPED);

  // Update the server with the session specifics.
  {
    model_associator_->AssociateForeignSpecifics(meta_specifics, 0);
    model_associator_->AssociateForeignSpecifics(tab_specifics, 0);
  }

  // Check that the foreign session was associated and retrieve the data.
  std::vector<const ForeignSession*> foreign_sessions;
  model_associator_->GetAllForeignSessions(&foreign_sessions);
  ASSERT_EQ(1U, foreign_sessions.size());
  ASSERT_EQ(machine_tag, foreign_sessions[0]->foreign_session_tag);
  ASSERT_EQ(1U,  foreign_sessions[0]->windows.size());
  ASSERT_EQ(1U, foreign_sessions[0]->windows[0]->tabs.size());
  ASSERT_EQ(1U, foreign_sessions[0]->windows[0]->tabs[0]->navigations.size());
  ASSERT_EQ(foreign_sessions[0]->foreign_session_tag, machine_tag);
  ASSERT_EQ(1, foreign_sessions[0]->windows[0]->selected_tab_index);
  ASSERT_EQ(1, foreign_sessions[0]->windows[0]->type);
  ASSERT_EQ(13, foreign_sessions[0]->windows[0]->tabs[0]->tab_visual_index);
  ASSERT_EQ(3,
      foreign_sessions[0]->windows[0]->tabs[0]->current_navigation_index);
  ASSERT_TRUE(foreign_sessions[0]->windows[0]->tabs[0]->pinned);
  ASSERT_EQ("app_id",
      foreign_sessions[0]->windows[0]->tabs[0]->extension_app_id);
  ASSERT_EQ(12,
      foreign_sessions[0]->windows[0]->tabs[0]->navigations[0].index());
  ASSERT_EQ(GURL("referrer"),
      foreign_sessions[0]->windows[0]->tabs[0]->navigations[0].referrer());
  ASSERT_EQ(string16(ASCIIToUTF16("title")),
      foreign_sessions[0]->windows[0]->tabs[0]->navigations[0].title());
  ASSERT_EQ(PageTransition::TYPED,
      foreign_sessions[0]->windows[0]->tabs[0]->navigations[0].transition());
  ASSERT_EQ(GURL("http://foo/1"),
      foreign_sessions[0]->windows[0]->tabs[0]->navigations[0].virtual_url());
}

// Test the DataTypeController on update.
TEST_F(ProfileSyncServiceSessionTest, UpdatedSyncNodeActionUpdate) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());
  int64 node_id = model_associator_->GetSyncIdFromSessionTag(
      model_associator_->GetCurrentMachineTag());
  scoped_ptr<SyncManager::ChangeRecord> record(new SyncManager::ChangeRecord);
  record->action = SyncManager::ChangeRecord::ACTION_UPDATE;
  record->id = node_id;
  ASSERT_FALSE(notified_of_update_);
  {
    sync_api::WriteTransaction trans(sync_service_->GetUserShare());
    change_processor_->ApplyChangesFromSyncModel(&trans, record.get(), 1);
  }
  ASSERT_TRUE(notified_of_update_);
}

// Test the DataTypeController on add.
TEST_F(ProfileSyncServiceSessionTest, UpdatedSyncNodeActionAdd) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  int64 node_id = model_associator_->GetSyncIdFromSessionTag(
      model_associator_->GetCurrentMachineTag());
  scoped_ptr<SyncManager::ChangeRecord> record(new SyncManager::ChangeRecord);
  record->action = SyncManager::ChangeRecord::ACTION_ADD;
  record->id = node_id;
  ASSERT_FALSE(notified_of_update_);
  {
    sync_api::WriteTransaction trans(sync_service_->GetUserShare());
    change_processor_->ApplyChangesFromSyncModel(&trans, record.get(), 1);
  }
  ASSERT_TRUE(notified_of_update_);
}

// Test the DataTypeController on delete.
TEST_F(ProfileSyncServiceSessionTest, UpdatedSyncNodeActionDelete) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  int64 node_id = model_associator_->GetSyncIdFromSessionTag(
      model_associator_->GetCurrentMachineTag());
  scoped_ptr<SyncManager::ChangeRecord> record(new SyncManager::ChangeRecord);
  record->action = SyncManager::ChangeRecord::ACTION_DELETE;
  record->id = node_id;
  ASSERT_FALSE(notified_of_update_);
  {
    sync_api::WriteTransaction trans(sync_service_->GetUserShare());
    change_processor_->ApplyChangesFromSyncModel(&trans, record.get(), 1);
  }
  ASSERT_TRUE(notified_of_update_);
}
// Test the TabNodePool when it starts off empty.
TEST_F(ProfileSyncServiceSessionTest, TabNodePoolEmpty) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  std::vector<int64> node_ids;
  ASSERT_EQ(0U, model_associator_->tab_pool_.capacity());
  ASSERT_TRUE(model_associator_->tab_pool_.empty());
  ASSERT_TRUE(model_associator_->tab_pool_.full());
  const size_t num_ids = 10;
  for (size_t i = 0; i < num_ids; ++i) {
    int64 id = model_associator_->tab_pool_.GetFreeTabNode();
    ASSERT_GT(id, -1);
    node_ids.push_back(id);
  }
  ASSERT_EQ(num_ids, model_associator_->tab_pool_.capacity());
  ASSERT_TRUE(model_associator_->tab_pool_.empty());
  ASSERT_FALSE(model_associator_->tab_pool_.full());
  for (size_t i = 0; i < num_ids; ++i) {
    model_associator_->tab_pool_.FreeTabNode(node_ids[i]);
  }
  ASSERT_EQ(num_ids, model_associator_->tab_pool_.capacity());
  ASSERT_FALSE(model_associator_->tab_pool_.empty());
  ASSERT_TRUE(model_associator_->tab_pool_.full());
}

// Test the TabNodePool when it starts off with nodes
TEST_F(ProfileSyncServiceSessionTest, TabNodePoolNonEmpty) {
  CreateRootTask task(this);
  ASSERT_TRUE(StartSyncService(&task, false));
  ASSERT_TRUE(task.success());

  const size_t num_starting_nodes = 3;
  for (size_t i = 0; i < num_starting_nodes; ++i) {
    model_associator_->tab_pool_.AddTabNode(i);
  }

  std::vector<int64> node_ids;
  ASSERT_EQ(num_starting_nodes, model_associator_->tab_pool_.capacity());
  ASSERT_FALSE(model_associator_->tab_pool_.empty());
  ASSERT_TRUE(model_associator_->tab_pool_.full());
  const size_t num_ids = 10;
  for (size_t i = 0; i < num_ids; ++i) {
    int64 id = model_associator_->tab_pool_.GetFreeTabNode();
    ASSERT_GT(id, -1);
    node_ids.push_back(id);
  }
  ASSERT_EQ(num_ids, model_associator_->tab_pool_.capacity());
  ASSERT_TRUE(model_associator_->tab_pool_.empty());
  ASSERT_FALSE(model_associator_->tab_pool_.full());
  for (size_t i = 0; i < num_ids; ++i) {
    model_associator_->tab_pool_.FreeTabNode(node_ids[i]);
  }
  ASSERT_EQ(num_ids, model_associator_->tab_pool_.capacity());
  ASSERT_FALSE(model_associator_->tab_pool_.empty());
  ASSERT_TRUE(model_associator_->tab_pool_.full());
}

}  // namespace browser_sync