// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/policy/cbcm_invalidations_initializer.h"

#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/values.h"
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "chrome/browser/device_identity/device_oauth2_token_store_desktop.h"
#include "components/os_crypt/async/browser/test_utils.h"
#include "components/policy/core/common/cloud/mock_cloud_policy_client.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "google_apis/gaia/gaia_urls.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace policy {

namespace {

static const char kFirstRefreshToken[] = "first_refresh_token";
static const char kSecondRefreshToken[] = "second_refresh_token";
static const char kFirstAccessToken[] = "first_access_token";
static const char kSecondAccessToken[] = "second_access_token";
static const char kServiceAccountEmail[] =
    "service_account@system.gserviceaccount.com";
static const char kOtherServiceAccountEmail[] =
    "other_service_account@system.gserviceaccount.com";
static const char kDMToken[] = "dm_token";
static const char kAuthCode[] = "auth_code";

}  // namespace

class FakeCloudPolicyClient : public MockCloudPolicyClient {
 public:
  void FetchRobotAuthCodes(
      DMAuth auth,
      enterprise_management::DeviceServiceApiAccessRequest::DeviceType
          device_type,
      const std::set<std::string>& oauth_scopes,
      RobotAuthCodeCallback callback) override {
    std::move(callback).Run(DM_STATUS_SUCCESS, kAuthCode);
  }
};

class CBCMInvalidationsInitializerTest
    : public testing::Test,
      public CBCMInvalidationsInitializer::Delegate {
 public:
  CBCMInvalidationsInitializerTest()
      : os_crypt_async_(os_crypt_async::GetTestOSCryptAsyncForTesting(
            /*is_sync_for_unittests=*/true)) {}

  void RefreshTokenSavedCallbackExpectSuccess(bool success) {
    EXPECT_TRUE(success);

    ++num_refresh_tokens_saved_;
  }

  // CBCMInvalidationsInitializer::Delegate:
  void StartInvalidations() override { ++num_invalidations_started_; }

  scoped_refptr<network::SharedURLLoaderFactory> GetURLLoaderFactory()
      override {
    return test_url_loader_factory_.GetSafeWeakWrapper();
  }

  bool IsInvalidationsServiceStarted() const override {
    return num_invalidations_started_ > 0;
  }

 protected:
  int num_refresh_tokens_saved() const { return num_refresh_tokens_saved_; }

  int num_invalidations_started() const { return num_invalidations_started_; }

  FakeCloudPolicyClient* policy_client() { return &mock_policy_client_; }

  TestingPrefServiceSimple* testing_local_state() {
    return &testing_local_state_;
  }

  network::TestURLLoaderFactory* test_url_loader_factory() {
    return &test_url_loader_factory_;
  }

  std::string MakeTokensFromAuthCodesResponse(const std::string& refresh_token,
                                              const std::string& access_token) {
    base::DictValue dict;
    dict.Set("access_token", access_token);
    dict.Set("refresh_token", refresh_token);
    dict.Set("expires_in", 9999);
    return base::WriteJson(dict).value_or("");
  }

 private:
  void SetUp() override {
    DeviceOAuth2TokenStoreDesktop::RegisterPrefs(
        testing_local_state_.registry());
    DeviceOAuth2TokenServiceFactory::Initialize(
        GetURLLoaderFactory(), &testing_local_state_, os_crypt_async_.get());
    mock_policy_client_.SetDMToken(kDMToken);
  }

  void TearDown() override {
    DeviceOAuth2TokenServiceFactory::Shutdown();
  }

  int num_refresh_tokens_saved_ = 0;
  int num_invalidations_started_ = 0;

  FakeCloudPolicyClient mock_policy_client_;
  network::TestURLLoaderFactory test_url_loader_factory_;
  content::BrowserTaskEnvironment task_environment_;
  TestingPrefServiceSimple testing_local_state_;
  std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_async_;
};

TEST_F(CBCMInvalidationsInitializerTest, InvalidationsStartDisabled) {
  CBCMInvalidationsInitializer initializer(this);

  EXPECT_FALSE(IsInvalidationsServiceStarted());
}

TEST_F(CBCMInvalidationsInitializerTest,
       InvalidationsStartIfRefreshTokenPresent) {
  CBCMInvalidationsInitializer initializer(this);

  DeviceOAuth2TokenServiceFactory::Get()->SetServiceAccountEmail(
      kServiceAccountEmail);
  DeviceOAuth2TokenServiceFactory::Get()->SetAndSaveRefreshToken(
      kFirstRefreshToken,
      base::BindRepeating(&CBCMInvalidationsInitializerTest::
                              RefreshTokenSavedCallbackExpectSuccess,
                          base::Unretained(this)));

  EXPECT_EQ(1, num_refresh_tokens_saved());
  EXPECT_TRUE(
      DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable());

  EXPECT_FALSE(IsInvalidationsServiceStarted());

  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);

  EXPECT_TRUE(IsInvalidationsServiceStarted());
}

TEST_F(CBCMInvalidationsInitializerTest,
       InvalidationsStartIfRefreshTokenAbsent) {
  CBCMInvalidationsInitializer initializer(this);

  EXPECT_FALSE(IsInvalidationsServiceStarted());

  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);
  EXPECT_EQ(1, test_url_loader_factory()->NumPending());
  EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_token_url().spec(),
            test_url_loader_factory()->GetPendingRequest(0)->request.url);

  EXPECT_TRUE(IsInvalidationsServiceStarted());

  EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest(
      GaiaUrls::GetInstance()->oauth2_token_url().spec(),
      MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken)));

  EXPECT_TRUE(IsInvalidationsServiceStarted());
}

