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

#include <memory>
#include <string>

#include "base/functional/callback.h"
#include "base/memory/ref_counted.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/browsing_data/browsing_data_api.h"
#include "chrome/browser/extensions/browsing_data_test_utils.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/account_reconcilor_factory.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/sync/sync_service_factory.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "chrome/test/base/platform_browser_test.h"
#include "components/services/storage/public/mojom/local_storage_control.mojom.h"
#include "components/services/storage/public/mojom/storage_usage_info.mojom.h"
#include "components/signin/public/base/signin_buildflags.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/service/sync_user_settings.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/test/test_extension_dir.h"
#include "google_apis/gaia/gaia_urls.h"
#include "google_apis/gaia/google_service_auth_error.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/base/features.h"
#include "net/cookies/canonical_cookie.h"
#include "net/cookies/cookie_inclusion_status.h"
#include "services/network/public/mojom/cookie_manager.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/dom_storage/storage_area.mojom.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#endif

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/test/base/ui_test_utils.h"
#endif  //! BUILDFLAG(IS_ANDROID)

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using extensions::api_test_utils::RunFunctionAndReturnSingleResult;

namespace {
namespace utils = extensions::browsing_data_test_utils;

class ExtensionBrowsingDataTest : public PlatformBrowserTest {
 public:
  ExtensionBrowsingDataTest() = default;
  ExtensionBrowsingDataTest(const ExtensionBrowsingDataTest&) = delete;
  ExtensionBrowsingDataTest& operator=(const ExtensionBrowsingDataTest&) =
      delete;
  ~ExtensionBrowsingDataTest() override = default;

  Profile* GetProfile() { return chrome_test_utils::GetProfile(this); }
};

class ExtensionBrowsingDataTestWithStoragePartitioning
    : public ExtensionBrowsingDataTest {
 public:
  ExtensionBrowsingDataTestWithStoragePartitioning() {
    scoped_feature_list_.InitAndEnableFeature(
        net::features::kThirdPartyStoragePartitioning);
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// TODO(http://crbug.com/40802227): appcache is a noop and should be removed.
const char kRemoveEverythingArguments[] =
    R"([{"since": 1000}, {
    "appcache": true, "cache": true, "cookies": true,
    "downloads": true, "fileSystems": true, "formData": true,
    "history": true, "indexedDB": true, "localStorage": true,
    "serverBoundCertificates": true, "passwords": true,
    "pluginData": true, "serviceWorkers": true, "cacheStorage": true,
    "webSQL": true
    }])";

#if BUILDFLAG(ENABLE_DICE_SUPPORT)

// Sets the SAPISID Gaia cookie, which is monitored by the AccountReconcilor.
bool SetGaiaCookieForProfile(Profile* profile) {
  GURL google_url = GaiaUrls::GetInstance()->secure_google_url();
  auto cookie = net::CanonicalCookie::CreateUnsafeCookieForTesting(
      "SAPISID", std::string(), "." + google_url.GetHost(), "/", base::Time(),
      base::Time(), base::Time(), base::Time(),
      /*secure=*/true, false, net::CookieSameSite::NO_RESTRICTION,
      net::COOKIE_PRIORITY_DEFAULT, net::CookieSourceType::kOther);

  base::test::TestFuture<net::CookieAccessResult> set_cookie_future;
  network::mojom::CookieManager* cookie_manager =
      profile->GetDefaultStoragePartition()
          ->GetCookieManagerForBrowserProcess();
  cookie_manager->SetCanonicalCookie(
      *cookie, google_url, net::CookieOptions::MakeAllInclusive(),
      mojo::WrapCallbackWithDefaultInvokeIfNotRun(
          set_cookie_future.GetCallback(),
          net::CookieAccessResult(
              net::CookieInclusionStatus::MakeFromReasonsForTesting(
                  /*exclusions=*/{net::CookieInclusionStatus::ExclusionReason::
                                      EXCLUDE_UNKNOWN_ERROR}))));
  return set_cookie_future.Get().status.IsInclude();
}
#endif

}  // namespace

