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

#include "ui/actions/actions.h"

#include <optional>

#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/test/metrics/histogram_tester.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/class_property.h"

namespace actions {

enum class ContextValues {
  kContextNone,
  kContextKeyboard,
  kContextMouse,
  kContextTouch,
};

namespace {

const std::u16string kActionText = u"Test Action";
const std::u16string kChild1Text = u"Child Action 1";
const std::u16string kChild2Text = u"Child Action 2";
const std::u16string kActionAccessibleText = u"Accessible Action Text";
const std::u16string kActionTooltipText = u"Tooltip text";

#define TEST_ACTION_IDS                              \
  E(kActionTest1, , kActionTestStart, TestActionIds) \
  E(kActionTest2)                                    \
  E(kActionTest3)                                    \
  E(kActionTest4)

#include "ui/actions/action_id_macros.inc"

// clang-format off
enum TestActionIds : ActionId {
  kActionTestStart = kActionsEnd,

  TEST_ACTION_IDS

  kActionTestEnd,
};
// clang-format on

#include "ui/actions/action_id_macros.inc"

DEFINE_UI_CLASS_PROPERTY_KEY(ContextValues,
                             kContextValueKey,
                             ContextValues::kContextNone)

class ActionManagerTest : public testing::Test {
 public:
  ActionManagerTest() { ActionManager::ResetForTesting(); }
  ActionManagerTest(const ActionManagerTest&) = delete;
  ActionManagerTest& operator=(const ActionManagerTest&) = delete;
  ~ActionManagerTest() override { ActionManager::ResetForTesting(); }

 protected:
  void InitializeActions(ActionManager* manager) {
    // clang-format off
    manager->AddAction(ActionItem::Builder()
        .SetText(kActionText)
        .SetActionId(kActionTest1)
        .SetVisible(true)
        .SetEnabled(false)
        .AddChildren(
            ActionItem::Builder()
                .SetActionId(kActionTest2)
                .SetText(kChild1Text),
            ActionItem::Builder()
                .SetActionId(kActionTest3)
                .SetText(kChild2Text)).Build());
    // clang-format on
  }
  void SetupInitializer() {
    auto& manager = ActionManager::GetForTesting();
    initialization_subscription_ =
        manager.AppendActionItemInitializer(base::BindRepeating(
            &ActionManagerTest::InitializeActions, base::Unretained(this)));
  }

  void TearDown() override { ActionIdMap::ResetMapsForTesting(); }

 private:
  base::CallbackListSubscription initialization_subscription_;
};

using ActionItemTest = ActionManagerTest;
using ActionIdMapTest = ActionManagerTest;

}  // namespace
}  // namespace actions

DECLARE_EXPORTED_UI_CLASS_PROPERTY_TYPE(, actions::ContextValues)

DEFINE_UI_CLASS_PROPERTY_TYPE(actions::ContextValues)

