// Copyright 2024 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/sync_bookmarks/bookmark_local_data_batch_uploader.h"

#include <memory>
#include <utility>
#include <variant>

#include "base/strings/utf_ostream_operators.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/strings/grit/components_strings.h"
#include "components/sync/service/local_data_description.h"
#include "components/sync/test/test_matchers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

namespace sync_bookmarks {
namespace {

using ::syncer::IsEmptyLocalDataDescription;
using ::syncer::MatchesLocalDataDescription;
using ::syncer::MatchesLocalDataItemModel;
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::IsEmpty;
using ::testing::Not;

MATCHER_P2(MatchesTitleAndUrl, title, url, "") {
  if (!arg->is_url()) {
    *result_listener << "Expected URL bookmark but got folder.";
    return false;
  }
  if (arg->GetTitle() != title) {
    *result_listener << "Expected URL title \"" << title << "\" but got \""
                     << arg->GetTitle() << "\"";
    return false;
  }
  if (arg->url() != url) {
    *result_listener << "Expected URL \"" << url << "\" but got \""
                     << arg->url() << "\"";
    return false;
  }
  return true;
}

class BookmarkLocalDataBatchUploaderTest : public ::testing::Test {
 public:
  BookmarkLocalDataBatchUploaderTest() {
    pref_service_.registry()->RegisterBooleanPref(
        bookmarks::prefs::kEditBookmarksEnabled, true);
  }

  ~BookmarkLocalDataBatchUploaderTest() override = default;

  bookmarks::BookmarkModel* bookmark_model() { return bookmark_model_.get(); }
  PrefService* pref_service() { return &pref_service_; }

 private:
  base::test::ScopedFeatureList feature_list_{
      switches::kSyncEnableBookmarksInTransportMode};
  const std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_ =
      std::make_unique<bookmarks::BookmarkModel>(
          std::make_unique<bookmarks::TestBookmarkClient>());
  TestingPrefServiceSimple pref_service_;
};

TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionEmptyIfNullModel) {
  base::HistogramTester histogram_tester;
  BookmarkLocalDataBatchUploader uploader(nullptr, pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", 0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfModelNotLoaded) {
  base::HistogramTester histogram_tester;
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", 0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfTransportModeOff) {
  base::HistogramTester histogram_tester;
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      switches::kSyncEnableBookmarksInTransportMode);
  bookmark_model()->LoadEmptyForTest();
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", 0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfNoAccountFolders) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  ASSERT_FALSE(bookmark_model()->account_bookmark_bar_node());
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Local", GURL("http://local.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", 0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfEditBookmarksDislabed) {
  base::HistogramTester histogram_tester;
  pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Local", GURL("http://local.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", 0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionOnlyHasLocalData) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* local_node = bookmark_model()->AddURL(
      bookmark_model()->bookmark_bar_node(), /*index=*/0, u"Local",
      GURL("http://local.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(),
              MatchesLocalDataDescription(
                  syncer::DataType::BOOKMARKS,
                  ElementsAre(MatchesLocalDataItemModel(
                      local_node->id(),
                      syncer::LocalDataItemModel::PageUrlIcon(
                          GURL("http://local.com/")),
                      /*title=*/"Local", /*subtitle=*/IsEmpty())),
                  /*item_count=*/1u, /*domains=*/ElementsAre("local.com"),
                  /*domain_count=*/1u));
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionTopLevelEmptyFolder) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* folder = bookmark_model()->AddFolder(
      bookmark_model()->bookmark_bar_node(), /*index=*/0, u"folder");
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(),
              MatchesLocalDataDescription(
                  syncer::DataType::BOOKMARKS,
                  ElementsAre(MatchesLocalDataItemModel(
                      folder->id(), syncer::LocalDataItemModel::FolderIcon(),
                      /*title=*/"folder",
                      /*subtitle=*/"0 bookmarks")),
                  /*item_count=*/0u, /*domains=*/IsEmpty(),
                  /*domain_count=*/0u));
}

TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionFolderNesting) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();

  // Create the following structure:
  // bookmark_bar
  //   l1_folder
  //     l2_folder
  //       l3_url
  //     l2_url
  //   l1_url
  const bookmarks::BookmarkNode* l1_folder = bookmark_model()->AddFolder(
      bookmark_model()->bookmark_bar_node(), /*index=*/0, u"l1_folder");
  const bookmarks::BookmarkNode* l2_folder =
      bookmark_model()->AddFolder(l1_folder, /*index=*/0, u"l2_folder");
  bookmark_model()->AddURL(l2_folder, /*index=*/0, u"l3_url",
                           GURL("http://l3.com/"));
  bookmark_model()->AddURL(l1_folder, /*index=*/1, u"l2_url",
                           GURL("http://l2.com/"));
  const bookmarks::BookmarkNode* l1_bookmark =
      bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                               /*index=*/1, u"l1_url", GURL("http://l1.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(
      description.Get(),
      MatchesLocalDataDescription(
          syncer::DataType::BOOKMARKS,
          // The full list includes only the top-level items. The bookmark count
          // includes the URLs in the subtree (but not the folder).
          ElementsAre(MatchesLocalDataItemModel(
                          l1_folder->id(),
                          syncer::LocalDataItemModel::FolderIcon(), "l1_folder",
                          l10n_util::GetPluralStringFUTF8(
                              IDS_BULK_UPLOAD_BOOKMARK_FOLDER_SUBTITLE, 2)),
                      MatchesLocalDataItemModel(
                          l1_bookmark->id(),
                          syncer::LocalDataItemModel::PageUrlIcon(
                              GURL("http://l1.com/")),
                          /*title=*/"l1_url", /*subtitle=*/IsEmpty())),
          /*item_count=*/3u,
          /*domains=*/ElementsAre("l1.com", "l2.com", "l3.com"),
          /*domain_count=*/3u));
}

TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionHasSortedDomains) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* bookmark_a =
      bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                               /*index=*/0, u"Z", GURL("https://a.com"));
  const bookmarks::BookmarkNode* bookmark_b =
      bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                               /*index=*/0, u"A", GURL("http://b.com"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(
      description.Get(),
      MatchesLocalDataDescription(
          syncer::DataType::BOOKMARKS,
          // Ordered by recency.
          ElementsAre(
              MatchesLocalDataItemModel(
                  bookmark_b->id(),
                  syncer::LocalDataItemModel::PageUrlIcon(GURL("http://b.com")),
                  /*title=*/"A", /*subtitle=*/IsEmpty()),
              MatchesLocalDataItemModel(bookmark_a->id(),
                                        syncer::LocalDataItemModel::PageUrlIcon(
                                            GURL("https://a.com")),
                                        /*title=*/"Z", /*subtitle=*/IsEmpty())),
          /*item_count=*/2u,
          // Sorting is *not* by bookmark name, nor by full URL (http://b.com is
          // < https://a.com). It's by domain (a.com < b.com).
          /*domains=*/ElementsAre("a.com", "b.com"),
          /*domain_count=*/2u));
}

TEST_F(BookmarkLocalDataBatchUploaderTest, LocalDescriptionHasNoManagedUrls) {
  // Create a new model with the managed node enabled.
  auto client = std::make_unique<bookmarks::TestBookmarkClient>();
  bookmarks::BookmarkNode* managed_node = client->EnableManagedNode();
  auto model = std::make_unique<bookmarks::BookmarkModel>(std::move(client));
  model->LoadEmptyForTest();
  model->CreateAccountPermanentFolders();
  model->AddURL(managed_node, /*index=*/0, u"Managed",
                GURL("http://managed.com"));
  BookmarkLocalDataBatchUploader uploader(model.get(), pref_service());
  base::test::TestFuture<syncer::LocalDataDescription> description;

  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}

TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfNullModel) {
  BookmarkLocalDataBatchUploader uploader(nullptr, pref_service());

  uploader.TriggerLocalDataMigration();

  // Should not crash.
}

TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfModelNotLoaded) {
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigration();

  // Should not crash.
}

