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

#include "precomp.h"

#include "Common.hpp"

#include <future>

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

// This class is intended to test:
// WriteFile

class FileTests
{
    // Method isolation level will completely close and re-open the OpenConsole session for every
    // TEST_METHOD below. This saves us the time of cleaning up the mode state and the contents of
    // the buffer and cursor position for each test. Launching a new OpenConsole is much quicker.
    BEGIN_TEST_CLASS(FileTests)
        TEST_CLASS_PROPERTY(L"IsolationLevel", L"Method")
    END_TEST_CLASS();

    TEST_METHOD(TestUtf8WriteFileInvalid);

    TEST_METHOD(TestWriteFileRaw);
    TEST_METHOD(TestWriteFileProcessed);

    BEGIN_TEST_METHOD(TestWriteFileWrapEOL)
        TEST_METHOD_PROPERTY(L"Data:fFlagOn", L"{true, false}")
    END_TEST_METHOD();

    BEGIN_TEST_METHOD(TestWriteFileVTProcessing)
        TEST_METHOD_PROPERTY(L"Data:fVtOn", L"{true, false}")
        TEST_METHOD_PROPERTY(L"Data:fProcessedOn", L"{true, false}")
    END_TEST_METHOD();

    BEGIN_TEST_METHOD(TestWriteFileDisableNewlineAutoReturn)
        TEST_METHOD_PROPERTY(L"Data:fDisableAutoReturn", L"{true, false}")
        TEST_METHOD_PROPERTY(L"Data:fProcessedOn", L"{true, false}")
    END_TEST_METHOD();

    TEST_METHOD(TestWriteFileSuspended);

    TEST_METHOD(TestReadFileBasic);
    TEST_METHOD(TestReadFileBasicSync);
    TEST_METHOD(TestReadFileBasicEmpty);
    TEST_METHOD(TestReadFileLine);
    TEST_METHOD(TestReadFileLineSync);

    TEST_CLASS_SETUP(ClassSetup);
    TEST_CLASS_CLEANUP(ClassCleanup);

    TEST_METHOD_SETUP(MethodSetup);
    TEST_METHOD_CLEANUP(MethodCleanup);

    /*BEGIN_TEST_METHOD(TestReadFileEcho)
        TEST_METHOD_PROPERTY(L"Data:fUseBlockedRead", L"{true, false}")
    END_TEST_METHOD();*/
};

static HANDLE _cancellationEvent = nullptr;

bool FileTests::ClassSetup()
{
    _cancellationEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
    VERIFY_WIN32_BOOL_SUCCEEDED(!!_cancellationEvent, L"Create cancellation event.");
    return true;
}

bool FileTests::ClassCleanup()
{
    VERIFY_WIN32_BOOL_SUCCEEDED(CloseHandle(_cancellationEvent), L"Cleanup cancellation event.");
    return true;
}

bool FileTests::MethodSetup()
{
    VERIFY_WIN32_BOOL_SUCCEEDED(ResetEvent(_cancellationEvent), L"Reset cancellation event.");
    return true;
}

bool FileTests::MethodCleanup()
{
    VERIFY_WIN32_BOOL_SUCCEEDED(SetEvent(_cancellationEvent), L"Set cancellation event.");
    return true;
}

void FileTests::TestUtf8WriteFileInvalid()
{
    Log::Comment(L"Backup original console codepage.");
    const auto uiOriginalCP = GetConsoleOutputCP();
    auto restoreOriginalCP = wil::scope_exit([&] {
        Log::Comment(L"Restore original console codepage.");
        SetConsoleOutputCP(uiOriginalCP);
    });

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleOutputCP(CP_UTF8), L"Set output codepage to UTF8");

    DWORD dwWritten;
    DWORD dwExpectedWritten;
    const char* str;
    DWORD cbStr;

    // \x80 is an invalid UTF-8 continuation
    // \x40 is the @ symbol which is valid.
    str = "\x80\x40";
    cbStr = (DWORD)strlen(str);
    dwWritten = 0;
    dwExpectedWritten = cbStr;

    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr));
    VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten);

    // \x80 is an invalid UTF-8 continuation
    // \x40 is the @ symbol which is valid.
    str = "\x80\x40\x40";
    cbStr = (DWORD)strlen(str);
    dwWritten = 0;
    dwExpectedWritten = cbStr;

    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr));
    VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten);

    // \x80 is an invalid UTF-8 continuation
    // \x40 is the @ symbol which is valid.
    str = "\x80\x80\x80\x40";
    cbStr = (DWORD)strlen(str);
    dwWritten = 0;
    dwExpectedWritten = cbStr;

    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, str, cbStr, &dwWritten, nullptr));
    VERIFY_ARE_EQUAL(dwExpectedWritten, dwWritten);
}

