// 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/ui/cocoa/history_menu_bridge.h"

#import <Cocoa/Cocoa.h>

#include <initializer_list>
#include <memory>
#include <vector>

#include "base/memory/ptr_util.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/app_controller_mac.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/sessions/chrome_tab_restore_service_client.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "chrome/test/base/testing_profile.h"
#include "components/favicon_base/favicon_types.h"
#include "components/os_crypt/async/browser/test_utils.h"
#include "components/sessions/content/content_test_helper.h"
#include "components/sessions/core/serialized_navigation_entry_test_helper.h"
#include "components/sessions/core/tab_restore_service_impl.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/gfx/codec/png_codec.h"

namespace {

class MockTRS : public sessions::TabRestoreServiceImpl {
 public:
  MockTRS(Profile* profile, os_crypt_async::OSCryptAsync* os_crypt_async)
      : sessions::TabRestoreServiceImpl(
            std::make_unique<ChromeTabRestoreServiceClient>(profile),
            profile->GetPrefs(),
            nullptr,
            os_crypt_async) {}
  MOCK_CONST_METHOD0(entries, const sessions::TabRestoreService::Entries&());
};

sessions::tab_restore::Tab* CreateSessionTab(
    SessionID::id_type id,
    const std::string& url,
    const std::string& title,
    std::optional<tab_groups::TabGroupVisualData> visual_data = std::nullopt) {
  auto* tab = new sessions::tab_restore::Tab;
  tab->id = SessionID::FromSerializedValue(id);
  tab->current_navigation_index = 0;
  tab->navigations.push_back(
      sessions::ContentTestHelper::CreateNavigation(url, title));
  tab->group_visual_data = visual_data;
  return tab;
}

MockTRS::Entries CreateSessionEntries(
    std::initializer_list<sessions::tab_restore::Entry*> entries) {
  MockTRS::Entries ret;
  for (auto* entry : entries) {
    ret.emplace_back(entry);
  }
  return ret;
}

sessions::tab_restore::Window* CreateSessionWindow(
    SessionID::id_type id,
    std::initializer_list<sessions::tab_restore::Tab*> tabs) {
  auto* window = new sessions::tab_restore::Window;
  window->id = SessionID::FromSerializedValue(id);
  window->tabs.reserve(tabs.size());
  for (auto* tab : tabs) {
    window->tabs.emplace_back(std::move(tab));
  }
  return window;
}

sessions::tab_restore::Group* CreateSessionGroup(
    SessionID::id_type id,
    tab_groups::TabGroupVisualData visual_data,
    std::initializer_list<sessions::tab_restore::Tab*> tabs) {
  auto* group = new sessions::tab_restore::Group;
  group->id = SessionID::FromSerializedValue(id);
  group->visual_data = visual_data;
  group->tabs.reserve(tabs.size());
  for (auto* tab : tabs) {
    group->tabs.emplace_back(std::move(tab));
  }
  return group;
}

sessions::tab_restore::Split* CreateSessionSplit(
    SessionID::id_type id,
    std::initializer_list<sessions::tab_restore::Tab*> tabs) {
  auto* split = new sessions::tab_restore::Split;
  split->id = SessionID::FromSerializedValue(id);
  split->tabs.reserve(tabs.size());
  for (auto* tab : tabs) {
    split->tabs.emplace_back(tab);
  }
  return split;
}

std::unique_ptr<HistoryMenuBridge::HistoryItem> CreateItem(
    const std::u16string& title) {
  auto item = std::make_unique<HistoryMenuBridge::HistoryItem>();
  item->title = title;
  item->url = GURL(title);
  return item;
}

class MockBridge : public HistoryMenuBridge {
 public:
  explicit MockBridge(Profile* profile)
      : HistoryMenuBridge(profile),
        menu_([[NSMenu alloc] initWithTitle:@"History"]) {}

  NSMenu* HistoryMenu() override { return menu_; }

