// Copyright 2013 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/bind.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/managed_mode/managed_user_refresh_token_fetcher.h"
#include "chrome/browser/signin/oauth2_token_service.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/test_browser_thread.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "net/base/net_errors.h"
#include "net/base/url_util.h"
#include "net/http/http_request_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/test_url_fetcher_factory.h"
#include "net/url_request/url_fetcher_delegate.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace {

const char kManagedUserId[] = "abcdef";
const char kName[] = "Homestar";
const char kDeviceName[] = "Compy";

const char kAccessToken[] = "accesstoken";
const char kAuthorizationCode[] = "authorizationcode";
const char kManagedUserToken[] = "managedusertoken";

const char kIssueTokenResponseFormat[] =
    "{"
    "  \"code\": \"%s\""
    "}";

const char kGetRefreshTokenResponseFormat[] =
    "{"
    "  \"access_token\": \"<ignored>\","
    "  \"expires_in\": 12345,"
    "  \"refresh_token\": \"%s\""
    "}";

// MockOAuth2TokenService ---------------------------------------------

class MockOAuth2TokenService : public OAuth2TokenService {
 public:
  class Request : public OAuth2TokenService::Request {
   public:
    Request(const OAuth2TokenService::ScopeSet& scopes,
            OAuth2TokenService::Consumer* consumer,
            MockOAuth2TokenService* owner);
    virtual ~Request();

    void Succeed();
    void Fail(GoogleServiceAuthError::State error);

    const OAuth2TokenService::ScopeSet& scopes() const { return scopes_; }

   private:
    OAuth2TokenService::ScopeSet scopes_;

    OAuth2TokenService::Consumer* consumer_;

    MockOAuth2TokenService* owner_;
  };

  MockOAuth2TokenService();
  virtual ~MockOAuth2TokenService();

  Request* request() const { return request_; }

  void ClearRequest(Request* request);

 private:
  // OAuth2TokenService overrides:
  virtual scoped_ptr<OAuth2TokenService::Request> StartRequest(
     const OAuth2TokenService::ScopeSet& scopes,
      OAuth2TokenService::Consumer* consumer) OVERRIDE;
  virtual std::string GetRefreshToken() OVERRIDE;

  Request* request_;

  DISALLOW_COPY_AND_ASSIGN(MockOAuth2TokenService);
};

MockOAuth2TokenService::Request::Request(
    const OAuth2TokenService::ScopeSet& scopes,
    OAuth2TokenService::Consumer* consumer,
    MockOAuth2TokenService* owner)
    : scopes_(scopes),
      consumer_(consumer),
      owner_(owner) {}

MockOAuth2TokenService::Request::~Request() {
  owner_->ClearRequest(this);
}

void MockOAuth2TokenService::Request::Succeed() {
  base::Time expiration_date = base::Time::Now() +
                               base::TimeDelta::FromHours(1);
  consumer_->OnGetTokenSuccess(this, kAccessToken, expiration_date);
}

void MockOAuth2TokenService::Request::Fail(
    GoogleServiceAuthError::State error) {
  consumer_->OnGetTokenFailure(this, GoogleServiceAuthError(error));
}

MockOAuth2TokenService::MockOAuth2TokenService()
    : OAuth2TokenService(NULL),
      request_(NULL) {}

MockOAuth2TokenService::~MockOAuth2TokenService() {
  EXPECT_FALSE(request_);
}

void MockOAuth2TokenService::ClearRequest(
    MockOAuth2TokenService::Request* request) {
  if (request_ == request)
    request_ = NULL;
}

scoped_ptr<OAuth2TokenService::Request> MockOAuth2TokenService::StartRequest(
    const OAuth2TokenService::ScopeSet& scopes,
    OAuth2TokenService::Consumer* consumer) {
  scoped_ptr<Request> request(new Request(scopes, consumer, this));
  request_ = request.get();
  return request.PassAs<OAuth2TokenService::Request>();
}

std::string MockOAuth2TokenService::GetRefreshToken() {
  NOTREACHED();
  return std::string();
}

// Utility methods --------------------------------------------------

// Slightly hacky way to extract a value from a URL-encoded POST request body.
bool GetValueForKey(const std::string& encoded_string,
                    const std::string& key,
                    std::string* value) {
  GURL url("http://example.com/?" + encoded_string);
  return net::GetValueForKeyInQuery(url, key, value);
}