namespace actions {

// Verifies that the test harness functions correctly.
TEST_F(ActionManagerTest, Harness) {
  auto* manager = &ActionManager::GetForTesting();
  ActionManager::ResetForTesting();
  auto* new_manager = &ActionManager::GetForTesting();
  EXPECT_EQ(manager, new_manager);
}

// Verifies that the Initializers are properly called.
TEST_F(ActionManagerTest, InitializerTest) {
  bool initializer_called = false;
  auto& manager = ActionManager::GetForTesting();
  auto subscription = manager.AppendActionItemInitializer(base::BindRepeating(
      [](bool* called, ActionManager* manager) { *called = true; },
      &initializer_called));
  EXPECT_FALSE(initializer_called);
  manager.IndexActions();
  EXPECT_TRUE(initializer_called);
}

TEST_F(ActionManagerTest, ActionRegisterAndInvoke) {
  const std::u16string text = u"Test Action";
  int action_invoked_count = 0;
  auto& manager = ActionManager::GetForTesting();
  auto subscription = manager.AppendActionItemInitializer(base::BindRepeating(
      [](int* invoked_count, const std::u16string& text,
         ActionManager* manager) {
        auto action = std::make_unique<ActionItem>(base::BindRepeating(
            [](int* invoked_count, actions::ActionItem* action,
               ActionInvocationContext context) {
              ++*invoked_count;
              EXPECT_EQ(*invoked_count, action->GetInvokeCount());
              EXPECT_GE(base::TimeTicks::Now(), *action->GetLastInvokeTime());
            },
            invoked_count));
        action->SetActionId(kActionCut);
        action->SetText(text);
        action->SetEnabled(true);
        action->SetVisible(true);
        manager->AddAction(std::move(action));
      },
      &action_invoked_count, text));
  EXPECT_EQ(action_invoked_count, 0);
  auto* action = manager.FindAction(kActionCut);
  ASSERT_TRUE(action);
  EXPECT_EQ(action->GetText(), text);
  EXPECT_TRUE(action->GetEnabled());
  EXPECT_TRUE(action->GetVisible());
  EXPECT_EQ(action->GetInvokeCount(), 0);
  EXPECT_EQ(action->GetLastInvokeTime(), std::nullopt);
  auto action_id = action->GetActionId();
  ASSERT_TRUE(action_id);
  EXPECT_EQ(*action_id, kActionCut);
  action->InvokeAction();
  EXPECT_GT(action_invoked_count, 0);
}

TEST_F(ActionManagerTest, ActionNotFound) {
  auto& manager = ActionManager::GetForTesting();
  auto* action = manager.FindAction(kActionPaste);
  EXPECT_FALSE(action);
}

TEST_F(ActionItemTest, ScopedFindActionTest) {
  // clang-format off
  auto builder = ActionItem::Builder()
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(false)
      .AddChildren(
          ActionItem::Builder()
              .SetActionId(kActionTest2)
              .SetText(kChild1Text),
          ActionItem::Builder()
              .SetActionId(kActionTest3)
              .SetChecked(true)
              .SetText(kChild2Text));
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  auto* action_test1 = manager.FindAction(kActionTest1);
  ASSERT_TRUE(action_test1);
  auto* action_test2 = manager.FindAction(kActionTest2, action_test1);
  ASSERT_TRUE(action_test2);
  auto* action_test3 = manager.FindAction(kActionTest3, action_test2);
  EXPECT_FALSE(action_test3);
}

TEST_F(ActionIdMapTest, TestCreateActionId) {
  const std::string new_action_id_1 = "kNewActionId1";
  const std::string new_action_id_2 = "kNewActionId2";
  const std::string existing_action_id = "kActionPaste";

  auto result_1 = ActionIdMap::CreateActionId(new_action_id_1);
  EXPECT_TRUE(result_1.second);

  auto result_2 = ActionIdMap::CreateActionId(new_action_id_2);
  EXPECT_TRUE(result_2.second);
  EXPECT_NE(result_1.first, result_2.first);

  auto result_2_dupe = ActionIdMap::CreateActionId(new_action_id_2);
  EXPECT_FALSE(result_2_dupe.second);
  EXPECT_EQ(result_2.first, result_2_dupe.first);

  auto result_existing = ActionIdMap::CreateActionId(existing_action_id);
  EXPECT_FALSE(result_existing.second);
}

TEST_F(ActionIdMapTest, MapBetweenEnumAndString) {
  auto result_1 = ActionIdMap::CreateActionId("kNewActionId1");
  EXPECT_TRUE(result_1.second);

  const std::string expected_action_string = "kActionPaste";
  auto actual_action_string = ActionIdMap::ActionIdToString(kActionPaste);
  ASSERT_THAT(actual_action_string, testing::Optional(expected_action_string));

  // Map back from enum to string
  auto actual_action_id =
      ActionIdMap::StringToActionId(actual_action_string.value());
  EXPECT_THAT(actual_action_id, testing::Optional(kActionPaste));

  const std::vector<std::string> strings{"kActionPaste", "kActionCut"};
  const std::vector<ActionId> action_ids{kActionPaste, kActionCut};

  auto actual_strings = ActionIdMap::ActionIdsToStrings(action_ids);
  ASSERT_EQ(strings.size(), actual_strings.size());
  EXPECT_THAT(actual_strings[0], testing::Optional(strings[0]));
  EXPECT_THAT(actual_strings[1], testing::Optional(strings[1]));

  auto actual_action_ids = ActionIdMap::StringsToActionIds(strings);
  ASSERT_EQ(action_ids.size(), actual_action_ids.size());
  EXPECT_THAT(actual_action_ids[0], testing::Optional(action_ids[0]));
  EXPECT_THAT(actual_action_ids[1], testing::Optional(action_ids[1]));
}

#define MAP_ACTION_IDS_TO_STRINGS
#include "ui/actions/action_id_macros.inc"

TEST_F(ActionIdMapTest, MergeMaps) {
  auto test_action_map = base::flat_map<ActionId, std::string>(
      std::vector<std::pair<ActionId, std::string>>{TEST_ACTION_IDS});
  ActionIdMap::AddActionIdToStringMappings(test_action_map);

  const std::string expected_action_string = "kActionPaste";
  auto actual_action_string = ActionIdMap::ActionIdToString(kActionPaste);
  EXPECT_THAT(actual_action_string, testing::Optional(expected_action_string));

  const std::string expected_string = "kActionTest2";
  auto actual_string = ActionIdMap::ActionIdToString(kActionTest2);
  EXPECT_THAT(actual_string, testing::Optional(expected_string));
}

#include "ui/actions/action_id_macros.inc"
#undef MAP_ACTION_IDS_TO_STRINGS

TEST_F(ActionIdMapTest, TestEnumNotFound) {
  const std::string unknown_action = "kActionUnknown";
  auto unknown_id = ActionIdMap::StringToActionId(unknown_action);
  EXPECT_FALSE(unknown_id.has_value());

  const ActionId invalid_action_id = static_cast<ActionId>(-1);
  auto unknown_id_string = ActionIdMap::ActionIdToString(invalid_action_id);
  EXPECT_FALSE(unknown_id_string.has_value());
}

TEST_F(ActionItemTest, ActionBuilderTest) {
  const std::u16string text = u"Test Action";
  // clang-format off
  auto builder =
      ActionItem::Builder()
        .SetText(text)
        .SetVisible(false)
        .SetActionId(actions::kActionCopy)
        .SetInvokeActionCallback(
            base::DoNothing());
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());

