// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/strings/utf_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/menu_manager.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/extensions/api/context_menus.h"
#include "chrome/test/base/ui_test_utils.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/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "extensions/browser/extension_action.h"
#include "extensions/test/result_catcher.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/base/models/menu_model.h"

namespace extensions {

class ExtensionContextMenuApiTest : public ExtensionApiTest {
 public:
  ExtensionContextMenuApiTest() = default;
};

IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ServiceWorkerContextMenus) {
  ASSERT_TRUE(RunExtensionTest({.name = "context_menus/event_page"},
                               {.load_as_service_worker = true}))
      << message_;
}

// crbug.com/51436 -- creating context menus from multiple script contexts
// should work.
IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContextMenusFromMultipleContexts) {
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(RunExtensionTest("context_menus/add_from_multiple_contexts"))
      << message_;
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension) << message_;

  {
    // Tell the extension to update the page action state.
    ResultCatcher catcher;
    ui_test_utils::NavigateToURL(browser(),
                                 extension->GetResourceURL("popup.html"));
    ASSERT_TRUE(catcher.GetNextResult());
  }

  {
    // Tell the extension to update the page action state again.
    ResultCatcher catcher;
    ui_test_utils::NavigateToURL(browser(),
                                 extension->GetResourceURL("popup2.html"));
    ASSERT_TRUE(catcher.GetNextResult());
  }
}

class ExtensionContextMenuVisibilityApiTest
    : public ExtensionContextMenuApiTest {
 public:
  ExtensionContextMenuVisibilityApiTest()
      : top_level_model_(nullptr), menu_(nullptr), top_level_index_(-1) {}

  void SetUpTestExtension() {
    extension_ = LoadExtension(
        test_data_dir_.AppendASCII("context_menus/item_visibility/"));
  }

  // Sets up the top-level model, which is the list of menu items (both related
  // and unrelated to extensions) that is passed to UI code to be displayed.
  bool SetupTopLevelMenuModel() {
    content::RenderFrameHost* frame =
        browser()->tab_strip_model()->GetActiveWebContents()->GetMainFrame();
    content::ContextMenuParams params;
    params.page_url = frame->GetLastCommittedURL();

    // Create context menu.
    menu_.reset(new TestRenderViewContextMenu(frame, params));
    menu_->Init();

    // Get menu model.
    bool valid_setup = menu_->GetMenuModelAndItemIndex(
        menu_->extension_items().ConvertToExtensionsCustomCommandId(0),
        &top_level_model_, &top_level_index_);

    EXPECT_GT(top_level_index(), 0);

    return valid_setup;
  }

  void CallAPI(const std::string& script) { CallAPI(extension_, script); }

  void CallAPI(const Extension* extension, const std::string& script) {
    content::WebContents* background_page = GetBackgroundPage(extension->id());
    bool error = false;
    ASSERT_TRUE(
        content::ExecuteScriptAndExtractBool(background_page, script, &error));
    ASSERT_FALSE(error);
  }

  // Verifies that the UI menu model has the given number of extension menu
  // items, |num_items|, of a menu model |type|.
  void VerifyNumExtensionItemsInMenuModel(int num_items,
                                          ui::MenuModel::ItemType type) {
    int num_found = 0;
    for (int i = 0; i < top_level_model_->GetItemCount(); i++) {
      int command_id = top_level_model_->GetCommandIdAt(i);
      if (ContextMenuMatcher::IsExtensionsCustomCommandId(command_id) &&
          top_level_model_->GetTypeAt(i) == type) {
        num_found++;
      }
    }
    ASSERT_TRUE(num_found == num_items);
  }

  // Verifies that the context menu is valid and contains the given number of
  // menu items, |num_items|.
  void VerifyNumContextMenuItems(size_t num_items) {
    ASSERT_TRUE(menu());
    EXPECT_EQ(num_items,
              (menu_->extension_items().extension_item_map().size()));
  }

  // Verifies a context menu item's visibility, title, and item type.
  void VerifyMenuItem(const std::string& title,
                      ui::MenuModel* model,
                      int index,
                      ui::MenuModel::ItemType type,
                      bool visible) {
    EXPECT_EQ(base::ASCIIToUTF16(title), model->GetLabelAt(index));
    ASSERT_EQ(type, model->GetTypeAt(index));
    EXPECT_EQ(visible, model->IsVisibleAt(index));
  }

  int top_level_index() { return top_level_index_; }

  TestRenderViewContextMenu* menu() { return menu_.get(); }

  const Extension* extension() { return extension_; }

  ui::MenuModel* top_level_model_;

 private:
  content::WebContents* GetBackgroundPage(const std::string& extension_id) {
    return process_manager()
        ->GetBackgroundHostForExtension(extension_id)
        ->host_contents();
  }

  ProcessManager* process_manager() { return ProcessManager::Get(profile()); }

  const Extension* extension_;
  std::unique_ptr<TestRenderViewContextMenu> menu_;
  int top_level_index_;

  DISALLOW_COPY_AND_ASSIGN(ExtensionContextMenuVisibilityApiTest);
};

IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ContextMenusBasics) {
  ASSERT_TRUE(RunExtensionTest("context_menus/basics")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ContextMenusNoPerms) {
  ASSERT_TRUE(RunExtensionTest("context_menus/no_perms")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ContextMenusMultipleIds) {
  ASSERT_TRUE(RunExtensionTest("context_menus/item_ids")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ContextMenusEventPage) {
  ASSERT_TRUE(RunExtensionTest("context_menus/event_page")) << message_;
}

// Tests showing a single visible menu item in the top-level menu model, which
// includes actions like "Back", "View Page Source", "Inspect", etc.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ShowOneTopLevelItem) {
  SetUpTestExtension();
  CallAPI("create({title: 'item', visible: true});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(1);

  VerifyMenuItem("item", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_COMMAND, true);

  // There should be no submenu model.
  EXPECT_FALSE(top_level_model_->GetSubmenuModelAt(top_level_index()));
}

// Tests hiding a menu item in the top-level menu model, which includes actions
// like "Back", "View Page Source", "Inspect", etc.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       HideTopLevelItem) {
  SetUpTestExtension();
  CallAPI("create({id: 'item1', title: 'item', visible: true});");
  CallAPI("update('item1', {visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(1);

  VerifyMenuItem("item", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_COMMAND, false);

  // There should be no submenu model.
  EXPECT_FALSE(top_level_model_->GetSubmenuModelAt(top_level_index()));
}

// Tests hiding a parent menu item, when it is hidden and so are all of its
// children.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       HideTopLevelSubmenuItemIfHiddenAndChildrenHidden) {
  SetUpTestExtension();
  CallAPI("create({id: 'id', title: 'parent', visible: false});");
  CallAPI("create({title: 'child1', parentId: 'id', visible: false});");
  CallAPI("create({title: 'child2', parentId: 'id', visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(3);

  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, false);

  // Since the extension submenu is hidden, the previous separator should not be
  // in the model.
  EXPECT_NE(ui::MenuModel::TYPE_SEPARATOR,
            top_level_model_->GetTypeAt(top_level_index() - 1));

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(2, submodel->GetItemCount());

  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("child2", submodel, 1, ui::MenuModel::TYPE_COMMAND, false);
}

// Tests hiding a parent menu item, when it is hidden and some of its children
// are visible.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       HideTopLevelSubmenuItemIfHiddenAndSomeChildrenVisible) {
  SetUpTestExtension();
  CallAPI("create({id: 'id', title: 'parent', visible: false});");
  CallAPI("create({title: 'child1', parentId: 'id', visible: false});");
  CallAPI("create({title: 'child2', parentId: 'id', visible: true});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(3);

  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, false);

  // Since the extension submenu is hidden, the previous separator should not be
  // in the model.
  EXPECT_NE(ui::MenuModel::TYPE_SEPARATOR,
            top_level_model_->GetTypeAt(top_level_index() - 1));

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(2, submodel->GetItemCount());

  // Though the children's internal visibility state remains unchanged, the ui
  // code will hide the children if the parent is hidden.
  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("child2", submodel, 1, ui::MenuModel::TYPE_COMMAND, true);
}

// Tests showing a single top-level parent menu item, when it is visible, but
// all of its child items are hidden. The child items' hidden states are tested
// too. Recall that a top-level item can be either a parent item specified by
// the developer or parent item labeled with the extension's name. In this case,
// we test the former.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ShowTopLevelItemIfAllItsChildrenAreHidden) {
  SetUpTestExtension();
  CallAPI("create({id: 'id', title: 'parent', visible: true});");
  CallAPI("create({title: 'child1', parentId: 'id', visible: false});");
  CallAPI("create({title: 'child2', parentId: 'id', visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(3);

  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(2, submodel->GetItemCount());

  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("child2", submodel, 1, ui::MenuModel::TYPE_COMMAND, false);
}

// Tests showing a top-level parent menu item as a submenu, when some of its
// child items are visibile. The child items' visibilities are tested too.
// Recall that a top-level item can be either a parent item specified by the
// developer or parent item labeled with the extension's name. In this case, we
// test the former.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ShowTopLevelSubmenuItemIfSomeOfChildrenAreVisible) {
  SetUpTestExtension();
  CallAPI("create({id: 'id', title: 'parent', visible: true});");
  CallAPI("create({title: 'child1', parentId: 'id', visible: true});");
  CallAPI("create({title: 'child2', parentId: 'id', visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(3);

  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(2, submodel->GetItemCount());

  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, true);
  VerifyMenuItem("child2", submodel, 1, ui::MenuModel::TYPE_COMMAND, false);
}

