// Copyright 2014 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 "components/signin/core/browser/gaia_cookie_manager_service.h"

#include <algorithm>
#include <memory>
#include <string>
#include <vector>

#include "base/location.h"
#include "base/macros.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/threading/thread_task_runner_handle.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/core/browser/account_tracker_service.h"
#include "components/signin/core/browser/signin_pref_names.h"
#include "components/signin/core/browser/test_signin_client.h"
#include "google_apis/gaia/fake_oauth2_token_service.h"
#include "google_apis/gaia/gaia_constants.h"
#include "google_apis/gaia/gaia_urls.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

class MockObserver : public GaiaCookieManagerService::Observer {
 public:
  explicit MockObserver(GaiaCookieManagerService* helper) : helper_(helper) {
    helper_->AddObserver(this);
  }

  ~MockObserver() override { helper_->RemoveObserver(this); }

  MOCK_METHOD2(OnAddAccountToCookieCompleted,
               void(const std::string&, const GoogleServiceAuthError&));
  MOCK_METHOD3(OnGaiaAccountsInCookieUpdated,
               void(const std::vector<gaia::ListedAccount>&,
                    const std::vector<gaia::ListedAccount>&,
                    const GoogleServiceAuthError&));

 private:
  GaiaCookieManagerService* helper_;

  DISALLOW_COPY_AND_ASSIGN(MockObserver);
};

// Counts number of InstrumentedGaiaCookieManagerService created.
// We can EXPECT_* to be zero at the end of our unit tests
// to make sure everything is properly deleted.

int total = 0;

bool AreAccountListsEqual(const std::vector<gaia::ListedAccount>& left,
                          const std::vector<gaia::ListedAccount>& right) {
  if (left.size() != right.size())
    return false;

  for (size_t i = 0u; i < left.size(); ++i) {
    const gaia::ListedAccount& left_account = left[i];
    const gaia::ListedAccount& actual_account = right[i];
    // If both accounts have an ID, use it for the comparison.
    if (!left_account.id.empty() && !actual_account.id.empty()) {
      if (left_account.id != actual_account.id)
        return false;
    } else if (left_account.email != actual_account.email ||
               left_account.gaia_id != actual_account.gaia_id ||
               left_account.raw_email != actual_account.raw_email ||
               left_account.valid != actual_account.valid ||
               left_account.signed_out != actual_account.signed_out ||
               left_account.verified != actual_account.verified) {
      return false;
    }
  }
  return true;
}

// Custom matcher for ListedAccounts.
MATCHER_P(ListedAccountEquals, expected, "") {
  return AreAccountListsEqual(expected, arg);
}

class InstrumentedGaiaCookieManagerService : public GaiaCookieManagerService {
 public:
  InstrumentedGaiaCookieManagerService(OAuth2TokenService* token_service,
                                       SigninClient* signin_client)
      : GaiaCookieManagerService(
            token_service,
            signin_client,
            base::BindRepeating(&SigninClient::GetURLLoaderFactory,
                                base::Unretained(signin_client))) {
    total++;
  }

  ~InstrumentedGaiaCookieManagerService() override { total--; }

  MOCK_METHOD0(StartFetchingUbertoken, void());
  MOCK_METHOD0(StartFetchingListAccounts, void());
  MOCK_METHOD0(StartFetchingLogOut, void());
  MOCK_METHOD0(StartFetchingMergeSession, void());
  MOCK_METHOD1(StartFetchingAccessTokenForMultilogin,
               void(const std::string& account_id));
  MOCK_METHOD0(SetAccountsInCookieWithTokens, void());
  MOCK_METHOD1(OnSetAccountsFinished,
               void(const GoogleServiceAuthError& error));

 private:
  DISALLOW_COPY_AND_ASSIGN(InstrumentedGaiaCookieManagerService);
};

class GaiaCookieManagerServiceTest : public testing::Test {
 public:
  GaiaCookieManagerServiceTest()
      : no_error_(GoogleServiceAuthError::NONE),
        error_(GoogleServiceAuthError::SERVICE_ERROR),
        canceled_(GoogleServiceAuthError::REQUEST_CANCELED) {}

  class RequestMockImpl : public OAuth2TokenService::Request {
   public:
    RequestMockImpl(std::string account_id) { account_id_ = account_id; }
    std::string GetAccountId() const override { return account_id_; }

   private:
    std::string account_id_;
  };

  void SetUp() override {
    AccountTrackerService::RegisterPrefs(pref_service_.registry());
    signin_client_.reset(new TestSigninClient(&pref_service_));
  }

  OAuth2TokenService* token_service() { return &token_service_; }
  TestSigninClient* signin_client() { return signin_client_.get(); }

  void SimulateUbertokenSuccess(GaiaCookieManagerService* gcms,
                                const std::string& uber_token) {
    gcms->OnUbertokenFetchComplete(
        GoogleServiceAuthError(GoogleServiceAuthError::NONE), uber_token);
  }

