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

#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/ui/views/menu_test_base.h"
#include "ui/base/themed_vector_icon.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"

using base::ASCIIToUTF16;

// Simple test for clicking a menu item.  This template class clicks on an
// item and checks that the returned id matches.  The index of the item
// is the template argument.
template <int INDEX>
class MenuItemViewTestBasic : public MenuTestBase {
 public:
  MenuItemViewTestBasic() = default;
  MenuItemViewTestBasic(const MenuItemViewTestBasic&) = delete;
  MenuItemViewTestBasic& operator=(const MenuItemViewTestBasic&) = delete;
  ~MenuItemViewTestBasic() override = default;

  // MenuTestBase implementation
  void BuildMenu(views::MenuItemView* menu) override {
    menu->AppendMenuItem(1, u"item 1");
    menu->AppendMenuItem(2, u"item 2");
    menu->AppendSeparator();
    menu->AppendMenuItem(3, u"item 3");
  }

  // Click on item INDEX.
  void DoTestWithMenuOpen() override {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_TRUE(submenu->IsShowing());
    ASSERT_EQ(3u, submenu->GetMenuItems().size());

    // click an item and pass control to the next step
    views::MenuItemView* item = submenu->GetMenuItemAt(INDEX);
    ASSERT_TRUE(item);
    Click(item, CreateEventTask(this, &MenuItemViewTestBasic::Step2));
  }

  // Check the clicked item and complete the test.
  void Step2() {
    ASSERT_FALSE(menu()->GetSubmenu()->IsShowing());
    ASSERT_EQ(INDEX + 1, last_command());
    Done();
  }
};

// Click each item of a 3-item menu (with separator).
using MenuItemViewTestBasic0 = MenuItemViewTestBasic<0>;
using MenuItemViewTestBasic1 = MenuItemViewTestBasic<1>;
using MenuItemViewTestBasic2 = MenuItemViewTestBasic<2>;
// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestBasic0, SelectItem0)
VIEW_TEST(MenuItemViewTestBasic1, SelectItem1)
VIEW_TEST(MenuItemViewTestBasic2, SelectItem2)

// Test class for inserting a menu item while the menu is open.
template <int INSERT_INDEX, int SELECT_INDEX>
class MenuItemViewTestInsert : public MenuTestBase {
 public:
  MenuItemViewTestInsert() = default;
  MenuItemViewTestInsert(const MenuItemViewTestInsert&) = delete;
  MenuItemViewTestInsert& operator=(const MenuItemViewTestInsert&) = delete;
  ~MenuItemViewTestInsert() override = default;

  // MenuTestBase implementation
  void BuildMenu(views::MenuItemView* menu) override {
    menu->AppendMenuItem(1, u"item 1");
    menu->AppendMenuItem(2, u"item 2");
  }

  // Insert item at INSERT_INDEX and click item at SELECT_INDEX.
  void DoTestWithMenuOpen() override {
    LOG(ERROR) << "\nDoTestWithMenuOpen\n";
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_TRUE(submenu->IsShowing());
    ASSERT_EQ(2u, submenu->GetMenuItems().size());

    inserted_item_ = menu()->AddMenuItemAt(
        INSERT_INDEX, 1000, u"inserted item", std::u16string(),
        std::u16string(), ui::ImageModel(), ui::ImageModel(),
        views::MenuItemView::Type::kNormal, ui::NORMAL_SEPARATOR);
    ASSERT_TRUE(inserted_item_);
    menu()->ChildrenChanged();

    // click an item and pass control to the next step
    views::MenuItemView* item = submenu->GetMenuItemAt(SELECT_INDEX);
    ASSERT_TRUE(item);
    Click(item, CreateEventTask(this, &MenuItemViewTestInsert::Step2));
  }

  // Check clicked item and complete test.
  void Step2() {
    LOG(ERROR) << "\nStep2\n";
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_FALSE(submenu->IsShowing());
    ASSERT_EQ(3u, submenu->GetMenuItems().size());

    if (SELECT_INDEX == INSERT_INDEX) {
      ASSERT_EQ(1000, last_command());
    } else if (SELECT_INDEX < INSERT_INDEX) {
      ASSERT_EQ(SELECT_INDEX + 1, last_command());
    } else {
      ASSERT_EQ(SELECT_INDEX, last_command());
    }

    LOG(ERROR) << "\nDone\n";
    Done();
  }

