// 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 "chrome/browser/extensions/bookmarks/bookmarks_helpers.h"

#include <stdint.h>

#include <memory>

#include "base/memory/raw_ptr.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
#include "chrome/browser/extensions/bookmarks/bookmarks_error_constants.h"
#include "chrome/browser/sync/local_or_syncable_bookmark_sync_service_factory.h"
#include "chrome/common/extensions/api/bookmarks.h"
#include "chrome/test/base/testing_profile.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/managed/managed_bookmark_service.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/bookmarks/test/test_bookmark_client.h"
#include "components/signin/public/base/signin_switches.h"
#include "components/sync_bookmarks/bookmark_sync_service.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/buildflags/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;

namespace extensions {

using api::bookmarks::BookmarkTreeNode;
using ::testing::Eq;
using ::testing::ExplainMatchResult;

namespace bookmarks_helpers {

// Matches a BookmarkTreeNode that represents a folder node with the given
// `expected_type` and `expected_unmodifiable`.
MATCHER_P2(MatchesFolder, expected_type, expected_unmodifiable, "") {
  return ExplainMatchResult(Eq(expected_type), arg.folder_type,
                            result_listener) &&
         ExplainMatchResult(Eq(expected_unmodifiable), arg.unmodifiable,
                            result_listener) &&
         ExplainMatchResult(Eq(std::nullopt), arg.url, result_listener);
}

class ExtensionBookmarksTest : public testing::Test {
 public:
  ExtensionBookmarksTest() = default;
  ExtensionBookmarksTest(const ExtensionBookmarksTest&) = delete;
  ExtensionBookmarksTest& operator=(const ExtensionBookmarksTest&) = delete;
  ~ExtensionBookmarksTest() override = default;

  void SetUp() override {
    TestingProfile::Builder profile_builder;
    profile_builder.AddTestingFactory(
        BookmarkModelFactory::GetInstance(),
        BookmarkModelFactory::GetDefaultFactory());
    profile_builder.AddTestingFactory(
        ManagedBookmarkServiceFactory::GetInstance(),
        ManagedBookmarkServiceFactory::GetDefaultFactory());

    profile_ = profile_builder.Build();
    model_ = BookmarkModelFactory::GetForBrowserContext(profile_.get());
    managed_ = ManagedBookmarkServiceFactory::GetForProfile(profile_.get());
    bookmarks::test::WaitForBookmarkModelToLoad(model_);

    node_ = model_->AddURL(model_->other_node(), 0, u"Digg",
                           GURL("http://www.reddit.com"));
    model_->SetNodeMetaInfo(node_, "some_key1", "some_value1");
    model_->SetNodeMetaInfo(node_, "some_key2", "some_value2");
    model_->AddURL(model_->other_node(), 0, u"News",
                   GURL("http://www.foxnews.com"));
    folder_ = model_->AddFolder(model_->other_node(), 0, u"outer folder");
    model_->SetNodeMetaInfo(folder_, "some_key1", "some_value1");
    model_->AddFolder(folder_, 0, u"inner folder 1");
    model_->AddFolder(folder_, 0, u"inner folder 2");
    node2_ = model_->AddURL(folder_, 0, u"Digg", GURL("http://reddit.com"));
    model_->SetNodeMetaInfo(node2_, "some_key2", "some_value2");
    model_->AddURL(folder_, 0, u"CNet", GURL("http://cnet.com"));

    // Add a URL to the mobile node so that it is not hidden.
    model_->AddURL(model_->mobile_node(), 0, u"Mobile bookmark",
                   GURL("http://www.mobile.com"));

    // Add a URL to the managed node so that it is not hidden.
    model_->AddURL(managed_->managed_node(), 0, u"Managed bookmark",
                   GURL("http://www.managed.com"));
  }

  // A simple wrapper to get a single BookmarkTreeNode from a BookmarkNode (with
  // no recursion).
  BookmarkTreeNode GetSingleBookmarkTreeNode(const BookmarkNode* node) {
    CHECK(node);
    return GetBookmarkTreeNode(model_, managed_, node,
                               /*recurse=*/false,
                               /*only_folders=*/false);
  }