  void SimulateUbertokenFailure(GaiaCookieManagerService* gcms,
                                const GoogleServiceAuthError& error) {
    gcms->OnUbertokenFetchComplete(error, /*uber_token=*/std::string());
  }

  void SimulateAccessTokenFailure(OAuth2TokenService::Consumer* consumer,
                                  OAuth2TokenService::Request* request,
                                  const GoogleServiceAuthError& error) {
    consumer->OnGetTokenFailure(request, error);
  }

  void SimulateAccessTokenSuccess(OAuth2TokenService::Consumer* consumer,
                                  OAuth2TokenService::Request* request) {
    OAuth2AccessTokenConsumer::TokenResponse token_response =
        OAuth2AccessTokenConsumer::TokenResponse("AccessToken", base::Time(),
                                                 "Idtoken");
    consumer->OnGetTokenSuccess(request, token_response);
  }

  void SimulateMergeSessionSuccess(GaiaAuthConsumer* consumer,
                                   const std::string& data) {
    consumer->OnMergeSessionSuccess(data);
  }

  void SimulateMergeSessionFailure(GaiaAuthConsumer* consumer,
                                   const GoogleServiceAuthError& error) {
    consumer->OnMergeSessionFailure(error);
  }

  void SimulateMultiloginFinished(GaiaAuthConsumer* consumer,
                                  const OAuthMultiloginResult& result) {
    consumer->OnOAuthMultiloginFinished(result);
  }

  void SimulateListAccountsSuccess(GaiaAuthConsumer* consumer,
                                   const std::string& data) {
    consumer->OnListAccountsSuccess(data);
  }

  void SimulateLogOutSuccess(GaiaAuthConsumer* consumer) {
    consumer->OnLogOutSuccess();
  }

  void SimulateLogOutFailure(GaiaAuthConsumer* consumer,
                             const GoogleServiceAuthError& error) {
    consumer->OnLogOutFailure(error);
  }

  void SimulateGetCheckConnectionInfoSuccess(const std::string& data) {
    signin_client_->test_url_loader_factory()->AddResponse(
        GaiaUrls::GetInstance()
            ->GetCheckConnectionInfoURLWithSource(GaiaConstants::kChromeSource)
            .spec(),
        data);
    base::RunLoop().RunUntilIdle();
  }

  void SimulateGetCheckConnectionInfoResult(const std::string& url,
                                            const std::string& result) {
    signin_client_->test_url_loader_factory()->AddResponse(url, result);
    base::RunLoop().RunUntilIdle();
  }

  void Advance(scoped_refptr<base::TestMockTimeTaskRunner> test_task_runner,
               base::TimeDelta advance_by) {
    test_task_runner->FastForwardBy(advance_by +
                                    base::TimeDelta::FromMilliseconds(1));
    test_task_runner->RunUntilIdle();
  }

  bool IsLoadPending(const std::string& url) {
    return signin_client_->test_url_loader_factory()->IsPending(
        GURL(url).spec());
  }

  bool IsLoadPending() {
    return signin_client_->test_url_loader_factory()->NumPending() > 0;
  }

  const GoogleServiceAuthError& no_error() { return no_error_; }
  const GoogleServiceAuthError& error() { return error_; }
  const GoogleServiceAuthError& canceled() { return canceled_; }

  scoped_refptr<network::SharedURLLoaderFactory> factory() const {
    return signin_client_->GetURLLoaderFactory();
  }

 private:
  base::test::ScopedTaskEnvironment task_environment_;
  FakeOAuth2TokenService token_service_;
  GoogleServiceAuthError no_error_;
  GoogleServiceAuthError error_;
  GoogleServiceAuthError canceled_;
  TestingPrefServiceSimple pref_service_;
  std::unique_ptr<TestSigninClient> signin_client_;
};

}  // namespace

using ::testing::_;

TEST_F(GaiaCookieManagerServiceTest, Success) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token");
}

TEST_F(GaiaCookieManagerServiceTest, FailedMergeSession) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);
  base::HistogramTester histograms;

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionFailure(&helper, error());
  // Persistent error incurs no further retries.
  DCHECK(!helper.is_running());
  histograms.ExpectUniqueSample("OAuth2Login.MergeSessionFailure",
                                GoogleServiceAuthError::SERVICE_ERROR, 1);
}

TEST_F(GaiaCookieManagerServiceTest, AddAccountCookiesDisabled) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);
  signin_client()->set_are_signin_cookies_allowed(false);

  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", canceled()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
}

TEST_F(GaiaCookieManagerServiceTest, MergeSessionRetried) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(helper, StartFetchingMergeSession());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionFailure(&helper, canceled());
  DCHECK(helper.is_running());
  Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  SimulateMergeSessionSuccess(&helper, "token");
  DCHECK(!helper.is_running());
}