// Tests showing a single top-level parent menu item, when it is visible and has
// a visible submenu, but submenu has child items where all of submenu's child
// items are hidden. Recall that a top-level item can be either a parent item
// specified by the developer or parent item labeled with the extension's name.
// In this case, we test the former.
IN_PROC_BROWSER_TEST_F(
    ExtensionContextMenuVisibilityApiTest,
    ShowTopLevelItemWithASubmenuWhereAllSubmenusChildrenAreHidden) {
  SetUpTestExtension();

  CallAPI("create({id: 'parent', title: 'parent', visible: true});");
  CallAPI(
      "create({id: 'child1', title: 'child1', parentId: 'parent', visible: "
      "true});");
  CallAPI("create({title: 'child2', parentId: 'child1', visible: false});");
  CallAPI("create({title: 'child3', parentId: 'child1', visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());
  VerifyNumContextMenuItems(4);

  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(1, submodel->GetItemCount());

  // When a parent item is specified by the developer (as opposed to generated),
  // its visibility is determined by the specified state.
  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_SUBMENU, true);

  submodel = submodel->GetSubmenuModelAt(0);
  ASSERT_TRUE(submodel);
  EXPECT_EQ(2, submodel->GetItemCount());

  VerifyMenuItem("child2", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("child3", submodel, 1, ui::MenuModel::TYPE_COMMAND, false);
}

// Tests hiding a top-level parent menu item, when all of its child items are
// hidden. Recall that a top-level item can be either a parent item specified by
// the developer or parent item labeled with the extension's name. In this case,
// we test the latter. This extension-named top-level item should be hidden,
// when all of its child items are hidden.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       HideExtensionNamedTopLevelItemIfAllChildrenAreHidden) {
  SetUpTestExtension();
  CallAPI("create({title: 'item1', visible: false});");
  CallAPI("create({title: 'item2', visible: false});");
  CallAPI("create({title: 'item3', visible: false});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(3);

  VerifyMenuItem(extension()->name(), top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, false);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(3, submodel->GetItemCount());

  VerifyMenuItem("item1", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("item2", submodel, 1, ui::MenuModel::TYPE_COMMAND, false);
  VerifyMenuItem("item3", submodel, 2, ui::MenuModel::TYPE_COMMAND, false);
}

// Tests updating a top-level parent menu item, when the submenu item is not
// visible first and is then updated to visible. Recall that a top-level item
// can be either a parent item specified by the developer or parent item labeled
// with the extension's name. In this case, we test the former.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       UpdateTopLevelItem) {
  SetUpTestExtension();

  CallAPI("create({id: 'parent', title: 'parent', visible: true});");
  CallAPI(
      "create({id: 'child1', title: 'child1', parentId: 'parent', visible: "
      "false});");

  // Verify that the child item is hidden.
  ASSERT_TRUE(SetupTopLevelMenuModel());
  VerifyNumContextMenuItems(2);
  VerifyMenuItem("parent", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(1, submodel->GetItemCount());
  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, false);

  // Update child1 to visible.
  CallAPI("update('child1', {visible: true});");

  // Verify that the child item is visible.
  VerifyMenuItem("child1", submodel, 0, ui::MenuModel::TYPE_COMMAND, true);
}

// Tests updating a top-level parent menu item, when the menu item is not
// visible first and is then updated to visible. Recall that a top-level item
// can be either a parent item specified by the developer or parent item labeled
// with the extension's name. In this case, we test the latter.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       UpdateExtensionNamedTopLevelItem) {
  SetUpTestExtension();
  CallAPI("create({id: 'item1', title: 'item1', visible: false});");
  CallAPI("update('item1', {visible: true});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(1);
  VerifyMenuItem("item1", top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_COMMAND, true);
}

// Tests showing a top-level parent menu item, when some of its child items are
// visible. The child items' visibilities are tested as well. Recall that a
// top-level item can be either a parent item specified by the developer or
// parent item labeled with the extension's name. In this case, we test the
// latter.
//
// Also, this tests that hiding a parent item should hide its children even if
// they are set as visible.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ShowExtensionNamedTopLevelItemIfSomeChildrenAreVisible) {
  SetUpTestExtension();
  CallAPI("create({title: 'item1'});");
  CallAPI("create({title: 'item2'});");
  CallAPI("create({title: 'item3', id: 'item3', visible: false});");
  CallAPI("create({title: 'child1', visible: true, parentId: 'item3'});");
  CallAPI("create({title: 'child2', visible: true, parentId: 'item3'});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumContextMenuItems(5);

  VerifyMenuItem(extension()->name(), top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);

  ui::MenuModel* submodel =
      top_level_model_->GetSubmenuModelAt(top_level_index());
  ASSERT_TRUE(submodel);
  EXPECT_EQ(3, submodel->GetItemCount());

  VerifyMenuItem("item1", submodel, 0, ui::MenuModel::TYPE_COMMAND, true);
  VerifyMenuItem("item2", submodel, 1, ui::MenuModel::TYPE_COMMAND, true);
  VerifyMenuItem("item3", submodel, 2, ui::MenuModel::TYPE_SUBMENU, false);

  ui::MenuModel* item3_submodel = submodel->GetSubmenuModelAt(2);
  ASSERT_TRUE(item3_submodel);
  EXPECT_EQ(2, item3_submodel->GetItemCount());

  // Though the children's internal visibility state remains unchanged, the ui
  // code will hide the children if the parent is hidden.
  VerifyMenuItem("child1", item3_submodel, 0, ui::MenuModel::TYPE_COMMAND,
                 true);
  VerifyMenuItem("child2", item3_submodel, 1, ui::MenuModel::TYPE_COMMAND,
                 true);
}

// Tests that more than one extension named top-level parent menu item can be
// displayed in the context menu.
IN_PROC_BROWSER_TEST_F(ExtensionContextMenuVisibilityApiTest,
                       ShowMultipleExtensionNamedTopLevelItemsWithChidlren) {
  const Extension* e1 =
      LoadExtension(test_data_dir_.AppendASCII("context_menus/simple/one"));
  const Extension* e2 =
      LoadExtension(test_data_dir_.AppendASCII("context_menus/simple/two"));

  CallAPI(e1, "create({title: 'item1'});");
  CallAPI(e1, "create({title: 'item2'});");
  CallAPI(e2, "create({title: 'item1'});");
  CallAPI(e2, "create({title: 'item2'});");

  ASSERT_TRUE(SetupTopLevelMenuModel());

  VerifyNumExtensionItemsInMenuModel(2, ui::MenuModel::TYPE_SUBMENU);

  // The UI menu model organizes extension menu items alphabetically by
  // extension name, regardless of installation order. For example, if an
  // extension named "aaa" was installed after extension "bbb", then extension
  // "aaa" item would precede "bbb" in the context menu.
  VerifyMenuItem(e1->name(), top_level_model_, top_level_index(),
                 ui::MenuModel::TYPE_SUBMENU, true);
  VerifyMenuItem(e2->name(), top_level_model_, top_level_index() + 1,
                 ui::MenuModel::TYPE_SUBMENU, true);
}

}  // namespace extensions
