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

#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"

#import <AppKit/AppKit.h>

#import <optional>
#import <string>

#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/scoped_feature_list.h"
#import "base/uuid.h"
#import "chrome/app/chrome_command_ids.h"
#import "chrome/browser/bookmarks/bookmark_merged_surface_service.h"
#import "chrome/browser/bookmarks/bookmark_merged_surface_service_factory.h"
#import "chrome/browser/bookmarks/bookmark_model_factory.h"
#import "chrome/browser/bookmarks/bookmark_parent_folder.h"
#import "chrome/browser/bookmarks/bookmark_test_helpers.h"
#import "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
#import "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#import "chrome/test/base/browser_with_test_window_test.h"
#import "chrome/test/base/testing_profile.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_node.h"
#import "components/bookmarks/common/bookmark_metrics.h"
#import "components/bookmarks/test/bookmark_test_helpers.h"
#import "components/signin/public/base/signin_switches.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"

using base::ASCIIToUTF16;
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;

class BookmarkMenuBridgeTest : public BrowserWithTestWindowTest {
 public:
  BookmarkMenuBridgeTest() = default;

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

  void SetUp() override {
    BrowserWithTestWindowTest::SetUp();
    service_ = BookmarkMergedSurfaceServiceFactory::GetForProfile(profile());
    CHECK(service_);
    WaitForBookmarkMergedSurfaceServiceToLoad(service_);
    menu_ = [[NSMenu alloc] initWithTitle:@"test"];

    bridge_ = std::make_unique<BookmarkMenuBridge>(profile(), menu_);
    CHECK(bridge_->IsMenuRoot(menu_));
  }

  void TearDown() override {
    bridge_ = nullptr;
    service_ = nullptr;
    BrowserWithTestWindowTest::TearDown();
  }

  TestingProfile::TestingFactories GetTestingFactories() override {
    return {TestingProfile::TestingFactory{
                BookmarkModelFactory::GetInstance(),
                BookmarkModelFactory::GetDefaultFactory()},
            TestingProfile::TestingFactory{
                ManagedBookmarkServiceFactory::GetInstance(),
                ManagedBookmarkServiceFactory::GetDefaultFactory()},
            TestingProfile::TestingFactory{
                BookmarkMergedSurfaceServiceFactory::GetInstance(),
                BookmarkMergedSurfaceServiceFactory::GetDefaultFactory()}};
  }

  void UpdateRootMenu() { bridge_->UpdateRootMenuIfInvalid(); }

  // We are a friend of BookmarkMenuBridge (and have access to
  // protected methods), but none of the classes generated by TEST_F()
  // are.  This (and AddNodeToMenu()) are simple wrappers to let
  // derived test classes have access to protected methods.
  void ClearBookmarkMenu() { bridge_->ClearBookmarkMenu(); }

  void InvalidateMenu() { bridge_->InvalidateMenu(); }
  bool menu_is_valid() { return bridge_->IsMenuValid(); }

  void AddNodeToMenu(BookmarkMenuBridge* bridge,
                     const BookmarkNode* root,
                     NSMenu* menu) {
    bridge->AddNodeToMenu(root, menu, /*recurse=*/false);
  }

  NSMenuItem* MenuItemForNode(BookmarkMenuBridge* bridge,
                              const BookmarkNode* node) {
    return bridge->MenuItemForNode(node);
  }

  NSMenuItem* AddTestMenuItem(NSMenu* menu, NSString* title, SEL selector) {
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
                                                  action:nullptr
                                           keyEquivalent:@""];
    if (selector) {
      [item setAction:selector];
    }
    [menu addItem:item];
    return item;
  }

  BookmarkMergedSurfaceService* service() { return service_; }

 protected:
  base::test::ScopedFeatureList scoped_feature_list{
      switches::kSyncEnableBookmarksInTransportMode};
  NSMenu* __strong menu_;
  raw_ptr<BookmarkMergedSurfaceService> service_;
  std::unique_ptr<BookmarkMenuBridge> bridge_;

 private:
  CocoaTestHelper cocoa_test_helper_;
};