 private:
  NSMenu* __strong menu_;
};

class HistoryMenuBridgeTest : public BrowserWithTestWindowTest {
 public:
  bool ShouldMenuItemBeVisible(NSMenuItem* item) {
    return bridge_->ShouldMenuItemBeVisible(item);
  }

 protected:
  void SetUp() override {
    BrowserWithTestWindowTest::SetUp();
    [AppController.sharedController setLastProfileForTesting:profile()];

    bridge_ = std::make_unique<MockBridge>(profile());
    os_crypt_async_ = os_crypt_async::GetTestOSCryptAsyncForTesting(true);
  }

  void TearDown() override {
    bridge_.reset();
    BrowserWithTestWindowTest::TearDown();
  }

  TestingProfile::TestingFactories GetTestingFactories() override {
    return {TestingProfile::TestingFactory{
                FaviconServiceFactory::GetInstance(),
                FaviconServiceFactory::GetDefaultFactory()},
            TestingProfile::TestingFactory{
                HistoryServiceFactory::GetInstance(),
                HistoryServiceFactory::GetDefaultFactory()}};
  }

  // We are a friend of HistoryMenuBridge (and have access to
  // protected methods), but none of the classes generated by TEST_F()
  // are. Wraps common commands.
  void ClearMenuSection(NSMenu* menu, NSInteger tag) {
    bridge_->ClearMenuSection(menu, tag);
  }

  void AddItemToBridgeMenu(std::unique_ptr<HistoryMenuBridge::HistoryItem> item,
                           NSMenu* menu,
                           NSInteger tag,
                           NSInteger index) {
    bridge_->AddItemToMenu(std::move(item), menu, tag, index);
  }

  NSMenuItem* AddItemToMenu(NSMenu* menu,
                            NSString* title,
                            SEL selector,
                            int tag) {
    NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:title
                                                  action:nullptr
                                           keyEquivalent:@""];
    [item setTag:tag];
    if (selector) {
      [item setAction:selector];
      [item setTarget:bridge_->controller_];
    }
    [menu addItem:item];
    return item;
  }

  void GetFaviconForHistoryItem(HistoryMenuBridge::HistoryItem* item) {
    bridge_->GetFaviconForHistoryItem(item);
  }

  void GotFaviconData(HistoryMenuBridge::HistoryItem* item,
                      const favicon_base::FaviconImageResult& image_result) {
    bridge_->GotFaviconData(item, image_result);
  }

  void CancelFaviconRequest(HistoryMenuBridge::HistoryItem* item) {
    bridge_->CancelFaviconRequest(item);
  }

  const std::map<NSMenuItem*, std::unique_ptr<HistoryMenuBridge::HistoryItem>>&
  menu_item_map() {
    return bridge_->menu_item_map_;
  }

 private:
  CocoaTestHelper cocoa_test_helper_;

 protected:
  std::unique_ptr<MockBridge> bridge_;
  std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_async_;
};

class HistoryMenuBridgeLifetimeTest : public testing::Test {
 public:
  NSMenu* HistoryMenu(HistoryMenuBridge* bridge) {
    return bridge->HistoryMenu();
  }

  const std::map<NSMenuItem*, std::unique_ptr<HistoryMenuBridge::HistoryItem>>&
  menu_item_map(HistoryMenuBridge* bridge) {
    return bridge->menu_item_map_;
  }

  void AddItemToBridgeMenu(HistoryMenuBridge* bridge,
                           std::unique_ptr<HistoryMenuBridge::HistoryItem> item,
                           NSMenu* menu,
                           NSInteger tag,
                           NSInteger index) {
    bridge->AddItemToMenu(std::move(item), menu, tag, index);
  }

 private:
  content::BrowserTaskEnvironment task_environment_;
};

