// 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 <stddef.h>

#include <memory>
#include <optional>
#include <set>
#include <string_view>
#include <tuple>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/browsertest_util.h"
#include "chrome/browser/extensions/context_menu_matcher.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/menu_manager_test_observer.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_context_menu/context_menu_test_util.h"
#include "chrome/browser/tab_list/tab_list_interface.h"
#include "chrome/common/channel_info.h"
#include "components/version_info/channel.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/extension_api_frame_id_map.h"
#include "extensions/browser/extension_host_test_helper.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/process_manager.h"
#include "extensions/browser/state_store.h"
#include "extensions/browser/state_store_test_observer.h"
#include "extensions/browser/test_management_policy.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/features/feature_channel.h"
#include "extensions/common/mojom/view_type.mojom.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "net/dns/mock_host_resolver.h"
#include "ui/base/models/menu_model.h"
#include "ui/menus/simple_menu_model.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/wm/window_pin_util.h"
#include "chrome/browser/ash/boca/on_task/on_task_locked_controller.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#endif

#if BUILDFLAG(IS_ANDROID)
#include "chrome/browser/extensions/test_extension_menu_model_android.h"
#else
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

using content::WebContents;
using ui::MenuModel;

namespace extensions {
namespace {

// Defines `PlatformContextMenu`, which maps to different types based on
// platform. This is similar to PlatformBrowserTest, which maps to
// InProcessBrowserTest or AndroidBrowserTest based on platform.
#if BUILDFLAG(IS_ANDROID)
using PlatformContextMenu = TestExtensionMenuModel;
#else
using PlatformContextMenu = TestRenderViewContextMenu;
#endif

constexpr char kPersistentExtensionId[] = "knldjmfmopnpolahpmmgbagdohdnhkik";

// Creates a platform-specific context menu containing extension items for a
// given `frame`.
std::unique_ptr<PlatformContextMenu> CreateContextMenu(
    content::RenderFrameHost* frame,
    const content::ContextMenuParams& params) {
#if BUILDFLAG(IS_ANDROID)
  auto menu = std::make_unique<TestExtensionMenuModel>(*frame, params);
  menu->PopulateModel();
  return menu;
#else
  auto menu = std::make_unique<TestRenderViewContextMenu>(*frame, params);
  menu->Init();
  return menu;
#endif  // BUILDFLAG(ENABLE_EXTENSIONS)
}

// Creates a platform-specific context menu containing extension items for a
// given `web_contents` with arguments for various parameters. See
// `content::ContextMenuParams` for parameter descriptions.
std::unique_ptr<PlatformContextMenu> CreateContextMenu(
    content::WebContents* web_contents,
    const GURL& page_url,
    const GURL& link_url = GURL(),
    bool is_subframe = false) {
  content::RenderFrameHost* frame = web_contents->GetPrimaryMainFrame();
  content::ContextMenuParams params;
  params.page_url = page_url;
  params.frame_url = page_url;
  params.link_url = link_url;
  params.is_subframe = is_subframe;
  return CreateContextMenu(frame, params);
}

}  // namespace

class ExtensionContextMenuBrowserTest : public ExtensionBrowserTest {
 public:
  ExtensionContextMenuBrowserTest() = default;
  ~ExtensionContextMenuBrowserTest() override = default;
  ExtensionContextMenuBrowserTest(const ExtensionContextMenuBrowserTest&) =
      delete;
  ExtensionContextMenuBrowserTest& operator=(
      const ExtensionContextMenuBrowserTest&) = delete;

  // Shortcut to return the current MenuManager.
  MenuManager* menu_manager() { return MenuManager::Get(profile()); }

  // Returns a pointer to the currently loaded extension with |name|, or null
  // if not found.
  const Extension* GetExtensionNamed(const std::string& name) {
    const ExtensionSet& extensions =
        ExtensionRegistry::Get(profile())->enabled_extensions();
    for (const auto& ext : extensions) {
      if (ext->name() == name) {
        return ext.get();
      }
    }
    return nullptr;
  }

  // This gets all the items that any extension has registered for possible
  // inclusion in context menus.
  MenuItem::List GetItems() {
    MenuItem::List result;
    std::set<MenuItem::ExtensionKey> extension_ids =
        menu_manager()->ExtensionIds();
    for (const auto& extension_id : extension_ids) {
      const MenuItem::OwnedList* list = menu_manager()->MenuItems(extension_id);
      for (const auto& item : *list) {
        result.push_back(item.get());
      }
    }
    return result;
  }

  // This creates a test menu for a page with |page_url| and |link_url|, looks
  // for an extension item with the given |label|, and returns true if the item
  // was found.
  bool MenuHasItemWithLabel(const GURL& frame_url,
                            const GURL& link_url,
                            bool is_subframe,
                            const std::string& label) {
    std::unique_ptr<PlatformContextMenu> menu = CreateContextMenu(
        GetActiveWebContents(), frame_url, link_url, is_subframe);
    return MenuHasExtensionItemWithLabel(menu.get(), label);
  }

