// Copyright 2023 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_model_view.h"

#include <memory>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/uuid.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_uuids.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/signin/public/base/signin_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#include "base/test/metrics/histogram_tester.h"
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

namespace sync_bookmarks {

namespace {

using testing::Eq;
using testing::IsNull;
using testing::NotNull;

class BookmarkModelViewTest : public testing::Test {
 protected:
  BookmarkModelViewTest() {
    // Enable all possible permanent folders to verify how BookmarkModelView
    // does the filtering.
    auto client = std::make_unique<bookmarks::TestBookmarkClient>();
    managed_node_ = client->EnableManagedNode();
    model_ =
        bookmarks::TestBookmarkClient::CreateModelWithClient(std::move(client));

    model_->CreateAccountPermanentFolders();

    EXPECT_THAT(model_->bookmark_bar_node(), NotNull());
    EXPECT_THAT(model_->other_node(), NotNull());
    EXPECT_THAT(model_->mobile_node(), NotNull());
    EXPECT_THAT(model_->account_bookmark_bar_node(), NotNull());
    EXPECT_THAT(model_->account_other_node(), NotNull());
    EXPECT_THAT(model_->account_mobile_node(), NotNull());
    EXPECT_THAT(managed_node_, NotNull());

    EXPECT_NE(model_->bookmark_bar_node(), model_->account_bookmark_bar_node());
    EXPECT_NE(model_->other_node(), model_->account_other_node());
    EXPECT_NE(model_->mobile_node(), model_->account_mobile_node());
  }

  ~BookmarkModelViewTest() override = default;