void CheckMenuItemVisibility(HistoryMenuBridgeTest* test, bool is_incognito) {
  // Make sure the items belong to both original and incognito mode are visible.
  NSInteger always_visible_items[] = {IDC_HOME, IDC_BACK, IDC_FORWARD};
  for (NSInteger tag : always_visible_items) {
    // Create a fake item with tag.
    NSMenuItem* item = [[NSMenuItem alloc] init];
    item.tag = tag;
    EXPECT_TRUE(test->ShouldMenuItemBeVisible(item));
  }

  // Check visibility of items belong to regular mode. They should be visible
  // for regular mode, not for incognito mode.
  NSInteger regular_visible_items[] = {
      HistoryMenuBridge::kRecentlyClosedSeparator,
      HistoryMenuBridge::kRecentlyClosedTitle,
      HistoryMenuBridge::kVisitedSeparator,
      HistoryMenuBridge::kVisitedTitle,
      HistoryMenuBridge::kShowFullSeparator,
      IDC_SHOW_HISTORY};
  for (NSInteger tag : regular_visible_items) {
    // Create a fake item with tag.
    NSMenuItem* item = [[NSMenuItem alloc] init];
    item.tag = tag;
    EXPECT_EQ(!is_incognito, test->ShouldMenuItemBeVisible(item));
  }
}

// Edge case test for clearing until the end of a menu.
TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuUntilEnd) {
  NSMenu* menu = [[NSMenu alloc] initWithTitle:@"history foo"];
  AddItemToMenu(menu, @"HEADER", nullptr, HistoryMenuBridge::kVisitedTitle);

  NSInteger tag = HistoryMenuBridge::kVisited;
  AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag);
  AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag);
  AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag);
  AddItemToMenu(menu, @"delta", @selector(openHistoryMenuItem:), tag);

  ClearMenuSection(menu, HistoryMenuBridge::kVisited);

  EXPECT_EQ(1, [menu numberOfItems]);
  EXPECT_NSEQ(@"HEADER",
              [[menu itemWithTag:HistoryMenuBridge::kVisitedTitle] title]);
}

// Skip menu items that are not hooked up to |-openHistoryMenuItem:|.
TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuSkipping) {
  NSMenu* menu = [[NSMenu alloc] initWithTitle:@"history foo"];
  AddItemToMenu(menu, @"HEADER", nullptr, HistoryMenuBridge::kVisitedTitle);

  NSInteger tag = HistoryMenuBridge::kVisited;
  AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag);
  AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag);
  AddItemToMenu(menu, @"TITLE", nullptr,
                HistoryMenuBridge::kRecentlyClosedTitle);
  AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag);

  ClearMenuSection(menu, tag);

  EXPECT_EQ(2, [menu numberOfItems]);
  EXPECT_NSEQ(@"HEADER",
              [[menu itemWithTag:HistoryMenuBridge::kVisitedTitle] title]);
  EXPECT_NSEQ(@"TITLE", [[menu itemAtIndex:1] title]);
}

// Edge case test for clearing an empty menu.
TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuEmpty) {
  NSMenu* menu = [[NSMenu alloc] initWithTitle:@"history foo"];
  AddItemToMenu(menu, @"HEADER", nullptr, HistoryMenuBridge::kVisited);

  ClearMenuSection(menu, HistoryMenuBridge::kVisited);

  EXPECT_EQ(1, [menu numberOfItems]);
  EXPECT_NSEQ(@"HEADER",
              [[menu itemWithTag:HistoryMenuBridge::kVisited] title]);
}

// Test that AddItemToMenu() properly adds HistoryItem objects as menus.
TEST_F(HistoryMenuBridgeTest, AddItemToMenu) {
  NSMenu* menu = [[NSMenu alloc] initWithTitle:@"history foo"];

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

  AddItemToBridgeMenu(CreateItem(short_url), menu, 100, 0);
  AddItemToBridgeMenu(CreateItem(long_url), menu, 101, 1);

  EXPECT_EQ(2, [menu numberOfItems]);

  EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:0] action]);
  EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:1] action]);

  EXPECT_EQ(100, [[menu itemAtIndex:0] tag]);
  EXPECT_EQ(101, [[menu itemAtIndex:1] tag]);

  // Make sure a short title looks fine
  NSString* s = [[menu itemAtIndex:0] title];
  EXPECT_EQ(base::SysNSStringToUTF16(s), short_url);

  // Make sure a super-long title gets trimmed
  s = [[menu itemAtIndex:0] title];
  EXPECT_TRUE([s length] < long_url.length());

  // 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([[[menu itemAtIndex:0] toolTip] length],
            (2 * short_url.length() - 5));
  EXPECT_GE([[[menu itemAtIndex:1] toolTip] length],
            (2 * long_url.length() - 5));
}