  // Click on a context menu identified by |target_menu_item_id|, and returns
  // the result of chrome.test.sendMessage. The .js test file that sets up the
  // context menu should call chrome.test.sendMessage in its onclick event.
  std::string ClickMenuInFrame(content::RenderFrameHost* frame,
                               const std::string& target_menu_item_id) {
    content::ContextMenuParams params;
    if (frame->GetParent()) {
      params.frame_url = frame->GetLastCommittedURL();
    } else {
      params.page_url = frame->GetLastCommittedURL();
    }

    std::unique_ptr<PlatformContextMenu> menu =
        CreateContextMenu(frame, params);

    MenuItem::Id menu_item_id;
    menu_item_id.string_uid = target_menu_item_id;
    int command_id = -1;
    if (!FindCommandId(menu.get(), menu_item_id, &command_id)) {
      return "Menu item not found: " + target_menu_item_id;
    }

    ExtensionTestMessageListener listener;
    menu->ExecuteCommand(command_id, 0);
    if (!listener.WaitUntilSatisfied()) {
      return "Onclick never fired for menu item: " + target_menu_item_id;
    }

    return listener.message();
  }

  bool MenuHasExtensionItemWithLabel(PlatformContextMenu* menu,
                                     const std::string& label) {
    std::u16string label16 = base::UTF8ToUTF16(label);
    for (const auto& it : menu->extension_items().extension_item_map_) {
      const MenuItem::Id& id = it.second;
      std::u16string tmp_label;
      EXPECT_TRUE(GetItemLabel(menu, id, &tmp_label));
      if (tmp_label == label16) {
        return true;
      }
    }
    return false;
  }

  // Looks in the menu for an extension item with |id|, and if it is found and
  // has a label, that is put in |result| and we return true. Otherwise returns
  // false.
  bool GetItemLabel(PlatformContextMenu* menu,
                    const MenuItem::Id& id,
                    std::u16string* result) const {
    int command_id = 0;
    if (!FindCommandId(menu, id, &command_id)) {
      return false;
    }

    std::optional<std::pair<MenuModel*, size_t>> model_and_index =
        menu->GetMenuModelAndItemIndex(command_id);
    if (!model_and_index) {
      return false;
    }
    MenuModel* model = model_and_index->first;
    size_t index = model_and_index->second;
    *result = model->GetLabelAt(index);
    return true;
  }

  // Given an extension menu item id, tries to find the corresponding command id
  // in the menu.
  bool FindCommandId(PlatformContextMenu* menu,
                     const MenuItem::Id& id,
                     int* command_id) const {
    for (const auto& it : menu->extension_items().extension_item_map_) {
      if (it.second == id) {
        *command_id = it.first;
        return true;
      }
    }
    return false;
  }

  // Given a menu item id, executes the item's command.
  void ExecuteCommand(PlatformContextMenu* menu,
                      const ExtensionId& extension_id,
                      const std::string& item_uid) {
    MenuItem::Id id(false, MenuItem::ExtensionKey(extension_id));
    id.string_uid = item_uid;
    int command_id = -1;
    ASSERT_TRUE(FindCommandId(menu, id, &command_id));
    menu->ExecuteCommand(command_id, 0);
  }

  // Verifies a radio item's selection state (checked or unchecked).
  void VerifyRadioItemSelectionState(PlatformContextMenu* menu,
                                     const ExtensionId& extension_id,
                                     const std::string& radio_uid,
                                     bool should_be_checked) {
    MenuItem::Id id(false, MenuItem::ExtensionKey(extension_id));
    id.string_uid = radio_uid;
    int command_id = -1;
    ASSERT_TRUE(FindCommandId(menu, id, &command_id));
    EXPECT_EQ(should_be_checked, menu->IsCommandIdChecked(command_id));
  }

  base::FilePath GetRootDir() const {
    return test_data_dir_.AppendASCII("context_menus");
  }
};

class ExtensionContextMenuLazyTest : public ExtensionContextMenuBrowserTest {
 public:
  ExtensionContextMenuLazyTest() = default;
  ~ExtensionContextMenuLazyTest() override = default;
  ExtensionContextMenuLazyTest(const ExtensionContextMenuLazyTest&) = delete;
  ExtensionContextMenuLazyTest& operator=(const ExtensionContextMenuLazyTest&) =
      delete;

  void SetUpOnMainThread() override {
    ExtensionContextMenuBrowserTest::SetUpOnMainThread();
    // Set shorter delays to prevent test timeouts.
    ProcessManager::SetEventPageIdleTimeForTesting(1);
    ProcessManager::SetEventPageSuspendingTimeForTesting(100);
  }