// Tests that the menu has separators for different bookmark sources.
TEST_F(BookmarkMenuBridgeTest, TestBookmarkMenuAutoSeparator) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  bridge_->BookmarkMergedSurfaceServiceLoaded();
  UpdateRootMenu();
  // The bare menu after loading has no submenus and separators.
  EXPECT_EQ(0, [menu_ numberOfItems]);

  const char* url = "http://www.zim-bop-a-dee.com/";

  // Add an account bookmark. After reloading, there should be 2 items: a new
  // separator and the new bookmark.
  // ---------------------
  //    + Account bookmark
  const BookmarkNode* account_root = model->account_bookmark_bar_node();
  model->AddURL(account_root, 0, u"Account bookmark", GURL(url));
  UpdateRootMenu();
  EXPECT_EQ(2, [menu_ numberOfItems]);

  // Add a local bookmark.
  // ---------------------
  //    + Account bookmark
  //    + Bookmark
  const BookmarkNode* root = model->bookmark_bar_node();
  model->AddURL(root, 0, u"Bookmark", GURL(url));
  UpdateRootMenu();
  EXPECT_EQ(3, [menu_ numberOfItems]);

  // Add bookmarks to the other and mobile folders.
  // ---------------------
  //    + Account bookmark
  //    + Bookmark
  // ---------------------
  //    + Other Bookmarks   (1 child)
  //    + Mobile Bookmarks  (1 child)
  const BookmarkNode* account_other_root = model->account_other_node();
  const BookmarkNode* mobile_root = model->mobile_node();
  model->AddURL(account_other_root, 0, u"Account other bookmark", GURL(url));
  model->AddURL(mobile_root, 0, u"Mobile bookmark", GURL(url));
  UpdateRootMenu();
  EXPECT_EQ(6, [menu_ numberOfItems]);

  // Add more bookmarks to the other and mobile folders.
  // ---------------------
  //    + Account bookmark
  //    + Bookmark
  // ---------------------
  //    + Other Bookmarks   (2 children)
  //    + Mobile Bookmarks  (2 children)
  const BookmarkNode* other_root = model->other_node();
  const BookmarkNode* account_mobile_root = model->account_mobile_node();
  model->AddURL(account_mobile_root, 0, u"Account other bookmark", GURL(url));
  model->AddURL(other_root, 0, u"Mobile bookmark", GURL(url));
  UpdateRootMenu();
  EXPECT_EQ(6, [menu_ numberOfItems]);

  // Remove some bookmarks and reload. We should still have 6 items.
  // ---------------------
  //    + Account bookmark
  //    + Bookmark
  // ---------------------
  //    + Other Bookmarks   (1 child)
  //    + Mobile Bookmarks  (1 child)
  model->Remove(account_other_root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  model->Remove(mobile_root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  UpdateRootMenu();
  EXPECT_EQ(6, [menu_ numberOfItems]);

  // Remove some bookmarks and reload. We should have 4 items since there are
  // still two separators.
  // ---------------------
  //    + Bookmark
  // ---------------------
  //    + Mobile Bookmarks  (1 child)
  model->Remove(account_root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  model->Remove(other_root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  UpdateRootMenu();
  EXPECT_EQ(4, [menu_ numberOfItems]);

  // Remove the last bookmark bar node, which removes the separator above the
  // bookmark bar items.
  // ---------------------
  //    + Mobile Bookmarks  (1 child)
  model->Remove(root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  UpdateRootMenu();
  EXPECT_EQ(2, [menu_ numberOfItems]);

  // Remove the last bookmark, which removes the last separator.
  model->Remove(account_mobile_root->children().front().get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);

  UpdateRootMenu();
  EXPECT_EQ(0, [menu_ numberOfItems]);
}

// Tests that ClearBookmarkMenu() removes all bookmark menus.
TEST_F(BookmarkMenuBridgeTest, TestClearBookmarkMenu) {
  AddTestMenuItem(menu_, @"hi mom", nil);
  AddTestMenuItem(menu_, @"not", @selector(openBookmarkMenuItem:));
  NSMenuItem* test_item = AddTestMenuItem(menu_, @"hi mom", nil);
  [test_item setSubmenu:[[NSMenu alloc] initWithTitle:@"bar"]];
  AddTestMenuItem(menu_, @"not", @selector(openBookmarkMenuItem:));
  AddTestMenuItem(menu_, @"zippy", @selector(length));
  [menu_ addItem:[NSMenuItem separatorItem]];

  ClearBookmarkMenu();

  // Make sure all bookmark items are removed, all items with
  // submenus removed, and all separator items are gone.
  EXPECT_EQ(2, [menu_ numberOfItems]);
  for (NSMenuItem* item in [menu_ itemArray]) {
    EXPECT_NSNE(@"not", [item title]);
  }
}

// Tests that the menu is invalidated on bookmark model updates.
TEST_F(BookmarkMenuBridgeTest, TestInvalidation) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  model->AddURL(model->account_bookmark_bar_node(), 0, u"Google Maps",
                GURL("https://google.com/map"));
  model->AddURL(model->bookmark_bar_node(), 0, u"Google",
                GURL("https://google.com"));
  bridge_->BookmarkMergedSurfaceServiceLoaded();

  EXPECT_FALSE(menu_is_valid());
  UpdateRootMenu();
  EXPECT_TRUE(menu_is_valid());

  InvalidateMenu();
  EXPECT_FALSE(menu_is_valid());
  InvalidateMenu();
  EXPECT_FALSE(menu_is_valid());
  UpdateRootMenu();
  EXPECT_TRUE(menu_is_valid());
  UpdateRootMenu();
  EXPECT_TRUE(menu_is_valid());

  model->AddURL(model->bookmark_bar_node(), 0, u"Bookmark",
                GURL("http://www.zim-bop-a-dee.com/"));
  EXPECT_FALSE(menu_is_valid());
  UpdateRootMenu();
  EXPECT_TRUE(menu_is_valid());

  model->AddURL(model->account_bookmark_bar_node(), 0, u"Account Bookmark",
                GURL("http://www.zim-bop-a-dee-account.com/"));
  EXPECT_FALSE(menu_is_valid());
  UpdateRootMenu();
  EXPECT_TRUE(menu_is_valid());
}

// Test that AddNodeToMenu() properly adds bookmark nodes as menus,
// including the recursive case.
TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMenu) {
  std::u16string empty;

  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* account_root = model->account_bookmark_bar_node();
  const BookmarkNode* root = model->bookmark_bar_node();

  const char* short_url = "http://foo/";
  const char* long_url = "http://super-duper-long-url--."
                         "that.cannot.possibly.fit.even-in-80-columns"
                         "or.be.reasonably-displayed-in-a-menu"
                         "without.looking-ridiculous.com/";  // 140 chars total

  // Account bookmark.
  model->AddURL(account_root, 0, ASCIIToUTF16(short_url), GURL(short_url));
  // Local submenu.
  const BookmarkNode* local_folder_node = model->AddFolder(root, 0, empty);
  model->AddURL(local_folder_node, 0, empty, GURL("http://sub"));
  // Local bookmark with long url as title.
  model->AddURL(root, 1, ASCIIToUTF16(long_url), GURL(long_url));

  UpdateRootMenu();
  EXPECT_EQ(4, [menu_ numberOfItems]);  // including a separator.

  // Verify the first bookmark is there with the right action.
  NSMenuItem* item = [menu_ itemWithTitle:@(short_url)];
  EXPECT_TRUE(item);
  EXPECT_EQ(@selector(openBookmarkMenuItem:), [item action]);
  EXPECT_EQ(NO, [item hasSubmenu]);
  NSMenuItem* short_item = item;
  NSMenuItem* long_item = nil;

  // Check that we have 1 submenu that is not "Other Bookmarks" or "Mobile
  // Bookmarks".
  int subs = 0;
  for (item in [menu_ itemArray]) {
    if ([item hasSubmenu]) {
      subs++;
    }
  }
  EXPECT_EQ(1, subs);

  for (item in [menu_ itemArray]) {
    if ([[item title] hasPrefix:@"http://super-duper"]) {
      long_item = item;
      break;
    }
  }
  EXPECT_TRUE(long_item);

  // Make sure a short title looks fine.
  NSString* s = [short_item title];
  EXPECT_NSEQ(@(short_url), s);

  // Long titles are now truncated with middle ellipsis.
  s = [long_item title];
  EXPECT_LT([s length], strlen(long_url));
  // Should contain ellipsis character.
  EXPECT_TRUE([s containsString:@"…"]);
  // Should start with the beginning of the original URL.
  EXPECT_TRUE([s hasPrefix:@"http://super-duper"]);
  // Should end with the end of the original URL.
  EXPECT_TRUE([s hasSuffix:@".com/"]);

  // Confirm tooltips and confirm they are not trimmed (like the item
  // name might be).  Add tolerance for URL fixer-upping;
  // e.g. http://foo becomes http://foo/)
  EXPECT_GE([[short_item toolTip] length], strlen(short_url) - 3);
  EXPECT_GE([[long_item toolTip] length], strlen(long_url) - 3);

  // Make sure the favicon is non-nil (should be either the default site
  // icon or a favicon, if present).
  EXPECT_TRUE([short_item image]);
  EXPECT_TRUE([long_item image]);
}

// Makes sure our internal map of BookmarkNode to NSMenuItem works.
TEST_F(BookmarkMenuBridgeTest, TestGetMenuItemForNode) {
  std::u16string empty;
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  const BookmarkNode* bookmark_bar = model->bookmark_bar_node();
  UpdateRootMenu();
  EXPECT_EQ(0u, [menu_ numberOfItems]);

  const BookmarkNode* node = model->AddFolder(bookmark_bar, 0, empty);
  UpdateRootMenu();
  EXPECT_EQ(2u, [menu_ numberOfItems]);

  NSMenu* submenu = [[menu_ itemAtIndex:1] submenu];
  EXPECT_TRUE(submenu);
  EXPECT_TRUE([submenu delegate]);
  EXPECT_EQ(0u, [submenu numberOfItems]);

  BookmarkParentFolder folder = BookmarkParentFolder::FromFolderNode(node);

  bridge_->UpdateNonRootMenu(submenu, folder);
  // Updating the menu clears the delegate to prevent further updates.
  EXPECT_FALSE([submenu delegate]);

  // Since the folder is currently empty, a single node is added saying (empty).
  EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);

  model->AddURL(node, 0, u"Test Item", GURL("http://test"));
  UpdateRootMenu();
  // There will be a new submenu each time, Cocoa will update it if needed.
  bridge_->UpdateNonRootMenu([[menu_ itemAtIndex:1] submenu], folder);

  EXPECT_TRUE(MenuItemForNode(bridge_.get(), node->children().front().get()));

  model->AddURL(node, 1, u"Test 2", GURL("http://second-test"));

  UpdateRootMenu();
  NSMenu* old_menu = [[menu_ itemAtIndex:1] submenu];
  EXPECT_TRUE([old_menu delegate]);

  // If the menu was never built, ensure UpdateRootMenu() also clears delegates
  // from unbuilt submenus, since they will no longer be reachable.
  InvalidateMenu();
  UpdateRootMenu();
  EXPECT_NE(old_menu, [[menu_ itemAtIndex:1] submenu]);
  EXPECT_FALSE([old_menu delegate]);

  bridge_->UpdateNonRootMenu([[menu_ itemAtIndex:1] submenu], folder);
  EXPECT_TRUE(MenuItemForNode(bridge_.get(), node->children()[0].get()));
  EXPECT_TRUE(MenuItemForNode(bridge_.get(), node->children()[1].get()));

  const BookmarkNode* removed_node = node->children()[0].get();
  EXPECT_EQ(2u, node->children().size());
  model->Remove(node->children()[0].get(),
                bookmarks::metrics::BookmarkEditSource::kOther, FROM_HERE);
  EXPECT_EQ(1u, node->children().size());

  EXPECT_FALSE(menu_is_valid());
  UpdateRootMenu();

  // Initially both will be false, but the submenu corresponding to the folder
  // will have a delegate set again, allowing it to be updated on demand.
  EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node));
  EXPECT_FALSE(MenuItemForNode(bridge_.get(), node->children()[0].get()));

  UpdateRootMenu();
  bridge_->UpdateNonRootMenu([[menu_ itemAtIndex:1] submenu], folder);

  EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node));
  EXPECT_TRUE(MenuItemForNode(bridge_.get(), node->children()[0].get()));

  const BookmarkNode empty_node(/*id=*/0, base::Uuid::GenerateRandomV4(),
                                GURL("http://no-where/"));
  EXPECT_FALSE(MenuItemForNode(bridge_.get(), &empty_node));
  EXPECT_FALSE(MenuItemForNode(bridge_.get(), nullptr));
}