TEST_F(GaiaCookieManagerServiceTest, MergeSessionRetriedTwice) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);
  base::HistogramTester histograms;

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(helper, StartFetchingMergeSession()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionFailure(&helper, canceled());
  DCHECK(helper.is_running());
  Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  SimulateMergeSessionFailure(&helper, canceled());
  DCHECK(helper.is_running());
  Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  SimulateMergeSessionSuccess(&helper, "token");
  DCHECK(!helper.is_running());
  histograms.ExpectUniqueSample("OAuth2Login.MergeSessionRetry",
                                GoogleServiceAuthError::REQUEST_CANCELED, 2);
}

TEST_F(GaiaCookieManagerServiceTest, FailedUbertoken) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  SimulateUbertokenFailure(&helper, error());
}

TEST_F(GaiaCookieManagerServiceTest, AccessTokenSuccess) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens());

  const std::vector<std::string> account_ids = {account_id1, account_id2};

  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);

  SimulateAccessTokenSuccess(&helper, &request2);

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_UNAVAILABLE);

  // Transient error, retry.
  SimulateAccessTokenFailure(&helper, &request1, error);

  DCHECK(helper.is_running());
  Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());

  SimulateAccessTokenSuccess(&helper, &request1);
}

TEST_F(GaiaCookieManagerServiceTest,
       AccessTokenFailureTransientErrorMaxRetriesReached) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_UNAVAILABLE);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(signin::kMaxFetcherRetries - 1);
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(0);

  const std::vector<std::string> account_ids = {account_id1, account_id2};

  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);

  EXPECT_LT(helper.GetBackoffEntry()->GetTimeUntilRelease(),
            base::TimeDelta::FromMilliseconds(1100));

  // Transient error, retry, fail when maximum number of retries is reached.
  for (int i = 0; i < signin::kMaxFetcherRetries - 1; ++i) {
    SimulateAccessTokenFailure(&helper, &request1, error);
    Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  }
  SimulateAccessTokenFailure(&helper, &request1, error);
  // Check that no Multilogin is triggered.
  SimulateAccessTokenSuccess(&helper, &request2);
}

TEST_F(GaiaCookieManagerServiceTest, AccessTokenFailurePersistentError) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";

  GoogleServiceAuthError error(
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(0);

  const std::vector<std::string> account_ids = {account_id1, account_id2};

  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);

  SimulateAccessTokenFailure(&helper, &request1, error);

  // Check that no Multilogin is triggered.
  SimulateAccessTokenSuccess(&helper, &request2);
}

TEST_F(GaiaCookieManagerServiceTest, FetcherRetriesZeroedBetweenCalls) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_UNAVAILABLE);

  std::string data =
      R"()]}'
        {
          "status": "OK",
          "cookies":[
          {
              "name":"SID",
              "value":"vAlUe1",
              "domain":".google.ru",
              "path":"/",
              "isSecure":true,
              "isHttpOnly":false,
              "priority":"HIGH",
              "maxAge":63070000
            }
          ]
        }
      )";
  OAuthMultiloginResult result(data);
  ASSERT_EQ(result.error().state(), GoogleServiceAuthError::State::NONE);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  // retry call
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(signin::kMaxFetcherRetries - 1);
  // retry call
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(1);
  EXPECT_CALL(helper,
              OnSetAccountsFinished(GoogleServiceAuthError::AuthErrorNone()))
      .Times(1);

  const std::vector<std::string> account_ids = {account_id1, account_id2};

  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);
  // Transient error, retry.
  // Succeed when only one retry is left. Simulate Multilogin failure. Check
  // that it retries.
  for (int i = 0; i < signin::kMaxFetcherRetries - 1; ++i) {
    SimulateAccessTokenFailure(&helper, &request1, error);
    Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  }
  SimulateAccessTokenSuccess(&helper, &request1);
  SimulateAccessTokenSuccess(&helper, &request2);

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  helper.StartFetchingMultiLogin(accounts);
  SimulateMultiloginFinished(&helper, OAuthMultiloginResult(error));
  SimulateMultiloginFinished(&helper, result);
}