 protected:
  const Extension* LoadContextMenuExtension(std::string_view subdirectory) {
    base::FilePath extension_dir = GetRootDir().AppendASCII(subdirectory);
    return LoadExtension(extension_dir);
  }

  // Helper to load an extension from context_menus/top_level/|subdirectory| in
  // the extensions test data dir.
  const Extension* LoadTopLevelContextMenuExtension(
      std::string_view subdirectory) {
    base::FilePath extension_dir =
        GetRootDir().AppendASCII("top_level").AppendASCII(subdirectory);
    return LoadExtension(extension_dir, {});
  }

  const Extension* LoadContextMenuExtensionWithIncognitoFlags(
      std::string_view subdirectory) {
    base::FilePath extension_dir = GetRootDir().AppendASCII(subdirectory);
    return LoadExtension(extension_dir, {.allow_in_incognito = true});
  }

  // This creates an extension that starts |enabled| and then switches to
  // |!enabled|.
  void TestEnabledContextMenu(bool enabled) {
    ExtensionTestMessageListener begin("begin", ReplyBehavior::kWillReply);
    ExtensionTestMessageListener create("create", ReplyBehavior::kWillReply);
    ExtensionTestMessageListener update("update");
    ASSERT_TRUE(LoadContextMenuExtension("enabled"));

    ASSERT_TRUE(begin.WaitUntilSatisfied());

    if (enabled) {
      begin.Reply("start enabled");
    } else {
      begin.Reply("start disabled");
    }

    // Wait for the extension to tell us it's created an item.
    ASSERT_TRUE(create.WaitUntilSatisfied());

    GURL page_url("http://www.google.com");

    // Create and build our test context menu.
    std::unique_ptr<PlatformContextMenu> menu =
        CreateContextMenu(GetActiveWebContents(), page_url);

    // Look for the extension item in the menu, and make sure it's |enabled|.
    int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
    ASSERT_EQ(enabled, menu->IsCommandIdEnabled(command_id));
    create.Reply("go");

    // Update the item and make sure it is now |!enabled|.
    ASSERT_TRUE(update.WaitUntilSatisfied());
    ASSERT_EQ(!enabled, menu->IsCommandIdEnabled(command_id));
  }
};

// Tests adding a simple context menu item.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, Simple) {
  ExtensionTestMessageListener listener1("created item");
  ExtensionTestMessageListener listener2("onclick fired");
  ASSERT_TRUE(LoadContextMenuExtension("simple"));

  // Wait for the extension to tell us it's created an item.
  ASSERT_TRUE(listener1.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  // Create and build our test context menu.
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), page_url);

  // Look for the extension item in the menu, and execute it.
  int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
  ASSERT_TRUE(menu->IsCommandIdEnabled(command_id));
  menu->ExecuteCommand(command_id, 0);

  // Wait for the extension's script to tell us its onclick fired.
  ASSERT_TRUE(listener2.WaitUntilSatisfied());
}

// Tests that context menus for event page and Service Worker-based
// extensions are stored properly.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, PRE_Persistent) {
  StateStoreTestObserver observer(profile());
  ResultCatcher catcher;
  const Extension* extension = LoadContextMenuExtension("persistent");
  ASSERT_TRUE(extension);

  // Wait for the extension to tell us it's been installed and the
  // context menu has been created.
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();

  // Wait for the context menu to be stored.
  observer.WaitForExtensionAndKey(extension->id(), "context_menus");
}

IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, Persistent) {
  MenuManagerTestObserver observer(menu_manager());
  ResultCatcher catcher;

  // Wait for the context menu to finish loading.
  observer.WaitForExtension(kPersistentExtensionId);

  // Open a tab to trigger the update.
  ASSERT_TRUE(NavigateToURLInNewTab(GURL("chrome://version/")));
  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
}

// Tests that updating the first radio item in a radio list from checked to
// unchecked should not work. The radio button should remain checked because
// context menu radio lists should always have one item selected.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest,
                       UpdateCheckedStateOfFirstRadioItem) {
  ExtensionTestMessageListener listener_created_radio1("created radio1 item");
  ExtensionTestMessageListener listener_created_radio2("created radio2 item");
  ExtensionTestMessageListener listener_created_item1("created normal item");
  ExtensionTestMessageListener listener_created_item2(
      "created second normal item");

  ExtensionTestMessageListener listener_radio2_clicked("onclick radio2");
  ExtensionTestMessageListener listener_item1_clicked("onclick normal item");
  ExtensionTestMessageListener listener_radio1_updated("radio1 updated");

  const Extension* extension = LoadContextMenuExtension("radio_check");
  ASSERT_TRUE(extension);

  // Check that all menu items are created.
  ASSERT_TRUE(listener_created_radio1.WaitUntilSatisfied());
  ASSERT_TRUE(listener_created_radio2.WaitUntilSatisfied());
  ASSERT_TRUE(listener_created_item1.WaitUntilSatisfied());
  ASSERT_TRUE(listener_created_item2.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  // Create and build our test context menu.
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), page_url);

  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio1", true);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio2", false);

  // Clicking the regular item calls chrome.contextMenus.update to uncheck the
  // first radio item.
  ExecuteCommand(menu.get(), extension->id(), "item1");
  ASSERT_TRUE(listener_item1_clicked.WaitUntilSatisfied());

  // Unchecking the second item should not deselect it. Recall that context menu
  // radio lists should always have one item selected.
  ASSERT_TRUE(listener_radio1_updated.WaitUntilSatisfied());
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio1", true);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio2", false);
}

