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

#include "components/unified_consent/unified_consent_service.h"

#include <map>
#include <memory>
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/signin/public/identity_manager/identity_test_environment.h"
#include "components/sync/base/features.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/service/sync_prefs.h"
#include "components/sync/test/test_sync_service.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "components/unified_consent/pref_names.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace unified_consent {

class UnifiedConsentServiceTest : public testing::Test {
 public:
  using SyncState = UnifiedConsentService::SyncState;

  static bool ShouldEnableUrlKeyedAnonymizedDataCollection(
      SyncState old_sync_state,
      SyncState new_sync_state) {
    return UnifiedConsentService::ShouldEnableUrlKeyedAnonymizedDataCollection(
        old_sync_state, new_sync_state);
  }
  static bool ShouldDisableUrlKeyedAnonymizedDataCollection(
      SyncState old_sync_state,
      SyncState new_sync_state) {
    return UnifiedConsentService::ShouldDisableUrlKeyedAnonymizedDataCollection(
        old_sync_state, new_sync_state);
  }
  static const char* SyncStateToString(SyncState state) {
    switch (state) {
      case SyncState::kSignedOut:
        return "kSignedOut";
      case SyncState::kSignedInWithoutHistory:
        return "kSignedInWithoutHistory";
      case SyncState::kSignedInWithHistoryWaitingForPassphrase:
        return "kSignedInWithHistoryWaitingForPassphrase";
      case SyncState::kSignedInWithHistoryAndExplicitPassphrase:
        return "kSignedInWithHistoryAndExplicitPassphrase";
      case SyncState::kSignedInWithHistoryAndNoPassphrase:
        return "kSignedInWithHistoryAndNoPassphrase";
    }
    NOTREACHED();
  }

  UnifiedConsentServiceTest() {
    UnifiedConsentService::RegisterPrefs(pref_service_.registry());
    syncer::SyncPrefs::RegisterProfilePrefs(pref_service_.registry());
    // `identity_test_environment_` starts signed-out while `sync_service_`
    // starts signed-in, make them consistent.
    sync_service_.SetSignedOut();
  }

  UnifiedConsentServiceTest(const UnifiedConsentServiceTest&) = delete;
  UnifiedConsentServiceTest& operator=(const UnifiedConsentServiceTest&) =
      delete;

  ~UnifiedConsentServiceTest() override {
    if (consent_service_)
      consent_service_->Shutdown();
  }

  void CreateConsentService(bool is_new_profile = false) {
    consent_service_ = std::make_unique<UnifiedConsentService>(
        &pref_service_, identity_test_environment_.identity_manager(),
        &sync_service_, std::vector<std::string>()
#if BUILDFLAG(IS_CHROMEOS)
                            ,
        is_new_profile
#endif
    );

    sync_service_.FireStateChanged();
    // Run until idle so the migration can finish.
    base::RunLoop().RunUntilIdle();
  }

  void ResetConsentService() {
    if (consent_service_) {
      consent_service_->Shutdown();
      consent_service_.reset();
    }
  }

#if BUILDFLAG(IS_CHROMEOS)
  unified_consent::MigrationState GetMigrationState() {
    int migration_state_int =
        pref_service_.GetInteger(prefs::kUnifiedConsentMigrationState);
    return static_cast<unified_consent::MigrationState>(migration_state_int);
  }
#endif

  void SignIn(signin::ConsentLevel consent_level) {
    CoreAccountInfo account_info = identity_test_environment_.SetPrimaryAccount(
        "testaccount@gmail.com", consent_level);
    sync_service_.SetSignedIn(consent_level, account_info);
  }

  void SignOut() {
    identity_test_environment_.ClearPrimaryAccount();
    sync_service_.SetSignedOut();
  }

  void SetHistorySyncEnabled(bool enabled) {
    sync_service_.GetUserSettings()->SetSelectedType(
        syncer::UserSelectableType::kHistory, enabled);
  }

  void SetIsUsingExplicitPassphrase(bool enabled) {
    sync_service_.SetIsUsingExplicitPassphrase(enabled);
  }

  void FireSyncStateChanged() { sync_service_.FireStateChanged(); }