TEST_F(GaiaCookieManagerServiceTest, MultiloginSuccessAndCookiesSet) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";
  const std::vector<std::string> account_ids = {account_id1, account_id2};

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_UNAVAILABLE);

  std::string data =
      R"()]}'
        {
          "status": "OK",
          "cookies":[
            {
              "name":"SID",
              "value":"vAlUe1",
              "domain":".google.ru",
              "path":"/",
              "isSecure":true,
              "isHttpOnly":false,
              "priority":"HIGH",
              "maxAge":63070000
            },
            {
              "name":"SID",
              "value":"vAlUe1",
              "domain":".google.ru",
              "path":"/",
              "isSecure":true,
              "isHttpOnly":false,
              "priority":"HIGH",
              "maxAge":63070000
            },
            {
              "name":"HSID",
              "value":"vAlUe4",
              "host":"google.fr",
              "path":"/",
              "isSecure":true,
              "isHttpOnly":false,
              "priority":"HIGH",
              "maxAge":0
            }
          ]
        }
      )";
  OAuthMultiloginResult result(data);
  ASSERT_EQ(result.error().state(), GoogleServiceAuthError::State::NONE);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(1);
  EXPECT_CALL(helper,
              OnSetAccountsFinished(GoogleServiceAuthError::AuthErrorNone()))
      .Times(1);

  // Needed to insert request in the queue.
  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  helper.StartFetchingMultiLogin(accounts);

  SimulateMultiloginFinished(&helper, OAuthMultiloginResult(error));

  DCHECK(helper.is_running());
  Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());

  SimulateMultiloginFinished(&helper, result);
}

TEST_F(GaiaCookieManagerServiceTest, MultiloginFailurePersistentError) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";
  const std::vector<std::string> account_ids = {account_id1, account_id2};

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_ERROR);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(1);

  // Needed to insert request in the queue.
  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  helper.StartFetchingMultiLogin(accounts);

  SimulateMultiloginFinished(&helper, OAuthMultiloginResult(error));
}

TEST_F(GaiaCookieManagerServiceTest, MultiloginFailureMaxRetriesReached) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  auto test_task_runner = base::MakeRefCounted<base::TestMockTimeTaskRunner>();
  base::ScopedClosureRunner task_runner_ =
      base::ThreadTaskRunnerHandle::OverrideForTesting(test_task_runner);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";
  const std::vector<std::string> account_ids = {account_id1, account_id2};

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  GoogleServiceAuthError error(GoogleServiceAuthError::SERVICE_UNAVAILABLE);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  // This is the retry call, the first call is skipped as we call
  // StartFetchingMultiLogim explicitly instead.
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens())
      .Times(signin::kMaxFetcherRetries - 1);
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(1);

  // Needed to insert request in the queue.
  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  helper.StartFetchingMultiLogin(accounts);

  // Transient error, retry, fail when maximum number of retries is reached.
  for (int i = 0; i < signin::kMaxFetcherRetries - 1; ++i) {
    SimulateMultiloginFinished(&helper, OAuthMultiloginResult(error));
    Advance(test_task_runner, helper.GetBackoffEntry()->GetTimeUntilRelease());
  }
  SimulateMultiloginFinished(&helper, OAuthMultiloginResult(error));
}

TEST_F(GaiaCookieManagerServiceTest,
       MultiloginFailureInvalidGaiaCredentialsMobile) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";
  const std::vector<std::string> account_ids = {account_id1, account_id2};

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);

  GoogleServiceAuthError error(
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);

  std::string data_ok =
      R"()]}'
        {
          "status": "OK",
          "cookies":[
          {
              "name":"SID",
              "value":"vAlUe1",
              "domain":".google.ru",
              "path":"/",
              "isSecure":true,
              "isHttpOnly":false,
              "priority":"HIGH",
              "maxAge":63070000
            }
          ]
        }
      )";
  OAuthMultiloginResult result_ok(data_ok);
  ASSERT_EQ(result_ok.error().state(), GoogleServiceAuthError::State::NONE);

  std::string data_failed =
      R"()]}'
      {
        "status": "INVALID_TOKENS",
        "failed_accounts": [
          {
            "obfuscated_id": "12345", "status": "RECOVERABLE"
          },
          {
            "obfuscated_id": "23456", "status": "OK"
          }
        ]
      }
    )";
  OAuthMultiloginResult result_failed(data_failed);
  ASSERT_EQ(result_failed.error().state(),
            GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(1);

  // On mobile token_service is expected to retry getting access tokens first.
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(0);

  // Expect to try to refetch failed account one more time.
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);

  // This time access tokens are supposed to be correct.
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(1);
  EXPECT_CALL(helper,
              OnSetAccountsFinished(GoogleServiceAuthError::AuthErrorNone()))
      .Times(1);

  // Needed to insert request in the queue.
  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  // Both requests for access tokens are successful but they could be returned
  // from cache and be stale.
  SimulateAccessTokenSuccess(&helper, &request1);
  SimulateAccessTokenSuccess(&helper, &request2);
  // Both tokens are inserted in the map.
  EXPECT_EQ(2u, helper.access_tokens_.size());

  helper.StartFetchingMultiLogin(accounts);
  // Access tokens were stale, Multilogin failed.
  SimulateMultiloginFinished(&helper, result_failed);

  // GaiaCookieManagerService should retry fetching access token again,
  // it should be removed from the token map.
  EXPECT_EQ(1u, helper.access_tokens_.size());
  EXPECT_EQ(helper.access_tokens_.begin()->first, account_id2);

  // This time access token is fresh.
  SimulateAccessTokenSuccess(&helper, &request1);

  EXPECT_EQ(2u, helper.access_tokens_.size());

  // And Multilogin is successful.
  SimulateMultiloginFinished(&helper, result_ok);

  // The end.
}