void FileTests::TestWriteFileRaw()
{
    // \x7 is bell
    // \x8 is backspace
    // \x9 is tab
    // \xa is linefeed
    // \xd is carriage return
    // All should be ignored/printed in raw mode.
    auto strTest = "z\x7y\x8z\x9y\xaz\xdy";
    const auto cchTest = (DWORD)strlen(strTest);
    String strReadBackExpected(strTest);

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 };
    csbiexBefore.cbSize = sizeof(csbiexBefore);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Retrieve screen buffer properties before writing.");

    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, 0), L"Set raw write mode.");

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexBefore.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    DWORD dwWritten = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, strTest, cchTest, &dwWritten, nullptr), L"Write text into buffer using WriteFile");
    VERIFY_ARE_EQUAL(cchTest, dwWritten);

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
    csbiexAfter.cbSize = sizeof(csbiexAfter);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Retrieve screen buffer properties after writing.");

    csbiexBefore.dwCursorPosition.X += (SHORT)cchTest;
    VERIFY_ARE_EQUAL(csbiexBefore.dwCursorPosition, csbiexAfter.dwCursorPosition, L"Verify cursor moved expected number of squares for the write length.");

    const auto cbReadBackBuffer = cchTest + 2; // +1 so we can read back a "space" that should be after what we wrote. +1 more so this can be null terminated for String class comparison.
    auto strReadBack = wil::make_unique_failfast<char[]>(cbReadBackBuffer);
    ZeroMemory(strReadBack.get(), cbReadBackBuffer * sizeof(char));

    DWORD dwRead = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, strReadBack.get(), cchTest + 1, coordZero, &dwRead), L"Read back the data in the buffer.");
    // +1 to read back the space that should be after the text we wrote

    strReadBackExpected += " "; // add in the space that should appear after the written text (buffer should be space filled when empty)

    VERIFY_ARE_EQUAL(strReadBackExpected, String(strReadBack.get()), L"Ensure that the buffer contents match what we expected based on what we wrote.");
}

void WriteFileHelper(HANDLE hOut,
                     CONSOLE_SCREEN_BUFFER_INFOEX& csbiexBefore,
                     CONSOLE_SCREEN_BUFFER_INFOEX& csbiexAfter,
                     PCSTR psTest,
                     DWORD cchTest)
{
    csbiexBefore.cbSize = sizeof(csbiexBefore);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Retrieve screen buffer properties before writing.");

    DWORD dwWritten = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, psTest, cchTest, &dwWritten, nullptr), L"Write text into buffer using WriteFile");
    VERIFY_ARE_EQUAL(cchTest, dwWritten, L"Verify all characters were written.");

    csbiexAfter.cbSize = sizeof(csbiexAfter);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Retrieve screen buffer properties after writing.");
}

void ReadBackHelper(HANDLE hOut,
                    COORD coordReadBackPos,
                    DWORD dwReadBackLength,
                    wistd::unique_ptr<char[]>& pszReadBack)
{
    // Add one so it can be zero terminated.
    auto cbBuffer = dwReadBackLength + 1;
    auto pszRead = wil::make_unique_failfast<char[]>(cbBuffer);
    ZeroMemory(pszRead.get(), cbBuffer * sizeof(char));

    DWORD dwRead = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(ReadConsoleOutputCharacterA(hOut, pszRead.get(), dwReadBackLength, coordReadBackPos, &dwRead), L"Read back data in the buffer.");
    VERIFY_ARE_EQUAL(dwReadBackLength, dwRead, L"Verify API reports we read back the number of characters we asked for.");

    pszReadBack.swap(pszRead);
}