  content::BrowserTaskEnvironment task_environment_;
  std::unique_ptr<TestingProfile> profile_;
  raw_ptr<bookmarks::ManagedBookmarkService> managed_ = nullptr;
  raw_ptr<BookmarkModel> model_ = nullptr;
  raw_ptr<const BookmarkNode> node_ = nullptr;
  raw_ptr<const BookmarkNode> node2_ = nullptr;
  raw_ptr<const BookmarkNode> folder_ = nullptr;
};

// TODO(crbug.com/414844449): This test depends on which permanent folders are
// visible when empty (e.g. the bookmarks bar). This behaviour is different on
// Android Desktop vs. other Desktop platforms. It results in different node
// counts when you count from the root. That's why only this test is affected,
// the others count children from a different node. Once the behavior for
// Android Desktop has been decided this test should be re-enabled. See also
// bookmarks_apitest.cc.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(ExtensionBookmarksTest, GetFullTreeFromRoot) {
  BookmarkTreeNode tree =
      GetBookmarkTreeNode(model_, managed_, model_->root_node(),
                          /*recurse=*/true,
                          /*only_folders=*/false);
  ASSERT_EQ(4U, tree.children->size());
}
#endif  // !BUILDFLAG(IS_ANDROID)

TEST_F(ExtensionBookmarksTest, GetTreeFromOtherPermanentNode) {
  BookmarkTreeNode tree =
      GetBookmarkTreeNode(model_, managed_, model_->other_node(),
                          /*recurse=*/true,
                          /*only_folders=*/false);
  ASSERT_EQ(3U, tree.children->size());
}

TEST_F(ExtensionBookmarksTest, GetFoldersOnlyFromOtherPermanentNode) {
  BookmarkTreeNode tree =
      GetBookmarkTreeNode(model_, managed_, model_->other_node(),
                          /*recurse=*/true,
                          /*only_folders=*/true);
  ASSERT_EQ(1U, tree.children->size());
}

TEST_F(ExtensionBookmarksTest, GetSubtree) {
  BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, folder_,
                                              /*recurse=*/true,
                                              /*only_folders=*/false);
  ASSERT_EQ(4U, tree.children->size());
  const BookmarkTreeNode& digg = tree.children->at(1);
  ASSERT_EQ("Digg", digg.title);
}

TEST_F(ExtensionBookmarksTest, GetSubtreeFoldersOnly) {
  BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, folder_,
                                              /*recurse=*/true,
                                              /*only_folders=*/true);
  ASSERT_EQ(2U, tree.children->size());
  const BookmarkTreeNode& inner_folder = tree.children->at(1);
  ASSERT_EQ("inner folder 1", inner_folder.title);
}

TEST_F(ExtensionBookmarksTest, GetModifiableNode) {
  BookmarkTreeNode tree = GetBookmarkTreeNode(model_, managed_, node_,
                                              /*recurse=*/false,
                                              /*only_folders=*/false);
  EXPECT_EQ("Digg", tree.title);
  ASSERT_TRUE(tree.url);
  EXPECT_EQ("http://www.reddit.com/", *tree.url);
  EXPECT_EQ(api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone,
            tree.unmodifiable);
  EXPECT_EQ(api::bookmarks::FolderType::kNone, tree.folder_type);
}

TEST_F(ExtensionBookmarksTest, GetManagedNode) {
  const BookmarkNode* managed_bookmark =
      model_->AddURL(managed_->managed_node(), 0, u"Chromium",
                     GURL("http://www.chromium.org/"));
  BookmarkTreeNode tree =
      GetBookmarkTreeNode(model_, managed_, managed_bookmark,
                          /*recurse=*/false,
                          /*only_folders=*/false);
  EXPECT_EQ("Chromium", tree.title);
  EXPECT_EQ("http://www.chromium.org/", *tree.url);
  EXPECT_EQ(api::bookmarks::BookmarkTreeNodeUnmodifiable::kManaged,
            tree.unmodifiable);
  EXPECT_EQ(api::bookmarks::FolderType::kNone, tree.folder_type);
}

