// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

#include "precomp.h"

#include "../../inc/consoletaeftemplates.hpp"

#include <memory>
#include <utility>
#include <iostream>
#include <iomanip>
#include <sstream>

#define KEY_STATE_TOGGLED (0x1)
#define KEY_STATE_PRESSED (0x80)
#define KEY_STATE_RELEASED (0x0)

#define KEY_MESSAGE_CONTEXT_CODE (0x20000000)
#define KEY_MESSAGE_UPKEY_CODE (0xC0000000)
#define SINGLE_KEY_REPEAT (0x00000001)
#define EXTENDED_KEY_FLAG (0x01000000)

#define SLEEP_WAIT_TIME (2 * 1000)
#define GERMAN_KEYBOARD_LAYOUT (MAKELANGID(LANG_GERMAN, SUBLANG_GERMAN))

using namespace WEX::TestExecution;
using namespace WEX::Common;
using namespace WEX::Logging;

class KeyPressTests
{
    BEGIN_TEST_CLASS(KeyPressTests)
    END_TEST_CLASS()

    void TurnOffModifierKeys(HWND hwnd)
    {
        // these are taken from GetControlKeyState.
        static const WPARAM modifiers[8] = {
            VK_LMENU,
            VK_RMENU,
            VK_LCONTROL,
            VK_RCONTROL,
            VK_SHIFT,
            VK_NUMLOCK,
            VK_SCROLL,
            VK_CAPITAL
        };
        for (unsigned int i = 0; i < 8; ++i)
        {
            PostMessage(hwnd, CM_SET_KEY_STATE, modifiers[i], KEY_STATE_RELEASED);
        }
    }

