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

#include "chrome/browser/global_keyboard_shortcuts_mac.h"

#include <AppKit/NSEvent.h>
#include <Carbon/Carbon.h>
#include <stddef.h>

#include <initializer_list>

#include "base/check_op.h"
#include "chrome/app/chrome_command_ids.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/buildflags.h"
#include "ui/events/keycodes/keyboard_code_conversion_mac.h"

namespace {

enum class CommandKeyState : bool {
  kUp,
  kDown,
};
enum class ShiftKeyState : bool {
  kUp,
  kDown,
};
enum class OptionKeyState : bool {
  kUp,
  kDown,
};
enum class ControlKeyState : bool {
  kUp,
  kDown,
};

int CommandForKeys(int vkey_code,
                   CommandKeyState command,
                   ShiftKeyState shift = ShiftKeyState::kUp,
                   OptionKeyState option = OptionKeyState::kUp,
                   ControlKeyState control = ControlKeyState::kUp) {
  NSUInteger modifier_flags = 0;
  if (command == CommandKeyState::kDown)
    modifier_flags |= NSEventModifierFlagCommand;
  if (shift == ShiftKeyState::kDown)
    modifier_flags |= NSEventModifierFlagShift;
  if (option == OptionKeyState::kDown)
    modifier_flags |= NSEventModifierFlagOption;
  if (control == ControlKeyState::kDown)
    modifier_flags |= NSEventModifierFlagControl;

  switch (vkey_code) {
    case kVK_UpArrow:
    case kVK_DownArrow:
    case kVK_LeftArrow:
    case kVK_RightArrow:
      // Docs say that this is set for numpad *and* arrow keys.
      modifier_flags |= NSEventModifierFlagNumericPad;
      [[fallthrough]];
    case kVK_Help:
    case kVK_ForwardDelete:
    case kVK_Home:
    case kVK_End:
    case kVK_PageUp:
    case kVK_PageDown:
      // Docs say that this is set for function keys *and* the cluster of six
      // navigation keys in the center of the keyboard *and* arrow keys.
      modifier_flags |= NSEventModifierFlagFunction;
      break;
    default:
      break;
  }

  unichar shifted_character;
  unichar character;
  int result = ui::MacKeyCodeForWindowsKeyCode(
      ui::KeyboardCodeFromKeyCode(vkey_code), modifier_flags,
      &shifted_character, &character);
  DCHECK_NE(result, -1);

  NSEvent* event = [NSEvent
                 keyEventWithType:NSEventTypeKeyDown
                         location:NSZeroPoint
                    modifierFlags:modifier_flags
                        timestamp:0.0
                     windowNumber:0
                          context:nil
                       characters:[NSString stringWithFormat:@"%C", character]
      charactersIgnoringModifiers:[NSString
                                      stringWithFormat:@"%C", shifted_character]
                        isARepeat:NO
                          keyCode:vkey_code];

  return CommandForKeyEvent(event).chrome_command;
}

}  // namespace

TEST(GlobalKeyboardShortcuts, BasicFunctionality) {
  // Test that an invalid shortcut translates into an invalid command id.
  const int kInvalidCommandId = -1;
  const int no_key_code = 0;
  EXPECT_EQ(
      kInvalidCommandId,
      CommandForKeys(no_key_code, CommandKeyState::kUp, ShiftKeyState::kUp,
                     OptionKeyState::kUp, ControlKeyState::kUp));

  // Check that all known keyboard shortcuts return valid results.
  for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) {
    CommandKeyState command =
        shortcut.command_key ? CommandKeyState::kDown : CommandKeyState::kUp;
    ShiftKeyState shift =
        shortcut.shift_key ? ShiftKeyState::kDown : ShiftKeyState::kUp;
    OptionKeyState option =
        shortcut.opt_key ? OptionKeyState::kDown : OptionKeyState::kUp;
    ControlKeyState control =
        shortcut.cntrl_key ? ControlKeyState::kDown : ControlKeyState::kUp;

    int cmd_num =
        CommandForKeys(shortcut.vkey_code, command, shift, option, control);
    EXPECT_EQ(cmd_num, shortcut.chrome_command);
  }
  // Test that switching tabs triggers off keycodes and not characters (visible
  // with the Italian keyboard layout).
  EXPECT_EQ(
      IDC_SELECT_TAB_0,
      CommandForKeys(kVK_ANSI_1, CommandKeyState::kDown, ShiftKeyState::kUp,
                     OptionKeyState::kUp, ControlKeyState::kUp));
}

TEST(GlobalKeyboardShortcuts, KeypadNumberKeysMatch) {
  // Test that the shortcuts that are generated by keypad number keys match the
  // equivalent keys.
  static const struct {
    int keycode;
    int keypad_keycode;
  } equivalents[] = {
    {kVK_ANSI_0, kVK_ANSI_Keypad0},
    {kVK_ANSI_1, kVK_ANSI_Keypad1},
    {kVK_ANSI_2, kVK_ANSI_Keypad2},
    {kVK_ANSI_3, kVK_ANSI_Keypad3},
    {kVK_ANSI_4, kVK_ANSI_Keypad4},
    {kVK_ANSI_5, kVK_ANSI_Keypad5},
    {kVK_ANSI_6, kVK_ANSI_Keypad6},
    {kVK_ANSI_7, kVK_ANSI_Keypad7},
    {kVK_ANSI_8, kVK_ANSI_Keypad8},
    {kVK_ANSI_9, kVK_ANSI_Keypad9},
  };

  // We only consider unshifted keys. A shifted numpad key gives a different
  // keyEquivalent than a shifted number key.
  const ShiftKeyState shift = ShiftKeyState::kUp;
  for (auto equivalent : equivalents) {
    for (CommandKeyState command :
         {CommandKeyState::kUp, CommandKeyState::kDown}) {
      for (OptionKeyState option :
           {OptionKeyState::kUp, OptionKeyState::kDown}) {
        for (ControlKeyState control :
             {ControlKeyState::kUp, ControlKeyState::kDown}) {
          EXPECT_EQ(CommandForKeys(equivalent.keycode, command, shift, option,
                                   control),
                    CommandForKeys(equivalent.keypad_keycode, command, shift,
                                   option, control));
        }
      }
    }
  }
}
