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

#include "precomp.h"
#include "WexTestClass.h"

#include "CommonState.hpp"

#include "globals.h"

#include "..\interactivity\win32\Clipboard.hpp"
#include "..\interactivity\inc\ServiceLocator.hpp"

#include "dbcs.h"

#include <cctype>

#ifdef BUILD_ONECORE_INTERACTIVITY
#include "..\..\interactivity\inc\VtApiRedirection.hpp"
#endif

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

using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace Microsoft::Console::Interactivity;
using namespace Microsoft::Console::Interactivity::Win32;

static const WORD altScanCode = 0x38;
static const WORD leftShiftScanCode = 0x2A;

class ClipboardTests
{
    TEST_CLASS(ClipboardTests);

    CommonState* m_state;

    TEST_CLASS_SETUP(ClassSetup)
    {
        m_state = new CommonState();

        m_state->PrepareGlobalFont();
        m_state->PrepareGlobalScreenBuffer();
        m_state->PrepareGlobalInputBuffer();

        return true;
    }

    TEST_CLASS_CLEANUP(ClassCleanup)
    {
        m_state->CleanupGlobalInputBuffer();
        m_state->CleanupGlobalScreenBuffer();
        m_state->CleanupGlobalFont();

        return true;
    }

    TEST_METHOD_SETUP(MethodSetup)
    {
        m_state->FillTextBuffer();
        return true;
    }

    TEST_METHOD_CLEANUP(MethodCleanup)
    {
        return true;
    }

    const UINT cRectsSelected = 4;

    std::vector<std::wstring> SetupRetrieveFromBuffers(bool fLineSelection, std::vector<SMALL_RECT>& selection)
    {
        const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
        // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases
        // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc.

        // set up and try to retrieve the first 4 rows from the buffer
        const auto& screenInfo = gci.GetActiveOutputBuffer();

        selection.clear();
        selection.emplace_back(SMALL_RECT{ 0, 0, 8, 0 });
        selection.emplace_back(SMALL_RECT{ 0, 1, 14, 1 });
        selection.emplace_back(SMALL_RECT{ 0, 2, 14, 2 });
        selection.emplace_back(SMALL_RECT{ 0, 3, 8, 3 });

        return Clipboard::Instance().RetrieveTextFromBuffer(screenInfo,
                                                            fLineSelection,
                                                            selection)
            .text;
    }

#pragma prefast(push)
#pragma prefast(disable : 26006, "Specifically trying to check unterminated strings in this test.")
    TEST_METHOD(TestRetrieveFromBuffer)
    {
        // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases
        // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc.

        std::vector<SMALL_RECT> selection;
        const auto text = SetupRetrieveFromBuffers(false, selection);

        // verify trailing bytes were trimmed
        // there are 2 double-byte characters in our sample string (see CommonState.hpp for sample)
        // the width is right - left
        VERIFY_ARE_EQUAL((short)wcslen(text[0].data()), selection[0].Right - selection[0].Left + 1);

        // since we're not in line selection, the line should be \r\n terminated
        PCWCHAR tempPtr = text[0].data();
        tempPtr += text[0].size();
        tempPtr -= 2;
        VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n"));

        // since we're not in line selection, spaces should be trimmed from the end
        tempPtr = text[0].data();
        tempPtr += selection[0].Right - selection[0].Left - 2;
        tempPtr++;
        VERIFY_IS_NULL(wcsrchr(tempPtr, L' '));

        // final line of selection should not contain CR/LF
        tempPtr = text[3].data();
        tempPtr += text[3].size();
        tempPtr -= 2;
        VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n"));
    }
#pragma prefast(pop)

    TEST_METHOD(TestRetrieveLineSelectionFromBuffer)
    {
        // NOTE: This test requires innate knowledge of how the common buffer text is emitted in order to test all cases
        // Please see CommonState.hpp for information on the buffer state per row, the row contents, etc.

        std::vector<SMALL_RECT> selection;
        const auto text = SetupRetrieveFromBuffers(true, selection);

        // row 2, no wrap
        // no wrap row before the end should have CR/LF
        PCWCHAR tempPtr = text[2].data();
        tempPtr += text[2].size();
        tempPtr -= 2;
        VERIFY_ARE_EQUAL(String(tempPtr), String(L"\r\n"));

        // no wrap row should trim spaces at the end
        tempPtr = text[2].data();
        VERIFY_IS_NULL(wcsrchr(tempPtr, L' '));

        // row 1, wrap
        // wrap row before the end should *not* have CR/LF
        tempPtr = text[1].data();
        tempPtr += text[1].size();
        tempPtr -= 2;
        VERIFY_ARE_NOT_EQUAL(String(tempPtr), String(L"\r\n"));

        // wrap row should have spaces at the end
        tempPtr = text[1].data();
        const wchar_t* ptr = wcsrchr(tempPtr, L' ');
        VERIFY_IS_NOT_NULL(ptr);
    }