TEST_F(CBCMInvalidationsInitializerTest,
       InvalidationsDontRestartOnNextPolicyFetch) {
  CBCMInvalidationsInitializer initializer(this);

  EXPECT_FALSE(IsInvalidationsServiceStarted());

  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);

  EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest(
      GaiaUrls::GetInstance()->oauth2_token_url().spec(),
      MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken)));

  EXPECT_TRUE(IsInvalidationsServiceStarted());
  EXPECT_EQ(0, test_url_loader_factory()->NumPending());

  // When the next policy fetch happens, it'll contain the same service account.
  // In this case, avoid starting the invalidations services again.
  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);
  EXPECT_EQ(0, test_url_loader_factory()->NumPending());

  EXPECT_EQ(1, num_invalidations_started());
}

TEST_F(CBCMInvalidationsInitializerTest,
       CanHandleServiceAccountChangedAfterFetchingInSameSession) {
  CBCMInvalidationsInitializer initializer(this);

  EXPECT_FALSE(IsInvalidationsServiceStarted());

  // Simulate that a policy sets a service account and triggers a fetch.
  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);
  EXPECT_TRUE(IsInvalidationsServiceStarted());
  EXPECT_EQ(1, test_url_loader_factory()->NumPending());
  EXPECT_EQ(GaiaUrls::GetInstance()->oauth2_token_url().spec(),
            test_url_loader_factory()->GetPendingRequest(0)->request.url);
  EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest(
      GaiaUrls::GetInstance()->oauth2_token_url().spec(),
      MakeTokensFromAuthCodesResponse(kFirstRefreshToken, kFirstAccessToken)));

  EXPECT_EQ(0, test_url_loader_factory()->NumPending());
  EXPECT_TRUE(
      DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable());
  EXPECT_EQ(CoreAccountId::FromRobotEmail(kServiceAccountEmail),
            DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId());
  std::string first_refresh_token =
      testing_local_state()->GetString(kCBCMServiceAccountRefreshToken);

  // Simulate that a policy comes in with a different service account. This
  // should trigger a re-initialization of the service account.
  initializer.OnServiceAccountSet(policy_client(), kOtherServiceAccountEmail);
  EXPECT_EQ(1, test_url_loader_factory()->NumPending());
  EXPECT_TRUE(IsInvalidationsServiceStarted());
  EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest(
      GaiaUrls::GetInstance()->oauth2_token_url().spec(),
      MakeTokensFromAuthCodesResponse(kSecondRefreshToken,
                                      kSecondAccessToken)));

  EXPECT_EQ(1, num_invalidations_started());
  EXPECT_TRUE(
      DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable());
  // Now a different refresh token and email should be present. The token
  // themselves aren't validated because they're encrypted. Verifying that it
  // changed is sufficient.
  EXPECT_EQ(CoreAccountId::FromRobotEmail(kOtherServiceAccountEmail),
            DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId());
  EXPECT_NE(first_refresh_token,
            testing_local_state()->GetString(kCBCMServiceAccountRefreshToken));
}

TEST_F(CBCMInvalidationsInitializerTest,
       CanHandleServiceAccountChangedWhenAccountPresentOnStartup) {
  CBCMInvalidationsInitializer initializer(this);

  // Set up the token service as if there was already a service account set up
  // on start up.
  DeviceOAuth2TokenServiceFactory::Get()->SetServiceAccountEmail(
      kServiceAccountEmail);
  DeviceOAuth2TokenServiceFactory::Get()->SetAndSaveRefreshToken(
      kFirstRefreshToken,
      base::BindRepeating(&CBCMInvalidationsInitializerTest::
                              RefreshTokenSavedCallbackExpectSuccess,
                          base::Unretained(this)));

  EXPECT_EQ(1, num_refresh_tokens_saved());
  EXPECT_TRUE(
      DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable());
  std::string first_refresh_token =
      testing_local_state()->GetString(kCBCMServiceAccountRefreshToken);

  EXPECT_FALSE(IsInvalidationsServiceStarted());
  // On first policy store load, this will be called and invalidations started.
  initializer.OnServiceAccountSet(policy_client(), kServiceAccountEmail);
  EXPECT_EQ(0, test_url_loader_factory()->NumPending());
  EXPECT_TRUE(IsInvalidationsServiceStarted());
  // The same refresh token should be present in local state.
  EXPECT_EQ(first_refresh_token,
            testing_local_state()->GetString(kCBCMServiceAccountRefreshToken));

  // Simulate that a new policy is fetched with a different service account.
  // This should result in a gaia call for the service account initialization.
  initializer.OnServiceAccountSet(policy_client(), kOtherServiceAccountEmail);
  EXPECT_EQ(1, test_url_loader_factory()->NumPending());
  EXPECT_TRUE(test_url_loader_factory()->SimulateResponseForPendingRequest(
      GaiaUrls::GetInstance()->oauth2_token_url().spec(),
      MakeTokensFromAuthCodesResponse(kSecondRefreshToken,
                                      kSecondAccessToken)));

  EXPECT_TRUE(IsInvalidationsServiceStarted());
  EXPECT_TRUE(
      DeviceOAuth2TokenServiceFactory::Get()->RefreshTokenIsAvailable());
  // Now a different refresh token and email should be present. The token
  // themselves aren't validated because they're encrypted. Verifying that it
  // changed is sufficient.
  EXPECT_EQ(CoreAccountId::FromRobotEmail(kOtherServiceAccountEmail),
            DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId());
  EXPECT_NE(first_refresh_token,
            testing_local_state()->GetString(kCBCMServiceAccountRefreshToken));
}

}  // namespace policy