  PrefService* pref_service() { return &pref_service_; }
  UnifiedConsentService* consent_service() { return consent_service_.get(); }

 private:
  base::test::SingleThreadTaskEnvironment task_environment_;
  sync_preferences::TestingPrefServiceSyncable pref_service_;
  // identity_test_environment_ and sync_service_ must stay private and be
  // controlled together by SignIn() and SignOut().
  signin::IdentityTestEnvironment identity_test_environment_;
  syncer::TestSyncService sync_service_;
  std::unique_ptr<UnifiedConsentService> consent_service_;
};

TEST_F(UnifiedConsentServiceTest, DefaultValuesWhenSignedOut) {
  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

TEST_F(UnifiedConsentServiceTest, EnableUrlKeyedAnonymizedDataCollection) {
  CreateConsentService();
  SignIn(signin::ConsentLevel::kSync);
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Enable services and check expectations.
  consent_service()->SetUrlKeyedAnonymizedDataCollectionEnabled(true);
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

#if !BUILDFLAG(IS_CHROMEOS)
// Tests that in all cases, on non-ChromeOS, initializing the
// UnifiedConsentService does not affect the
// UrlKeyedAnonymizedDataCollectionEnabled state.
TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_InitializeNoChangeToUrlKeyedAnonymizedDataCollection) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  ResetConsentService();

  SignIn(signin::ConsentLevel::kSignin);
  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  ResetConsentService();

  SetHistorySyncEnabled(true);
  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  ResetConsentService();

  SignIn(signin::ConsentLevel::kSync);
  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}
#else
// Tests that on ChromeOS, initializing the UnifiedConsentService does not
// affect the UrlKeyedAnonymizedDataCollectionEnabled state if it is an
// existing profile.
TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_InitializeNoChangeIfExistingProfile) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  // User is already signed in with history sync enabled.
  SignIn(signin::ConsentLevel::kSignin);

  ASSERT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Instantiating the consent service with `is_new_profile = false` (simulating
  // an existing profile) should NOT enable it.
  CreateConsentService(/*is_new_profile=*/false);
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_InitializeEnablesUrlKeyedAnonymizedDataCollection) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  // User is already signed in with history sync enabled.
  SignIn(signin::ConsentLevel::kSignin);

  ASSERT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Instantiating the consent service on a new profile should automatically
  // enable it on ChromeOS.
  CreateConsentService(/*is_new_profile=*/true);
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_InitializeNoChangeIfExplicitPassphrase) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  // User is already signed in with history sync enabled and has an explicit
  // passphrase.
  SignIn(signin::ConsentLevel::kSignin);
  SetIsUsingExplicitPassphrase(true);

  ASSERT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Instantiating the consent service on a new profile should NOT enable it
  // on ChromeOS because the user has an explicit passphrase.
  CreateConsentService(/*is_new_profile=*/true);
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}
#endif

// Tests that `kUrlKeyedAnonymizedDataCollectionEnabled` does not change for
// sync users when history sync opt-in state changes.
TEST_F(UnifiedConsentServiceTest, ReplaceSync_HistorySyncIgnoredForSyncUsers) {
  SignIn(signin::ConsentLevel::kSync);
  CreateConsentService();
  consent_service()->SetUrlKeyedAnonymizedDataCollectionEnabled(true);
  ASSERT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  ResetConsentService();

  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);
  CreateConsentService();
  ASSERT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  SetHistorySyncEnabled(false);
  FireSyncStateChanged();
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

#if !BUILDFLAG(IS_CHROMEOS)
// Tests that kUrlKeyedAnonymizedDataCollectionEnabled is enabled after
// syncing user signs out, then in again and enabled history sync opt-in.
TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_SyncUserMovesToSigninWithHistorySyncEnabled) {
  SignIn(signin::ConsentLevel::kSync);
  CreateConsentService();
  consent_service()->SetUrlKeyedAnonymizedDataCollectionEnabled(true);
  ASSERT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  ResetConsentService();

  // Clearing the primary account and resetting history sync should disable
  // `kUrlKeyedAnonymizedDataCollectionEnabled`.
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);
  CreateConsentService();
  ASSERT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
  SignOut();
  FireSyncStateChanged();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Enabling history sync should enable
  // `kUrlKeyedAnonymizedDataCollectionEnabled`.
  SignIn(signin::ConsentLevel::kSignin);
  SetHistorySyncEnabled(true);
  FireSyncStateChanged();
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Disabling history sync should disable
  // `kUrlKeyedAnonymizedDataCollectionEnabled`.
  SetHistorySyncEnabled(false);
  FireSyncStateChanged();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