#if BUILDFLAG(ENABLE_DICE_SUPPORT)
// Test that Sync is not paused when browsing data is cleared.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, Syncing) {
  Profile* profile = GetProfile();
  // Set a Gaia cookie.
  ASSERT_TRUE(SetGaiaCookieForProfile(profile));
  // Set a Sync account and a secondary account.
  const char kPrimaryAccountEmail[] = "primary@email.com";
  const char kSecondaryAccountEmail[] = "secondary@email.com";

  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfile(profile);
  AccountInfo primary_account_info = signin::MakePrimaryAccountAvailable(
      identity_manager, kPrimaryAccountEmail, signin::ConsentLevel::kSignin);
  AccountInfo secondary_account_info =
      signin::MakeAccountAvailable(identity_manager, kSecondaryAccountEmail);

  // Clear browsing data.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(
      function.get(), kRemoveEverythingArguments, GetProfile()));
  // Check that the Sync token was not revoked.
  EXPECT_TRUE(identity_manager->HasAccountWithRefreshToken(
      primary_account_info.account_id));
  EXPECT_FALSE(
      identity_manager->HasAccountWithRefreshTokenInPersistentErrorState(
          primary_account_info.account_id));
  // Check that the secondary token was revoked.
  EXPECT_FALSE(identity_manager->HasAccountWithRefreshToken(
      secondary_account_info.account_id));
}

// Test that Sync remained in error when browsing data is cleared if Sync was in
// authentication error.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, SyncError) {
  Profile* profile = GetProfile();
  // Set a Gaia cookie.
  ASSERT_TRUE(SetGaiaCookieForProfile(profile));
  // Set a Sync account with authentication error.
  const char kAccountEmail[] = "account@email.com";
  signin::IdentityManager* identity_manager =
      IdentityManagerFactory::GetForProfile(profile);
  AccountInfo account_info = signin::MakePrimaryAccountAvailable(
      identity_manager, kAccountEmail, signin::ConsentLevel::kSignin);
  signin::UpdatePersistentErrorOfRefreshTokenForAccount(
      identity_manager, account_info.account_id,
      GoogleServiceAuthError::FromInvalidGaiaCredentialsReason(
          GoogleServiceAuthError::InvalidGaiaCredentialsReason::
              CREDENTIALS_REJECTED_BY_SERVER));

  // Clear browsing data.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(
      function.get(), kRemoveEverythingArguments, GetProfile()));
  // Check that the account was not removed and Sync remains in auth error.
  EXPECT_TRUE(
      identity_manager->HasAccountWithRefreshToken(account_info.account_id));
  EXPECT_EQ(GoogleServiceAuthError::InvalidGaiaCredentialsReason::
                CREDENTIALS_REJECTED_BY_SERVER,
            identity_manager
                ->GetErrorStateOfRefreshTokenForAccount(account_info.account_id)
                .GetInvalidGaiaCredentialsReason());
}

// Test that the tokens are revoked when browsing data is cleared when there is
// no primary account.
IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, NotSyncing) {
  Profile* profile = GetProfile();
  // Set a Gaia cookie.
  ASSERT_TRUE(SetGaiaCookieForProfile(profile));
  // Set a non-Sync account.
  const char kAccountEmail[] = "account@email.com";
  auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
  AccountInfo account_info =
      signin::MakeAccountAvailable(identity_manager, kAccountEmail);
  // Clear browsing data.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(
      function.get(), kRemoveEverythingArguments, GetProfile()));
  // Check that the account was removed.
  EXPECT_FALSE(
      identity_manager->HasAccountWithRefreshToken(account_info.account_id));
}
#endif

IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageAll) {
  const blink::StorageKey key1 =
      blink::StorageKey::CreateFromStringForTesting("https://example.com");
  const blink::StorageKey key2 =
      blink::StorageKey::CreateFromStringForTesting("https://other.com");
  // Create some local storage for each of the origins.
  utils::CreateLocalStorageForKey(GetProfile(), key1);
  utils::CreateLocalStorageForKey(GetProfile(), key2);
  // Verify that the data is actually stored.
  auto usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(2U, usage_infos.size());
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key1));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key2));

  // Clear the data for everything.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(
      function.get(), kRemoveEverythingArguments, GetProfile()));

  usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(0U, usage_infos.size());
}

IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageIncognito) {
  const blink::StorageKey key1 =
      blink::StorageKey::CreateFromStringForTesting("https://example.com");
  const blink::StorageKey key2 =
      blink::StorageKey::CreateFromStringForTesting("https://other.com");
  // Create some local storage for each of the origins.
  auto* incognito_profile = GetProfile()->GetPrimaryOTRProfile(true);
  utils::CreateLocalStorageForKey(incognito_profile, key1);
  utils::CreateLocalStorageForKey(incognito_profile, key2);
  // Verify that the data is actually stored.
  auto usage_infos = utils::GetLocalStorageInfo(incognito_profile);
  EXPECT_EQ(2U, usage_infos.size());
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key1));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key2));

  // Clear the data for everything.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(
      function.get(), kRemoveEverythingArguments, incognito_profile));

  usage_infos = utils::GetLocalStorageInfo(incognito_profile);
  EXPECT_EQ(0U, usage_infos.size());
}

IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTest, DeleteLocalStorageOrigin) {
  const blink::StorageKey key1 =
      blink::StorageKey::CreateFromStringForTesting("https://example.com");
  const blink::StorageKey key2 =
      blink::StorageKey::CreateFromStringForTesting("https://other.com");
  // Create some local storage for each of the origins.
  utils::CreateLocalStorageForKey(GetProfile(), key1);
  utils::CreateLocalStorageForKey(GetProfile(), key2);
  // Verify that the data is actually stored.
  auto usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(2U, usage_infos.size());
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key1));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key2));

  // Clear the data only for example.com.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  const char removeArgs[] =
      R"([{
    "origins": ["https://example.com"]
    }, {
    "localStorage": true
    }])";
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(function.get(), removeArgs,
                                                GetProfile()));

  usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(1U, usage_infos.size());
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key1));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key2));
}

IN_PROC_BROWSER_TEST_F(ExtensionBrowsingDataTestWithStoragePartitioning,
                       DeleteLocalStoragePartitioned) {
  ASSERT_TRUE(blink::StorageKey::IsThirdPartyStoragePartitioningEnabled());
  const auto kOrigin = url::Origin::Create(GURL("https://example.com"));
  const auto kDifferentOrigin = url::Origin::Create(GURL("https://other.com"));
  const auto kDifferentSubdomain =
      url::Origin::Create(GURL("https://maps.example.com"));
  const auto kAnotherOrigin =
      url::Origin::Create(GURL("https://something.com"));

  // First-party key for the origin being deleted.
  auto key1 =
      blink::StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin),
                                blink::mojom::AncestorChainBit::kSameSite);
  // Third-party embedded on the origin being deleted.
  auto key2 =
      blink::StorageKey::Create(kDifferentOrigin, net::SchemefulSite(kOrigin),
                                blink::mojom::AncestorChainBit::kCrossSite);
  // Cross-site same origin embedded on the origin being deleted.
  auto key3 =
      blink::StorageKey::Create(kOrigin, net::SchemefulSite(kOrigin),
                                blink::mojom::AncestorChainBit::kCrossSite);
  // Third-party same origin embedded on a different site.
  auto key4 =
      blink::StorageKey::Create(kOrigin, net::SchemefulSite(kDifferentOrigin),
                                blink::mojom::AncestorChainBit::kCrossSite);
  // First-party key for an origin not being deleted.
  auto key5 = blink::StorageKey::Create(
      kDifferentOrigin, net::SchemefulSite(kDifferentOrigin),
      blink::mojom::AncestorChainBit::kSameSite);
  // First-party key for a different subdomain for the origin being deleted.
  auto key6 = blink::StorageKey::Create(
      kDifferentSubdomain, net::SchemefulSite(kDifferentSubdomain),
      blink::mojom::AncestorChainBit::kSameSite);
  // Third-party key with a top-level-site equal to a different subdomain for
  // the origin being deleted.
  auto key7 = blink::StorageKey::Create(
      kAnotherOrigin, net::SchemefulSite(kDifferentSubdomain),
      blink::mojom::AncestorChainBit::kCrossSite);
  // Cross-site different subdomain origin embedded with itself as the top-level
  // site.
  auto key8 = blink::StorageKey::Create(
      kDifferentSubdomain, net::SchemefulSite(kDifferentSubdomain),
      blink::mojom::AncestorChainBit::kCrossSite);

  std::vector<blink::StorageKey> keys = {key1, key2, key3, key4,
                                         key5, key6, key7, key8};
  // Create some local storage for each of the keys.
  for (const auto& key : keys) {
    utils::CreateLocalStorageForKey(GetProfile(), key);
  }

  // Verify that the data is actually stored.
  auto usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(keys.size(), usage_infos.size());
  for (const auto& key : keys) {
    EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key));
  }

  // Clear the data for example.com.
  auto function = base::MakeRefCounted<BrowsingDataRemoveFunction>();
  const char removeArgs[] =
      R"([{
    "origins": ["https://example.com"]
    }, {
    "localStorage": true
    }])";
  EXPECT_FALSE(RunFunctionAndReturnSingleResult(function.get(), removeArgs,
                                                GetProfile()));

  usage_infos = utils::GetLocalStorageInfo(GetProfile());
  EXPECT_EQ(3U, usage_infos.size());
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key1));
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key2));
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key3));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key4));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key5));
  EXPECT_TRUE(utils::UsageInfosHasStorageKey(usage_infos, key6));
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key7));
  EXPECT_FALSE(utils::UsageInfosHasStorageKey(usage_infos, key8));
}