// Tests that Loaded() adds the "Other Bookmarks" nodes as lazily loadable
// submenus.
TEST_F(BookmarkMenuBridgeTest, TestAddNodeToOther) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* other_root = model->other_node();
  const BookmarkNode* account_other_root = model->account_other_node();

  const char* url1 = "http://foo/";
  model->AddURL(account_other_root, 0, ASCIIToUTF16(url1), GURL(url1));
  const char* url2 = "http://bar/";
  model->AddURL(other_root, 0, ASCIIToUTF16(url2), GURL(url2));

  UpdateRootMenu();
  ASSERT_GT([menu_ numberOfItems], 0);
  NSMenuItem* other = [menu_ itemAtIndex:([menu_ numberOfItems] - 1)];
  EXPECT_TRUE(other);
  EXPECT_TRUE([other hasSubmenu]);
  EXPECT_EQ(base::SysNSStringToUTF8([other title]), "Other Bookmarks");

  // The "other" submenu is loaded lazily.
  EXPECT_EQ(0u, [[other submenu] numberOfItems]);
  bridge_->UpdateNonRootMenu([other submenu],
                             BookmarkParentFolder::OtherFolder());

  ASSERT_GT([[other submenu] numberOfItems], 0);
  EXPECT_NSEQ(@"http://foo/", [[[other submenu] itemAtIndex:0] title]);
  EXPECT_NSEQ(@"http://bar/", [[[other submenu] itemAtIndex:1] title]);
}