 private:
  raw_ptr<views::MenuItemView, AcrossTasksDanglingUntriaged> inserted_item_ =
      nullptr;
};

// MenuItemViewTestInsertXY inserts an item at index X and selects the
// item at index Y (after the insertion).  The tests here cover
// inserting at the beginning, middle, and end, crossbarred with
// selecting the first and last item.
using MenuItemViewTestInsert00 = MenuItemViewTestInsert<0, 0>;
using MenuItemViewTestInsert02 = MenuItemViewTestInsert<0, 2>;
using MenuItemViewTestInsert10 = MenuItemViewTestInsert<1, 0>;
using MenuItemViewTestInsert12 = MenuItemViewTestInsert<1, 2>;
using MenuItemViewTestInsert20 = MenuItemViewTestInsert<2, 0>;
using MenuItemViewTestInsert22 = MenuItemViewTestInsert<2, 2>;

// If this flakes, disable and log details in http://crbug.com/40432443.
#if defined(MEMORY_SANITIZER)
#define MAYBE_InsertItem00 DISABLED_InsertItem00
#else
#define MAYBE_InsertItem00 InsertItem00
#endif
VIEW_TEST(MenuItemViewTestInsert00, MAYBE_InsertItem00)

// TODO(b/523255): Test is failing consistently on "Linux Tests (Wayland)".
// If this flakes, disable and log details in http://crbug.com/40432443.
// #if defined(MEMORY_SANITIZER)
// #define MAYBE_InsertItem02 DISABLED_InsertItem02
// #else
// #define MAYBE_InsertItem02 InsertItem02
// #endif
VIEW_TEST(MenuItemViewTestInsert02, DISABLED_InsertItem02)

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestInsert10, InsertItem10)

// If this flakes, disable and log details in http://crbug.com/40432443.
#if defined(MEMORY_SANITIZER)
#define MAYBE_InsertItem12 DISABLED_InsertItem12
#else
#define MAYBE_InsertItem12 InsertItem12
#endif
VIEW_TEST(MenuItemViewTestInsert12, MAYBE_InsertItem12)

// If this flakes, disable and log details in http://crbug.com/40432443.
#if defined(MEMORY_SANITIZER)
#define MAYBE_InsertItem20 DISABLED_InsertItem20
#else
#define MAYBE_InsertItem20 InsertItem20
#endif
VIEW_TEST(MenuItemViewTestInsert20, MAYBE_InsertItem20)

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestInsert22, InsertItem22)

// Test class for inserting a menu item while a submenu is open.
template <int INSERT_INDEX>
class MenuItemViewTestInsertWithSubmenu : public MenuTestBase {
 public:
  MenuItemViewTestInsertWithSubmenu() = default;
  MenuItemViewTestInsertWithSubmenu(const MenuItemViewTestInsertWithSubmenu&) =
      delete;
  MenuItemViewTestInsertWithSubmenu& operator=(
      const MenuItemViewTestInsertWithSubmenu&) = delete;
  ~MenuItemViewTestInsertWithSubmenu() override = default;

  // MenuTestBase implementation
  void BuildMenu(views::MenuItemView* menu) override {
    submenu_ = menu->AppendSubMenu(1, u"My Submenu");
    submenu_->AppendMenuItem(101, u"submenu item 1");
    submenu_->AppendMenuItem(101, u"submenu item 2");
    menu->AppendMenuItem(2, u"item 2");
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuStart), 0);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupStart), 0);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupEnd), 0);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuEnd), 0);
  }

  // Post submenu.
  void DoTestWithMenuOpen() override {
    Click(submenu_,
          CreateEventTask(this, &MenuItemViewTestInsertWithSubmenu::Step2));
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuStart), 1);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupStart), 1);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupEnd), 0);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuEnd), 0);
  }

  // Insert item at INSERT_INDEX.
  void Step2() {
    inserted_item_ = menu()->AddMenuItemAt(
        INSERT_INDEX, 1000, u"inserted item", std::u16string(),
        std::u16string(), ui::ImageModel(), ui::ImageModel(),
        views::MenuItemView::Type::kNormal, ui::NORMAL_SEPARATOR);
    ASSERT_TRUE(inserted_item_);
    menu()->ChildrenChanged();

    Click(inserted_item_,
          CreateEventTask(this, &MenuItemViewTestInsertWithSubmenu::Step3));
  }

  void Step3() {
    Done();
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuStart), 1);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupStart), 2);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuPopupEnd), 2);
    EXPECT_EQ(GetAXEventCount(ax::mojom::Event::kMenuEnd), 1);
  }

 private:
  raw_ptr<views::MenuItemView, AcrossTasksDanglingUntriaged> submenu_ = nullptr;
  raw_ptr<views::MenuItemView, AcrossTasksDanglingUntriaged> inserted_item_ =
      nullptr;
};