TEST_F(GaiaCookieManagerServiceTest,
       MultiloginFailureInvalidGaiaCredentialsDesktop) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  const std::string account_id1 = "12345";
  const std::string account_id2 = "23456";
  const std::vector<std::string> account_ids = {account_id1, account_id2};

  std::vector<GaiaAuthFetcher::MultiloginTokenIDPair> accounts =
      std::vector<GaiaAuthFetcher::MultiloginTokenIDPair>();
  accounts.emplace_back(account_id1, "AccessToken");
  accounts.emplace_back(account_id2, "AccessToken");

  RequestMockImpl request1(account_id1);
  RequestMockImpl request2(account_id2);

  GoogleServiceAuthError error(
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);

  std::string data_failed =
      R"()]}'
      {
        "status": "INVALID_TOKENS",
        "failed_accounts": [
          {
            "obfuscated_id": "12345", "status": "RECOVERABLE"
          },
          {
            "obfuscated_id": "23456", "status": "OK"
          }
        ]
      }
    )";
  OAuthMultiloginResult result_failed(data_failed);
  ASSERT_EQ(result_failed.error().state(),
            GoogleServiceAuthError::State::INVALID_GAIA_CREDENTIALS);

  testing::InSequence mock_sequence;
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id2))
      .Times(1);
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(1);
  // Expect to try to refetch failed account one more time.
  EXPECT_CALL(helper, StartFetchingAccessTokenForMultilogin(account_id1))
      .Times(1);
  // And fail right away.
  EXPECT_CALL(helper, SetAccountsInCookieWithTokens()).Times(0);
  EXPECT_CALL(helper, OnSetAccountsFinished(error)).Times(1);

  // Needed to insert request in the queue.
  helper.SetAccountsInCookie(account_ids, gaia::GaiaSource::kChrome);

  // Both requests for access tokens are successful but they could be returned
  // from cache and be stale.
  SimulateAccessTokenSuccess(&helper, &request1);
  SimulateAccessTokenSuccess(&helper, &request2);
  // Both tokens are inserted in the map.
  EXPECT_EQ(2u, helper.access_tokens_.size());

  helper.StartFetchingMultiLogin(accounts);

  // On desktop refresh tokens were used and failed. Token service will retry
  // fetching access token but refresh token is supposed to be set in error and
  // request will return with immediate error.
  SimulateMultiloginFinished(&helper, result_failed);

  SimulateAccessTokenFailure(&helper, &request1, error);
}

TEST_F(GaiaCookieManagerServiceTest, ContinueAfterSuccess) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");
  SimulateMergeSessionSuccess(&helper, "token2");
}

TEST_F(GaiaCookieManagerServiceTest, ContinueAfterFailure1) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", error()));
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionFailure(&helper, error());
  SimulateMergeSessionSuccess(&helper, "token2");
}

TEST_F(GaiaCookieManagerServiceTest, ContinueAfterFailure2) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", error()));
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateUbertokenFailure(&helper, error());
  SimulateMergeSessionSuccess(&helper, "token2");
}

TEST_F(GaiaCookieManagerServiceTest, AllRequestsInMultipleGoes) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(4);
  EXPECT_CALL(observer, OnAddAccountToCookieCompleted(_, no_error())).Times(4);

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token1");

  helper.AddAccountToCookie("acc3@gmail.com", gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token2");
  SimulateMergeSessionSuccess(&helper, "token3");

  helper.AddAccountToCookie("acc4@gmail.com", gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token4");
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsNoQueue) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");

  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  SimulateLogOutSuccess(&helper);
  ASSERT_FALSE(helper.is_running());
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsFails) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");

  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  SimulateLogOutFailure(&helper, error());
  // CookieManagerService is still running; it is retrying the failed logout.
  ASSERT_TRUE(helper.is_running());
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsAfterOneAddInQueue) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token1");
  SimulateLogOutSuccess(&helper);
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsAfterTwoAddsInQueue) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", canceled()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  // The Log Out should prevent this AddAccount from being fetched.
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token1");
  SimulateLogOutSuccess(&helper);
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsTwice) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");

  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  // Only one LogOut will be fetched.
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  SimulateLogOutSuccess(&helper);
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsBeforeAdd) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc3@gmail.com", no_error()));
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");

  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc3@gmail.com", gaia::GaiaSource::kChrome);

  SimulateLogOutSuccess(&helper);
  // After LogOut the MergeSession should be fetched.
  SimulateMergeSessionSuccess(&helper, "token2");
}