// Tests that Loaded() adds the "Mobile Bookmarks" nodes as lazily loadable
// submenus.
TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMobile) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* account_mobile_root = model->account_mobile_node();
  const BookmarkNode* mobile_root = model->mobile_node();

  const char* url1 = "http://foo/";
  model->AddURL(account_mobile_root, 0, ASCIIToUTF16(url1), GURL(url1));
  const char* url2 = "http://bar/";
  model->AddURL(mobile_root, 0, ASCIIToUTF16(url2), GURL(url2));

  UpdateRootMenu();
  ASSERT_GT([menu_ numberOfItems], 0);
  NSMenuItem* mobile = [menu_ itemAtIndex:([menu_ numberOfItems] - 1)];
  EXPECT_TRUE(mobile);
  EXPECT_TRUE([mobile hasSubmenu]);
  EXPECT_EQ(base::SysNSStringToUTF8([mobile title]), "Mobile Bookmarks");

  // The "Mobile Bookmarks" submenu is loaded lazily.
  EXPECT_EQ(0u, [[mobile submenu] numberOfItems]);
  bridge_->UpdateNonRootMenu([mobile submenu],
                             BookmarkParentFolder::MobileFolder());

  ASSERT_GT([[mobile submenu] numberOfItems], 0);
  EXPECT_NSEQ(@"http://foo/", [[[mobile submenu] itemAtIndex:0] title]);
  EXPECT_NSEQ(@"http://bar/", [[[mobile submenu] itemAtIndex:1] title]);
}