// MenuItemViewTestInsertWithSubmenuX posts a menu and its submenu,
// then inserts an item in the top-level menu at X.
using MenuItemViewTestInsertWithSubmenu0 = MenuItemViewTestInsertWithSubmenu<0>;
using MenuItemViewTestInsertWithSubmenu1 = MenuItemViewTestInsertWithSubmenu<1>;

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestInsertWithSubmenu0, InsertItemWithSubmenu0)

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestInsertWithSubmenu1, InsertItemWithSubmenu1)

// Test class for removing a menu item while the menu is open.
template <int REMOVE_INDEX, int SELECT_INDEX>
class MenuItemViewTestRemove : public MenuTestBase {
 public:
  MenuItemViewTestRemove() = default;
  MenuItemViewTestRemove(const MenuItemViewTestRemove&) = delete;
  MenuItemViewTestRemove& operator=(const MenuItemViewTestRemove&) = delete;
  ~MenuItemViewTestRemove() override = default;

  // MenuTestBase implementation
  void BuildMenu(views::MenuItemView* menu) override {
    menu->AppendMenuItem(1, u"item 1");
    menu->AppendMenuItem(2, u"item 2");
    menu->AppendMenuItem(3, u"item 3");
  }

  // Remove item at REMOVE_INDEX and click item at SELECT_INDEX.
  void DoTestWithMenuOpen() override {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_TRUE(submenu->IsShowing());
    ASSERT_EQ(3u, submenu->GetMenuItems().size());

    // remove
    menu()->RemoveMenuItem(submenu->GetMenuItemAt(REMOVE_INDEX));
    menu()->ChildrenChanged();

    // click
    views::MenuItemView* item = submenu->GetMenuItemAt(SELECT_INDEX);
    ASSERT_TRUE(item);
    Click(item, CreateEventTask(this, &MenuItemViewTestRemove::Step2));
  }

  // Check clicked item and complete test.
  void Step2() {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_FALSE(submenu->IsShowing());
    ASSERT_EQ(2u, submenu->GetMenuItems().size());

    if (SELECT_INDEX < REMOVE_INDEX) {
      ASSERT_EQ(SELECT_INDEX + 1, last_command());
    } else {
      ASSERT_EQ(SELECT_INDEX + 2, last_command());
    }

    Done();
  }
};

using MenuItemViewTestRemove00 = MenuItemViewTestRemove<0, 0>;
using MenuItemViewTestRemove01 = MenuItemViewTestRemove<0, 1>;
using MenuItemViewTestRemove10 = MenuItemViewTestRemove<1, 0>;
using MenuItemViewTestRemove11 = MenuItemViewTestRemove<1, 1>;
using MenuItemViewTestRemove20 = MenuItemViewTestRemove<2, 0>;
using MenuItemViewTestRemove21 = MenuItemViewTestRemove<2, 1>;
// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestRemove00, RemoveItem00)

// If this flakes, disable and log details in http://crbug.com/40432443.
// Super flaky on Wayland.
#if BUILDFLAG(IS_OZONE)
#define MAYBE_RemoveItem01 DISABLED_RemoveItem01
#else
#define MAYBE_RemoveItem01 RemoveItem01
#endif
VIEW_TEST(MenuItemViewTestRemove01, MAYBE_RemoveItem01)

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestRemove10, RemoveItem10)