// Tests that updating a checked radio button (that is not the first item) to be
// unchecked should not work. The radio button should remain checked because
// context menu radio lists should always have one item selected.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest,
                       UpdateCheckedStateOfNonfirstRadioItem) {
  ExtensionTestMessageListener listener_created_radio1("created radio1 item");
  ExtensionTestMessageListener listener_created_radio2("created radio2 item");
  ExtensionTestMessageListener listener_created_item1("created normal item");
  ExtensionTestMessageListener listener_created_item2(
      "created second normal item");

  ExtensionTestMessageListener listener_radio2_clicked("onclick radio2");
  ExtensionTestMessageListener listener_item2_clicked(
      "onclick second normal item");
  ExtensionTestMessageListener listener_radio2_updated("radio2 updated");

  const Extension* extension = LoadContextMenuExtension("radio_check");
  ASSERT_TRUE(extension);

  // Check that all menu items are created.
  ASSERT_TRUE(listener_created_radio1.WaitUntilSatisfied());
  ASSERT_TRUE(listener_created_radio2.WaitUntilSatisfied());
  ASSERT_TRUE(listener_created_item1.WaitUntilSatisfied());

  ASSERT_TRUE(listener_created_item2.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  // Create and build our test context menu.
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), page_url);

  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio1", true);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio2", false);

  // Click on the second radio button. This should uncheck the first radio item
  // and check the second item.
  ExecuteCommand(menu.get(), extension->id(), "radio2");
  ASSERT_TRUE(listener_radio2_clicked.WaitUntilSatisfied());
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio1", false);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio2", true);

  // Clicking the regular item calls chrome.contextMenus.update to uncheck the
  // second radio item.
  ExecuteCommand(menu.get(), extension->id(), "item2");
  ASSERT_TRUE(listener_item2_clicked.WaitUntilSatisfied());

  // Unchecking the second item should not deselect it. Recall that context menu
  // radio lists should always have one item selected.
  ASSERT_TRUE(listener_radio2_updated.WaitUntilSatisfied());
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio1", false);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "radio2", true);
}

// Tests that setting "documentUrlPatterns" for an item properly restricts
// those items to matching pages.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, Patterns) {
  ExtensionTestMessageListener listener("created items");

  ASSERT_TRUE(LoadContextMenuExtension("patterns"));

  // Wait for the js test code to create its two items with patterns.
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // Check that a document url that should match the items' patterns appears.
  GURL google_url("http://www.google.com");
  ASSERT_TRUE(MenuHasItemWithLabel(google_url, GURL(), false,
                                   std::string("test_item1")));
  ASSERT_TRUE(MenuHasItemWithLabel(google_url, GURL(), false,
                                   std::string("test_item2")));

  // Now check with a non-matching url.
  GURL test_url("http://www.test.com");
  ASSERT_FALSE(
      MenuHasItemWithLabel(test_url, GURL(), false, std::string("test_item1")));
  ASSERT_FALSE(
      MenuHasItemWithLabel(test_url, GURL(), false, std::string("test_item2")));
}

// Tests registering an item with a very long title that should get truncated in
// the actual menu displayed.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, LongTitle) {
  ExtensionTestMessageListener listener("created");

  // Load the extension and wait until it's created a menu item.
  ASSERT_TRUE(LoadContextMenuExtension("long_title"));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // Make sure we have an item registered with a long title.
  size_t limit = ContextMenuMatcher::kMaxExtensionItemTitleLength;
  MenuItem::List items = GetItems();
  ASSERT_EQ(1u, items.size());
  MenuItem* item = items.at(0);
  ASSERT_GT(item->title().size(), limit);

  // Create a context menu, then find the item's label. It should be properly
  // truncated.
  GURL url("http://foo.com/");
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), url);

  std::u16string label;
  ASSERT_TRUE(GetItemLabel(menu.get(), item->id(), &label));
  ASSERT_TRUE(label.size() <= limit);
}