TEST_F(GaiaCookieManagerServiceTest, LogOutAllAccountsBeforeLogoutAndAdd) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc3@gmail.com", no_error()));

  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token1");

  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  // Second LogOut will never be fetched.
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc3@gmail.com", gaia::GaiaSource::kChrome);

  SimulateLogOutSuccess(&helper);
  // After LogOut the MergeSession should be fetched.
  SimulateMergeSessionSuccess(&helper, "token2");
}

TEST_F(GaiaCookieManagerServiceTest, PendingSigninThenSignout) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  // From the first Signin.
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));

  // From the sign out and then re-sign in.
  EXPECT_CALL(helper, StartFetchingLogOut());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc3@gmail.com", no_error()));

  // Total sign in 2 times, not enforcing ordered sequences.
  EXPECT_CALL(helper, StartFetchingUbertoken()).Times(2);

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token1");
  SimulateLogOutSuccess(&helper);

  helper.AddAccountToCookie("acc3@gmail.com", gaia::GaiaSource::kChrome);
  SimulateMergeSessionSuccess(&helper, "token3");
}

TEST_F(GaiaCookieManagerServiceTest, CancelSignIn) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  EXPECT_CALL(helper, StartFetchingUbertoken());
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc2@gmail.com", canceled()));
  EXPECT_CALL(observer,
              OnAddAccountToCookieCompleted("acc1@gmail.com", no_error()));
  EXPECT_CALL(helper, StartFetchingLogOut());

  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  helper.LogOutAllAccounts(gaia::GaiaSource::kChrome);

  SimulateMergeSessionSuccess(&helper, "token1");
  SimulateLogOutSuccess(&helper);
}

TEST_F(GaiaCookieManagerServiceTest, ListAccountsFirstReturnsEmpty) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  std::vector<gaia::ListedAccount> list_accounts;
  std::vector<gaia::ListedAccount> signed_out_accounts;

  EXPECT_CALL(helper, StartFetchingListAccounts());

  ASSERT_FALSE(helper.ListAccounts(&list_accounts, &signed_out_accounts));
  ASSERT_TRUE(list_accounts.empty());
  ASSERT_TRUE(signed_out_accounts.empty());
}

TEST_F(GaiaCookieManagerServiceTest, ListAccountsFindsOneAccount) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  std::vector<gaia::ListedAccount> list_accounts;
  std::vector<gaia::ListedAccount> expected_accounts;
  gaia::ListedAccount listed_account;
  listed_account.email = "a@b.com";
  listed_account.raw_email = "a@b.com";
  listed_account.gaia_id = "8";
  expected_accounts.push_back(listed_account);

  std::vector<gaia::ListedAccount> signed_out_accounts;
  std::vector<gaia::ListedAccount> expected_signed_out_accounts;

  EXPECT_CALL(helper, StartFetchingListAccounts());
  EXPECT_CALL(observer, OnGaiaAccountsInCookieUpdated(
                            ListedAccountEquals(expected_accounts),
                            ListedAccountEquals(expected_signed_out_accounts),
                            no_error()));

  ASSERT_FALSE(helper.ListAccounts(&list_accounts, &signed_out_accounts));

  SimulateListAccountsSuccess(
      &helper,
      "[\"f\", [[\"b\", 0, \"n\", \"a@b.com\", \"p\", 0, 0, 0, 0, 1, \"8\"]]]");
}

TEST_F(GaiaCookieManagerServiceTest, ListAccountsFindsSignedOutAccounts) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  std::vector<gaia::ListedAccount> list_accounts;
  std::vector<gaia::ListedAccount> expected_accounts;
  gaia::ListedAccount listed_account;
  listed_account.email = "a@b.com";
  listed_account.raw_email = "a@b.com";
  listed_account.gaia_id = "8";
  expected_accounts.push_back(listed_account);

  std::vector<gaia::ListedAccount> signed_out_accounts;
  std::vector<gaia::ListedAccount> expected_signed_out_accounts;
  gaia::ListedAccount signed_out_account;
  signed_out_account.email = "c@d.com";
  signed_out_account.raw_email = "c@d.com";
  signed_out_account.gaia_id = "9";
  signed_out_account.signed_out = true;
  expected_signed_out_accounts.push_back(signed_out_account);

  EXPECT_CALL(helper, StartFetchingListAccounts());
  EXPECT_CALL(observer, OnGaiaAccountsInCookieUpdated(
                            ListedAccountEquals(expected_accounts),
                            ListedAccountEquals(expected_signed_out_accounts),
                            no_error()));

  ASSERT_FALSE(helper.ListAccounts(&list_accounts, &signed_out_accounts));

  SimulateListAccountsSuccess(
      &helper,
      "[\"f\","
      "[[\"b\", 0, \"n\", \"a@b.com\", \"p\", 0, 0, 0, 0, 1, \"8\"],"
      " [\"b\", 0, \"n\", \"c@d.com\", \"p\", 0, 0, 0, 0, 1, \"9\","
      "null,null,null,1]]]");
}