  auto* action = manager.FindAction(actions::kActionCopy);
  ASSERT_TRUE(action);
  EXPECT_EQ(action->GetText(), text);
  EXPECT_FALSE(action->GetVisible());
  EXPECT_EQ(action->GetInvokeCount(), 0);
  action->InvokeAction();
  EXPECT_EQ(action->GetInvokeCount(), 1);
}

TEST_F(ActionItemTest, ActionBuilderChildrenTest) {
  const size_t expected_child_count = 2;
  ActionItem* root_action = nullptr;
  ActionItem* child_action1 = nullptr;
  ActionItem* child_action2 = nullptr;
  int action_invoked_count = 0;
  // clang-format off
  auto builder = ActionItem::Builder(
      base::BindRepeating([](int* invoked_count, actions::ActionItem* action,
          ActionInvocationContext context){
        ++*invoked_count;
      }, &action_invoked_count))
      .CopyAddressTo(&root_action)
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(false)
      .AddChildren(
          ActionItem::Builder(base::DoNothing())
              .CopyAddressTo(&child_action1)
              .SetActionId(kActionTest2)
              .SetText(kChild1Text),
          ActionItem::Builder(base::DoNothing())
              .CopyAddressTo(&child_action2)
              .SetActionId(kActionTest3)
              .SetChecked(true)
              .SetIsShowingBubble(true)
              .SetText(kChild2Text));
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  ASSERT_TRUE(root_action);
  ASSERT_TRUE(child_action1);
  ASSERT_TRUE(child_action2);

  EXPECT_EQ(root_action->GetChildren().children().size(), expected_child_count);

  EXPECT_EQ(child_action1->GetText(), kChild1Text);
  auto child_action_id1 = child_action1->GetActionId();
  ASSERT_TRUE(child_action_id1);
  EXPECT_EQ(child_action_id1.value(), kActionTest2);
  EXPECT_FALSE(child_action1->GetChecked());
  EXPECT_FALSE(child_action1->GetIsShowingBubble());

  EXPECT_EQ(child_action2->GetText(), kChild2Text);
  auto child_action_id2 = child_action2->GetActionId();
  ASSERT_TRUE(child_action_id2);
  EXPECT_EQ(child_action_id2.value(), kActionTest3);
  EXPECT_TRUE(child_action2->GetChecked());
  EXPECT_TRUE(child_action2->GetIsShowingBubble());

  EXPECT_FALSE(root_action->GetEnabled());
  EXPECT_EQ(action_invoked_count, 0);
  root_action->InvokeAction();
  // `root_action` is not enabled, so InvokeAction() shouldn't trigger callback.
  EXPECT_EQ(action_invoked_count, 0);

  // The child actions should trigger their callbacks since they're enabled.
  child_action1->InvokeAction();
  EXPECT_EQ(child_action1->GetInvokeCount(), 1);
  child_action2->InvokeAction();
  EXPECT_EQ(child_action2->GetInvokeCount(), 1);
}