void SendResponse(net::TestURLFetcher* url_fetcher,
                  const std::string& response) {
  url_fetcher->set_status(
      net::URLRequestStatus(net::URLRequestStatus::SUCCESS, 0));
  url_fetcher->set_response_code(net::HTTP_OK);
  url_fetcher->SetResponseString(response);
  url_fetcher->delegate()->OnURLFetchComplete(url_fetcher);
}

void SetNetworkError(net::TestURLFetcher* url_fetcher, int error) {
  url_fetcher->set_status(
      net::URLRequestStatus(net::URLRequestStatus::FAILED, error));
  url_fetcher->delegate()->OnURLFetchComplete(url_fetcher);
}

void SetHttpError(net::TestURLFetcher* url_fetcher, int error) {
  url_fetcher->set_status(net::URLRequestStatus());
  url_fetcher->set_response_code(error);
  url_fetcher->delegate()->OnURLFetchComplete(url_fetcher);
}

}  // namespace

class ManagedUserRefreshTokenFetcherTest : public testing::Test {
 public:
  ManagedUserRefreshTokenFetcherTest();
  virtual ~ManagedUserRefreshTokenFetcherTest() {}

 protected:
  void StartFetching();

  MockOAuth2TokenService::Request* GetOAuth2TokenServiceRequest();
  net::TestURLFetcher* GetIssueTokenRequest();
  net::TestURLFetcher* GetRefreshTokenRequest();

  void MakeIssueTokenRequestSucceed();
  void MakeRefreshTokenFetchSucceed();

  void Reset();

  const GoogleServiceAuthError& error() const { return error_; }
  const std::string& token() const { return token_; }

 private:
  void OnTokenFetched(const GoogleServiceAuthError& error,
                      const std::string& token);

  base::WeakPtrFactory<ManagedUserRefreshTokenFetcherTest> weak_ptr_factory_;
  base::MessageLoop message_loop_;
  content::TestBrowserThread ui_thread_;
  TestingProfile profile_;
  MockOAuth2TokenService oauth2_token_service_;
  net::TestURLFetcherFactory url_fetcher_factory_;
  scoped_ptr<ManagedUserRefreshTokenFetcher> token_fetcher_;

  GoogleServiceAuthError error_;
  std::string token_;
};

ManagedUserRefreshTokenFetcherTest::ManagedUserRefreshTokenFetcherTest()
    : weak_ptr_factory_(this),
      ui_thread_(content::BrowserThread::UI, &message_loop_),
      token_fetcher_(
          ManagedUserRefreshTokenFetcher::Create(&oauth2_token_service_,
                                          profile_.GetRequestContext())),
      error_(GoogleServiceAuthError::NONE) {}

void ManagedUserRefreshTokenFetcherTest::StartFetching() {
  token_fetcher_->Start(kManagedUserId, UTF8ToUTF16(kName), kDeviceName,
                        base::Bind(
                            &ManagedUserRefreshTokenFetcherTest::OnTokenFetched,
                            weak_ptr_factory_.GetWeakPtr()));
}

MockOAuth2TokenService::Request*
ManagedUserRefreshTokenFetcherTest::GetOAuth2TokenServiceRequest() {
  MockOAuth2TokenService::Request* request = oauth2_token_service_.request();

  OAuth2TokenService::ScopeSet scopes = request->scopes();
  EXPECT_EQ(1u, scopes.size());
  EXPECT_EQ(1u, scopes.count(GaiaUrls::GetInstance()->oauth1_login_scope()));
  return request;
}

net::TestURLFetcher*
ManagedUserRefreshTokenFetcherTest::GetIssueTokenRequest() {
  net::TestURLFetcher* url_fetcher = url_fetcher_factory_.GetFetcherByID(1);
  if (!url_fetcher)
    return NULL;

  EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_issue_token_url(),
            url_fetcher->GetOriginalURL().spec());
  std::string access_token;
  net::HttpRequestHeaders headers;
  url_fetcher->GetExtraRequestHeaders(&headers);
  EXPECT_TRUE(headers.GetHeader("Authorization", &access_token));
  EXPECT_EQ(std::string("Bearer ") + kAccessToken, access_token);
  const std::string upload_data = url_fetcher->upload_data();
  std::string managed_user_id;
  EXPECT_TRUE(GetValueForKey(upload_data, "profile_id", &managed_user_id));
  EXPECT_EQ(kManagedUserId, managed_user_id);
  std::string name;
  EXPECT_TRUE(GetValueForKey(upload_data, "profile_name", &name));
  EXPECT_EQ(kName, name);
  std::string device_name;
  EXPECT_TRUE(GetValueForKey(upload_data, "device_name", &device_name));
  EXPECT_EQ(kDeviceName, device_name);
  return url_fetcher;
}