// Test that the menu is created for a set of simple tabs.
TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabs) {
  std::unique_ptr<MockTRS> trs(new MockTRS(profile(), os_crypt_async_.get()));
  auto entries{CreateSessionEntries({
      CreateSessionTab(24, "http://google.com", "Google"),
      CreateSessionTab(42, "http://apple.com", "Apple"),
  })};

  using ::testing::ReturnRef;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));

  bridge_->TabRestoreServiceChanged(trs.get());

  NSMenu* menu = bridge_->HistoryMenu();
  ASSERT_EQ(2U, [[menu itemArray] count]);

  NSMenuItem* item1 = [menu itemAtIndex:0];
  MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1);
  EXPECT_TRUE(hist1);
  EXPECT_EQ(24, hist1->session_id.id());
  EXPECT_NSEQ(@"Google", [item1 title]);

  NSMenuItem* item2 = [menu itemAtIndex:1];
  MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist2);
  EXPECT_EQ(42, hist2->session_id.id());
  EXPECT_NSEQ(@"Apple", [item2 title]);

  EXPECT_EQ(2u, menu_item_map().size());
  ClearMenuSection(menu, HistoryMenuBridge::kRecentlyClosed);
  EXPECT_EQ(0u, menu_item_map().size());
  EXPECT_EQ(0u, [[menu itemArray] count]);
}

// Test that the menu is created for a mix of windows and tabs.
TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabsAndWindows) {
  std::unique_ptr<MockTRS> trs(new MockTRS(profile(), os_crypt_async_.get()));
  auto entries{CreateSessionEntries({
      CreateSessionTab(24, "http://google.com", "Google"),
      CreateSessionWindow(30,
                          {
                              CreateSessionTab(31, "http://foo.com", "foo"),
                              CreateSessionTab(32, "http://bar.com", "bar"),
                          }),
      CreateSessionTab(42, "http://apple.com", "Apple"),
      CreateSessionWindow(
          50,
          {
              CreateSessionTab(51, "http://magic.com", "magic"),
              CreateSessionTab(52, "http://goats.com", "goats"),
              CreateSessionTab(53, "http://teleporter.com", "teleporter"),
          }),
  })};

  using ::testing::ReturnRef;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));

  bridge_->TabRestoreServiceChanged(trs.get());

  NSMenu* menu = bridge_->HistoryMenu();
  ASSERT_EQ(4U, [[menu itemArray] count]);

  NSMenuItem* item1 = [menu itemAtIndex:0];
  MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1);
  EXPECT_TRUE(hist1);
  EXPECT_EQ(24, hist1->session_id.id());
  EXPECT_NSEQ(@"Google", [item1 title]);

  NSMenuItem* item2 = [menu itemAtIndex:1];
  MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist2);
  EXPECT_EQ(30, hist2->session_id.id());
  EXPECT_EQ(2U, hist2->tabs.size());
  // Do not test full menu item title because it is localized. Just verify that
  // it contains the right number of tabs.
  EXPECT_TRUE([[item2 title] containsString:@"2"]);
  NSMenu* submenu1 = [item2 submenu];
  EXPECT_EQ(4U, [[submenu1 itemArray] count]);
  // Do not test Restore All Tabs because it is localized.
  EXPECT_TRUE([[submenu1 itemAtIndex:1] isSeparatorItem]);
  EXPECT_NSEQ(@"foo", [[submenu1 itemAtIndex:2] title]);
  EXPECT_NSEQ(@"bar", [[submenu1 itemAtIndex:3] title]);
  EXPECT_EQ(31, hist2->tabs[0]->session_id.id());
  EXPECT_EQ(32, hist2->tabs[1]->session_id.id());

  NSMenuItem* item3 = [menu itemAtIndex:2];
  MockBridge::HistoryItem* hist3 = bridge_->HistoryItemForMenuItem(item3);
  EXPECT_TRUE(hist3);
  EXPECT_EQ(42, hist3->session_id.id());
  EXPECT_NSEQ(@"Apple", [item3 title]);

  NSMenuItem* item4 = [menu itemAtIndex:3];
  MockBridge::HistoryItem* hist4 = bridge_->HistoryItemForMenuItem(item4);
  EXPECT_TRUE(hist4);
  EXPECT_EQ(50, hist4->session_id.id());
  EXPECT_EQ(3U, hist4->tabs.size());
  // Do not test full menu item title because it is localized. Just verify that
  // it contains the right number of tabs.
  EXPECT_TRUE([[item4 title] containsString:@"3"]);
  NSMenu* submenu2 = [item4 submenu];
  EXPECT_EQ(5U, [[submenu2 itemArray] count]);
  // Do not test Restore All Tabs because it is localized.
  EXPECT_TRUE([[submenu2 itemAtIndex:1] isSeparatorItem]);
  EXPECT_NSEQ(@"magic", [[submenu2 itemAtIndex:2] title]);
  EXPECT_NSEQ(@"goats", [[submenu2 itemAtIndex:3] title]);
  EXPECT_NSEQ(@"teleporter", [[submenu2 itemAtIndex:4] title]);
  EXPECT_EQ(51, hist4->tabs[0]->session_id.id());
  EXPECT_EQ(52, hist4->tabs[1]->session_id.id());
  EXPECT_EQ(53, hist4->tabs[2]->session_id.id());

  // 9 items from |entries|, plus 2 "Restore All Tabs" items, one for each
  // window entry.
  EXPECT_EQ(11u, menu_item_map().size());
  ClearMenuSection(menu, HistoryMenuBridge::kRecentlyClosed);
  EXPECT_EQ(0u, menu_item_map().size());
  EXPECT_EQ(0u, [[menu itemArray] count]);
}