    TEST_METHOD(CanConvertTextToInputEvents)
    {
        std::wstring wstr = L"hello world";
        std::deque<std::unique_ptr<IInputEvent>> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(),
                                                                                                wstr.size());
        VERIFY_ARE_EQUAL(wstr.size() * 2, events.size());
        IInputServices* pInputServices = ServiceLocator::LocateInputServices();
        for (wchar_t wch : wstr)
        {
            std::deque<bool> keydownPattern{ true, false };
            for (bool isKeyDown : keydownPattern)
            {
                VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType());
                std::unique_ptr<KeyEvent> keyEvent;
                keyEvent.reset(static_cast<KeyEvent* const>(events.front().release()));
                events.pop_front();

                const short keyState = pInputServices->VkKeyScanW(wch);
                VERIFY_ARE_NOT_EQUAL(-1, keyState);
                const WORD virtualScanCode = static_cast<WORD>(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC));

                VERIFY_ARE_EQUAL(wch, keyEvent->GetCharData());
                VERIFY_ARE_EQUAL(isKeyDown, keyEvent->IsKeyDown());
                VERIFY_ARE_EQUAL(1, keyEvent->GetRepeatCount());
                VERIFY_ARE_EQUAL(static_cast<DWORD>(0), keyEvent->GetActiveModifierKeys());
                VERIFY_ARE_EQUAL(virtualScanCode, keyEvent->GetVirtualScanCode());
                VERIFY_ARE_EQUAL(LOBYTE(keyState), keyEvent->GetVirtualKeyCode());
            }
        }
    }

    TEST_METHOD(CanConvertUppercaseText)
    {
        std::wstring wstr = L"HeLlO WoRlD";
        size_t uppercaseCount = 0;
        for (wchar_t wch : wstr)
        {
            std::isupper(wch) ? ++uppercaseCount : 0;
        }
        std::deque<std::unique_ptr<IInputEvent>> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(),
                                                                                                wstr.size());

        VERIFY_ARE_EQUAL((wstr.size() + uppercaseCount) * 2, events.size());
        IInputServices* pInputServices = ServiceLocator::LocateInputServices();
        VERIFY_IS_NOT_NULL(pInputServices);
        for (wchar_t wch : wstr)
        {
            std::deque<bool> keydownPattern{ true, false };
            for (bool isKeyDown : keydownPattern)
            {
                Log::Comment(NoThrowString().Format(L"testing char: %C; keydown: %d", wch, isKeyDown));

                VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType());
                std::unique_ptr<KeyEvent> keyEvent;
                keyEvent.reset(static_cast<KeyEvent* const>(events.front().release()));
                events.pop_front();

                const short keyScanError = -1;
                const short keyState = pInputServices->VkKeyScanW(wch);
                VERIFY_ARE_NOT_EQUAL(keyScanError, keyState);
                const WORD virtualScanCode = static_cast<WORD>(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC));

                if (std::isupper(wch))
                {
                    // uppercase letters have shift key events
                    // surrounding them, making two events per letter
                    // (and another two for the keyup)
                    VERIFY_IS_FALSE(events.empty());

                    VERIFY_ARE_EQUAL(InputEventType::KeyEvent, events.front()->EventType());
                    std::unique_ptr<KeyEvent> keyEvent2;
                    keyEvent2.reset(static_cast<KeyEvent* const>(events.front().release()));
                    events.pop_front();

                    const short keyState2 = pInputServices->VkKeyScanW(wch);
                    VERIFY_ARE_NOT_EQUAL(keyScanError, keyState);
                    const WORD virtualScanCode2 = static_cast<WORD>(pInputServices->MapVirtualKeyW(wch, MAPVK_VK_TO_VSC));

                    if (isKeyDown)
                    {
                        // shift then letter
                        const KeyEvent shiftDownEvent({ TRUE, 1, VK_SHIFT, leftShiftScanCode, L'\0', SHIFT_PRESSED });
                        VERIFY_ARE_EQUAL(shiftDownEvent, *keyEvent);

                        const KeyEvent expectedKeyEvent({ TRUE, 1, LOBYTE(keyState2), virtualScanCode2, wch, SHIFT_PRESSED });
                        VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent2);
                    }
                    else
                    {
                        // letter then shift
                        const KeyEvent expectedKeyEvent({ FALSE, 1, LOBYTE(keyState), virtualScanCode, wch, SHIFT_PRESSED });
                        VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent);

                        const KeyEvent shiftUpEvent({ FALSE, 1, VK_SHIFT, leftShiftScanCode, L'\0', 0 });
                        VERIFY_ARE_EQUAL(shiftUpEvent, *keyEvent2);
                    }
                }
                else
                {
                    const KeyEvent expectedKeyEvent({ !!isKeyDown, 1, LOBYTE(keyState), virtualScanCode, wch, 0 });
                    VERIFY_ARE_EQUAL(expectedKeyEvent, *keyEvent);
                }
            }
        }
    }