TEST_F(ExtensionBookmarksTest, GetLocalPermanentAndManagedFolders) {
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->bookmark_bar_node()),
      MatchesFolder(api::bookmarks::FolderType::kBookmarksBar,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->other_node()),
      MatchesFolder(api::bookmarks::FolderType::kOther,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->mobile_node()),
      MatchesFolder(api::bookmarks::FolderType::kMobile,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(managed_->managed_node()),
      MatchesFolder(api::bookmarks::FolderType::kManaged,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kManaged));
}

TEST_F(ExtensionBookmarksTest, GetAccountPermanentNodes) {
  base::test::ScopedFeatureList features{
      switches::kSyncEnableBookmarksInTransportMode};
  model_->CreateAccountPermanentFolders();
  // Add a URL to the mobile node so that it is not hidden.
  model_->AddURL(model_->account_mobile_node(), 0, u"Mobile bookmark",
                 GURL("http://www.mobile.com"));

  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->account_bookmark_bar_node()),
      MatchesFolder(api::bookmarks::FolderType::kBookmarksBar,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->account_other_node()),
      MatchesFolder(api::bookmarks::FolderType::kOther,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
  EXPECT_THAT(
      GetSingleBookmarkTreeNode(model_->account_mobile_node()),
      MatchesFolder(api::bookmarks::FolderType::kMobile,
                    api::bookmarks::BookmarkTreeNodeUnmodifiable::kNone));
}

TEST_F(ExtensionBookmarksTest,
       GetTreePopulatesSyncingProperty_BookmarksInTransportModeDisabled) {
  // Check that local permanent system nodes are not syncing.
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);

  // Check that non-permanent local nodes are not syncing.
  EXPECT_FALSE(GetSingleBookmarkTreeNode(node_).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(folder_).syncing);
}

TEST_F(ExtensionBookmarksTest,
       GetTreePopulatesSyncingProperty_SyncFeatureEnabledIncludingBookmarks) {
  // Pretend sync-the-feature is on for bookmarks.
  LocalOrSyncableBookmarkSyncServiceFactory::GetForProfile(profile_.get())
      ->SetIsTrackingMetadataForTesting();

  // Check that local-or-syncable permanent nodes are syncing.
  EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);

  // Check that non-permanent local nodes are syncing.
  EXPECT_TRUE(GetSingleBookmarkTreeNode(node_).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(folder_).syncing);

  // Managed nodes are never syncing.
  EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);
  const BookmarkNode* managed_bookmark =
      model_->AddURL(managed_->managed_node(), 0, u"Chromium",
                     GURL("http://www.chromium.org"));
  EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_bookmark).syncing);
}

TEST_F(ExtensionBookmarksTest,
       GetTreePopulatesSyncingProperty_AccountNodesEnabled) {
  base::test::ScopedFeatureList features{
      switches::kSyncEnableBookmarksInTransportMode};
  model_->CreateAccountPermanentFolders();
  // Add a URL to the mobile node so that it is not hidden.
  model_->AddURL(model_->account_mobile_node(), 0, u"Mobile bookmark",
                 GURL("http://www.mobile.com"));

  // Check that local permanent nodes are not syncing.
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->bookmark_bar_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->other_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(model_->mobile_node()).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(managed_->managed_node()).syncing);

  // Check that non-permanent local nodes are not syncing.
  EXPECT_FALSE(GetSingleBookmarkTreeNode(node_).syncing);
  EXPECT_FALSE(GetSingleBookmarkTreeNode(folder_).syncing);

  // Check that account permanent nodes are syncing.
  EXPECT_TRUE(
      GetSingleBookmarkTreeNode(model_->account_bookmark_bar_node()).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->account_other_node()).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(model_->account_mobile_node()).syncing);

  // Check that non-permanent account nodes are syncing.
  const BookmarkNode* account_bookmark = model_->AddURL(
      model_->account_other_node(), 0, u"Digg", GURL("http://www.reddit.com"));
  const BookmarkNode* account_folder =
      model_->AddFolder(model_->account_other_node(), 0, u"outer folder");
  EXPECT_TRUE(GetSingleBookmarkTreeNode(account_bookmark).syncing);
  EXPECT_TRUE(GetSingleBookmarkTreeNode(account_folder).syncing);
}