// Test that the menu is created for a set of groups.
TEST_F(HistoryMenuBridgeTest, RecentlyClosedGroups) {
  tab_groups::TabGroupVisualData visual_data1(
      std::u16string(), tab_groups::TabGroupColorId::kGrey);
  tab_groups::TabGroupVisualData visual_data2(
      u"title", tab_groups::TabGroupColorId::kBlue);

  std::unique_ptr<MockTRS> trs(new MockTRS(profile(), os_crypt_async_.get()));
  auto entries{CreateSessionEntries({
      CreateSessionGroup(30, visual_data1,
                         {
                             CreateSessionTab(31, "http://foo.com", "foo"),
                             CreateSessionTab(32, "http://bar.com", "bar"),
                         }),
      CreateSessionGroup(
          50, visual_data2,
          {
              CreateSessionTab(51, "http://magic.com", "magic"),
              CreateSessionTab(52, "http://goats.com", "goats"),
              CreateSessionTab(53, "http://teleporter.com", "teleporter"),
          }),
  })};

  using ::testing::ReturnRef;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));

  bridge_->TabRestoreServiceChanged(trs.get());

  NSMenu* menu = bridge_->HistoryMenu();
  ASSERT_EQ(2U, [[menu itemArray] count]);

  NSMenuItem* item1 = [menu itemAtIndex:0];
  MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1);
  EXPECT_TRUE(hist1);
  EXPECT_EQ(30, hist1->session_id.id());
  EXPECT_EQ(2U, hist1->tabs.size());
  // Do not test full menu item title because it is localized. Just verify that
  // it contains the right number of tabs and no title.
  EXPECT_TRUE([[item1 title] containsString:@"2"]);
  EXPECT_FALSE([[item1 title] containsString:@"title"]);

  NSMenu* submenu1 = [item1 submenu];
  EXPECT_EQ(4U, [[submenu1 itemArray] count]);
  // Do not test Restore All Tabs because it is localized.
  EXPECT_TRUE([[submenu1 itemAtIndex:1] isSeparatorItem]);
  EXPECT_NSEQ(@"foo", [[submenu1 itemAtIndex:2] title]);
  EXPECT_NSEQ(@"bar", [[submenu1 itemAtIndex:3] title]);

  // Do not show group indicator because it's from the group submenu.
  EXPECT_EQ(nil, [[submenu1 itemAtIndex:2] attributedTitle]);
  EXPECT_EQ(nil, [[submenu1 itemAtIndex:3] attributedTitle]);

  EXPECT_EQ(31, hist1->tabs[0]->session_id.id());
  EXPECT_EQ(32, hist1->tabs[1]->session_id.id());

  NSMenuItem* item2 = [menu itemAtIndex:1];
  MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist2);
  EXPECT_EQ(50, hist2->session_id.id());
  EXPECT_EQ(3U, hist2->tabs.size());
  // Do not test full menu item title because it is localized. Just verify that
  // it contains the right number of tabs and title.
  EXPECT_TRUE([[item2 title] containsString:@"3"]);
  EXPECT_TRUE([[item2 title] containsString:@"title"]);

  NSMenu* submenu2 = [item2 submenu];
  EXPECT_EQ(5U, [[submenu2 itemArray] count]);
  // Do not test Restore All Tabs because it is localized.
  EXPECT_TRUE([[submenu2 itemAtIndex:1] isSeparatorItem]);
  EXPECT_NSEQ(@"magic", [[submenu2 itemAtIndex:2] title]);
  EXPECT_NSEQ(@"goats", [[submenu2 itemAtIndex:3] title]);
  EXPECT_NSEQ(@"teleporter", [[submenu2 itemAtIndex:4] title]);
  EXPECT_EQ(51, hist2->tabs[0]->session_id.id());
  EXPECT_EQ(52, hist2->tabs[1]->session_id.id());
  EXPECT_EQ(53, hist2->tabs[2]->session_id.id());
}