// Tests that each bookmark item has a favicon.
TEST_F(BookmarkMenuBridgeTest, TestFaviconLoading) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* root = model->account_bookmark_bar_node();
  const BookmarkNode* node =
      model->AddURL(root, 0, u"Test Item", GURL("http://favicon-test"));
  UpdateRootMenu();
  NSMenuItem* item = [menu_ itemWithTitle:@"Test Item"];
  EXPECT_TRUE([item image]);
  [item setImage:nil];
  bridge_->BookmarkNodeFaviconChanged(node);
  EXPECT_TRUE([item image]);  // default favicon
}

// Tests that the menu is updated when a bookmark has its title changed.
TEST_F(BookmarkMenuBridgeTest, TestChangeTitle) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* root = model->account_bookmark_bar_node();
  const BookmarkNode* node =
      model->AddURL(root, 0, u"Test Item", GURL("http://title-test"));
  const BookmarkNode* folder_node = model->AddFolder(root, 1, u"Test Folder");
  UpdateRootMenu();

  NSMenuItem* item = [menu_ itemWithTitle:@"Test Item"];
  EXPECT_TRUE([item image]);
  model->SetTitle(node, u"New Title",
                  bookmarks::metrics::BookmarkEditSource::kOther);
  item = [menu_ itemWithTitle:@"Test Item"];
  EXPECT_FALSE(item);
  item = [menu_ itemWithTitle:@"New Title"];
  EXPECT_TRUE(item);

  item = [menu_ itemWithTitle:@"Test Folder"];
  EXPECT_TRUE([item image]);
  model->SetTitle(folder_node, u"New Folder Title",
                  bookmarks::metrics::BookmarkEditSource::kOther);
  item = [menu_ itemWithTitle:@"Test Folder"];
  EXPECT_FALSE(item);
  item = [menu_ itemWithTitle:@"New Folder Title"];
  EXPECT_TRUE(item);
}