TEST_F(GaiaCookieManagerServiceTest, ListAccountsAcceptsNull) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  ASSERT_FALSE(helper.ListAccounts(nullptr, nullptr));

  SimulateListAccountsSuccess(
      &helper,
      "[\"f\","
      "[[\"b\", 0, \"n\", \"a@b.com\", \"p\", 0, 0, 0, 0, 1, \"8\"],"
      " [\"b\", 0, \"n\", \"c@d.com\", \"p\", 0, 0, 0, 0, 1, \"9\","
      "null,null,null,1]]]");

  std::vector<gaia::ListedAccount> signed_out_accounts;
  ASSERT_TRUE(helper.ListAccounts(nullptr, &signed_out_accounts));
  ASSERT_EQ(1u, signed_out_accounts.size());

  std::vector<gaia::ListedAccount> accounts;
  ASSERT_TRUE(helper.ListAccounts(&accounts, nullptr));
  ASSERT_EQ(1u, accounts.size());
}

TEST_F(GaiaCookieManagerServiceTest, ListAccountsAfterOnCookieChange) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  MockObserver observer(&helper);

  std::vector<gaia::ListedAccount> list_accounts;
  std::vector<gaia::ListedAccount> empty_list_accounts;
  std::vector<gaia::ListedAccount> signed_out_accounts;
  std::vector<gaia::ListedAccount> empty_signed_out_accounts;

  std::vector<gaia::ListedAccount> nonempty_list_accounts;
  gaia::ListedAccount listed_account;
  listed_account.email = "a@b.com";
  listed_account.raw_email = "a@b.com";
  listed_account.gaia_id = "8";
  nonempty_list_accounts.push_back(listed_account);

  // Add a single account.
  EXPECT_CALL(helper, StartFetchingListAccounts());
  EXPECT_CALL(observer,
              OnGaiaAccountsInCookieUpdated(
                  ListedAccountEquals(nonempty_list_accounts),
                  ListedAccountEquals(empty_signed_out_accounts), no_error()));
  ASSERT_FALSE(helper.ListAccounts(&list_accounts, &signed_out_accounts));
  ASSERT_TRUE(list_accounts.empty());
  ASSERT_TRUE(signed_out_accounts.empty());
  SimulateListAccountsSuccess(
      &helper,
      "[\"f\", [[\"b\", 0, \"n\", \"a@b.com\", \"p\", 0, 0, 0, 0, 1, \"8\"]]]");

  // Sanity-check that ListAccounts returns the cached data.
  ASSERT_TRUE(helper.ListAccounts(&list_accounts, &signed_out_accounts));
  ASSERT_TRUE(AreAccountListsEqual(nonempty_list_accounts, list_accounts));
  ASSERT_TRUE(signed_out_accounts.empty());

  EXPECT_CALL(helper, StartFetchingListAccounts());
  EXPECT_CALL(observer,
              OnGaiaAccountsInCookieUpdated(
                  ListedAccountEquals(empty_list_accounts),
                  ListedAccountEquals(empty_signed_out_accounts), no_error()));
  helper.ForceOnCookieChangeProcessing();

  // OnCookieChange should invalidate the cached data.

  // Clear the list before calling |ListAccounts()| to make sure that
  // GaiaCookieManagerService repopulates it with the stale cached information.
  list_accounts.clear();

  ASSERT_FALSE(helper.ListAccounts(&list_accounts, &signed_out_accounts));
  ASSERT_TRUE(AreAccountListsEqual(nonempty_list_accounts, list_accounts));
  ASSERT_TRUE(signed_out_accounts.empty());
  SimulateListAccountsSuccess(&helper, "[\"f\",[]]");
}

TEST_F(GaiaCookieManagerServiceTest, ExternalCcResultFetcher) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  GaiaCookieManagerService::ExternalCcResultFetcher result_fetcher(&helper);
  EXPECT_CALL(helper, StartFetchingMergeSession());
  result_fetcher.Start();

  // Simulate a successful completion of GetCheckConnctionInfo.
  SimulateGetCheckConnectionInfoSuccess(
      "[{\"carryBackToken\": \"yt\", \"url\": \"http://www.yt.com\"},"
      " {\"carryBackToken\": \"bl\", \"url\": \"http://www.bl.com\"}]");

  // Simulate responses for the two connection URLs.
  GaiaCookieManagerService::ExternalCcResultFetcher::LoaderToToken loaders =
      result_fetcher.get_loader_map_for_testing();
  ASSERT_EQ(2u, loaders.size());
  ASSERT_TRUE(IsLoadPending("http://www.yt.com"));
  ASSERT_TRUE(IsLoadPending("http://www.bl.com"));

  ASSERT_EQ("bl:null,yt:null", result_fetcher.GetExternalCcResult());
  SimulateGetCheckConnectionInfoResult("http://www.yt.com", "yt_result");
  ASSERT_EQ("bl:null,yt:yt_result", result_fetcher.GetExternalCcResult());
  SimulateGetCheckConnectionInfoResult("http://www.bl.com", "bl_result");
  ASSERT_EQ("bl:bl_result,yt:yt_result", result_fetcher.GetExternalCcResult());
}