TEST_F(HistoryMenuBridgeTest, RecentlyClosedSplits) {
  std::unique_ptr<MockTRS> trs(new MockTRS(profile(), os_crypt_async_.get()));

  auto entries{CreateSessionEntries({
      CreateSessionSplit(
          30,
          {
              CreateSessionTab(31, "http://left.com", "Left View"),
              CreateSessionTab(32, "http://right.com", "Right View"),
          }),
  })};

  using ::testing::ReturnRef;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));

  bridge_->TabRestoreServiceChanged(trs.get());

  NSMenu* menu = bridge_->HistoryMenu();
  ASSERT_EQ(1U, [[menu itemArray] count]);

  // Verify the parent Split item
  NSMenuItem* parent_item = [menu itemAtIndex:0];
  MockBridge::HistoryItem* hist_parent =
      bridge_->HistoryItemForMenuItem(parent_item);
  EXPECT_TRUE(hist_parent);
  EXPECT_EQ(30, hist_parent->session_id.id());
  EXPECT_EQ(2U, hist_parent->tabs.size());
  EXPECT_NSEQ(@"Split View", [parent_item title]);

  // Verify the submenu containing the individual tabs
  NSMenu* submenu = [parent_item submenu];
  EXPECT_EQ(4U,
            [[submenu itemArray] count]);  // Restore All + Separator + 2 Tabs

  EXPECT_NSEQ(@"Left View", [[submenu itemAtIndex:2] title]);
  EXPECT_NSEQ(@"Right View", [[submenu itemAtIndex:3] title]);

  EXPECT_EQ(31, hist_parent->tabs[0]->session_id.id());
  EXPECT_EQ(32, hist_parent->tabs[1]->session_id.id());
}