// Checks that Context Menus are ordered alphabetically by their name when
// extensions have only one single Context Menu item and by the extension name
// when multiples Context Menu items are created.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, TopLevel) {
  // We expect to see the following items in the menu:
  //   An Extension with multiple Context Menus
  //     Context Menu #1
  //     Context Menu #2
  //   Context Menu #1 - Extension #2
  //   Context Menu #2 - Extension #3
  //   Context Menu #3 - Extension #1
  //   Ze Extension with multiple Context Menus
  //     Context Menu #1
  //     Context Menu #2

  // Load extensions and wait until it's created a single menu item.
  ExtensionTestMessageListener listener1("created item");
  ASSERT_TRUE(LoadTopLevelContextMenuExtension("single1"));
  ASSERT_TRUE(listener1.WaitUntilSatisfied());

  ExtensionTestMessageListener listener2("created item");
  ASSERT_TRUE(LoadTopLevelContextMenuExtension("single2"));
  ASSERT_TRUE(listener2.WaitUntilSatisfied());

  ExtensionTestMessageListener listener3("created item");
  ASSERT_TRUE(LoadTopLevelContextMenuExtension("single3"));
  ASSERT_TRUE(listener3.WaitUntilSatisfied());

  // Load extensions and wait until it's created two menu items.
  ExtensionTestMessageListener listener4("created items");
  ASSERT_TRUE(LoadTopLevelContextMenuExtension("multi4"));
  ASSERT_TRUE(listener4.WaitUntilSatisfied());

  ExtensionTestMessageListener listener5("created items");
  ASSERT_TRUE(LoadTopLevelContextMenuExtension("multi5"));
  ASSERT_TRUE(listener5.WaitUntilSatisfied());

  GURL url("http://foo.com/");
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), url);

  std::optional<std::pair<MenuModel*, size_t>> model_and_index =
      menu->GetMenuModelAndItemIndex(
          ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0));
  ASSERT_TRUE(model_and_index);
  MenuModel* model = model_and_index->first;
  size_t index = model_and_index->second;
  EXPECT_EQ(u"An Extension with multiple Context Menus",
            model->GetLabelAt(index++));
  EXPECT_EQ(u"Context Menu #1 - Extension #2", model->GetLabelAt(index++));
  EXPECT_EQ(u"Context Menu #2 - Extension #3", model->GetLabelAt(index++));
  EXPECT_EQ(u"Context Menu #3 - Extension #1", model->GetLabelAt(index++));
  EXPECT_EQ(u"Ze Extension with multiple Context Menus",
            model->GetLabelAt(index++));
}

// Checks that in |menu|, the item at |index| has type |expected_type| and a
// label of |expected_label|.
static void ExpectLabelAndType(const char* expected_label,
                               MenuModel::ItemType expected_type,
                               const MenuModel& menu,
                               size_t index) {
  EXPECT_EQ(expected_type, menu.GetTypeAt(index));
  EXPECT_EQ(base::UTF8ToUTF16(expected_label), menu.GetLabelAt(index));
}

// In the separators test we build a submenu with items and separators in two
// different ways - this is used to verify the results in both cases. Separators
// are not included on OS_CHROMEOS.
static void VerifyMenuForSeparatorsTest(const MenuModel& menu) {
  // We expect to see the following items in the menu:
  //  radio1
  //  radio2
  //  --separator-- (automatically added)
  //  normal1
  //  --separator--
  //  normal2
  //  --separator--
  //  radio3
  //  radio4
  //  --separator--
  //  normal3

  size_t index = 0;
#if BUILDFLAG(IS_CHROMEOS)
  ASSERT_EQ(7u, menu.GetItemCount());
#else
  ASSERT_EQ(11u, menu.GetItemCount());
#endif  // BUILDFLAG(IS_CHROMEOS)
  ExpectLabelAndType("radio1", MenuModel::TYPE_RADIO, menu, index++);
  ExpectLabelAndType("radio2", MenuModel::TYPE_RADIO, menu, index++);
#if !BUILDFLAG(IS_CHROMEOS)
  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
#endif  // !BUILDFLAG(IS_CHROMEOS)
  ExpectLabelAndType("normal1", MenuModel::TYPE_COMMAND, menu, index++);
#if !BUILDFLAG(IS_CHROMEOS)
  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
#endif  // !BUILDFLAG(IS_CHROMEOS)
  ExpectLabelAndType("normal2", MenuModel::TYPE_COMMAND, menu, index++);
#if !BUILDFLAG(IS_CHROMEOS)
  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
#endif  // !BUILDFLAG(IS_CHROMEOS)
  ExpectLabelAndType("radio3", MenuModel::TYPE_RADIO, menu, index++);
  ExpectLabelAndType("radio4", MenuModel::TYPE_RADIO, menu, index++);
#if !BUILDFLAG(IS_CHROMEOS)
  EXPECT_EQ(MenuModel::TYPE_SEPARATOR, menu.GetTypeAt(index++));
#endif  // !BUILDFLAG(IS_CHROMEOS)
  ExpectLabelAndType("normal3", MenuModel::TYPE_COMMAND, menu, index++);
}