net::TestURLFetcher*
ManagedUserRefreshTokenFetcherTest::GetRefreshTokenRequest() {
  net::TestURLFetcher* url_fetcher = url_fetcher_factory_.GetFetcherByID(0);
  if (!url_fetcher)
    return NULL;

  EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_token_url(),
            url_fetcher->GetOriginalURL().spec());
  std::string auth_code;
  EXPECT_TRUE(GetValueForKey(url_fetcher->upload_data(), "code", &auth_code));
  EXPECT_EQ(kAuthorizationCode, auth_code);
  return url_fetcher;
}

void ManagedUserRefreshTokenFetcherTest::MakeIssueTokenRequestSucceed() {
  SendResponse(GetIssueTokenRequest(),
               base::StringPrintf(kIssueTokenResponseFormat,
                                  kAuthorizationCode));
}

void ManagedUserRefreshTokenFetcherTest::MakeRefreshTokenFetchSucceed() {
  SendResponse(GetRefreshTokenRequest(),
               base::StringPrintf(kGetRefreshTokenResponseFormat,
                                  kManagedUserToken));
}

void ManagedUserRefreshTokenFetcherTest::Reset() {
  token_fetcher_.reset();
}

void ManagedUserRefreshTokenFetcherTest::OnTokenFetched(
    const GoogleServiceAuthError& error,
    const std::string& token) {
  error_ = error;
  token_ = token;
}

// Tests --------------------------------------------------------

TEST_F(ManagedUserRefreshTokenFetcherTest, Success) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  MakeRefreshTokenFetchSucceed();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(kManagedUserToken, token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, ExpiredAccessToken) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  SetHttpError(GetIssueTokenRequest(), net::HTTP_UNAUTHORIZED);
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  MakeRefreshTokenFetchSucceed();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(kManagedUserToken, token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, ExpiredAccessTokenRetry) {
  // If we get a 401 error for the second time, we should give up instead of
  // retrying again.
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  SetHttpError(GetIssueTokenRequest(), net::HTTP_UNAUTHORIZED);
  GetOAuth2TokenServiceRequest()->Succeed();
  SetHttpError(GetIssueTokenRequest(), net::HTTP_UNAUTHORIZED);

  EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error().state());
  EXPECT_EQ(net::ERR_FAILED, error().network_error());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, MalformedIssueTokenResponse) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  SendResponse(GetIssueTokenRequest(), "choke");

  EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error().state());
  EXPECT_EQ(net::ERR_INVALID_RESPONSE, error().network_error());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, FetchAccessTokenFailure) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Fail(
      GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS);

  EXPECT_EQ(GoogleServiceAuthError::INVALID_GAIA_CREDENTIALS, error().state());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, IssueTokenNetworkError) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  SetNetworkError(GetIssueTokenRequest(), net::ERR_SSL_PROTOCOL_ERROR);

  EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error().state());
  EXPECT_EQ(net::ERR_SSL_PROTOCOL_ERROR, error().network_error());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, FetchRefreshTokenNetworkError) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  SetNetworkError(GetRefreshTokenRequest(), net::ERR_CONNECTION_REFUSED);
  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  SetNetworkError(GetRefreshTokenRequest(), net::ERR_CONNECTION_REFUSED);

  EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error().state());
  EXPECT_EQ(net::ERR_FAILED, error().network_error());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest,
       FetchRefreshTokenTransientNetworkError) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  SetNetworkError(GetRefreshTokenRequest(), net::ERR_CONNECTION_REFUSED);

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  MakeRefreshTokenFetchSucceed();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(kManagedUserToken, token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, FetchRefreshTokenBadRequest) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  SetHttpError(GetRefreshTokenRequest(), net::HTTP_BAD_REQUEST);

  EXPECT_EQ(GoogleServiceAuthError::CONNECTION_FAILED, error().state());
  EXPECT_EQ(net::ERR_FAILED, error().network_error());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, CancelWhileFetchingAccessToken) {
  StartFetching();
  Reset();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, CancelWhileCallingIssueToken) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  Reset();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(std::string(), token());
}

TEST_F(ManagedUserRefreshTokenFetcherTest, CancelWhileFetchingRefreshToken) {
  StartFetching();
  GetOAuth2TokenServiceRequest()->Succeed();
  MakeIssueTokenRequestSucceed();
  Reset();

  EXPECT_EQ(GoogleServiceAuthError::NONE, error().state());
  EXPECT_EQ(std::string(), token());
}