// Tests that we properly request an icon from the FaviconService.
TEST_F(HistoryMenuBridgeTest, GetFaviconForHistoryItem) {
  // Create a fake item.
  HistoryMenuBridge::HistoryItem item;
  item.title = u"Title";
  item.url = GURL("http://google.com");

  // Request the icon.
  GetFaviconForHistoryItem(&item);

  // Make sure the item was modified properly.
  EXPECT_TRUE(item.icon_requested);
  EXPECT_NE(base::CancelableTaskTracker::kBadTaskId, item.icon_task_id);

  // Cancel the request.
  CancelFaviconRequest(&item);
}

TEST_F(HistoryMenuBridgeTest, GotFaviconData) {
  // Create a dummy bitmap.
  SkBitmap bitmap;
  bitmap.allocN32Pixels(25, 25);
  bitmap.eraseARGB(255, 255, 0, 0);

  // Set up the HistoryItem.
  HistoryMenuBridge::HistoryItem item;
  item.menu_item = [[NSMenuItem alloc] init];
  GetFaviconForHistoryItem(&item);

  // Cancel the request so there will be no race.
  CancelFaviconRequest(&item);

  // Pretend to be called back.
  favicon_base::FaviconImageResult image_result;
  image_result.image = gfx::Image::CreateFrom1xBitmap(bitmap);
  GotFaviconData(&item, image_result);

  // Make sure the callback works.
  EXPECT_FALSE(item.icon_requested);
  EXPECT_TRUE(item.icon);
  EXPECT_TRUE([item.menu_item image]);
}

TEST_F(HistoryMenuBridgeTest, MenuItemVisibilityForRegularMode) {
  CheckMenuItemVisibility(this, false);
}

TEST_F(HistoryMenuBridgeTest, MenuItemVisibilityForIncognitoMode) {
  bridge_ = std::make_unique<MockBridge>(
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true));
  CheckMenuItemVisibility(this, true);
}

// Does a full setup and tear down of the bridge.
TEST_F(HistoryMenuBridgeLifetimeTest, ShutdownAfterProfile) {
  TestingProfile::Builder profile_builder;
  profile_builder.AddTestingFactory(
      TabRestoreServiceFactory::GetInstance(),
      TabRestoreServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(HistoryServiceFactory::GetInstance(),
                                    HistoryServiceFactory::GetDefaultFactory());
  std::unique_ptr<TestingProfile> profile = profile_builder.Build();

  auto bridge = std::make_unique<HistoryMenuBridge>(profile.get());
  // Should not crash.
  bridge.reset();
  profile.reset();
}

// Does a full setup and tear down of the bridge.
TEST_F(HistoryMenuBridgeLifetimeTest, ShutdownBeforeProfile) {
  TestingProfile::Builder profile_builder;
  profile_builder.AddTestingFactory(
      TabRestoreServiceFactory::GetInstance(),
      TabRestoreServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(HistoryServiceFactory::GetInstance(),
                                    HistoryServiceFactory::GetDefaultFactory());
  std::unique_ptr<TestingProfile> profile = profile_builder.Build();

  auto bridge = std::make_unique<HistoryMenuBridge>(profile.get());
  bridge.reset();
  profile.reset();
}

// Initializes the menu, then destroys the Profile but keeps the
// HistoryMenuBridge around.
TEST_F(HistoryMenuBridgeLifetimeTest, StillValidAfterProfileShutdown) {
  TestingProfile::Builder profile_builder;
  profile_builder.AddTestingFactory(FaviconServiceFactory::GetInstance(),
                                    FaviconServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(
      TabRestoreServiceFactory::GetInstance(),
      TabRestoreServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(HistoryServiceFactory::GetInstance(),
                                    HistoryServiceFactory::GetDefaultFactory());
  std::unique_ptr<TestingProfile> profile = profile_builder.Build();
  base::FilePath profile_dir = profile->GetPath();
  // Ensure the AppController is the NSApp delegate.
  std::ignore = AppController.sharedController;

  auto bridge = std::make_unique<MockBridge>(profile.get());
  std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_async =
      os_crypt_async::GetTestOSCryptAsyncForTesting(true);
  std::unique_ptr<MockTRS> trs(
      new MockTRS(profile.get(), os_crypt_async.get()));
  auto entries{CreateSessionEntries({
      CreateSessionTab(24, "http://google.com", "Google"),
      CreateSessionTab(42, "http://apple.com", "Apple"),
  })};

  using ::testing::ReturnRef;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries));

  bridge->TabRestoreServiceChanged(trs.get());

  NSMenu* menu = HistoryMenu(bridge.get());
  AddItemToBridgeMenu(bridge.get(), CreateItem(u"http://example.com/"), menu,
                      100, 2);
  AddItemToBridgeMenu(bridge.get(), CreateItem(u"http://example.org/"), menu,
                      101, 3);

  // Destroy the Profile.
  bridge->OnProfileWillBeDestroyed();
  trs.reset();
  profile.reset();
  ASSERT_EQ(nullptr, bridge->profile());
  EXPECT_EQ(profile_dir, bridge->profile_dir());

  // The menu should still contain items after Profile destruction.
  ASSERT_EQ(4u, [[menu itemArray] count]);

  NSMenuItem* item1 = [menu itemAtIndex:0];
  MockBridge::HistoryItem* hist1 = bridge->HistoryItemForMenuItem(item1);
  EXPECT_TRUE(hist1);
  EXPECT_EQ(24, hist1->session_id.id());
  EXPECT_NSEQ(@"Google", [item1 title]);

  NSMenuItem* item2 = [menu itemAtIndex:1];
  MockBridge::HistoryItem* hist2 = bridge->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist2);
  EXPECT_EQ(42, hist2->session_id.id());
  EXPECT_NSEQ(@"Apple", [item2 title]);

  NSMenuItem* item3 = [menu itemAtIndex:2];
  MockBridge::HistoryItem* hist3 = bridge->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist3);
  EXPECT_NSEQ(@"http://example.com/", [item3 title]);

  NSMenuItem* item4 = [menu itemAtIndex:3];
  MockBridge::HistoryItem* hist4 = bridge->HistoryItemForMenuItem(item2);
  EXPECT_TRUE(hist4);
  EXPECT_NSEQ(@"http://example.org/", [item4 title]);

  EXPECT_EQ(4u, menu_item_map(bridge.get()).size());

  bridge.reset();
}