void FileTests::TestWriteFileProcessed()
{
    // \x7 is bell
    // \x8 is backspace
    // \x9 is tab
    // \xa is linefeed
    // \xd is carriage return
    // All should cause activity in processed mode.

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, ENABLE_PROCESSED_OUTPUT), L"Set processed write mode.");

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    // Declare variables needed for each character test.
    CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 };
    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
    COORD coordExpected = { 0 };
    PCSTR pszTest;
    DWORD cchTest;
    PCSTR pszReadBackExpected;
    DWORD cchReadBack;
    wistd::unique_ptr<char[]> pszReadBack;

    // 1. Test bell (\x7)
    {
        pszTest = "z\x7";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "z ";
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write z and a bell. Cursor should move once as bell should have made audible noise (can't really test) and not moved or printed anything.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X += 1;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved once for printable character and not for bell.");

        // Read back written data.
        ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }

    // 2. Test backspace (\x8)
    {
        pszTest = "yx\x8";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "yx ";
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write two characters and a backspace. Cursor should move only one forward as the backspace should have moved the cursor back one after printing the second character.
        // The backspace character itself is typically non-destructive so it should only affect the cursor, not the buffer contents.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X += 1;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved twice forward for printable characters and once backward for backspace.");

        // Read back written data.
        ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }

    // 3. Test tab (\x9)
    {
        // The tab character will space pad out the buffer to the next multiple-of-8 boundary.
        // NOTE: This is dependent on the previous tests running first.
        pszTest = "\x9";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "     ";
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write tab character. Cursor should move out to the next multiple-of-8 and leave space characters in its wake.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X = 8;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved forward to position 8 for tab.");

        // Read back written data.
        ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }

    // 4. Test linefeed (\xa)
    {
        // The line feed character should move us down to the next line.
        pszTest = "\xaQ";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "Q ";
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write line feed character. Cursor should move down a line and then the Q from our string should be printed.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected.X = 1;
        coordExpected.Y = 1;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved down a line and then one character over for linefeed + Q.");

        // Read back written data from the 2nd line.
        COORD coordRead;
        coordRead.Y = 1;
        coordRead.X = 0;
        ReadBackHelper(hOut, coordRead, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }

    // 5. Test carriage return (\xd)
    {
        // The carriage return character should move us to the front of the line.
        pszTest = "J\xd";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "QJ "; // J written, then move to beginning of line.
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write text and carriage return character. Cursor should end up at the beginning of this line. The J should have been printed in the line before we moved.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X = 0;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved to beginning of line for carriage return character.");

        // Read back text written from the 2nd line.
        ReadBackHelper(hOut, csbiexAfter.dwCursorPosition, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }

    // 6. Print a character over the top of the existing
    {
        // After the carriage return, try typing on top of the Q with a K
        pszTest = "K";
        cchTest = (DWORD)strlen(pszTest);
        pszReadBackExpected = "KJ "; // NOTE: This is based on the previous test(s).
        cchReadBack = (DWORD)strlen(pszReadBackExpected);

        // Write text. Cursor should end up on top of the J.
        WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTest, cchTest);
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X += 1;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved over one for printing character.");

        // Read back text written from the 2nd line.
        ReadBackHelper(hOut, csbiexBefore.dwCursorPosition, cchReadBack, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify text matches what we expected to be written into the buffer.");
    }
}

void FileTests::TestWriteFileWrapEOL()
{
    bool fFlagOn;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fFlagOn", fFlagOn));

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test.");

    if (fFlagOn)
    {
        VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, ENABLE_WRAP_AT_EOL_OUTPUT), L"Set wrap at EOL.");
    }
    else
    {
        VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, 0), L"Make sure wrap at EOL is off.");
    }

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    // Fill first row of the buffer with Z characters until 1 away from the end.
    for (SHORT i = 0; i < csbiexOriginal.dwSize.X - 1; i++)
    {
        WriteFile(hOut, "Z", 1, nullptr, nullptr);
    }

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 };
    csbiexBefore.cbSize = sizeof(csbiexBefore);
    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
    csbiexAfter.cbSize = sizeof(csbiexAfter);
    COORD coordExpected = { 0 };

    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexBefore), L"Get cursor position information before attempting to wrap at end of line.");
    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "Y", 1, nullptr, nullptr), L"Write of final character in line succeeded.");
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Get cursor position information after attempting to wrap at end of line.");

    if (fFlagOn)
    {
        Log::Comment(L"Cursor should go down a row if we tried to print at end of line.");
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.Y++;
        coordExpected.X = 0;
    }
    else
    {
        Log::Comment(L"Cursor shouldn't move when printing at end of line.");
        coordExpected = csbiexBefore.dwCursorPosition;
    }

    VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved as expected based on flag state.");
}