#ifdef __INSIDE_WINDOWS
    TEST_METHOD(CanConvertCharsRequiringAltGr)
    {
        const std::wstring wstr = L"\x20ac"; // € char U+20AC
        std::deque<std::unique_ptr<IInputEvent>> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(),
                                                                                                wstr.size());

        // should be converted to:
        // 1. AltGr keydown
        // 2. € keydown
        // 3. € keyup
        // 4. AltGr keyup
        const size_t convertedSize = 4;
        VERIFY_ARE_EQUAL(convertedSize, events.size());

        const short keyState = VkKeyScanW(wstr[0]);
        const WORD virtualKeyCode = LOBYTE(keyState);

        std::deque<KeyEvent> expectedEvents;
        expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', (ENHANCED_KEY | LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) });
        expectedEvents.push_back({ TRUE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) });
        expectedEvents.push_back({ FALSE, 1, virtualKeyCode, 0, wstr[0], (LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED) });
        expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, L'\0', ENHANCED_KEY });

        for (size_t i = 0; i < events.size(); ++i)
        {
            const KeyEvent currentKeyEvent = *reinterpret_cast<const KeyEvent* const>(events[i].get());
            VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i));
        }
    }
#endif

    TEST_METHOD(CanConvertCharsOutsideKeyboardLayout)
    {
        const std::wstring wstr = L"\xbc"; // ¼ char U+00BC
        const UINT outputCodepage = CP_JAPANESE;
        ServiceLocator::LocateGlobals().getConsoleInformation().OutputCP = outputCodepage;
        std::deque<std::unique_ptr<IInputEvent>> events = Clipboard::Instance().TextToKeyEvents(wstr.c_str(),
                                                                                                wstr.size());

        // should be converted to:
        // 1. left alt keydown
        // 2. 1st numpad keydown
        // 3. 1st numpad keyup
        // 4. 2nd numpad keydown
        // 5. 2nd numpad keyup
        // 6. left alt keyup
        const size_t convertedSize = 6;
        VERIFY_ARE_EQUAL(convertedSize, events.size());

        std::deque<KeyEvent> expectedEvents;
        expectedEvents.push_back({ TRUE, 1, VK_MENU, altScanCode, L'\0', LEFT_ALT_PRESSED });
        expectedEvents.push_back({ TRUE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED });
        expectedEvents.push_back({ FALSE, 1, 0x66, 0x4D, L'\0', LEFT_ALT_PRESSED });
        expectedEvents.push_back({ TRUE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED });
        expectedEvents.push_back({ FALSE, 1, 0x63, 0x51, L'\0', LEFT_ALT_PRESSED });
        expectedEvents.push_back({ FALSE, 1, VK_MENU, altScanCode, wstr[0], 0 });

        for (size_t i = 0; i < events.size(); ++i)
        {
            const KeyEvent currentKeyEvent = *reinterpret_cast<const KeyEvent* const>(events[i].get());
            VERIFY_ARE_EQUAL(expectedEvents[i], currentKeyEvent, NoThrowString().Format(L"i == %d", i));
        }
    }
};