TEST_F(HistoryMenuBridgeLifetimeTest, EmptyTabRestoreService) {
  TestingProfile::Builder profile_builder;
  profile_builder.AddTestingFactory(FaviconServiceFactory::GetInstance(),
                                    FaviconServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(
      TabRestoreServiceFactory::GetInstance(),
      TabRestoreServiceFactory::GetDefaultFactory());
  profile_builder.AddTestingFactory(HistoryServiceFactory::GetInstance(),
                                    HistoryServiceFactory::GetDefaultFactory());
  std::unique_ptr<TestingProfile> profile = profile_builder.Build();
  base::FilePath profile_dir = profile->GetPath();

  auto bridge = std::make_unique<MockBridge>(profile.get());
  NSMenu* menu = HistoryMenu(bridge.get());

  // Prepopulate the menu with some recently closed item.
  AddItemToBridgeMenu(bridge.get(), CreateItem(u"http://foo/"), menu,
                      HistoryMenuBridge::kRecentlyClosed, 0);
  EXPECT_EQ(1, [menu numberOfItems]);

  // Load an empty `TabRestoreService`. `TabRestoreServiceChanged()` is not
  // called because the service is empty.
  std::unique_ptr<os_crypt_async::OSCryptAsync> os_crypt_async =
      os_crypt_async::GetTestOSCryptAsyncForTesting(true);
  std::unique_ptr<MockTRS> trs(
      new MockTRS(profile.get(), os_crypt_async.get()));
  MockTRS::Entries no_entries;
  EXPECT_CALL(*trs.get(), entries()).WillOnce(testing::ReturnRef(no_entries));
  bridge->TabRestoreServiceLoaded(trs.get());

  // Recently closed tabs are removed.
  EXPECT_EQ(0, [menu numberOfItems]);

  bridge.reset();
}


}  // namespace