void FileTests::TestWriteFileVTProcessing()
{
    bool fVtOn;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fVtOn", fVtOn));

    bool fProcessedOn;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fProcessedOn", fProcessedOn));

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test.");

    DWORD dwFlags = 0;
    WI_SetFlagIf(dwFlags, ENABLE_VIRTUAL_TERMINAL_PROCESSING, fVtOn);
    WI_SetFlagIf(dwFlags, ENABLE_PROCESSED_OUTPUT, fProcessedOn);
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwFlags), L"Turn on relevant flags for test.");

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    auto pszTestString = "\x1b"
                         "[14m";
    const auto cchTest = (DWORD)strlen(pszTestString);

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 };
    csbiexBefore.cbSize = sizeof(csbiexBefore);
    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
    csbiexAfter.cbSize = sizeof(csbiexAfter);

    WriteFileHelper(hOut, csbiexBefore, csbiexAfter, pszTestString, cchTest);

    // We only expect characters to be processed and not printed if both processed mode and VT mode are on.
    const auto fProcessedNotPrinted = fProcessedOn && fVtOn;

    if (fProcessedNotPrinted)
    {
        auto pszReadBackExpected = "      ";
        const auto cchReadBackExpected = (DWORD)strlen(pszReadBackExpected);

        VERIFY_ARE_EQUAL(csbiexBefore.dwCursorPosition, csbiexAfter.dwCursorPosition, L"Verify cursor didn't move because the VT sequence was processed instead of printed.");

        wistd::unique_ptr<char[]> pszReadBack;
        ReadBackHelper(hOut, coordZero, cchReadBackExpected, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszReadBackExpected), String(pszReadBack.get()), L"Verify that nothing was printed into the buffer.");
    }
    else
    {
        auto coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X += (SHORT)cchTest;
        VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Verify cursor moved as characters should have been emitted, not consumed.");

        wistd::unique_ptr<char[]> pszReadBack;
        ReadBackHelper(hOut, coordZero, cchTest, pszReadBack);
        VERIFY_ARE_EQUAL(String(pszTestString), String(pszReadBack.get()), L"Verify that original test string was printed into the buffer.");
    }
}

void FileTests::TestWriteFileDisableNewlineAutoReturn()
{
    bool fDisableAutoReturn;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fDisableAutoReturn", fDisableAutoReturn));

    bool fProcessedOn;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fProcessedOn", fProcessedOn));

    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test.");

    DWORD dwMode = 0;
    WI_SetFlagIf(dwMode, DISABLE_NEWLINE_AUTO_RETURN, fDisableAutoReturn);
    WI_SetFlagIf(dwMode, ENABLE_PROCESSED_OUTPUT, fProcessedOn);
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwMode), L"Set console mode for test.");

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexBefore = { 0 };
    csbiexBefore.cbSize = sizeof(csbiexBefore);
    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
    csbiexAfter.cbSize = sizeof(csbiexAfter);
    COORD coordExpected = { 0 };

    WriteFileHelper(hOut, csbiexBefore, csbiexAfter, "abc", 3);
    coordExpected = csbiexBefore.dwCursorPosition;
    coordExpected.X += 3;
    VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Cursor should have moved right to the end of the text written.");

    WriteFileHelper(hOut, csbiexBefore, csbiexAfter, "\n", 1);

    if (fProcessedOn)
    {
        if (fDisableAutoReturn)
        {
            coordExpected = csbiexBefore.dwCursorPosition;
            coordExpected.Y += 1;
        }
        else
        {
            coordExpected = csbiexBefore.dwCursorPosition;
            coordExpected.Y += 1;
            coordExpected.X = 0;
        }
    }
    else
    {
        coordExpected = csbiexBefore.dwCursorPosition;
        coordExpected.X += 1;
    }

    VERIFY_ARE_EQUAL(coordExpected, csbiexAfter.dwCursorPosition, L"Cursor should move to expected position.");
}