TEST_F(BookmarkLocalDataBatchUploaderTest, MigrationNoOpsIfTransportModeOff) {
  base::test::ScopedFeatureList feature_list;
  feature_list.InitAndDisableFeature(
      switches::kSyncEnableBookmarksInTransportMode);
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                           /*index=*/0, u"Local", GURL("http://local.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigration();

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       MigrationNoOpsIfAccountNodesMissing) {
  bookmark_model()->LoadEmptyForTest();
  ASSERT_FALSE(bookmark_model()->account_bookmark_bar_node());
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                           /*index=*/0, u"Local", GURL("http://local.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigration();

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       FullMigrationNoOpsIfEditBookmarksDisalbed) {
  pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                           /*index=*/0, u"Local", GURL("http://local.com/"));
  ASSERT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              IsEmpty());
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigration();

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              IsEmpty());
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       PartialMigrationNoOpsIfEditBookmarksDisalbed) {
  pref_service()->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* local_node = bookmark_model()->AddURL(
      bookmark_model()->bookmark_bar_node(),
      /*index=*/0, u"Local", GURL("http://local.com/"));
  ASSERT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              IsEmpty());
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigrationForItems(
      {syncer::LocalDataItemModel::DataId(local_node->id())});

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Local", "http://local.com/")));
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              IsEmpty());
}

// Note: Most of the merging logic is verified in the unit tests for
// LocalBookmarkModelMerger, this test only checks the communication between the
// 2 layers.
TEST_F(BookmarkLocalDataBatchUploaderTest, FullMigrationUploadsLocalBookmarks) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                           /*index=*/0, u"Local", GURL("http://local.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigration();

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Account", "http://account.com/"),
                          MatchesTitleAndUrl(u"Local", "http://local.com/")));
  EXPECT_THAT(bookmark_model()->account_mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_other_node()->children(), IsEmpty());
}

// Note: Most of the merging logic is verified in the unit tests for
// LocalBookmarkModelMerger, this test only checks the communication between the
// 2 layers.
TEST_F(BookmarkLocalDataBatchUploaderTest,
       PartialMigrationUploadsSelectedLocalBookmarks) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* local_node1 = bookmark_model()->AddURL(
      bookmark_model()->bookmark_bar_node(),
      /*index=*/0, u"Local", GURL("http://local1.com/"));
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(),
                           /*index=*/1, u"Local", GURL("http://local2.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));
  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  uploader.TriggerLocalDataMigrationForItems(
      {syncer::LocalDataItemModel::DataId(local_node1->id())});

  EXPECT_THAT(bookmark_model()->bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Local", "http://local2.com/")));
  EXPECT_THAT(bookmark_model()->mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->other_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_bookmark_bar_node()->children(),
              ElementsAre(MatchesTitleAndUrl(u"Account", "http://account.com/"),
                          MatchesTitleAndUrl(u"Local", "http://local1.com/")));
  EXPECT_THAT(bookmark_model()->account_mobile_node()->children(), IsEmpty());
  EXPECT_THAT(bookmark_model()->account_other_node()->children(), IsEmpty());
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfLocalCountExceedsLimit) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Local 1", GURL("http://local1.com/"));
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/1,
                           u"Local 2", GURL("http://local2.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  // Total local count is 5 (3 local permanent folders + 2 local URLs).
  // A limit of 4 will block it.
  uploader.SetMaxBookmarksLimitForTesting(4);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());

  histogram_tester.ExpectUniqueSample(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", /*sample=*/true,
      /*expected_bucket_count=*/1);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionEmptyIfLocalAndAccountCountExceedsLimit) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Local", GURL("http://local.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  // Local count is 4 (3 local perm + 1 local URL) <= 5 (passes local check).
  // Combined count is 8 (4 local + 4 account) > 5 (fails combined check).
  uploader.SetMaxBookmarksLimitForTesting(5);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       LocalDescriptionNotEmptyIfWithinLimits) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  const bookmarks::BookmarkNode* local_node = bookmark_model()->AddURL(
      bookmark_model()->bookmark_bar_node(), /*index=*/0, u"Local",
      GURL("http://local.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  uploader.SetMaxBookmarksLimitForTesting(100);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(
      description.Get(),
      MatchesLocalDataDescription(syncer::DataType::BOOKMARKS,
                                  ElementsAre(MatchesLocalDataItemModel(
                                      local_node->id(),
                                      syncer::LocalDataItemModel::PageUrlIcon(
                                          GURL("http://local.com/")),
                                      "Local", IsEmpty())),
                                  1u, ElementsAre("local.com"), 1u));

  histogram_tester.ExpectUniqueSample(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", /*sample=*/false,
      /*expected_bucket_count=*/1);
}