// Tests that any change to history sync opt-in, is reflected in the state
// of `kUrlKeyedAnonymizedDataCollectionEnabled`.
TEST_F(UnifiedConsentServiceTest,
       ReplaceSync_HistorySyncEnablesUrlKeyedAnonymizedDataCollection) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  SignIn(signin::ConsentLevel::kSignin);
  SetHistorySyncEnabled(false);

  CreateConsentService();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  SetHistorySyncEnabled(true);
  FireSyncStateChanged();
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  SetHistorySyncEnabled(false);
  FireSyncStateChanged();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

// This low-level unit test explicitly checks all state transitions. Most
// practical cases are (also) covered by browser tests, see
// UnifiedConsentSyncToSigninBrowserTest.
TEST_F(UnifiedConsentServiceTest, ReplaceSync_StateTransitions) {
  base::test::ScopedFeatureList scoped_feature_list(
      syncer::kReplaceSyncPromosWithSignInPromos);

  enum Outcome {
    kNoChange,
    kEnable,
    kDisable,
  };

  struct TestCase {
    const char* description;
    SyncState old_state;
    SyncState new_state;
    Outcome expected_outcome;
  } test_cases[] = {
      // From kSignedOut:
      {"No-op", SyncState::kSignedOut, SyncState::kSignedOut, kNoChange},
      {"Sign-in without history", SyncState::kSignedOut,
       SyncState::kSignedInWithoutHistory, kNoChange},
      {"Sign-in with history", SyncState::kSignedOut,
       SyncState::kSignedInWithHistoryWaitingForPassphrase, kEnable},
      {"Sign-in with passphrase", SyncState::kSignedOut,
       SyncState::kSignedInWithHistoryAndExplicitPassphrase, kNoChange},
      {"Sign-in with history", SyncState::kSignedOut,
       SyncState::kSignedInWithHistoryAndNoPassphrase, kEnable},
      // From kSignedInWithoutHistory:
      {"Sign-out without history", SyncState::kSignedInWithoutHistory,
       SyncState::kSignedOut, kNoChange},
      {"No-op", SyncState::kSignedInWithoutHistory,
       SyncState::kSignedInWithoutHistory, kNoChange},
      {"History opt-in", SyncState::kSignedInWithoutHistory,
       SyncState::kSignedInWithHistoryWaitingForPassphrase, kEnable},
      {"History opt-in but also passphrase", SyncState::kSignedInWithoutHistory,
       SyncState::kSignedInWithHistoryAndExplicitPassphrase, kNoChange},
      {"History opt-in", SyncState::kSignedInWithoutHistory,
       SyncState::kSignedInWithHistoryAndNoPassphrase, kEnable},
      // From kSignedInWithHistoryWaitingForPassphrase:
      {"Sign-out with history",
       SyncState::kSignedInWithHistoryWaitingForPassphrase,
       SyncState::kSignedOut, kDisable},
      {"History opt-out", SyncState::kSignedInWithHistoryWaitingForPassphrase,
       SyncState::kSignedInWithoutHistory, kDisable},
      {"No-op", SyncState::kSignedInWithHistoryWaitingForPassphrase,
       SyncState::kSignedInWithHistoryWaitingForPassphrase, kNoChange},
      {"Explicit passphrase determined",
       SyncState::kSignedInWithHistoryWaitingForPassphrase,
       SyncState::kSignedInWithHistoryAndExplicitPassphrase, kDisable},
      {"No passphrase determined",
       SyncState::kSignedInWithHistoryWaitingForPassphrase,
       SyncState::kSignedInWithHistoryAndNoPassphrase, kNoChange},
      // From kSignedInWithHistoryAndExplicitPassphrase:
      {"Sign-out with passphrase",
       SyncState::kSignedInWithHistoryAndExplicitPassphrase,
       SyncState::kSignedOut, kNoChange},
      {"History opt-out with passphrase",
       SyncState::kSignedInWithHistoryAndExplicitPassphrase,
       SyncState::kSignedInWithoutHistory, kNoChange},
      {"Passphrase became unknown (e.g. dashboard reset)",
       SyncState::kSignedInWithHistoryAndExplicitPassphrase,
       SyncState::kSignedInWithHistoryWaitingForPassphrase, kEnable},
      {"No-op", SyncState::kSignedInWithHistoryAndExplicitPassphrase,
       SyncState::kSignedInWithHistoryAndExplicitPassphrase, kNoChange},
      {"Passphrase turned off (e.g. dashboard reset)",
       SyncState::kSignedInWithHistoryAndExplicitPassphrase,
       SyncState::kSignedInWithHistoryAndNoPassphrase, kEnable},
      // From kSignedInWithHistoryAndNoPassphrase:
      {"Sign-out with history", SyncState::kSignedInWithHistoryAndNoPassphrase,
       SyncState::kSignedOut, kDisable},
      {"History opt-out", SyncState::kSignedInWithHistoryAndNoPassphrase,
       SyncState::kSignedInWithoutHistory, kDisable},
      {"Passphrase became unknown (e.g. dashboard reset)",
       SyncState::kSignedInWithHistoryAndNoPassphrase,
       SyncState::kSignedInWithHistoryWaitingForPassphrase, kNoChange},
      {"Passphrase turned on", SyncState::kSignedInWithHistoryAndNoPassphrase,
       SyncState::kSignedInWithHistoryAndExplicitPassphrase, kDisable},
      {"No-op", SyncState::kSignedInWithHistoryAndNoPassphrase,
       SyncState::kSignedInWithHistoryAndNoPassphrase, kNoChange},
  };

  for (const TestCase& test_case : test_cases) {
    SCOPED_TRACE(base::StringPrintf(
        "Test case: %s -> %s, %s", SyncStateToString(test_case.old_state),
        SyncStateToString(test_case.new_state), test_case.description));
    EXPECT_EQ(ShouldEnableUrlKeyedAnonymizedDataCollection(test_case.old_state,
                                                           test_case.new_state),
              test_case.expected_outcome == kEnable);
    EXPECT_EQ(ShouldDisableUrlKeyedAnonymizedDataCollection(
                  test_case.old_state, test_case.new_state),
              test_case.expected_outcome == kDisable);
  }
}