TEST_F(ExtensionBookmarksTest, RemoveNodeInvalidId) {
  int64_t invalid_id = model_->next_node_id();
  std::string error;
  EXPECT_FALSE(RemoveNode(model_, managed_, invalid_id, true, &error));
  EXPECT_EQ(bookmarks_errors::kNoNodeError, error);
}

TEST_F(ExtensionBookmarksTest, RemoveNodePermanent) {
  std::string error;
  EXPECT_FALSE(
      RemoveNode(model_, managed_, model_->other_node()->id(), true, &error));
  EXPECT_EQ(bookmarks_errors::kModifySpecialError, error);
}

TEST_F(ExtensionBookmarksTest, RemoveNodeManaged) {
  const BookmarkNode* managed_bookmark =
      model_->AddURL(managed_->managed_node(), 0, u"Chromium",
                     GURL("http://www.chromium.org"));
  std::string error;
  EXPECT_FALSE(
      RemoveNode(model_, managed_, managed_bookmark->id(), true, &error));
  EXPECT_EQ(bookmarks_errors::kModifyManagedError, error);
}

TEST_F(ExtensionBookmarksTest, RemoveNodeNotRecursive) {
  std::string error;
  EXPECT_FALSE(RemoveNode(model_, managed_, folder_->id(), false, &error));
  EXPECT_EQ(bookmarks_errors::kFolderNotEmptyError, error);
}

TEST_F(ExtensionBookmarksTest, RemoveNodeRecursive) {
  EXPECT_EQ(3u, model_->other_node()->children().size());
  std::string error;
  EXPECT_TRUE(RemoveNode(model_, managed_, folder_->id(), true, &error));
  EXPECT_EQ(2u, model_->other_node()->children().size());
}

TEST_F(ExtensionBookmarksTest, GetMetaInfo) {
  base::DictValue id_to_meta_info_map;
  GetMetaInfo(*model_->other_node(), id_to_meta_info_map);
  EXPECT_EQ(8u, id_to_meta_info_map.size());

  // Verify top level node.
  {
    const base::Value* value = id_to_meta_info_map.Find(
        base::NumberToString(model_->other_node()->id()));
    ASSERT_NE(value, nullptr);
    ASSERT_TRUE(value->is_dict());
    const base::DictValue& dict = value->GetDict();
    EXPECT_EQ(0u, dict.size());
  }

  // Verify bookmark with two meta info key/value pairs.
  {
    const base::Value* value =
        id_to_meta_info_map.Find(base::NumberToString(node_->id()));
    ASSERT_NE(value, nullptr);
    ASSERT_TRUE(value->is_dict());
    const base::DictValue& dict = value->GetDict();
    EXPECT_EQ(2u, dict.size());
    ASSERT_TRUE(dict.FindString("some_key1"));
    EXPECT_EQ("some_value1", *(dict.FindString("some_key1")));
    ASSERT_TRUE(dict.FindString("some_key2"));
    EXPECT_EQ("some_value2", *(dict.FindString("some_key2")));
  }

  // Verify folder with one meta info key/value pair.
  {
    const base::Value* value =
        id_to_meta_info_map.Find(base::NumberToString(folder_->id()));
    ASSERT_NE(value, nullptr);
    ASSERT_TRUE(value->is_dict());
    const base::DictValue& dict = value->GetDict();
    EXPECT_EQ(1u, dict.size());
    ASSERT_TRUE(dict.FindString("some_key1"));
    EXPECT_EQ("some_value1", *(dict.FindString("some_key1")));
  }

  // Verify bookmark in a subfolder with one meta info key/value pairs.
  {
    const base::Value* value =
        id_to_meta_info_map.Find(base::NumberToString(node2_->id()));
    ASSERT_NE(value, nullptr);
    ASSERT_TRUE(value->is_dict());
    const base::DictValue& dict = value->GetDict();
    EXPECT_EQ(1u, dict.size());
    ASSERT_FALSE(dict.FindString("some_key1"));
    ASSERT_TRUE(dict.FindString("some_key2"));
    EXPECT_EQ("some_value2", *(dict.FindString("some_key2")));
  }
}