    TEST_METHOD(TestContextMenuKey)
    {
        if (!OneCoreDelay::IsPostMessageWPresent())
        {
            Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping.");
            Log::Result(WEX::Logging::TestResults::Skipped);
            return;
        }

        Log::Comment(L"Checks that the context menu key is correctly added to the input buffer.");
        Log::Comment(L"This test will fail on some keyboard layouts. Ensure you're using a QWERTY keyboard if "
                     L"you're encountering a test failure here.");

        auto hwnd = GetConsoleWindow();
        VERIFY_IS_TRUE(!!IsWindow(hwnd));
        auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
        DWORD events = 0;

        // flush input buffer
        FlushConsoleInputBuffer(inputHandle);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events));
        VERIFY_ARE_EQUAL(events, 0u);

        // send context menu key event
        TurnOffModifierKeys(hwnd);
        Sleep(SLEEP_WAIT_TIME);
        auto scanCode = MapVirtualKeyW(VK_APPS, MAPVK_VK_TO_VSC);
        PostMessageW(hwnd, WM_KEYDOWN, VK_APPS, EXTENDED_KEY_FLAG | SINGLE_KEY_REPEAT | (scanCode << 16));
        Sleep(SLEEP_WAIT_TIME);

        INPUT_RECORD expectedRecord;
        expectedRecord.EventType = KEY_EVENT;
        expectedRecord.Event.KeyEvent.uChar.UnicodeChar = 0x0;
        expectedRecord.Event.KeyEvent.bKeyDown = true;
        expectedRecord.Event.KeyEvent.dwControlKeyState = ENHANCED_KEY;
        expectedRecord.Event.KeyEvent.dwControlKeyState |= (GetKeyState(VK_NUMLOCK) & KEY_STATE_TOGGLED) ? NUMLOCK_ON : 0;
        expectedRecord.Event.KeyEvent.wRepeatCount = SINGLE_KEY_REPEAT;
        expectedRecord.Event.KeyEvent.wVirtualKeyCode = VK_APPS;
        expectedRecord.Event.KeyEvent.wVirtualScanCode = (WORD)scanCode;

        // get the input record back and test it
        INPUT_RECORD record;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInputW(inputHandle, &record, 1, &events));
        VERIFY_IS_GREATER_THAN(events, 0u);
        VERIFY_ARE_EQUAL(expectedRecord, record);
    }

    BEGIN_TEST_METHOD(TestAltGr)
        TEST_METHOD_PROPERTY(L"Ignore[@DevTest=true]", L"false")
        TEST_METHOD_PROPERTY(L"Ignore[default]", L"true")
    END_TEST_METHOD()

    TEST_METHOD(TestCoalesceSameKeyPress)
    {
        if (!OneCoreDelay::IsSendMessageWPresent())
        {
            Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping.");
            Log::Result(WEX::Logging::TestResults::Skipped);
            return;
        }

        Log::Comment(L"Testing that key events are properly coalesced when the same key is pressed repeatedly");
        BOOL successBool;
        auto hwnd = GetConsoleWindow();
        VERIFY_IS_TRUE(!!IsWindow(hwnd));
        auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
        DWORD events = 0;

        // flush input buffer
        FlushConsoleInputBuffer(inputHandle);
        successBool = GetNumberOfConsoleInputEvents(inputHandle, &events);
        VERIFY_IS_TRUE(!!successBool);
        VERIFY_ARE_EQUAL(events, 0u);

        // send a bunch of 'a' keypresses to the console
        DWORD repeatCount = 1;
        const unsigned int messageSendCount = 1000;
        for (unsigned int i = 0; i < messageSendCount; ++i)
        {
            SendMessage(hwnd,
                        WM_CHAR,
                        0x41,
                        repeatCount);
        }

        // make sure the keypresses got processed and coalesced
        events = 0;
        successBool = GetNumberOfConsoleInputEvents(inputHandle, &events);
        VERIFY_IS_TRUE(!!successBool);
        VERIFY_IS_GREATER_THAN(events, 0u, NoThrowString().Format(L"%d", events));
        auto inputBuffer = std::make_unique<INPUT_RECORD[]>(1);
        PeekConsoleInput(inputHandle,
                         inputBuffer.get(),
                         1,
                         &events);
        VERIFY_ARE_EQUAL(events, 1u);
        VERIFY_ARE_EQUAL(inputBuffer[0].EventType, KEY_EVENT);
        VERIFY_ARE_EQUAL(inputBuffer[0].Event.KeyEvent.wRepeatCount, messageSendCount, NoThrowString().Format(L"%d", inputBuffer[0].Event.KeyEvent.wRepeatCount));
    }

    TEST_METHOD(TestCtrlKeyDownUp)
    {
        BEGIN_TEST_METHOD_PROPERTIES()
            // VKeys for A-Z
            // See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
            TEST_METHOD_PROPERTY(L"Data:vKey", L"{"
                                               "0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F,"
                                               "0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59,0x5A"
                                               "}")
        END_TEST_METHOD_PROPERTIES();

        if (!OneCoreDelay::IsSendMessageWPresent())
        {
            Log::Comment(L"Ctrl key eventing scenario can't be checked on platform without window message queuing.");
            Log::Result(WEX::Logging::TestResults::Skipped);
            return;
        }

        UINT vk;
        VERIFY_SUCCEEDED(TestData::TryGetValue(L"vKey", vk));

        Log::Comment(L"Testing the right number of input events is generated by Ctrl+Key press");
        BOOL successBool;
        auto hwnd = GetConsoleWindow();
        VERIFY_IS_TRUE(!!IsWindow(hwnd));
        auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
        DWORD events = 0;

        // Set the console to raw mode, so that it doesn't hijack any keypresses as shortcut keys
        SetConsoleMode(inputHandle, 0);

        // flush input buffer
        FlushConsoleInputBuffer(inputHandle);
        VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events));
        VERIFY_ARE_EQUAL(events, 0u);

        DWORD dwInMode = 0;
        GetConsoleMode(inputHandle, &dwInMode);
        Log::Comment(NoThrowString().Format(L"Mode:0x%x", dwInMode));

        UINT vkCtrl = VK_LCONTROL; // Need this instead of VK_CONTROL
        auto uiCtrlScancode = MapVirtualKey(vkCtrl, MAPVK_VK_TO_VSC);
        // According to
        // KEY_KEYDOWN https://msdn.microsoft.com/en-us/library/windows/desktop/ms646280(v=vs.85).aspx
        // KEY_UP https://msdn.microsoft.com/en-us/library/windows/desktop/ms646281(v=vs.85).aspx
        LPARAM CtrlFlags = (LOBYTE(uiCtrlScancode) << 16) | SINGLE_KEY_REPEAT;
        auto CtrlUpFlags = CtrlFlags | KEY_MESSAGE_UPKEY_CODE;

        auto uiScancode = MapVirtualKey(vk, MAPVK_VK_TO_VSC);
        LPARAM DownFlags = (LOBYTE(uiScancode) << 16) | SINGLE_KEY_REPEAT;
        auto UpFlags = DownFlags | KEY_MESSAGE_UPKEY_CODE;

        Log::Comment(NoThrowString().Format(L"Testing Ctrl+%c", vk));
        Log::Comment(NoThrowString().Format(L"DownFlags=0x%x, CtrlFlags=0x%x", DownFlags, CtrlFlags));
        Log::Comment(NoThrowString().Format(L"UpFlags=0x%x, CtrlUpFlags=0x%x", UpFlags, CtrlUpFlags));

        // Don't Use PostMessage, those events come in the wrong order.
        // Also can't use SendInput because of the whole test window backgrounding thing.
        //      It'd work locally, until you minimize the window.
        SendMessage(hwnd, WM_KEYDOWN, vkCtrl, CtrlFlags);
        SendMessage(hwnd, WM_KEYDOWN, vk, DownFlags);
        SendMessage(hwnd, WM_KEYUP, vk, UpFlags);
        SendMessage(hwnd, WM_KEYUP, vkCtrl, CtrlUpFlags);

        Sleep(50);

        events = 0;
        successBool = GetNumberOfConsoleInputEvents(inputHandle, &events);
        VERIFY_IS_TRUE(!!successBool);
        VERIFY_IS_GREATER_THAN(events, 0u, NoThrowString().Format(L"%d events found", events));

        auto inputBuffer = std::make_unique<INPUT_RECORD[]>(16);
        PeekConsoleInput(inputHandle,
                         inputBuffer.get(),
                         16,
                         &events);

        for (size_t i = 0; i < events; i++)
        {
            auto rc = inputBuffer[i];
            switch (rc.EventType)
            {
            case KEY_EVENT:
            {
                Log::Comment(NoThrowString().Format(
                    L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char: %c (0x%x) KeyState: 0x%x",
                    rc.Event.KeyEvent.bKeyDown,
                    rc.Event.KeyEvent.wRepeatCount,
                    rc.Event.KeyEvent.wVirtualKeyCode,
                    rc.Event.KeyEvent.wVirtualScanCode,
                    rc.Event.KeyEvent.uChar.UnicodeChar != 0 ? rc.Event.KeyEvent.uChar.UnicodeChar : ' ',
                    rc.Event.KeyEvent.uChar.UnicodeChar,
                    rc.Event.KeyEvent.dwControlKeyState));

                break;
            }
            default:
                Log::Comment(NoThrowString().Format(L"Another event type was found."));
            }
        }
        VERIFY_ARE_EQUAL(events, 4u);
        VERIFY_ARE_EQUAL(inputBuffer[0].EventType, KEY_EVENT);
        VERIFY_ARE_EQUAL(inputBuffer[1].EventType, KEY_EVENT);
        VERIFY_ARE_EQUAL(inputBuffer[2].EventType, KEY_EVENT);
        VERIFY_ARE_EQUAL(inputBuffer[3].EventType, KEY_EVENT);

        FlushConsoleInputBuffer(inputHandle);
    }

    TEST_METHOD(TestMaximize)
    {
        if (!OneCoreDelay::IsSendMessageWPresent())
        {
            Log::Comment(L"Injecting keys to the window message queue cannot be done on systems without a classic window message queue. Skipping.");
            Log::Result(WEX::Logging::TestResults::Skipped);
            return;
        }

        const auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
        const auto hwnd = GetConsoleWindow();
        VERIFY_IS_TRUE(!!IsWindow(hwnd));

        // Need the console to be in processed input for this to work
        SetConsoleMode(inputHandle, ENABLE_PROCESSED_INPUT);
        FlushConsoleInputBuffer(inputHandle);

        auto oldStyle = GetWindowLongW(hwnd, GWL_STYLE);
        auto oldExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE);

        // According to
        // KEY_KEYDOWN https://msdn.microsoft.com/en-us/library/windows/desktop/ms646280(v=vs.85).aspx
        // KEY_UP https://msdn.microsoft.com/en-us/library/windows/desktop/ms646281(v=vs.85).aspx
        const auto vsc = MapVirtualKey(VK_F11, MAPVK_VK_TO_VSC);
        const LPARAM F11Flags = (LOBYTE(vsc) << 16) | SINGLE_KEY_REPEAT;
        const auto F11UpFlags = F11Flags | KEY_MESSAGE_UPKEY_CODE;

        // Send F11 key down and up. lParam is VirtualScanCode and RepeatCount
        SendMessage(hwnd, WM_KEYDOWN, VK_F11, F11Flags);
        SendMessage(hwnd, WM_KEYUP, VK_F11, F11UpFlags);

        auto maxStyle = GetWindowLongW(hwnd, GWL_STYLE);
        auto maxExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE);

        // Send F11 key down and up. lParam is VirtualScanCode and RepeatCount
        SendMessage(hwnd, WM_KEYDOWN, VK_F11, F11Flags);
        SendMessage(hwnd, WM_KEYUP, VK_F11, F11UpFlags);

        auto newStyle = GetWindowLongW(hwnd, GWL_STYLE);
        auto newExStyle = GetWindowLongW(hwnd, GWL_EXSTYLE);

        // Maximize windows should not be Overlapped & have a popup
        // Extended style should have a window edge when not maximized
        VERIFY_IS_TRUE(WI_IsFlagSet(maxStyle, WS_POPUP));
        VERIFY_IS_TRUE(WI_AreAllFlagsClear(maxStyle, WS_OVERLAPPEDWINDOW));
        VERIFY_IS_TRUE(WI_IsFlagClear(maxExStyle, WS_EX_WINDOWEDGE));

        VERIFY_IS_TRUE(WI_IsFlagClear(newStyle, WS_POPUP));
        VERIFY_IS_TRUE(WI_AreAllFlagsSet(newStyle, WS_OVERLAPPEDWINDOW));
        VERIFY_IS_TRUE(WI_IsFlagSet(newExStyle, WS_EX_WINDOWEDGE));

        VERIFY_ARE_NOT_EQUAL(maxStyle, oldStyle);
        VERIFY_ARE_NOT_EQUAL(maxExStyle, oldExStyle);

        // Ignore the scrollbars when comparing styles
        WI_ClearAllFlags(oldStyle, WS_HSCROLL | WS_VSCROLL);
        WI_ClearAllFlags(newStyle, WS_HSCROLL | WS_VSCROLL);
        VERIFY_ARE_EQUAL(oldStyle, newStyle);
        VERIFY_ARE_EQUAL(oldExStyle, newExStyle);
    }
};