using BrowsingDataApiTest = extensions::ExtensionApiTest;

IN_PROC_BROWSER_TEST_F(BrowsingDataApiTest, ValidateFilters) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "background": {"service_worker": "background.js"},
           "permissions": ["browsingData"]
         })";

  static constexpr char kBackgroundJs[] = R"(chrome.test.runTests([
      async function originFilter() {
          await chrome.browsingData.remove(
              {'origins': ['https://example.com']},
              {'cookies': true});
          chrome.test.succeed();
      },
      async function emptyOriginsFilter() {
          // Define the expected error regex for empty `origins` list.
          const expectedError = new RegExp(
              '.* Array must have at least 1 items; found 0.');
          // Verify `chrome.browsingData.remove` throws when `origins` is empty.
          chrome.test.assertThrows(
              chrome.browsingData.remove.bind(
                  null, /* options */ {'origins': []},
                  /* dataToRemove */ {'cookies': true}),
              expectedError);
          chrome.test.succeed();
      },
  ]);)";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), kBackgroundJs);
  ASSERT_TRUE(RunExtensionTest(test_dir.UnpackedPath(), {}, {})) << message_;
}

#if !BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(BrowsingDataApiTest, UnsupportedDataType) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["browsingData"]
         })";

  static constexpr char kPageHtml[] = R"(<script src="page.js"></script>)";
  static constexpr char kPageJs[] =
      R"(chrome.browsingData.removePasswords({}, () => {
           chrome.test.succeed();
         });
        )";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);

  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  content::WebContentsConsoleObserver console_observer(
      browser()->GetTabStripModel()->GetActiveWebContents());
  console_observer.SetPattern(
      "Requested data type(s) are not supported: passwords.");

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), extension->GetResourceURL("page.html")));

  ASSERT_TRUE(console_observer.Wait());

  ASSERT_EQ(1u, console_observer.messages().size());
  EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kWarning,
            console_observer.messages()[0].log_level);
}

IN_PROC_BROWSER_TEST_F(BrowsingDataApiTest, MultipleUnsupportedDataTypes) {
  static constexpr char kManifest[] =
      R"({
           "name": "Test",
           "manifest_version": 3,
           "version": "0.1",
           "permissions": ["browsingData"]
         })";

  static constexpr char kPageHtml[] = R"(<script src="page.js"></script>)";
  static constexpr char kPageJs[] =
      R"(var options = {'since': 1000};
         var dataToRemove = {'passwords': true, 'pluginData': true, 'appcache': false, 'history': true};
         chrome.browsingData.remove(options, dataToRemove, () => {
           chrome.test.succeed();
         });)";

  extensions::TestExtensionDir test_dir;
  test_dir.WriteManifest(kManifest);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.html"), kPageHtml);
  test_dir.WriteFile(FILE_PATH_LITERAL("page.js"), kPageJs);

  const extensions::Extension* extension =
      LoadExtension(test_dir.UnpackedPath());
  ASSERT_TRUE(extension);

  content::WebContentsConsoleObserver console_observer(
      browser()->GetTabStripModel()->GetActiveWebContents());
  console_observer.SetPattern(
      "Requested data type(s) are not supported: passwords, pluginData.");

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), extension->GetResourceURL("page.html")));

  ASSERT_TRUE(console_observer.Wait());

  ASSERT_EQ(1u, console_observer.messages().size());
  EXPECT_EQ(blink::mojom::ConsoleMessageLevel::kWarning,
            console_observer.messages()[0].log_level);
}
#endif  //! BUILDFLAG(IS_ANDROID)