void SendKeyHelper(HANDLE hIn, WORD vk)
{
    INPUT_RECORD irPause = { 0 };
    irPause.EventType = KEY_EVENT;
    irPause.Event.KeyEvent.bKeyDown = TRUE;
    irPause.Event.KeyEvent.wVirtualKeyCode = vk;

    DWORD dwWritten = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputW(hIn, &irPause, 1u, &dwWritten), L"Key event sent.");
}

void PauseHelper(HANDLE hIn)
{
    SendKeyHelper(hIn, VK_PAUSE);
}

void UnpauseHelper(HANDLE hIn)
{
    SendKeyHelper(hIn, VK_ESCAPE);
}

void FileTests::TestWriteFileSuspended()
{
    const auto hOut = GetStdOutputHandle();
    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");

    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve screen buffer properties at beginning of test.");

    DWORD dwMode = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hOut, dwMode), L"Set console mode for test.");

    const COORD coordZero = { 0 };
    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"Cursor should be at 0,0 in fresh buffer.");

    VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "abc", 3, nullptr, nullptr), L"Test first write success.");
    PauseHelper(hIn);

    auto BlockedWrite = std::async([&] {
        Log::Comment(L"Background WriteFile scheduled.");
        VERIFY_WIN32_BOOL_SUCCEEDED(WriteFile(hOut, "def", 3, nullptr, nullptr), L"Test second write success.");
    });

    UnpauseHelper(hIn);

    BlockedWrite.wait();
}

void SendFullKeyStrokeHelper(HANDLE hIn, char ch)
{
    INPUT_RECORD ir[2];
    ZeroMemory(ir, ARRAYSIZE(ir) * sizeof(INPUT_RECORD));
    ir[0].EventType = KEY_EVENT;
    ir[0].Event.KeyEvent.bKeyDown = TRUE;
    ir[0].Event.KeyEvent.dwControlKeyState = ch < 0x20 ? LEFT_CTRL_PRESSED : 0; // set left_ctrl_pressed for control keys.
    ir[0].Event.KeyEvent.uChar.AsciiChar = ch;
    ir[0].Event.KeyEvent.wVirtualKeyCode = VkKeyScanA(ir[0].Event.KeyEvent.uChar.AsciiChar);
    ir[0].Event.KeyEvent.wVirtualScanCode = (WORD)MapVirtualKeyA(ir[0].Event.KeyEvent.wVirtualKeyCode, MAPVK_VK_TO_VSC);
    ir[0].Event.KeyEvent.wRepeatCount = 1;
    ir[1] = ir[0];
    ir[1].Event.KeyEvent.bKeyDown = FALSE;

    DWORD dwWritten = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(WriteConsoleInputA(hIn, ir, (DWORD)ARRAYSIZE(ir), &dwWritten), L"Writing key stroke.");
    VERIFY_ARE_EQUAL((DWORD)ARRAYSIZE(ir), dwWritten, L"Written matches expected.");
}

void FileTests::TestReadFileBasic()
{
    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    DWORD dwMode = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");

    auto ch = '\0';
    Log::Comment(L"Queue background blocking read file operation.");
    auto BackgroundRead = std::async([&] {
        DWORD dwRead = 0;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful.");
        VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character.");
    });

    const auto chExpected = 'a';
    Log::Comment(L"Send a key into the console.");
    SendFullKeyStrokeHelper(hIn, chExpected);

    Log::Comment(L"Wait for background to unblock.");
    BackgroundRead.wait();
    VERIFY_ARE_EQUAL(chExpected, ch);
}

void FileTests::TestReadFileBasicSync()
{
    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    DWORD dwMode = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");

    const auto chExpected = 'a';
    Log::Comment(L"Send a key into the console.");
    SendFullKeyStrokeHelper(hIn, chExpected);

    auto ch = '\0';
    Log::Comment(L"Read with synchronous blocking read.");
    DWORD dwRead = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful.");
    VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character.");

    VERIFY_ARE_EQUAL(chExpected, ch);
}