TEST_F(ActionItemTest, TestGetChildren) {
  ActionItemVector actions;
  auto& manager = ActionManager::GetForTesting();
  SetupInitializer();
  manager.GetActions(actions);
  EXPECT_FALSE(actions.empty());
  EXPECT_EQ(actions.size(), size_t{3});
}

TEST_F(ActionItemTest, TestItemBatchUpdate) {
  bool action_item_changed = false;
  ActionItem* root_action = nullptr;
  // clang-format off
  auto builder = ActionItem::Builder()
      .CopyAddressTo(&root_action)
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(false)
      .AddChildren(
          ActionItem::Builder()
              .SetActionId(kActionTest2)
              .SetText(kChild1Text),
          ActionItem::Builder()
              .SetActionId(kActionTest3)
              .SetChecked(true)
              .SetText(kChild2Text));
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  auto changed_subscription =
      root_action->AddActionChangedCallback(base::BindRepeating(
          [](bool* action_item_changed) { *action_item_changed = true; },
          &action_item_changed));
  {
    auto scoped_updater = root_action->BeginUpdate();
    root_action->SetEnabled(true);
    EXPECT_FALSE(action_item_changed);
    root_action->SetVisible(false);
    EXPECT_FALSE(action_item_changed);
  }
  EXPECT_TRUE(action_item_changed);
}

TEST_F(ActionItemTest, TestGroupIdExclusion) {
  ActionItem* action_test2 = nullptr;
  ActionItem* action_test3 = nullptr;
  // clang-format off
  auto builder = ActionItem::Builder()
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(false)
      .AddChildren(
          ActionItem::Builder()
              .CopyAddressTo(&action_test2)
              .SetActionId(kActionTest2)
              .SetGroupId(10)
              .SetText(kChild1Text),
          ActionItem::Builder()
              .CopyAddressTo(&action_test3)
              .SetActionId(kActionTest3)
              .SetGroupId(10)
              .SetChecked(true)
              .SetText(kChild2Text));
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddActions(std::move(builder).Build(),
                     ActionItem::Builder().SetActionId(kActionTest4).Build());
  ASSERT_TRUE(action_test2);
  ASSERT_TRUE(action_test2);
  EXPECT_FALSE(action_test2->GetChecked());
  EXPECT_TRUE(action_test3->GetChecked());
  action_test2->SetChecked(true);
  EXPECT_TRUE(action_test2->GetChecked());
  EXPECT_FALSE(action_test3->GetChecked());
}