// Tests a number of cases for auto-generated and explicitly added separators.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, Separators) {
  // Load the extension.
  ASSERT_TRUE(LoadContextMenuExtension("separators"));
  const Extension* extension = GetExtensionNamed("Separators Test");
  ASSERT_TRUE(extension);

  // Navigate to test1.html inside the extension, which should create a bunch
  // of items at the top-level (but they'll get pushed into an auto-generated
  // parent).
  ExtensionTestMessageListener listener1("test1 create finished");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            GURL(extension->GetResourceURL("test1.html"))));
  EXPECT_TRUE(listener1.WaitUntilSatisfied());

  GURL url("http://www.google.com/");
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), url);

  // The top-level item should be an "automagic parent" with the extension's
  // name.
  std::optional<std::pair<MenuModel*, size_t>> model_and_index =
      menu->GetMenuModelAndItemIndex(
          ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0));
  ASSERT_TRUE(model_and_index);
  MenuModel* model = model_and_index->first;
  size_t index = model_and_index->second;
  EXPECT_EQ(base::UTF8ToUTF16(extension->name()), model->GetLabelAt(index));
  ASSERT_EQ(MenuModel::TYPE_SUBMENU, model->GetTypeAt(index));

  // Get the submenu and verify the items there.
  MenuModel* submenu = model->GetSubmenuModelAt(index);
  ASSERT_TRUE(submenu);
  VerifyMenuForSeparatorsTest(*submenu);
  // Depends on `menu` so must be cleared before it is destroyed below.
  model = nullptr;

  // Now run our second test - navigate to test2.html which creates an explicit
  // parent node and populates that with the same items as in test1.
  ExtensionTestMessageListener listener2("test2 create finished");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(),
                            GURL(extension->GetResourceURL("test2.html"))));
  EXPECT_TRUE(listener2.WaitUntilSatisfied());
  menu = CreateContextMenu(GetActiveWebContents(), url);
  model_and_index = menu->GetMenuModelAndItemIndex(
      ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0));
  ASSERT_TRUE(model_and_index);
  model = model_and_index->first;
  index = model_and_index->second;
  EXPECT_EQ(u"parent", model->GetLabelAt(index));
  submenu = model->GetSubmenuModelAt(index);
  ASSERT_TRUE(submenu);
  VerifyMenuForSeparatorsTest(*submenu);
}

// Tests that targetUrlPattern keeps items from appearing when there is no
// target url.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, TargetURLs) {
  ExtensionTestMessageListener listener("created items");
  ASSERT_TRUE(LoadContextMenuExtension("target_urls"));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  GURL google_url("http://www.google.com");
  GURL non_google_url("http://www.foo.com");

  // No target url - the item should not appear.
  ASSERT_FALSE(
      MenuHasItemWithLabel(google_url, GURL(), false, std::string("item1")));

  // A matching target url - the item should appear.
  ASSERT_TRUE(MenuHasItemWithLabel(google_url, google_url, false,
                                   std::string("item1")));

  // A non-matching target url - the item should not appear.
  ASSERT_FALSE(MenuHasItemWithLabel(google_url, non_google_url, false,
                                    std::string("item1")));
}

// Tests adding of context menus in incognito mode.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, IncognitoSplit) {
  ExtensionTestMessageListener created("created item regular");
  ExtensionTestMessageListener created_incognito("created item incognito");

  ExtensionTestMessageListener onclick("onclick fired regular");
  ExtensionTestMessageListener onclick_incognito("onclick fired incognito");

  // Open an incognito window.
  BrowserWindowInterface* browser_incognito = CreateIncognitoBrowserWindow();
  TabListInterface* incognito_tab_list =
      TabListInterface::From(browser_incognito);
  // Ensure there's an initial tab (some platforms don't make one). See
  // http://crbug.com/477611601.
  if (incognito_tab_list->GetTabCount() == 0) {
    incognito_tab_list->OpenTab(GURL("about:blank"), /*index=*/-1);
  }
  ASSERT_TRUE(LoadContextMenuExtensionWithIncognitoFlags("incognito"));

  // Wait for the extension's processes to tell us they've created an item.
  ASSERT_TRUE(created.WaitUntilSatisfied());
  ASSERT_TRUE(created_incognito.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  // Create and build our test context menu.
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), page_url);
  WebContents* incognito_web_contents =
      incognito_tab_list->GetActiveTab()->GetContents();
  ASSERT_TRUE(incognito_web_contents);
  std::unique_ptr<PlatformContextMenu> menu_incognito =
      CreateContextMenu(incognito_web_contents, page_url);

  // Look for the extension item in the menu, and execute it.
  int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
  ASSERT_TRUE(menu->IsCommandIdEnabled(command_id));
  menu->ExecuteCommand(command_id, 0);

  // Wait for the extension's script to tell us its onclick fired. Ensure
  // that the incognito version doesn't fire until we explicitly click the
  // incognito menu item.
  ASSERT_TRUE(onclick.WaitUntilSatisfied());
  EXPECT_FALSE(onclick_incognito.was_satisfied());

  ASSERT_TRUE(menu_incognito->IsCommandIdEnabled(command_id));
  menu_incognito->ExecuteCommand(command_id, 0);
  ASSERT_TRUE(onclick_incognito.WaitUntilSatisfied());
}