TEST_F(BookmarkLocalDataBatchUploaderTest, CombinedCountExactLimitBoundary) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Local", GURL("http://local.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Account",
                           GURL("http://account.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  // Total combined count is 9 (1 root node + 3 local perm + 1 local URL + 3
  // account perm + 1 account URL). Limit = 9: Should be allowed (not empty).
  uploader.SetMaxBookmarksLimitForTesting(9);
  {
    base::test::TestFuture<syncer::LocalDataDescription> description;
    uploader.GetLocalDataDescription(description.GetCallback());
    EXPECT_THAT(description.Get(), Not(IsEmptyLocalDataDescription()));
  }

  // Limit = 8: Should be blocked (empty).
  uploader.SetMaxBookmarksLimitForTesting(8);
  {
    base::test::TestFuture<syncer::LocalDataDescription> description;
    uploader.GetLocalDataDescription(description.GetCallback());
    EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
  }

  histogram_tester.ExpectBucketCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", /*sample=*/false,
      /*expected_count=*/1);
  histogram_tester.ExpectBucketCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded", /*sample=*/true,
      /*expected_count=*/1);
  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded",
      /*expected_count=*/2);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       AggressiveCheckBlocksDuplicateThatWouldFit) {
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();
  // Add duplicate URLs.
  bookmark_model()->AddURL(bookmark_model()->bookmark_bar_node(), /*index=*/0,
                           u"Dup", GURL("http://dup.com/"));
  bookmark_model()->AddURL(bookmark_model()->account_bookmark_bar_node(),
                           /*index=*/0, u"Dup", GURL("http://dup.com/"));

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());

  // Simple sum is 8 (4 local + 4 account).
  // Actual post-merge count would be 4 (only account nodes remain, with 1 URL).
  // If the limit is set to 7:
  // - The simple sum check (8 > 7) blocks it.
  // - The deduplicated check (4 <= 7) would have allowed it.
  // This test verifies that the check indeed blocks the upload (returns an
  // empty description).
  uploader.SetMaxBookmarksLimitForTesting(7);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());

  EXPECT_THAT(description.Get(), IsEmptyLocalDataDescription());
}

TEST_F(BookmarkLocalDataBatchUploaderTest, DoesNotLogMetricIfNoLocalData) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  uploader.SetMaxBookmarksLimitForTesting(4);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());
  ASSERT_THAT(description.Get(), IsEmptyLocalDataDescription());

  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded",
      /*expected_count=*/0);
}

TEST_F(BookmarkLocalDataBatchUploaderTest,
       DoesNotLogMetricIfNoLocalDataAndWithinLimits) {
  base::HistogramTester histogram_tester;
  bookmark_model()->LoadEmptyForTest();
  bookmark_model()->CreateAccountPermanentFolders();

  BookmarkLocalDataBatchUploader uploader(bookmark_model(), pref_service());
  uploader.SetMaxBookmarksLimitForTesting(100);

  base::test::TestFuture<syncer::LocalDataDescription> description;
  uploader.GetLocalDataDescription(description.GetCallback());
  ASSERT_THAT(description.Get(), IsEmptyLocalDataDescription());

  histogram_tester.ExpectTotalCount(
      "Sync.BatchUpload.BookmarksDisabledDueToLimitExceeded",
      /*expected_count=*/0);
}

}  // namespace
}  // namespace sync_bookmarks