TEST_F(ActionItemTest, TestActionItemPinnableKey) {
  // clang-format off
  auto builder = ActionItem::Builder()
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(true);
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  auto* action_test1 = manager.FindAction(kActionTest1);
  ASSERT_TRUE(action_test1);
  ASSERT_EQ(action_test1->GetProperty(kActionItemPinnableKey),
            std::underlying_type_t<actions::ActionPinnableState>(
                actions::ActionPinnableState::kNotPinnable));
  action_test1->SetProperty(kActionItemPinnableKey, true);
  ASSERT_EQ(action_test1->GetProperty(kActionItemPinnableKey),
            std::underlying_type_t<actions::ActionPinnableState>(
                actions::ActionPinnableState::kPinnable));

  // test using builder
  builder =
      ActionItem::Builder()
          .SetText(kActionText)
          .SetActionId(kActionTest2)
          .SetProperty(kActionItemPinnableKey,
                       std::underlying_type_t<actions::ActionPinnableState>(
                           actions::ActionPinnableState::kPinnable))
          .SetVisible(true)
          .SetEnabled(true);

  manager.AddAction(std::move(builder).Build());
  auto* action_test2 = manager.FindAction(kActionTest2);
  ASSERT_TRUE(action_test2);
  ASSERT_EQ(action_test2->GetProperty(kActionItemPinnableKey),
            std::underlying_type_t<actions::ActionPinnableState>(
                actions::ActionPinnableState::kPinnable));
}

TEST_F(ActionItemTest, TestActionProperties) {
  constexpr int kGroupId = 5;
  // clang-format off
  auto builder = ActionItem::Builder()
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(true)
      .SetAccessibleName(kActionAccessibleText)
      .SetTooltipText(kActionTooltipText)
      .SetGroupId(kGroupId);
  // clang-format on
  auto action_item = std::move(builder).Build();
  EXPECT_EQ(action_item->GetText(), kActionText);
  EXPECT_EQ(action_item->GetActionId().value(), kActionTest1);
  EXPECT_TRUE(action_item->GetVisible());
  EXPECT_TRUE(action_item->GetEnabled());
  EXPECT_EQ(action_item->GetAccessibleName(), kActionAccessibleText);
  EXPECT_EQ(action_item->GetTooltipText(), kActionTooltipText);
  EXPECT_EQ(action_item->GetGroupId(), kGroupId);
}

TEST_F(ActionItemTest, TestActionWeakPtr) {
  base::WeakPtr<ActionItem> action_test2;
  base::WeakPtr<ActionItem> action_test3;
  // clang-format off
  auto builder = ActionItem::Builder()
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(false)
      .AddChildren(
          ActionItem::Builder()
              .CopyWeakPtrTo(&action_test2)
              .SetActionId(kActionTest2)
              .SetGroupId(10)
              .SetText(kChild1Text),
          ActionItem::Builder()
              .CopyWeakPtrTo(&action_test3)
              .SetActionId(kActionTest3)
              .SetGroupId(10)
              .SetChecked(true)
              .SetText(kChild2Text));
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  ASSERT_NE(action_test2.get(), nullptr);
  ASSERT_NE(action_test3.get(), nullptr);
  manager.ResetActions();
  EXPECT_EQ(action_test2.get(), nullptr);
  EXPECT_EQ(action_test3.get(), nullptr);
}