// Tests that the bookmark menu reflects custom ordering of bookmark nodes
// (interleaved account and local nodes).
TEST_F(BookmarkMenuBridgeTest, TestReorderBookmarkNodes) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* root = model->bookmark_bar_node();
  const BookmarkNode* account_root = model->account_bookmark_bar_node();

  // Bookmark bar
  //    + Account item 1
  //    + Account folder 1
  //    + Item 1
  //    + Folder 1
  const BookmarkNode* account_item1 = model->AddURL(
      account_root, 0, u"Account item 1", GURL("http://account-item-1/"));
  const BookmarkNode* account_folder1 =
      model->AddFolder(account_root, 1, u"Account folder 1");
  const BookmarkNode* item1 =
      model->AddURL(root, 0, u"Item 1", GURL("http://item-1/"));
  const BookmarkNode* folder1 = model->AddFolder(root, 1, u"Folder 1");

  UpdateRootMenu();
  ASSERT_EQ(5u, [menu_ numberOfItems]);  // including separator

  NSMenuItem* item = [menu_ itemAtIndex:1];
  EXPECT_EQ(account_item1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:2];
  EXPECT_EQ(account_folder1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:3];
  EXPECT_EQ(item1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:4];
  EXPECT_EQ(folder1->uuid(), bridge_->TagToGUID([item tag]));

  // Reordered bookmark bar
  //    + Item 1
  //    + Account item 1
  //    + Folder 1
  //    + Account folder 1
  service()->Move(item1, BookmarkParentFolder::BookmarkBarFolder(), 0u,
                  browser());
  service()->Move(folder1, BookmarkParentFolder::BookmarkBarFolder(), 2u,
                  browser());
  EXPECT_TRUE(service()->IsNonDefaultOrderingTracked(
      BookmarkParentFolder::BookmarkBarFolder()));

  UpdateRootMenu();
  ASSERT_EQ(5u, [menu_ numberOfItems]);

  item = [menu_ itemAtIndex:1];
  EXPECT_EQ(item1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:2];
  EXPECT_EQ(account_item1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:3];
  EXPECT_EQ(folder1->uuid(), bridge_->TagToGUID([item tag]));
  item = [menu_ itemAtIndex:4];
  EXPECT_EQ(account_folder1->uuid(), bridge_->TagToGUID([item tag]));
}