void FileTests::TestReadFileBasicEmpty()
{
    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    DWORD dwMode = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");

    auto ch = '\0';
    Log::Comment(L"Queue background blocking read file operation.");
    auto BackgroundRead = std::async([&] {
        DWORD dwRead = 0;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful.");
        VERIFY_ARE_EQUAL(0u, dwRead, L"We should have read nothing back. It should just return from Ctrl+Z");
    });

    const auto chExpected = '\x1a'; // ctrl+z character
    Log::Comment(L"Send a key into the console.");
    SendFullKeyStrokeHelper(hIn, chExpected);

    Log::Comment(L"Wait for background to unblock.");
    BackgroundRead.wait();
    VERIFY_ARE_EQUAL('\0', ch);
}

void FileTests::TestReadFileLine()
{
    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    DWORD dwMode = ENABLE_LINE_INPUT;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");

    auto ch = '\0';
    Log::Comment(L"Queue background blocking read file operation.");
    auto BackgroundRead = std::async([&] {
        DWORD dwRead = 0;
        VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, &dwRead, nullptr), L"Read file was successful.");
        VERIFY_ARE_EQUAL(1u, dwRead, L"Verify we read 1 character.");
    });

    const auto chExpected = 'a';
    Log::Comment(L"Send a key into the console.");
    SendFullKeyStrokeHelper(hIn, chExpected);

    auto status = BackgroundRead.wait_for(std::chrono::milliseconds(250));
    VERIFY_ARE_EQUAL(std::future_status::timeout, status, L"We should still be waiting for a result.");
    VERIFY_ARE_EQUAL('\0', ch, L"Character shouldn't be filled by background read yet.");

    Log::Comment(L"Send a line feed character, we should stay blocked.");
    SendFullKeyStrokeHelper(hIn, '\n');
    status = BackgroundRead.wait_for(std::chrono::milliseconds(250));
    VERIFY_ARE_EQUAL(std::future_status::timeout, status, L"We should still be waiting for a result.");
    VERIFY_ARE_EQUAL('\0', ch, L"Character shouldn't be filled by background read yet.");

    Log::Comment(L"Now send a carriage return into the console to signify the end of the input line.");
    SendFullKeyStrokeHelper(hIn, '\r');

    Log::Comment(L"Wait for background thread to unblock.");
    BackgroundRead.wait();
    VERIFY_ARE_EQUAL(chExpected, ch);
}

void FileTests::TestReadFileLineSync()
{
    const auto hIn = GetStdInputHandle();
    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");

    DWORD dwMode = ENABLE_LINE_INPUT;
    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");

    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");

    const auto chExpected = 'a';
    Log::Comment(L"Send a key into the console followed by a carriage return.");
    SendFullKeyStrokeHelper(hIn, chExpected);
    SendFullKeyStrokeHelper(hIn, '\r');

    auto ch = '\0';
    Log::Comment(L"Read back the input with a synchronous blocking read.");
    DWORD dwRead = 0;
    VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, nullptr, nullptr), L"Read file was successful.");
    VERIFY_ARE_EQUAL(0u, dwRead, L"Verify we read 0 characters.");

    VERIFY_ARE_EQUAL(chExpected, ch);
}