  base::test::ScopedFeatureList features_{
      switches::kSyncEnableBookmarksInTransportMode};
  std::unique_ptr<bookmarks::BookmarkModel> model_;
  raw_ptr<bookmarks::BookmarkNode> managed_node_;
};

TEST_F(BookmarkModelViewTest, ShouldExposeLocalOrSyncablePermanentFolders) {
  BookmarkModelViewUsingLocalOrSyncableNodes view(model_.get());

  EXPECT_THAT(view.bookmark_bar_node(), Eq(model_->bookmark_bar_node()));
  EXPECT_THAT(view.other_node(), Eq(model_->other_node()));
  EXPECT_THAT(view.mobile_node(), Eq(model_->mobile_node()));
}

TEST_F(BookmarkModelViewTest, ShouldExposeAccountPermanentFolders) {
  BookmarkModelViewUsingAccountNodes view(model_.get());

  EXPECT_THAT(view.bookmark_bar_node(),
              Eq(model_->account_bookmark_bar_node()));
  EXPECT_THAT(view.other_node(), Eq(model_->account_other_node()));
  EXPECT_THAT(view.mobile_node(), Eq(model_->account_mobile_node()));
}

TEST_F(BookmarkModelViewTest, ShouldIdentifySyncableNodes) {
  // Create some nodes to exercise the predicate on non-permanent folders.
  const bookmarks::BookmarkNode* folder1 = model_->AddFolder(
      /*parent=*/model_->bookmark_bar_node(), /*index=*/0, u"Title 1");
  const bookmarks::BookmarkNode* folder2 = model_->AddFolder(
      /*parent=*/model_->account_bookmark_bar_node(), /*index=*/0, u"Title 2");
  const bookmarks::BookmarkNode* nested_folder1 = model_->AddFolder(
      /*parent=*/folder1, /*index=*/0, u"Title 3");
  const bookmarks::BookmarkNode* nested_folder2 = model_->AddFolder(
      /*parent=*/folder2, /*index=*/0, u"Title 4");
  const bookmarks::BookmarkNode* managed_folder = model_->AddFolder(
      /*parent=*/managed_node_, /*index=*/0, u"Managed Title");

  BookmarkModelViewUsingLocalOrSyncableNodes view1(model_.get());
  BookmarkModelViewUsingAccountNodes view2(model_.get());

  // In `view1`, which uses local-or-syncable data, only the local-or-syncable
  // permanent folders and their descendants should be considered syncable.
  EXPECT_TRUE(view1.IsNodeSyncable(model_->bookmark_bar_node()));
  EXPECT_TRUE(view1.IsNodeSyncable(model_->other_node()));
  EXPECT_TRUE(view1.IsNodeSyncable(model_->mobile_node()));
  EXPECT_TRUE(view1.IsNodeSyncable(folder1));
  EXPECT_TRUE(view1.IsNodeSyncable(nested_folder1));
  EXPECT_FALSE(view1.IsNodeSyncable(model_->account_bookmark_bar_node()));
  EXPECT_FALSE(view1.IsNodeSyncable(model_->account_other_node()));
  EXPECT_FALSE(view1.IsNodeSyncable(model_->account_mobile_node()));
  EXPECT_FALSE(view1.IsNodeSyncable(folder2));
  EXPECT_FALSE(view1.IsNodeSyncable(nested_folder2));

  // In `view2`, which uses pure account data, only the corresponding
  // permanent folders and their descendants should be considered syncable,
  // which is the exact opposite.
  EXPECT_FALSE(view2.IsNodeSyncable(model_->bookmark_bar_node()));
  EXPECT_FALSE(view2.IsNodeSyncable(model_->other_node()));
  EXPECT_FALSE(view2.IsNodeSyncable(model_->mobile_node()));
  EXPECT_FALSE(view2.IsNodeSyncable(folder1));
  EXPECT_FALSE(view2.IsNodeSyncable(nested_folder1));
  EXPECT_TRUE(view2.IsNodeSyncable(model_->account_bookmark_bar_node()));
  EXPECT_TRUE(view2.IsNodeSyncable(model_->account_other_node()));
  EXPECT_TRUE(view2.IsNodeSyncable(model_->account_mobile_node()));
  EXPECT_TRUE(view2.IsNodeSyncable(folder2));
  EXPECT_TRUE(view2.IsNodeSyncable(nested_folder2));

  // Managed nodes should be excluded in all cases.
  EXPECT_FALSE(view1.IsNodeSyncable(managed_node_));
  EXPECT_FALSE(view1.IsNodeSyncable(managed_folder));
  EXPECT_FALSE(view2.IsNodeSyncable(managed_node_));
  EXPECT_FALSE(view2.IsNodeSyncable(managed_folder));
}

TEST_F(BookmarkModelViewTest, ShouldGetNodeByUuid) {
  const bookmarks::BookmarkNode* folder1 = model_->AddFolder(
      /*parent=*/model_->bookmark_bar_node(), /*index=*/0, u"Title 1");
  const bookmarks::BookmarkNode* folder2 = model_->AddFolder(
      /*parent=*/model_->account_bookmark_bar_node(), /*index=*/0, u"Title 2");

  ASSERT_NE(folder1->uuid(), folder2->uuid());

  BookmarkModelViewUsingLocalOrSyncableNodes view1(model_.get());
  BookmarkModelViewUsingAccountNodes view2(model_.get());

  EXPECT_THAT(view1.GetNodeByUuid(
                  base::Uuid::ParseLowercase(bookmarks::kBookmarkBarNodeUuid)),
              Eq(model_->bookmark_bar_node()));
  EXPECT_THAT(view2.GetNodeByUuid(
                  base::Uuid::ParseLowercase(bookmarks::kBookmarkBarNodeUuid)),
              Eq(model_->account_bookmark_bar_node()));

  // `folder1` should only be exposed in `view1`.
  EXPECT_THAT(view1.GetNodeByUuid(folder1->uuid()), Eq(folder1));
  EXPECT_THAT(view2.GetNodeByUuid(folder1->uuid()), IsNull());

  // `folder2` should only be exposed in `view2`.
  EXPECT_THAT(view1.GetNodeByUuid(folder2->uuid()), IsNull());
  EXPECT_THAT(view2.GetNodeByUuid(folder2->uuid()), Eq(folder2));
}

TEST_F(BookmarkModelViewTest, ShouldRemoveAllLocalOrSyncableNodes) {
  // Add two local bookmarks.
  model_->AddFolder(/*parent=*/model_->bookmark_bar_node(), /*index=*/0,
                    u"Title 1");
  model_->AddFolder(/*parent=*/model_->bookmark_bar_node(), /*index=*/1,
                    u"Title 2");

  // Add two account bookmarks.
  model_->AddFolder(/*parent=*/model_->account_bookmark_bar_node(), /*index=*/0,
                    u"Title 3");
  model_->AddFolder(/*parent=*/model_->account_bookmark_bar_node(), /*index=*/1,
                    u"Title 4");

  ASSERT_THAT(model_->bookmark_bar_node()->children().size(), Eq(2));
  ASSERT_THAT(model_->account_bookmark_bar_node()->children().size(), Eq(2));

  BookmarkModelViewUsingLocalOrSyncableNodes view1(model_.get());
  BookmarkModelViewUsingAccountNodes view2(model_.get());

  view1.RemoveAllSyncableNodes();

  // Only local-or-syncable nodes should have been removed.
  EXPECT_THAT(model_->bookmark_bar_node()->children().size(), Eq(0));
  ASSERT_THAT(model_->account_bookmark_bar_node(), NotNull());
  EXPECT_THAT(model_->account_bookmark_bar_node()->children().size(), Eq(2));
}

TEST_F(BookmarkModelViewTest, ShouldRemoveAllAccountNodes) {
  // Add two local bookmarks.
  model_->AddFolder(/*parent=*/model_->bookmark_bar_node(), /*index=*/0,
                    u"Title 1");
  model_->AddFolder(/*parent=*/model_->bookmark_bar_node(), /*index=*/1,
                    u"Title 2");

  // Add two account bookmarks.
  model_->AddFolder(/*parent=*/model_->account_bookmark_bar_node(), /*index=*/0,
                    u"Title 3");
  model_->AddFolder(/*parent=*/model_->account_bookmark_bar_node(), /*index=*/1,
                    u"Title 4");

  ASSERT_THAT(model_->bookmark_bar_node()->children().size(), Eq(2));
  ASSERT_THAT(model_->account_bookmark_bar_node()->children().size(), Eq(2));

  BookmarkModelViewUsingLocalOrSyncableNodes view1(model_.get());
  BookmarkModelViewUsingAccountNodes view2(model_.get());

  view2.RemoveAllSyncableNodes();

  // Only account nodes should have been removed.
  EXPECT_THAT(model_->bookmark_bar_node()->children().size(), Eq(2));
  EXPECT_THAT(model_->account_bookmark_bar_node(), IsNull());
  EXPECT_THAT(model_->account_mobile_node(), IsNull());
  EXPECT_THAT(model_->account_other_node(), IsNull());
}

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
TEST_F(BookmarkModelViewTest, ShouldRecordDeduplicationTimeMetric) {
  base::HistogramTester histogram_tester;

  BookmarkModelViewUsingAccountNodes view(model_.get());
  view.MaybeRemoveUnderlyingModelDuplicatesUponInitialSync();

  histogram_tester.ExpectTotalCount("Sync.BookmarksSignInDeduplicationTime", 1);
}
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)

}  // namespace

}  // namespace sync_bookmarks