// Tests that items with a context of frames only appear when the menu is
// invoked in a frame.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, Frames) {
  ExtensionTestMessageListener listener("created items");
  ASSERT_TRUE(LoadContextMenuExtension("frames"));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  ASSERT_TRUE(
      MenuHasItemWithLabel(page_url, GURL(), false, std::string("Page item")));
  ASSERT_FALSE(
      MenuHasItemWithLabel(page_url, GURL(), false, std::string("Frame item")));

  ASSERT_TRUE(
      MenuHasItemWithLabel(page_url, GURL(), true, std::string("Page item")));
  ASSERT_TRUE(
      MenuHasItemWithLabel(page_url, GURL(), true, std::string("Frame item")));
}

// Tests that info.frameId is correctly set when the context menu is invoked.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, ClickInFrame) {
  ExtensionTestMessageListener listener("created items");
  ASSERT_TRUE(LoadContextMenuExtension("frames"));
  GURL url_with_frame("data:text/html,<iframe name='child'>");
  ASSERT_TRUE(NavigateToURL(GetActiveWebContents(), url_with_frame));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // Click on a menu item in the main frame.
  EXPECT_EQ(
      "pageUrl=" + url_with_frame.spec() + ", frameUrl=undefined, frameId=0",
      ClickMenuInFrame(GetActiveWebContents()->GetPrimaryMainFrame(), "item1"));

  // Click on a menu item in the child frame.
  content::RenderFrameHost* child_frame = content::FrameMatchingPredicate(
      GetActiveWebContents()->GetPrimaryPage(),
      base::BindRepeating(&content::FrameMatchesName, "child"));
  ASSERT_TRUE(child_frame);
  int extension_api_frame_id = ExtensionApiFrameIdMap::GetFrameId(child_frame);
  EXPECT_EQ(
      base::StringPrintf("pageUrl=undefined, frameUrl=about:blank, frameId=%d",
                         extension_api_frame_id),
      ClickMenuInFrame(child_frame, "item1"));
}

// Tests enabling and disabling a context menu item. The item starts
// enabled.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, StartEnabled) {
  TestEnabledContextMenu(true);
}

// Tests enabling and disabling a context menu item. The item starts
// disabled.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, StartDisabled) {
  TestEnabledContextMenu(false);
}

#if !BUILDFLAG(IS_ANDROID)
// Not relevant on Android, which only supports service worker.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, EventPage) {
  GURL about_blank("about:blank");
  ExtensionHostTestHelper host_helper(profile());
  host_helper.RestrictToType(mojom::ViewType::kExtensionBackgroundPage);
  const Extension* extension = LoadContextMenuExtension("event_page");
  ASSERT_TRUE(extension);
  // Wait for the background page to cycle.
  host_helper.WaitForDocumentElementAvailable();
  host_helper.WaitForHostDestroyed();

  // Test that menu items appear while the page is unloaded.
  ASSERT_TRUE(
      MenuHasItemWithLabel(about_blank, GURL(), false, std::string("Item 1")));
  ASSERT_TRUE(MenuHasItemWithLabel(about_blank, GURL(), false,
                                   std::string("Checkbox 1")));

  // Test that checked menu items retain their checkedness.
  ExtensionHostTestHelper checkbox_checked(profile());
  host_helper.RestrictToType(mojom::ViewType::kExtensionBackgroundPage);
  std::unique_ptr<TestRenderViewContextMenu> menu(
      TestRenderViewContextMenu::Create(GetActiveWebContents(), about_blank));

  MenuItem::Id id(false, MenuItem::ExtensionKey(extension->id()));
  id.string_uid = "checkbox1";
  int command_id = -1;
  ASSERT_TRUE(FindCommandId(menu.get(), id, &command_id));
  EXPECT_FALSE(menu->IsCommandIdChecked(command_id));

  // Executing the checkbox also fires the onClicked event.
  ExtensionTestMessageListener listener("onClicked fired for checkbox1");
  menu->ExecuteCommand(command_id, 0);
  checkbox_checked.WaitForHostDestroyed();

  EXPECT_TRUE(menu->IsCommandIdChecked(command_id));
  ASSERT_TRUE(listener.WaitUntilSatisfied());
}