TEST_F(GaiaCookieManagerServiceTest, ExternalCcResultFetcherTimeout) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  GaiaCookieManagerService::ExternalCcResultFetcher result_fetcher(&helper);
  EXPECT_CALL(helper, StartFetchingMergeSession());
  result_fetcher.Start();

  // Simulate a successful completion of GetCheckConnctionInfo.
  SimulateGetCheckConnectionInfoSuccess(
      "[{\"carryBackToken\": \"yt\", \"url\": \"http://www.yt.com\"},"
      " {\"carryBackToken\": \"bl\", \"url\": \"http://www.bl.com\"}]");

  GaiaCookieManagerService::ExternalCcResultFetcher::LoaderToToken loaders =
      result_fetcher.get_loader_map_for_testing();
  ASSERT_EQ(2u, loaders.size());
  ASSERT_TRUE(IsLoadPending("http://www.yt.com"));
  ASSERT_TRUE(IsLoadPending("http://www.bl.com"));

  // Simulate response only for "yt".
  ASSERT_EQ("bl:null,yt:null", result_fetcher.GetExternalCcResult());
  SimulateGetCheckConnectionInfoResult("http://www.yt.com", "yt_result");
  ASSERT_EQ("bl:null,yt:yt_result", result_fetcher.GetExternalCcResult());

  // Now timeout.
  result_fetcher.TimeoutForTests();
  ASSERT_EQ("bl:null,yt:yt_result", result_fetcher.GetExternalCcResult());
  loaders = result_fetcher.get_loader_map_for_testing();
  ASSERT_EQ(0u, loaders.size());
}

TEST_F(GaiaCookieManagerServiceTest, ExternalCcResultFetcherTruncate) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());
  GaiaCookieManagerService::ExternalCcResultFetcher result_fetcher(&helper);
  EXPECT_CALL(helper, StartFetchingMergeSession());
  result_fetcher.Start();

  // Simulate a successful completion of GetCheckConnctionInfo.
  SimulateGetCheckConnectionInfoSuccess(
      "[{\"carryBackToken\": \"yt\", \"url\": \"http://www.yt.com\"}]");

  GaiaCookieManagerService::ExternalCcResultFetcher::LoaderToToken loaders =
      result_fetcher.get_loader_map_for_testing();
  ASSERT_EQ(1u, loaders.size());
  ASSERT_TRUE(IsLoadPending("http://www.yt.com"));

  // Simulate response for "yt" with a string that is too long.
  SimulateGetCheckConnectionInfoResult("http://www.yt.com",
                                       "1234567890123456trunc");
  ASSERT_EQ("yt:1234567890123456", result_fetcher.GetExternalCcResult());
}

TEST_F(GaiaCookieManagerServiceTest, UbertokenSuccessFetchesExternalCC) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());

  EXPECT_CALL(helper, StartFetchingUbertoken());
  helper.AddAccountToCookie("acc1@gmail.com", gaia::GaiaSource::kChrome);

  ASSERT_FALSE(IsLoadPending());
  SimulateUbertokenSuccess(&helper, "token");

  // Check there is now a fetcher that belongs to the ExternalCCResultFetcher.
  SimulateGetCheckConnectionInfoSuccess(
      "[{\"carryBackToken\": \"bl\", \"url\": \"http://www.bl.com\"}]");
  GaiaCookieManagerService::ExternalCcResultFetcher* result_fetcher =
      helper.external_cc_result_fetcher_for_testing();
  GaiaCookieManagerService::ExternalCcResultFetcher::LoaderToToken loaders =
      result_fetcher->get_loader_map_for_testing();
  ASSERT_EQ(1u, loaders.size());
  ASSERT_TRUE(IsLoadPending("http://www.bl.com"));
}

TEST_F(GaiaCookieManagerServiceTest, UbertokenSuccessFetchesExternalCCOnce) {
  InstrumentedGaiaCookieManagerService helper(token_service(), signin_client());

  helper.external_cc_result_fetcher_for_testing()->Start();

  EXPECT_CALL(helper, StartFetchingUbertoken());
  helper.AddAccountToCookie("acc2@gmail.com", gaia::GaiaSource::kChrome);
  // There is already a ExternalCCResultFetch underway. This will trigger
  // StartFetchingMergeSession.
  EXPECT_CALL(helper, StartFetchingMergeSession());
  SimulateUbertokenSuccess(&helper, "token3");
}