#if BUILDFLAG(IS_CHROMEOS)
TEST_F(UnifiedConsentServiceTest, Migration_UpdateSettings) {
  // Create user that syncs history and has no custom passphrase.
  SignIn(signin::ConsentLevel::kSync);
  // Url keyed data collection is off before the migration.
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  CreateConsentService();
  EXPECT_EQ(GetMigrationState(), unified_consent::MigrationState::kCompleted);
  // During the migration Url keyed data collection is enabled.
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}

TEST_F(UnifiedConsentServiceTest, Migration_NotSignedIn) {
  base::HistogramTester histogram_tester;

  CreateConsentService();
  // The user is signed out, so the migration is completed after the
  // creation of the consent service.
  EXPECT_EQ(GetMigrationState(), unified_consent::MigrationState::kCompleted);
}
#else
TEST_F(UnifiedConsentServiceTest, ClearPrimaryAccountDisablesSomeServices) {
  base::HistogramTester histogram_tester;

  CreateConsentService();
  SignIn(signin::ConsentLevel::kSync);

  // Precondition: Enable unified consent.
  consent_service()->SetUrlKeyedAnonymizedDataCollectionEnabled(true);
  EXPECT_TRUE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));

  // Clearing primary account revokes unfied consent and a couple of other
  // non-personalized services.
  SignOut();
  EXPECT_FALSE(pref_service()->GetBoolean(
      prefs::kUrlKeyedAnonymizedDataCollectionEnabled));
}
#endif  // BUILDFLAG(IS_CHROMEOS)

}  // namespace unified_consent