// Flaky on Mac and Windows. https://crbug.com/40112041
// Not relevant on Android, which only supports service worker.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
#define MAYBE_IncognitoSplitContextMenuCount \
  DISABLED_IncognitoSplitContextMenuCount
#else
#define MAYBE_IncognitoSplitContextMenuCount IncognitoSplitContextMenuCount
#endif
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest,
                       MAYBE_IncognitoSplitContextMenuCount) {
  ExtensionTestMessageListener created("created item regular");
  ExtensionTestMessageListener created_incognito("created item incognito");

  // Create an incognito profile.
  Profile* incognito =
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true);
  ASSERT_TRUE(incognito);
  ASSERT_TRUE(
      LoadContextMenuExtensionWithIncognitoFlags("incognito_event_page"));

  // Wait for the extension's processes to tell us they've created an item.
  ASSERT_TRUE(created.WaitUntilSatisfied());
  ASSERT_TRUE(created_incognito.WaitUntilSatisfied());
  ASSERT_EQ(2u, GetItems().size());

  profile()->DestroyOffTheRecordProfile(incognito);
  ASSERT_EQ(1u, GetItems().size());
}
#endif  // !BUILDFLAG(IS_ANDROID)

// Tests updating checkboxes' checked state to true and false.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuLazyTest, UpdateCheckboxes) {
  ExtensionTestMessageListener listener_context_menu_created("Menu created");
  const Extension* extension = LoadContextMenuExtension("checkboxes");
  ASSERT_TRUE(extension);

  ASSERT_TRUE(listener_context_menu_created.WaitUntilSatisfied());

  GURL page_url("http://www.google.com");

  // Create and build our test context menu.
  std::unique_ptr<PlatformContextMenu> menu =
      CreateContextMenu(GetActiveWebContents(), page_url);

  VerifyRadioItemSelectionState(menu.get(), extension->id(), "checkbox1",
                                false);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "checkbox2", true);

  ExtensionTestMessageListener listener_item1_clicked("onclick normal item");
  ExtensionTestMessageListener listener_unchecked_checkbox2(
      "checkbox2 unchecked");
  // Clicking the regular item calls chrome.contextMenus.update to uncheck the
  // second checkbox item.
  ExecuteCommand(menu.get(), extension->id(), "item1");
  ASSERT_TRUE(listener_item1_clicked.WaitUntilSatisfied());
  ASSERT_TRUE(listener_unchecked_checkbox2.WaitUntilSatisfied());

  VerifyRadioItemSelectionState(menu.get(), extension->id(), "checkbox1",
                                false);
  VerifyRadioItemSelectionState(menu.get(), extension->id(), "checkbox2",
                                false);
}

#if BUILDFLAG(IS_CHROMEOS)
// Extension context menu tests with and without locked fullscreen when locked
// and not locked for OnTask. Only relevant for non-web browser scenarios.
class ExtensionContextMenuLockedFullscreenTest
    : public ExtensionContextMenuBrowserTest,
      public testing::WithParamInterface<std::tuple<bool, bool>> {
 protected:
  bool IsLockedFullscreen() { return std::get<0>(GetParam()); }
  bool IsLockedForOnTask() { return std::get<1>(GetParam()); }
};

IN_PROC_BROWSER_TEST_P(ExtensionContextMenuLockedFullscreenTest,
                       VerifyItemStateForOnTask) {
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      IsLockedForOnTask());
  if (IsLockedFullscreen()) {
    ash::PinWindow(browser()->GetWindow()->GetNativeWindow(), /*trusted=*/true);
  }

  // Load test extension and wait for js test code to create context menu with
  // one item.
  ExtensionTestMessageListener listener("created context menu");
  base::FilePath extension_dir = GetRootDir().AppendASCII("locked_fullscreen");
  ASSERT_TRUE(
      LoadExtension(extension_dir, {.wait_for_registration_stored = true}));
  ASSERT_TRUE(listener.WaitUntilSatisfied());

  // Create / build the context menu and verify item state is enabled if the
  // instance is locked for OnTask or if the instance is not in locked
  // fullscreen mode.
  const GURL page_url("http://www.google.com");
  const std::unique_ptr<TestRenderViewContextMenu> menu(
      TestRenderViewContextMenu::Create(GetActiveWebContents(), page_url));
  int command_id = ContextMenuMatcher::ConvertToExtensionsCustomCommandId(0);
  bool expect_command_enabled = IsLockedForOnTask() || !IsLockedFullscreen();
  ASSERT_EQ(expect_command_enabled, menu->IsCommandIdEnabled(command_id));
}

INSTANTIATE_TEST_SUITE_P(ExtensionContextMenuLockedFullscreenTests,
                         ExtensionContextMenuLockedFullscreenTest,
                         ::testing::Combine(
                             /*IsLockedFullscreen=*/::testing::Bool(),
                             /*IsLockedForOnTask=*/::testing::Bool()));
#endif


}  // namespace extensions