// Tests that the entire menu is built recursively before the profile is
// destroyed.
TEST_F(BookmarkMenuBridgeTest, BuildMenuRecursivelyBeforeProfileDestruction) {
  BookmarkModel* model = bridge_->GetBookmarkModelForTesting();
  model->CreateAccountPermanentFolders();
  const BookmarkNode* root = model->bookmark_bar_node();
  const BookmarkNode* account_root = model->account_bookmark_bar_node();

  // Bookmark bar
  //    + Account item 1
  //    + Account folder 1
  //        + Account folder 2
  //            + Account item 2
  //    + Item 1
  //    + Folder 1
  //        + Folder 2
  //            + Item 2

  const BookmarkNode* account_item1 = model->AddURL(
      account_root, 0, u"Account item 1", GURL("http://account-item-1/"));
  base::Uuid account_item1_guid = account_item1->uuid();
  const BookmarkNode* account_folder1 =
      model->AddFolder(account_root, 1, u"Account folder 1");
  base::Uuid account_folder1_guid = account_folder1->uuid();
  const BookmarkNode* account_folder2 =
      model->AddFolder(account_folder1, 0, u"Account folder 2");
  base::Uuid account_folder2_guid = account_folder2->uuid();
  const BookmarkNode* account_item2 = model->AddURL(
      account_folder2, 0, u"Account item 2", GURL("http://account-item-2/"));
  base::Uuid account_item2_guid = account_item2->uuid();

  const BookmarkNode* item1 =
      model->AddURL(root, 0, u"Item 1", GURL("http://item-1/"));
  base::Uuid item1_guid = item1->uuid();
  const BookmarkNode* folder1 = model->AddFolder(root, 1, u"Folder 1");
  base::Uuid folder1_guid = folder1->uuid();
  const BookmarkNode* folder2 = model->AddFolder(folder1, 0, u"Folder 2");
  base::Uuid folder2_guid = folder2->uuid();
  const BookmarkNode* item2 =
      model->AddURL(folder2, 0, u"Item 2", GURL("http://item-2/"));
  base::Uuid item2_guid = item2->uuid();

  // We didn't show the menu or any submenus, so it shouldn't contain these
  // items.
  NSMenuItem* item = [menu_ itemWithTitle:@"Account item 1"];
  EXPECT_FALSE(item);
  item = [menu_ itemWithTitle:@"Account folder 1"];
  EXPECT_FALSE(item);
  item = [menu_ itemWithTitle:@"Item 1"];
  EXPECT_FALSE(item);
  item = [menu_ itemWithTitle:@"Folder 1"];
  EXPECT_FALSE(item);

  bridge_->OnProfileWillBeDestroyed();
  EXPECT_EQ(nullptr, bridge_->GetProfile());

  // OnProfileWillBeDestroyed() should've recursively populated the menu.
  item = [menu_ itemWithTitle:@"Account item 1"];
  EXPECT_TRUE(item);
  EXPECT_EQ(account_item1_guid, bridge_->TagToGUID([item tag]));
  item = [menu_ itemWithTitle:@"Account folder 1"];
  EXPECT_TRUE(item);
  EXPECT_EQ(account_folder1_guid, bridge_->TagToGUID([item tag]));
  EXPECT_TRUE([item hasSubmenu]);
  item = [[item submenu] itemWithTitle:@"Account folder 2"];
  EXPECT_TRUE(item);
  EXPECT_EQ(account_folder2_guid, bridge_->TagToGUID([item tag]));
  EXPECT_TRUE([item hasSubmenu]);
  item = [[item submenu] itemWithTitle:@"Account item 2"];
  EXPECT_TRUE(item);
  EXPECT_EQ(account_item2_guid, bridge_->TagToGUID([item tag]));

  item = [menu_ itemWithTitle:@"Item 1"];
  EXPECT_TRUE(item);
  EXPECT_EQ(item1_guid, bridge_->TagToGUID([item tag]));
  item = [menu_ itemWithTitle:@"Folder 1"];
  EXPECT_TRUE(item);
  EXPECT_EQ(folder1_guid, bridge_->TagToGUID([item tag]));
  EXPECT_TRUE([item hasSubmenu]);
  item = [[item submenu] itemWithTitle:@"Folder 2"];
  EXPECT_TRUE(item);
  EXPECT_EQ(folder2_guid, bridge_->TagToGUID([item tag]));
  EXPECT_TRUE([item hasSubmenu]);
  item = [[item submenu] itemWithTitle:@"Item 2"];
  EXPECT_TRUE(item);
  EXPECT_EQ(item2_guid, bridge_->TagToGUID([item tag]));
}