TEST_F(ActionItemTest, TextActionInvocationContext) {
  int action_invoked_count = 0;
  base::WeakPtr<ActionItem> action_test1;
  // clang-format off
  auto builder = ActionItem::Builder(
      base::BindRepeating([](int* invoked_count, actions::ActionItem* action,
          ActionInvocationContext context){
        ++*invoked_count;
        ContextValues context_value = context.GetProperty(kContextValueKey);
        EXPECT_EQ(context_value, ContextValues::kContextKeyboard);
      },  &action_invoked_count))
      .CopyWeakPtrTo(&action_test1)
      .SetText(kActionText)
      .SetActionId(kActionTest1)
      .SetVisible(true)
      .SetEnabled(true);
  // clang-format on
  auto& manager = ActionManager::GetForTesting();
  manager.AddAction(std::move(builder).Build());
  ASSERT_TRUE(action_test1);
  action_test1->InvokeAction(
      ActionInvocationContext::Builder()
          .SetProperty(kContextValueKey, ContextValues::kContextKeyboard)
          .Build());
  EXPECT_EQ(action_invoked_count, 1);
}

TEST_F(ActionItemTest, ActionInvocationContextEnumSetProperty) {
  ActionInvocationContext context =
      ActionInvocationContext::Builder()
          .SetProperty(kActionItemPinnableKey, ActionPinnableState::kPinnable)
          .Build();
  EXPECT_EQ(context.GetProperty(kActionItemPinnableKey),
            static_cast<std::underlying_type_t<ActionPinnableState>>(
                ActionPinnableState::kPinnable));
}

TEST_F(ActionItemTest, PopulateChildItemsCallback) {
  auto action_item = std::make_unique<ActionItem>();
  EXPECT_FALSE(action_item->HasPopulateChildActionsCallback());

  bool callback_called = false;
  BaseAction* callback_arg = nullptr;
  action_item->SetPopulateChildrenCallback(base::BindRepeating(
      [](bool* called, BaseAction** arg, BaseAction* action) {
        *called = true;
        *arg = action;
      },
      &callback_called, &callback_arg));

  EXPECT_TRUE(action_item->HasPopulateChildActionsCallback());
  EXPECT_FALSE(callback_called);

  action_item->PopulateChildItems();
  EXPECT_TRUE(callback_called);
  EXPECT_EQ(callback_arg, action_item.get());
}

TEST_F(ActionItemTest, PopulateChildItemsRecursive) {
  auto parent_action = std::make_unique<ActionItem>();
  auto child_action = std::make_unique<ActionItem>();
  auto* child_ptr = parent_action->AddChild(std::move(child_action));

  std::vector<std::string> call_order;
  parent_action->SetPopulateChildrenCallback(base::BindRepeating(
      [](std::vector<std::string>* order, BaseAction* action) {
        order->push_back("parent");
      },
      &call_order));

  child_ptr->SetPopulateChildrenCallback(
      base::BindRepeating([](std::vector<std::string>* order,
                             BaseAction* action) { order->push_back("child"); },
                          &call_order));

  parent_action->PopulateChildItems();
  ASSERT_EQ(call_order.size(), 2u);
  EXPECT_EQ(call_order[0], "child");
  EXPECT_EQ(call_order[1], "parent");
}

TEST_F(ActionItemTest, PopulateChildItemsWithoutCallback) {
  auto action_item = std::make_unique<ActionItem>();
  EXPECT_FALSE(action_item->HasPopulateChildActionsCallback());

  // Should not crash even if no callback is set.
  action_item->PopulateChildItems();
}

TEST_F(ActionItemTest, ActionBuilderSetPopulateChildrenCallback) {
  bool callback_called = false;
  auto builder =
      ActionItem::Builder().SetPopulateChildrenCallback(base::BindRepeating(
          [](bool* called, BaseAction* action) { *called = true; },
          &callback_called));
  auto action_item = std::move(builder).Build();
  ASSERT_TRUE(action_item);
  EXPECT_TRUE(action_item->HasPopulateChildActionsCallback());

  action_item->PopulateChildItems();
  EXPECT_TRUE(callback_called);
}

}  // namespace actions