void KeyPressTests::TestAltGr()
{
    Log::Comment(L"Checks that alt-gr behavior is maintained.");
    auto hwnd = GetConsoleWindow();
    VERIFY_IS_TRUE(!!IsWindow(hwnd));
    auto inputHandle = GetStdHandle(STD_INPUT_HANDLE);
    DWORD events = 0;

    // flush input buffer
    FlushConsoleInputBuffer(inputHandle);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events));
    VERIFY_ARE_EQUAL(events, 0u);

    // create german locale string
    std::wstringstream wss;
    wss << std::setfill(L'0') << std::setw(8) << std::hex << GERMAN_KEYBOARD_LAYOUT;
    auto germanKeyboardLayoutString = wss.str();

    // save current keyboard layout
    wchar_t originalLocaleId[KL_NAMELENGTH];
    GetKeyboardLayoutName(originalLocaleId);

    // make console window the topmost window
    SetForegroundWindow(hwnd);

    // change to german keyboard layout
    PostMessage(hwnd, CM_SET_KEYBOARD_LAYOUT, std::stoi(germanKeyboardLayoutString), NULL);
    Sleep(SLEEP_WAIT_TIME);
    LoadKeyboardLayout(germanKeyboardLayoutString.c_str(), KLF_ACTIVATE);

    // turn off all modifier keys
    TurnOffModifierKeys(hwnd);

    // set right control key to be pressed
    PostMessage(hwnd, CM_SET_KEY_STATE, VK_LCONTROL, KEY_STATE_PRESSED);
    PostMessage(hwnd, CM_SET_KEY_STATE, VK_CONTROL, KEY_STATE_PRESSED);
    // set right alt to be pressed
    PostMessage(hwnd, CM_SET_KEY_STATE, VK_RMENU, KEY_STATE_PRESSED);
    PostMessage(hwnd, CM_SET_KEY_STATE, VK_MENU, KEY_STATE_PRESSED);
    Sleep(SLEEP_WAIT_TIME);

    // flush input buffer in preparation of the key event
    FlushConsoleInputBuffer(inputHandle);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetNumberOfConsoleInputEvents(inputHandle, &events));
    VERIFY_ARE_EQUAL(events, 0u);

    // send the key event that will be turned into an '@'
    auto scanCode = MapVirtualKey('Q', MAPVK_VK_TO_VSC);
    PostMessage(hwnd, WM_KEYDOWN, 'Q', KEY_MESSAGE_CONTEXT_CODE | SINGLE_KEY_REPEAT | (scanCode << 16));
    Sleep(SLEEP_WAIT_TIME);

    // reset the keymap
    TurnOffModifierKeys(hwnd);

    // create expected input record
    INPUT_RECORD expectedRecord;
    expectedRecord.EventType = KEY_EVENT;
    expectedRecord.Event.KeyEvent.uChar.UnicodeChar = L'@';
    expectedRecord.Event.KeyEvent.bKeyDown = true;
    expectedRecord.Event.KeyEvent.dwControlKeyState = RIGHT_ALT_PRESSED | LEFT_CTRL_PRESSED;
    expectedRecord.Event.KeyEvent.wRepeatCount = SINGLE_KEY_REPEAT;
    expectedRecord.Event.KeyEvent.wVirtualKeyCode = L'Q';
    expectedRecord.Event.KeyEvent.wVirtualScanCode = (WORD)scanCode;

    // read input records and compare
    const auto maxRecordLookup = 20; // some arbitrary value to grab some records
    Log::Comment(L"Looking for input record matching:");
    Log::Comment(VerifyOutputTraits<INPUT_RECORD>::ToString(expectedRecord));
    INPUT_RECORD records[20];
    VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleInput(inputHandle, records, maxRecordLookup, &events));
    VERIFY_IS_GREATER_THAN(events, 0u);
    auto successBool = false;
    // look for the expected record somewhere in the returned records
    for (unsigned int i = 0; i < events; ++i)
    {
        Log::Comment(VerifyOutputTraits<INPUT_RECORD>::ToString(records[i]));
        if (VerifyCompareTraits<INPUT_RECORD, INPUT_RECORD>::AreEqual(records[i], expectedRecord))
        {
            successBool = true;
            break;
        }
    }
    VERIFY_IS_TRUE(successBool);

    // reset the keyboard layout
    WPARAM originalLocale;
    std::wstringstream localeStringStream(originalLocaleId);
    localeStringStream >> std::hex >> originalLocale;
    PostMessage(hwnd, CM_SET_KEYBOARD_LAYOUT, originalLocale, NULL);
    LoadKeyboardLayout(originalLocaleId, KLF_ACTIVATE | KLF_SUBSTITUTE_OK);
}