TEST_F(ExtensionBookmarksTest, PopulateBookmarkTreeNodeIndexConsistency) {
  const BookmarkNode* url1 = model_->AddURL(model_->bookmark_bar_node(), 0,
                                            u"URL1", GURL("http://url1.com"));
  const BookmarkNode* url2 = model_->AddURL(model_->bookmark_bar_node(), 1,
                                            u"URL2", GURL("http://url2.com"));
  const BookmarkNode* subfolder =
      model_->AddFolder(model_->bookmark_bar_node(), 2, u"Subfolder");
  const BookmarkNode* url3 =
      model_->AddURL(subfolder, 0, u"URL3", GURL("http://url3.com"));

  // Get the tree and verify indexes.
  BookmarkTreeNode tree =
      GetBookmarkTreeNode(model_, managed_, model_->bookmark_bar_node(),
                          /*recurse=*/true, /*only_folders=*/false);

  ASSERT_TRUE(tree.children.has_value());
  const std::vector<BookmarkTreeNode>& children = *tree.children;

  // Helper to find node by title.
  auto find_node = [](const std::vector<BookmarkTreeNode>& nodes,
                      const std::string& title) {
    return std::find_if(
        nodes.begin(), nodes.end(),
        [&title](const BookmarkTreeNode& node) { return node.title == title; });
  };

  // Verify each node's visible_index matches GetAPIIndexOf.
  auto url1_it = find_node(children, "URL1");
  ASSERT_NE(url1_it, children.end());
  EXPECT_EQ(GetAPIIndexOf(*url1), url1_it->index);

  auto url2_it = find_node(children, "URL2");
  ASSERT_NE(url2_it, children.end());
  EXPECT_EQ(GetAPIIndexOf(*url2), url2_it->index);

  auto subfolder_it = find_node(children, "Subfolder");
  ASSERT_NE(subfolder_it, children.end());
  EXPECT_EQ(GetAPIIndexOf(*subfolder), subfolder_it->index);

  ASSERT_TRUE(subfolder_it->children.has_value());
  auto url3_it = find_node(*subfolder_it->children, "URL3");
  ASSERT_NE(url3_it, subfolder_it->children->end());
  EXPECT_EQ(GetAPIIndexOf(*url3), url3_it->index);

  // Verify indexes reflect the node order.
  EXPECT_EQ(0U, url1_it->index);
  EXPECT_EQ(1U, url2_it->index);
  EXPECT_EQ(2U, subfolder_it->index);
  EXPECT_EQ(0U, url3_it->index);

  // Directly test PopulateBookmarkTreeNode.
  api::bookmarks::BookmarkTreeNode populated_node;
  PopulateBookmarkTreeNode(model_, managed_, url1,
                           /*recurse=*/false, /*only_folders=*/false,
                           std::nullopt, &populated_node);
  EXPECT_EQ(GetAPIIndexOf(*url1), populated_node.index);

  // Test recursive population.
  api::bookmarks::BookmarkTreeNode recursive_node;
  PopulateBookmarkTreeNode(model_, managed_, subfolder,
                           /*recurse=*/true, /*only_folders=*/false,
                           std::nullopt, &recursive_node);
  EXPECT_EQ(GetAPIIndexOf(*subfolder), recursive_node.index);
  ASSERT_TRUE(recursive_node.children.has_value());
  ASSERT_EQ(1U, recursive_node.children->size());
  EXPECT_EQ(GetAPIIndexOf(*url3), recursive_node.children->at(0).index);

  // Test only_folders with recursion.
  api::bookmarks::BookmarkTreeNode folders_node;
  PopulateBookmarkTreeNode(model_, managed_, model_->bookmark_bar_node(),
                           /*recurse=*/true, /*only_folders=*/true,
                           std::nullopt, &folders_node);
  ASSERT_TRUE(folders_node.children.has_value());
  ASSERT_EQ(1U, folders_node.children->size());  // Only the subfolder.
  EXPECT_EQ(GetAPIIndexOf(*subfolder), folders_node.children->at(0).index);
}

}  // namespace bookmarks_helpers
}  // namespace extensions