// If this flakes, disable and log details in http://crbug.com/40432443.
// Flaky on Wayland.
#if BUILDFLAG(IS_OZONE)
#define MAYBE_RemoveItem11 DISABLED_RemoveItem11
#else
#define MAYBE_RemoveItem11 RemoveItem11
#endif
VIEW_TEST(MenuItemViewTestRemove11, MAYBE_RemoveItem11)

// If this flakes, disable and log details in http://crbug.com/40432443.
VIEW_TEST(MenuItemViewTestRemove20, RemoveItem20)

// If this flakes, disable and log details in http://crbug.com/40432443.
// Flaky on Wayland.
#if BUILDFLAG(IS_OZONE) || BUILDFLAG(IS_LINUX)
#define MAYBE_RemoveItem21 DISABLED_RemoveItem21
#else
#define MAYBE_RemoveItem21 RemoveItem21
#endif
VIEW_TEST(MenuItemViewTestRemove21, MAYBE_RemoveItem21)

// Test class for removing a menu item while a submenu is open.
template <int REMOVE_INDEX>
class MenuItemViewTestRemoveWithSubmenu : public MenuTestBase {
 public:
  MenuItemViewTestRemoveWithSubmenu() = default;
  MenuItemViewTestRemoveWithSubmenu(const MenuItemViewTestRemoveWithSubmenu&) =
      delete;
  MenuItemViewTestRemoveWithSubmenu& operator=(
      const MenuItemViewTestRemoveWithSubmenu&) = delete;
  ~MenuItemViewTestRemoveWithSubmenu() override = default;

  // MenuTestBase implementation
  void BuildMenu(views::MenuItemView* menu) override {
    menu->AppendMenuItem(1, u"item 1");
    submenu_ = menu->AppendSubMenu(2, u"My Submenu");
    submenu_->AppendMenuItem(101, u"submenu item 1");
    submenu_->AppendMenuItem(102, u"submenu item 2");
  }

  // Post submenu.
  void DoTestWithMenuOpen() override {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_TRUE(submenu->IsShowing());

    Click(submenu_,
          CreateEventTask(this, &MenuItemViewTestRemoveWithSubmenu::Step2));
  }

  // Remove item at REMOVE_INDEX and press escape to exit the menu loop.
  void Step2() {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_TRUE(submenu->IsShowing());
    ASSERT_EQ(2u, submenu->GetMenuItems().size());

    // remove
    menu()->RemoveMenuItem(submenu->GetMenuItemAt(REMOVE_INDEX));
    menu()->ChildrenChanged();

    // click
    KeyPress(ui::VKEY_ESCAPE,
             CreateEventTask(this, &MenuItemViewTestRemoveWithSubmenu::Step3));
  }

  void Step3() {
    views::SubmenuView* submenu = menu()->GetSubmenu();
    ASSERT_TRUE(submenu);
    ASSERT_FALSE(submenu->IsShowing());
    ASSERT_EQ(1u, submenu->GetMenuItems().size());

    Done();
  }

 private:
  raw_ptr<views::MenuItemView, AcrossTasksDanglingUntriaged> submenu_ = nullptr;
};

using MenuItemViewTestRemoveWithSubmenu0 = MenuItemViewTestRemoveWithSubmenu<0>;
using MenuItemViewTestRemoveWithSubmenu1 = MenuItemViewTestRemoveWithSubmenu<1>;

// If this flakes, disable and log details in http://crbug.com/40432443.
// Flaky on Wayland.
#if BUILDFLAG(IS_OZONE)
#define MAYBE_RemoveItemWithSubmenu0 DISABLED_RemoveItemWithSubmenu0
#else
#define MAYBE_RemoveItemWithSubmenu0 RemoveItemWithSubmenu0
#endif
VIEW_TEST(MenuItemViewTestRemoveWithSubmenu0, MAYBE_RemoveItemWithSubmenu0)

// If this flakes, disable and log details in http://crbug.com/40432443.
// TODO(crbug.com/40244484): Flaky on Wayland.
#if BUILDFLAG(IS_OZONE)
#define MAYBE_RemoveItemWithSubmenu1 DISABLED_RemoveItemWithSubmenu1
#else
#define MAYBE_RemoveItemWithSubmenu1 RemoveItemWithSubmenu1
#endif
VIEW_TEST(MenuItemViewTestRemoveWithSubmenu1, MAYBE_RemoveItemWithSubmenu1)