//void FileTests::TestReadFileEcho()
//{
//    bool fUseBlockedRead;
//    VERIFY_SUCCEEDED(TestData::TryGetValue(L"fUseBlockedRead", fUseBlockedRead));
//
//    const auto hOut = GetStdOutputHandle();
//    VERIFY_IS_NOT_NULL(hOut, L"Verify we have the standard output handle.");
//
//    const auto hIn = GetStdInputHandle();
//    VERIFY_IS_NOT_NULL(hIn, L"Verify we have the standard input handle.");
//
//    DWORD dwMode = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT;
//    VERIFY_WIN32_BOOL_SUCCEEDED(SetConsoleMode(hIn, dwMode), L"Set input mode for test.");
//
//    VERIFY_WIN32_BOOL_SUCCEEDED(FlushConsoleInputBuffer(hIn), L"Flush input buffer in preparation for test.");
//
//    CONSOLE_SCREEN_BUFFER_INFOEX csbiexOriginal = { 0 };
//    csbiexOriginal.cbSize = sizeof(csbiexOriginal);
//    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexOriginal), L"Retrieve output screen buffer information.");
//
//    const COORD coordZero = { 0 };
//    VERIFY_ARE_EQUAL(coordZero, csbiexOriginal.dwCursorPosition, L"We expect the cursor to be at 0,0 for the start of this test.");
//
//    char ch = '\0';
//    std::future<void> BackgroundRead;
//    if (fUseBlockedRead)
//    {
//        Log::Comment(L"Queue background blocking read file operation.");
//        BackgroundRead = std::async([&] {
//            OVERLAPPED overlapped = { 0 };
//            wil::unique_event evt;
//            evt.create();
//            overlapped.hEvent = evt.get();
//
//            DWORD dwRead = 0;
//            VERIFY_WIN32_BOOL_SUCCEEDED(ReadFile(hIn, &ch, 1, nullptr, &overlapped), L"Read file was dispatched successfully.");
//
//            std::array<HANDLE, 2> handles;
//            handles[0] = _cancellationEvent;
//            handles[1] = overlapped.hEvent;
//
//            WaitForMultipleObjects(2, handles.data(), FALSE, INFINITE);
//            Log::Comment(L"Wait complete.");
//
//            VERIFY_ARE_EQUAL(0u, dwRead, L"Verify we read 0 characters.");
//        });
//    }
//
//    Log::Comment(L"Read back the first line of the buffer to see that it is empty.");
//    wistd::unique_ptr<char[]> pszBefore;
//    PCSTR pszBeforeExpected = "     ";
//    const auto cchBeforeExpected = (DWORD)strlen(pszBeforeExpected);
//    ReadBackHelper(hOut, coordZero, cchBeforeExpected, pszBefore);
//    VERIFY_ARE_EQUAL(String(pszBeforeExpected), String(pszBefore.get()), L"Verify the first few characters of the buffer are empty (spaces)");
//
//    PCSTR pszAfterExpected = "qzmp ";
//    COORD coordCursorAfter = { 0 };
//    const auto cchAfterExpected = (DWORD)strlen(pszAfterExpected);
//
//    Log::Comment(L"Now write in a few input characters to the buffer.");
//    for (DWORD i = 0; i < cchAfterExpected - 1; i++)
//    {
//        SendFullKeyStrokeHelper(hIn, pszAfterExpected[i]);
//        coordCursorAfter.X++;
//    }
//
//    Log::Comment(L"Read back the first line of the buffer to see if we've echoed characters.");
//    wistd::unique_ptr<char[]> pszAfter;
//    ReadBackHelper(hOut, coordZero, cchAfterExpected, pszAfter);
//
//    if (fUseBlockedRead)
//    {
//        VERIFY_ARE_EQUAL(String(pszAfterExpected), String(pszAfter.get()), L"Verify the characters written were echoed into the buffer.");
//    }
//    else
//    {
//        VERIFY_ARE_EQUAL(String(pszBeforeExpected), String(pszAfter.get()), L"Verify nothing should have been printed while no one was waiting on a read.");
//    }
//
//    CONSOLE_SCREEN_BUFFER_INFOEX csbiexAfter = { 0 };
//    csbiexAfter.cbSize = sizeof(csbiexAfter);
//    VERIFY_WIN32_BOOL_SUCCEEDED(GetConsoleScreenBufferInfoEx(hOut, &csbiexAfter), L"Get the cursor position after the writes.");
//
//    if (fUseBlockedRead)
//    {
//        VERIFY_ARE_EQUAL(coordCursorAfter, csbiexAfter.dwCursorPosition, L"Cursor should have moved with the writes.");
//    }
//    else
//    {
//        VERIFY_ARE_EQUAL(coordZero, csbiexAfter.dwCursorPosition, L"Cursor shouldn't move if no one is waiting with a read.");
//    }
//
//    if (fUseBlockedRead)
//    {
//        Log::Comment(L"Send newline to unblock the read.");
//        SendFullKeyStrokeHelper(hIn, '\r');
//        BackgroundRead.wait();
//    }
//}
