// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
//
// This test class creates an in-proc conpty host as well as a Terminal, to
// validate that strings written to the conpty create the same response on the
// terminal end. Tests can be written that validate both the contents of the
// host buffer as well as the terminal buffer. Every time that
// `renderer.PaintFrame()` is called, the tests will validate the expected
// output, and then flush the output of the VtEngine straight to the Terminal.

#include "pch.h"
#include "../../types/inc/Viewport.hpp"
#include "../../types/inc/convert.hpp"

#include "../renderer/inc/DummyRenderer.hpp"
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/vt/Xterm256Engine.hpp"
#include "../../renderer/vt/XtermEngine.hpp"

#include "../host/inputBuffer.hpp"
#include "../host/readDataCooked.hpp"
#include "../host/output.h"
#include "../host/_stream.h" // For WriteCharsLegacy
#include "../host/cmdline.h" // For WC_LIMIT_BACKSPACE
#include "test/CommonState.hpp"

#include "../cascadia/TerminalCore/Terminal.hpp"

#include "../../inc/TestUtils.h"

using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace Microsoft::Console::Types;
using namespace Microsoft::Console::Interactivity;
using namespace Microsoft::Console::VirtualTerminal;

using namespace Microsoft::Console;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;

using namespace Microsoft::Terminal::Core;

namespace TerminalCoreUnitTests
{
    class ConptyRoundtripTests;
};
using namespace TerminalCoreUnitTests;

class TerminalCoreUnitTests::ConptyRoundtripTests final
{
    // !!! DANGER: Many tests in this class expect the Terminal and Host buffers
    // to be 80x32. If you change these, you'll probably inadvertently break a
    // bunch of tests !!!
    static const til::CoordType TerminalViewWidth = 80;
    static const til::CoordType TerminalViewHeight = 32;

    // This test class is for tests that are supposed to emit something in the PTY layer
    // and then check that they've been staged for presentation correctly inside
    // the Terminal application. Which sequences were used to get here don't matter.
    TEST_CLASS(ConptyRoundtripTests);

    TEST_CLASS_SETUP(ClassSetup)
    {
        m_state = std::make_unique<CommonState>();

        m_state->InitEvents();
        m_state->PrepareGlobalFont();
        m_state->PrepareGlobalInputBuffer();
        m_state->PrepareGlobalRenderer();
        m_state->PrepareGlobalScreenBuffer(TerminalViewWidth, TerminalViewHeight, TerminalViewWidth, TerminalViewHeight);

        return true;
    }

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

        m_state.release();

        return true;
    }

    TEST_METHOD_SETUP(MethodSetup)
    {
        // STEP 1: Set up the Terminal
        term = std::make_unique<Terminal>();
        emptyRenderer = std::make_unique<DummyRenderer>(term.get());
        term->Create({ TerminalViewWidth, TerminalViewHeight }, 100, *emptyRenderer);

        // STEP 2: Set up the Conpty

        // Set up some sane defaults
        auto& g = ServiceLocator::LocateGlobals();
        auto& gci = g.getConsoleInformation();

        gci.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, INVALID_COLOR);
        gci.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, INVALID_COLOR);
        gci.SetFillAttribute(0x07); // DARK_WHITE on DARK_BLACK
        gci.CalculateDefaultColorIndices();

        m_state->PrepareNewTextBufferInfo(true, TerminalViewWidth, TerminalViewHeight);
        auto& currentBuffer = gci.GetActiveOutputBuffer();
        // Make sure a test hasn't left us in the alt buffer on accident
        VERIFY_IS_FALSE(currentBuffer._IsAltBuffer());
        VERIFY_SUCCEEDED(currentBuffer.SetViewportOrigin(true, { 0, 0 }, true));
        VERIFY_ARE_EQUAL(til::point{}, currentBuffer.GetTextBuffer().GetCursor().GetPosition());

        // Set up an xterm-256 renderer for conpty
        auto hFile = wil::unique_hfile(INVALID_HANDLE_VALUE);
        auto initialViewport = currentBuffer.GetViewport();

        auto vtRenderEngine = std::make_unique<Xterm256Engine>(std::move(hFile),
                                                               initialViewport);
        auto pfn = std::bind(&ConptyRoundtripTests::_writeCallback, this, std::placeholders::_1, std::placeholders::_2);
        vtRenderEngine->SetTestCallback(pfn);

        // Enable the resize quirk, as the Terminal is going to be reacting as if it's enabled.
        vtRenderEngine->SetResizeQuirk(true);

        // Configure the OutputStateMachine's _pfnFlushToTerminal
        // Use OutputStateMachineEngine::SetTerminalConnection
        g.pRender->AddRenderEngine(vtRenderEngine.get());
        gci.GetActiveOutputBuffer().SetTerminalConnection(vtRenderEngine.get());

        _pConApi = std::make_unique<ConhostInternalGetSet>(gci);

        // Manually set the console into conpty mode. We're not actually going
        // to set up the pipes for conpty, but we want the console to behave
        // like it would in conpty mode.
        g.EnableConptyModeForTests(std::move(vtRenderEngine));

        expectedOutput.clear();
        _checkConptyOutput = true;
        _logConpty = false;

        VERIFY_ARE_EQUAL(gci.GetActiveOutputBuffer().GetViewport().Dimensions(),
                         gci.GetActiveOutputBuffer().GetBufferSize().Dimensions(),
                         L"If this test fails, then there's a good chance "
                         L"another test resized the buffer but didn't use IsolationLevel:Method");
        VERIFY_ARE_EQUAL(gci.GetActiveOutputBuffer().GetViewport(),
                         gci.GetActiveOutputBuffer().GetBufferSize(),
                         L"If this test fails, then there's a good chance "
                         L"another test resized the buffer but didn't use IsolationLevel:Method");

        return true;
    }

    TEST_METHOD_CLEANUP(MethodCleanup)
    {
        m_state->CleanupNewTextBufferInfo();

        auto& engines = ServiceLocator::LocateGlobals().pRender->_engines;
        std::fill(engines.begin(), engines.end(), nullptr);

        VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Tests should drain all the output they push into the expected output buffer.");

        emptyRenderer = nullptr;
        term = nullptr;

        return true;
    }

    TEST_METHOD(ConptyOutputTestCanary);
    TEST_METHOD(SimpleWriteOutputTest);
    TEST_METHOD(WriteTwoLinesUsesNewline);
    TEST_METHOD(WriteAFewSimpleLines);

    TEST_METHOD(PassthroughClearScrollback);

    TEST_METHOD(PassthroughClearAll);

    TEST_METHOD(PassthroughHardReset);

    TEST_METHOD(PassthroughCursorShapeImmediately);

    TEST_METHOD(PassthroughDECCTR);

    TEST_METHOD(TestWrappingALongString);
    TEST_METHOD(TestAdvancedWrapping);
    TEST_METHOD(TestExactWrappingWithoutSpaces);
    TEST_METHOD(TestExactWrappingWithSpaces);

    TEST_METHOD(MoveCursorAtEOL);

    TEST_METHOD(TestResizeHeight);

    TEST_METHOD(OutputWrappedLinesAtTopOfBuffer);
    TEST_METHOD(OutputWrappedLinesAtBottomOfBuffer);
    TEST_METHOD(ScrollWithChangesInMiddle);
    TEST_METHOD(DontWrapMoveCursorInSingleFrame);
    TEST_METHOD(ClearHostTrickeryTest);
    TEST_METHOD(OverstrikeAtBottomOfBuffer);
    TEST_METHOD(MarginsWithStatusLine);
    TEST_METHOD(OutputWrappedLineWithSpace);
    TEST_METHOD(OutputWrappedLineWithSpaceAtBottomOfBuffer);

    TEST_METHOD(BreakLinesOnCursorMovement);

    TEST_METHOD(ScrollWithMargins);

    TEST_METHOD(TestCursorInDeferredEOLPositionOnNewLineWithSpaces);

    TEST_METHOD(ResizeRepaintVimExeBuffer);

    TEST_METHOD(ClsAndClearHostClearsScrollbackTest);

    TEST_METHOD(TestResizeWithCookedRead);

    TEST_METHOD(NewLinesAtBottomWithBackground);

    TEST_METHOD(WrapNewLineAtBottom);
    TEST_METHOD(WrapNewLineAtBottomLikeMSYS);

    TEST_METHOD(DeleteWrappedWord);

    TEST_METHOD(HyperlinkIdConsistency);

    TEST_METHOD(ResizeInitializeBufferWithDefaultAttrs);

    TEST_METHOD(ClearBufferSignal);

    TEST_METHOD(SimpleAltBufferTest);
    TEST_METHOD(AltBufferToAltBufferTest);

    TEST_METHOD(TestPowerLineFirstFrame);

    TEST_METHOD(AltBufferResizeCrash);

    TEST_METHOD(TestNoExtendedAttrsOptimization);
    TEST_METHOD(TestNoBackgroundAttrsOptimization);

private:
    bool _writeCallback(const char* const pch, const size_t cch);
    void _flushFirstFrame();
    void _resizeConpty(const til::CoordType sx, const til::CoordType sy);
    void _clearConpty();

    [[nodiscard]] std::tuple<TextBuffer*, TextBuffer*> _performResize(const til::size newSize);

    std::deque<std::string> expectedOutput;

    std::unique_ptr<CommonState> m_state;
    std::unique_ptr<Microsoft::Console::VirtualTerminal::ITerminalApi> _pConApi;

    // Tests can set these variables how they link to configure the behavior of the test harness.
    bool _checkConptyOutput{ true }; // If true, the test class will check that the output from conpty was expected
    bool _logConpty{ false }; // If true, the test class will log all the output from conpty. Helpful for debugging.

    std::unique_ptr<DummyRenderer> emptyRenderer;
    std::unique_ptr<Terminal> term;

    ApiRoutines _apiRoutines;
};

bool ConptyRoundtripTests::_writeCallback(const char* const pch, const size_t cch)
{
    auto actualString = std::string(pch, cch);

    if (_checkConptyOutput)
    {
        VERIFY_IS_GREATER_THAN(expectedOutput.size(),
                               static_cast<size_t>(0),
                               NoThrowString().Format(L"writing=\"%hs\", expecting %u strings", TestUtils::ReplaceEscapes(actualString).c_str(), expectedOutput.size()));

        auto first = expectedOutput.front();
        expectedOutput.pop_front();

        Log::Comment(NoThrowString().Format(L"Expected =\t\"%hs\"", TestUtils::ReplaceEscapes(first).c_str()));
        Log::Comment(NoThrowString().Format(L"Actual =\t\"%hs\"", TestUtils::ReplaceEscapes(actualString).c_str()));

        VERIFY_ARE_EQUAL(first.length(), cch);
        VERIFY_ARE_EQUAL(first, actualString);
    }
    else if (_logConpty)
    {
        Log::Comment(NoThrowString().Format(
            L"Writing \"%hs\" to Terminal", TestUtils::ReplaceEscapes(actualString).c_str()));
    }

    // Write the string back to our Terminal
    const auto converted = ConvertToW(CP_UTF8, actualString);
    term->Write(converted);

    return true;
}

void ConptyRoundtripTests::_flushFirstFrame()
{
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;

    expectedOutput.push_back("\x1b[2J");
    expectedOutput.push_back("\x1b[m");
    expectedOutput.push_back("\x1b[H"); // Go Home
    expectedOutput.push_back("\x1b[?25h");

    VERIFY_SUCCEEDED(renderer.PaintFrame());
}

void ConptyRoundtripTests::_resizeConpty(const til::CoordType sx,
                                         const til::CoordType sy)
{
    // Largely taken from implementation in PtySignalInputThread::_InputThread
    if (_pConApi->ResizeWindow(sx, sy))
    {
        auto& g = ServiceLocator::LocateGlobals();
        auto& gci = g.getConsoleInformation();
        VERIFY_SUCCEEDED(gci.GetVtIo()->SuppressResizeRepaint());
    }
}

void ConptyRoundtripTests::_clearConpty()
{
    // Taken verbatim from implementation in PtySignalInputThread::_DoClearBuffer
    auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
    THROW_IF_FAILED(gci.GetActiveOutputBuffer().ClearBuffer());
}

[[nodiscard]] std::tuple<TextBuffer*, TextBuffer*> ConptyRoundtripTests::_performResize(const til::size newSize)
{
    // IMPORTANT! Anyone calling this should make sure that the test is running
    // in IsolationLevel: Method. If you don't add that, then it might secretly
    // pollute other tests!

    Log::Comment(L"========== Resize the Terminal and conpty ==========");

    auto resizeResult = term->UserResize(newSize);
    VERIFY_SUCCEEDED(resizeResult);
    _resizeConpty(newSize.width, newSize.height);

    // After we resize, make sure to get the new textBuffers
    return { &ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveOutputBuffer().GetTextBuffer(),
             term->_mainBuffer.get() };
}

void ConptyRoundtripTests::ConptyOutputTestCanary()
{
    Log::Comment(NoThrowString().Format(
        L"This is a simple test to make sure that everything is working as expected."));

    _flushFirstFrame();
}

void ConptyRoundtripTests::SimpleWriteOutputTest()
{
    Log::Comment(NoThrowString().Format(
        L"Write some simple output, and make sure it gets rendered largely "
        L"unmodified to the terminal"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    expectedOutput.push_back("Hello World");
    hostSm.ProcessString(L"Hello World");

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    TestUtils::VerifyExpectedString(termTb, L"Hello World ", { 0, 0 });
}

void ConptyRoundtripTests::WriteTwoLinesUsesNewline()
{
    Log::Comment(NoThrowString().Format(
        L"Write two lines of output. We should use \r\n to move the cursor"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    hostSm.ProcessString(L"AAA");
    hostSm.ProcessString(L"\x1b[2;1H");
    hostSm.ProcessString(L"BBB");

    auto verifyData = [](TextBuffer& tb) {
        TestUtils::VerifyExpectedString(tb, L"AAA", { 0, 0 });
        TestUtils::VerifyExpectedString(tb, L"BBB", { 0, 1 });
    };

    verifyData(hostTb);

    expectedOutput.push_back("AAA");
    expectedOutput.push_back("\r\n");
    expectedOutput.push_back("BBB");

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyData(termTb);
}

void ConptyRoundtripTests::WriteAFewSimpleLines()
{
    Log::Comment(NoThrowString().Format(
        L"Write more lines of output. We should use \r\n to move the cursor"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    hostSm.ProcessString(L"AAA\n");
    hostSm.ProcessString(L"BBB\n");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"CCC");
    auto verifyData = [](TextBuffer& tb) {
        TestUtils::VerifyExpectedString(tb, L"AAA", { 0, 0 });
        TestUtils::VerifyExpectedString(tb, L"BBB", { 0, 1 });
        TestUtils::VerifyExpectedString(tb, L"   ", { 0, 2 });
        TestUtils::VerifyExpectedString(tb, L"CCC", { 0, 3 });
    };

    verifyData(hostTb);

    expectedOutput.push_back("AAA");
    expectedOutput.push_back("\r\n");
    expectedOutput.push_back("BBB");
    // Jump down to the fourth line because emitting spaces didn't do anything
    // and we will skip to emitting the CCC segment.
    expectedOutput.push_back("\x1b[4;1H");
    expectedOutput.push_back("CCC");

    // Cursor goes back on.
    expectedOutput.push_back("\x1b[?25h");

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyData(termTb);
}

void ConptyRoundtripTests::TestWrappingALongString()
{
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();
    _checkConptyOutput = false;

    const auto initialTermView = term->GetViewport();

    const auto charsToWrite = gsl::narrow_cast<til::CoordType>(TestUtils::Test100CharsString.size());
    VERIFY_ARE_EQUAL(100, charsToWrite);

    VERIFY_ARE_EQUAL(0, initialTermView.Top());
    VERIFY_ARE_EQUAL(32, initialTermView.BottomExclusive());

    hostSm.ProcessString(TestUtils::Test100CharsString);

    const auto secondView = term->GetViewport();

    VERIFY_ARE_EQUAL(0, secondView.Top());
    VERIFY_ARE_EQUAL(32, secondView.BottomExclusive());

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor wrapped to the second line
        VERIFY_ARE_EQUAL(charsToWrite % initialTermView.Width(), cursor.GetPosition().x);
        VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);

        // Verify that we marked the 0th row as _wrapped_
        const auto& row0 = tb.GetRowByOffset(0);
        VERIFY_IS_TRUE(row0.WasWrapForced());

        const auto& row1 = tb.GetRowByOffset(1);
        VERIFY_IS_FALSE(row1.WasWrapForced());

        TestUtils::VerifyExpectedString(tb, TestUtils::Test100CharsString, { 0, 0 });
    };

    verifyBuffer(hostTb);

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb);
}

void ConptyRoundtripTests::TestAdvancedWrapping()
{
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;
    const auto initialTermView = term->GetViewport();

    _flushFirstFrame();

    const auto charsToWrite = gsl::narrow_cast<til::CoordType>(TestUtils::Test100CharsString.size());
    VERIFY_ARE_EQUAL(100, charsToWrite);

    hostSm.ProcessString(TestUtils::Test100CharsString);
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"          ");
    hostSm.ProcessString(L"1234567890");

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor wrapped to the second line
        VERIFY_ARE_EQUAL(2, cursor.GetPosition().y);
        VERIFY_ARE_EQUAL(20, cursor.GetPosition().x);

        // Verify that we marked the 0th row as _wrapped_
        const auto& row0 = tb.GetRowByOffset(0);
        VERIFY_IS_TRUE(row0.WasWrapForced());

        const auto& row1 = tb.GetRowByOffset(1);
        VERIFY_IS_FALSE(row1.WasWrapForced());

        TestUtils::VerifyExpectedString(tb, TestUtils::Test100CharsString, { 0, 0 });
        TestUtils::VerifyExpectedString(tb, L"          1234567890", { 0, 2 });
    };

    verifyBuffer(hostTb);

    // First write the first 80 characters from the string
    expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)");
    // Without line breaking, write the remaining 20 chars
    expectedOutput.push_back(R"(qrstuvwxyz{|}~!"#$%&)");
    // This is the hard line break
    expectedOutput.push_back("\r\n");
    // Now write row 2 of the buffer
    expectedOutput.push_back("          1234567890");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb);
}

void ConptyRoundtripTests::TestExactWrappingWithoutSpaces()
{
    // This test (and TestExactWrappingWitSpaces) reveals a bug in the old
    // implementation.
    //
    // If a line _exactly_ wraps to the next line, we can't tell if the line
    // should really wrap, or manually break. The client app is writing a line
    // that's exactly the width of the buffer that manually breaks at the
    // end of the line, followed by another line.
    //
    // With the old PaintBufferLine interface, there's no way to know if this
    // case is because the line wrapped or not. Hence, the addition of the
    // `lineWrapped` parameter

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    const auto initialTermView = term->GetViewport();

    _flushFirstFrame();

    const auto charsToWrite = initialTermView.Width();
    VERIFY_ARE_EQUAL(80, charsToWrite);

    for (auto i = 0; i < charsToWrite; i++)
    {
        // This is a handy way of just printing the printable characters that
        // _aren't_ the space character.
        const auto wch = static_cast<wchar_t>(33 + (i % 94));
        hostSm.ProcessCharacter(wch);
    }

    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"1234567890");

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor wrapped to the second line
        VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
        VERIFY_ARE_EQUAL(10, cursor.GetPosition().x);

        // Verify that we marked the 0th row as _not wrapped_
        const auto& row0 = tb.GetRowByOffset(0);
        VERIFY_IS_FALSE(row0.WasWrapForced());

        const auto& row1 = tb.GetRowByOffset(1);
        VERIFY_IS_FALSE(row1.WasWrapForced());

        TestUtils::VerifyExpectedString(tb, LR"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)", { 0, 0 });
        TestUtils::VerifyExpectedString(tb, L"1234567890", { 0, 1 });
    };

    verifyBuffer(hostTb);

    // First write the first 80 characters from the string
    expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)");

    // This is the hard line break
    expectedOutput.push_back("\r\n");
    // Now write row 2 of the buffer
    expectedOutput.push_back("1234567890");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb);
}

void ConptyRoundtripTests::TestExactWrappingWithSpaces()
{
    // This test is also explained by the comment at the top of TestExactWrappingWithoutSpaces

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;
    const auto initialTermView = term->GetViewport();

    _flushFirstFrame();

    const auto charsToWrite = initialTermView.Width();
    VERIFY_ARE_EQUAL(80, charsToWrite);

    for (auto i = 0; i < charsToWrite; i++)
    {
        // This is a handy way of just printing the printable characters that
        // _aren't_ the space character.
        const auto wch = static_cast<wchar_t>(33 + (i % 94));
        hostSm.ProcessCharacter(wch);
    }

    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"          ");
    hostSm.ProcessString(L"1234567890");

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor wrapped to the second line
        VERIFY_ARE_EQUAL(1, cursor.GetPosition().y);
        VERIFY_ARE_EQUAL(20, cursor.GetPosition().x);

        // Verify that we marked the 0th row as _not wrapped_
        const auto& row0 = tb.GetRowByOffset(0);
        VERIFY_IS_FALSE(row0.WasWrapForced());

        const auto& row1 = tb.GetRowByOffset(1);
        VERIFY_IS_FALSE(row1.WasWrapForced());

        TestUtils::VerifyExpectedString(tb, LR"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)", { 0, 0 });
        TestUtils::VerifyExpectedString(tb, L"          1234567890", { 0, 1 });
    };

    verifyBuffer(hostTb);

    // First write the first 80 characters from the string
    expectedOutput.push_back(R"(!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop)");

    // This is the hard line break
    expectedOutput.push_back("\r\n");
    // Now write row 2 of the buffer
    expectedOutput.push_back("          1234567890");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb);
}

void ConptyRoundtripTests::MoveCursorAtEOL()
{
    // This is a test for GH#1245

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;
    _flushFirstFrame();

    Log::Comment(NoThrowString().Format(
        L"Write exactly a full line of text"));
    hostSm.ProcessString(std::wstring(TerminalViewWidth, L'A'));

    auto verifyData0 = [](TextBuffer& tb) {
        auto iter = tb.GetCellDataAt({ 0, 0 });
        TestUtils::VerifySpanOfText(L"A", iter, 0, TerminalViewWidth);
        TestUtils::VerifySpanOfText(L" ", iter, 0, TerminalViewWidth);
    };

    verifyData0(hostTb);

    // TODO: GH#405/#4415 - Before #405 merges, the VT sequences conpty emits
    // might change, but the buffer contents shouldn't.
    // If they do change and these tests break, that's to be expected.
    expectedOutput.push_back(std::string(TerminalViewWidth, 'A'));
    expectedOutput.push_back("\x1b[1;80H");

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyData0(termTb);

    Log::Comment(NoThrowString().Format(
        L"Emulate backspacing at a bash prompt when the previous line wrapped.\n"
        L"We'll move the cursor up to the last char of the prev line, and erase it."));
    hostSm.ProcessString(L"\x1b[1;80H");
    hostSm.ProcessString(L"\x1b[K");

    auto verifyData1 = [](TextBuffer& tb) {
        auto iter = tb.GetCellDataAt({ 0, 0 });
        // There should be 79 'A's, followed by a space, and the following line should be blank.
        TestUtils::VerifySpanOfText(L"A", iter, 0, TerminalViewWidth - 1);
        TestUtils::VerifySpanOfText(L" ", iter, 0, 1);
        TestUtils::VerifySpanOfText(L" ", iter, 0, TerminalViewWidth);

        auto& cursor = tb.GetCursor();
        VERIFY_ARE_EQUAL(TerminalViewWidth - 1, cursor.GetPosition().x);
        VERIFY_ARE_EQUAL(0, cursor.GetPosition().y);
    };

    verifyData1(hostTb);

    expectedOutput.push_back(" ");
    expectedOutput.push_back("\x1b[1;80H");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyData1(termTb);
}

void ConptyRoundtripTests::TestResizeHeight()
{
    // This test class is _60_ tests to ensure that resizing the terminal works
    // with conpty correctly. There's a lot of min/maxing in expressions here,
    // to account for the sheer number of cases here, and that we have to handle
    // both resizing larger and smaller all in one test.

    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
        TEST_METHOD_PROPERTY(L"Data:dx", L"{-1, 0, 1}")
        TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
        TEST_METHOD_PROPERTY(L"Data:printedRows", L"{1, 10, 50, 200}")
    END_TEST_METHOD_PROPERTIES()
    int dx, dy;
    int printedRows;
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"dx", dx), L"change in width of buffer");
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"dy", dy), L"change in height of buffer");
    VERIFY_SUCCEEDED(TestData::TryGetValue(L"printedRows", printedRows), L"Number of rows of text to print");

    _checkConptyOutput = false;

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();
    const auto initialHostView = si.GetViewport();
    const auto initialTermView = term->GetViewport();
    const auto initialTerminalBufferHeight = term->GetTextBuffer().GetSize().Height();

    VERIFY_ARE_EQUAL(0, initialHostView.Top());
    VERIFY_ARE_EQUAL(TerminalViewHeight, initialHostView.BottomExclusive());
    VERIFY_ARE_EQUAL(0, initialTermView.Top());
    VERIFY_ARE_EQUAL(TerminalViewHeight, initialTermView.BottomExclusive());

    Log::Comment(NoThrowString().Format(
        L"Print %d lines of output, which will scroll the viewport", printedRows));

    for (auto i = 0; i < printedRows; i++)
    {
        // This looks insane, but this expression is carefully crafted to give
        // us only printable characters, starting with `!` (0n33).
        // Similar statements are used elsewhere throughout this test.
        auto wstr = std::wstring(1, static_cast<wchar_t>((i) % 93) + 33);
        hostSm.ProcessString(wstr);
        hostSm.ProcessString(L"\r\n");
    }

    // Conpty doesn't have a scrollback, its view's origin is always 0,0
    const auto secondHostView = si.GetViewport();
    VERIFY_ARE_EQUAL(0, secondHostView.Top());
    VERIFY_ARE_EQUAL(TerminalViewHeight, secondHostView.BottomExclusive());

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    const auto secondTermView = term->GetViewport();
    // If we've printed more lines than the height of the buffer, then we're
    // expecting the viewport to have moved down. Otherwise, the terminal's
    // viewport will stay at 0,0.
    const auto expectedTerminalViewBottom = std::max(std::min(printedRows + 1, term->GetBufferHeight()), term->GetViewport().Height());

    VERIFY_ARE_EQUAL(expectedTerminalViewBottom, secondTermView.BottomExclusive());
    VERIFY_ARE_EQUAL(expectedTerminalViewBottom - initialTermView.Height(), secondTermView.Top());

    auto verifyTermData = [&expectedTerminalViewBottom, &printedRows, this, &initialTerminalBufferHeight](TextBuffer& termTb, const int resizeDy = 0) {
        // Some number of lines of text were lost from the scrollback. The
        // number of lines lost will be determined by whichever of the initial
        // or current buffer is smaller.
        const auto numLostRows = std::max(0,
                                          printedRows - std::min(term->GetTextBuffer().GetSize().Height(), initialTerminalBufferHeight) + 1);

        const auto rowsWithText = std::min(printedRows, expectedTerminalViewBottom) - 1 + std::min(resizeDy, 0);

        for (til::CoordType row = 0; row < rowsWithText; row++)
        {
            SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
            auto iter = termTb.GetCellDataAt({ 0, row });
            const wchar_t expectedChar = static_cast<wchar_t>((row + numLostRows) % 93) + 33;

            auto expectedString = std::wstring(1, expectedChar);

            if (iter->Chars() != expectedString)
            {
                Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
            }
            VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars());
            VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
        }
    };
    auto verifyHostData = [&si, &initialHostView, &printedRows](TextBuffer& hostTb, const int resizeDy = 0) {
        const auto hostView = si.GetViewport();

        // In the host, there are two regions we're interested in:

        // 1. the first section of the buffer with the output in it. Before
        //    we're resized, this will be filled with one character on each row.
        // 2. The second area below the first that's empty (filled with spaces).
        //    Initially, this is only one row.
        // After we resize, different things will happen.
        // * If we decrease the height of the buffer, the characters in the
        //   buffer will all move _up_ the same number of rows. We'll want to
        //   only check the first initialView+dy rows for characters.
        // * If we increase the height, rows will be added at the bottom. We'll
        //   want to check the initial viewport height for the original
        //   characters, but then we'll want to look for more blank rows at the
        //   bottom. The characters in the initial viewport won't have moved.

        const auto originalViewHeight = resizeDy < 0 ? initialHostView.Height() + resizeDy : initialHostView.Height();
        const auto rowsWithText = std::min(originalViewHeight - 1, printedRows);
        const auto scrolled = printedRows > initialHostView.Height();
        // The last row of the viewport should be empty
        // The second last row will have '0'+50
        // The third last row will have '0'+49
        // ...
        // The <height> last row will have '0'+(50-height+1)
        const auto firstChar = static_cast<wchar_t>(scrolled ?
                                                        (printedRows - originalViewHeight + 1) :
                                                        0);

        til::CoordType row = 0;
        // Don't include the last row of the viewport in this check, since it'll
        // be blank. We'll check it in the below loop.
        for (; row < rowsWithText; row++)
        {
            SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
            auto iter = hostTb.GetCellDataAt({ 0, row });

            const auto expectedChar = static_cast<wchar_t>(((firstChar + row) % 93) + 33);
            auto expectedString = std::wstring(1, static_cast<wchar_t>(expectedChar));

            if (iter->Chars() != expectedString)
            {
                Log::Comment(NoThrowString().Format(L"row [%d] was mismatched", row));
            }
            VERIFY_ARE_EQUAL(expectedString, (iter++)->Chars(), NoThrowString().Format(L"%s", expectedString.data()));
            VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
        }

        // Check that the remaining rows in the viewport are empty.
        for (; row < hostView.Height(); row++)
        {
            SetVerifyOutput settings(VerifyOutputSettings::LogOnlyFailures);
            auto iter = hostTb.GetCellDataAt({ 0, row });
            VERIFY_ARE_EQUAL(L" ", (iter)->Chars());
        }
    };

    verifyHostData(*hostTb);
    verifyTermData(*termTb);

    const til::size newViewportSize{ TerminalViewWidth + dx,
                                     TerminalViewHeight + dy };
    // After we resize, make sure to get the new textBuffers
    std::tie(hostTb, termTb) = _performResize(newViewportSize);

    // Conpty's doesn't have a scrollback, its view's origin is always 0,0
    const auto thirdHostView = si.GetViewport();
    VERIFY_ARE_EQUAL(0, thirdHostView.Top());
    VERIFY_ARE_EQUAL(newViewportSize.height, thirdHostView.BottomExclusive());

    // The Terminal should be stuck to the top of the viewport, unless dy<0,
    // rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
    // the old top, we actually shifted it down (because the output was at the
    // bottom of the window, not empty lines).
    const auto thirdTermView = term->GetViewport();
    if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
    {
        VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
        VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
    }
    else
    {
        VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
        VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
    }

    verifyHostData(*hostTb, dy);
    // Note that at this point, nothing should have changed with the Terminal.
    verifyTermData(*termTb, dy);

    Log::Comment(NoThrowString().Format(L"Paint a frame to update the Terminal"));
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Conpty's doesn't have a scrollback, its view's origin is always 0,0
    const auto fourthHostView = si.GetViewport();
    VERIFY_ARE_EQUAL(0, fourthHostView.Top());
    VERIFY_ARE_EQUAL(newViewportSize.height, fourthHostView.BottomExclusive());

    // The Terminal should be stuck to the top of the viewport, unless dy<0,
    // rows=50. In that set of cases, we _didn't_ pin the top of the Terminal to
    // the old top, we actually shifted it down (because the output was at the
    // bottom of the window, not empty lines).
    const auto fourthTermView = term->GetViewport();
    if (dy < 0 && (printedRows > initialTermView.Height() && printedRows < initialTerminalBufferHeight))
    {
        VERIFY_ARE_EQUAL(secondTermView.Top() - dy, thirdTermView.Top());
        VERIFY_ARE_EQUAL(expectedTerminalViewBottom, thirdTermView.BottomExclusive());
    }
    else
    {
        VERIFY_ARE_EQUAL(secondTermView.Top(), thirdTermView.Top());
        VERIFY_ARE_EQUAL(expectedTerminalViewBottom + dy, thirdTermView.BottomExclusive());
    }
    verifyHostData(*hostTb, dy);
    verifyTermData(*termTb, dy);
}

void ConptyRoundtripTests::PassthroughCursorShapeImmediately()
{
    // This is a test for GH#4106, and more indirectly, GH #2011.

    Log::Comment(NoThrowString().Format(
        L"Change the cursor shape with VT. This should immediately be flushed to the Terminal."));

    auto& g = ServiceLocator::LocateGlobals();
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    _logConpty = true;

    VERIFY_ARE_NOT_EQUAL(CursorType::VerticalBar, hostTb.GetCursor().GetType());
    VERIFY_ARE_NOT_EQUAL(CursorType::VerticalBar, termTb.GetCursor().GetType());

    expectedOutput.push_back("\x1b[5 q");
    hostSm.ProcessString(L"\x1b[5 q");

    VERIFY_ARE_EQUAL(CursorType::VerticalBar, hostTb.GetCursor().GetType());
    VERIFY_ARE_EQUAL(CursorType::VerticalBar, termTb.GetCursor().GetType());
}

void ConptyRoundtripTests::PassthroughClearScrollback()
{
    Log::Comment(NoThrowString().Format(
        L"Write more lines of output than there are lines in the viewport. Clear the scrollback with ^[[3J"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    _logConpty = true;

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
        expectedOutput.push_back("X");
        if (i < hostView.BottomInclusive())
        {
            expectedOutput.push_back("\r\n");
        }
        else
        {
            // After we hit the bottom of the viewport, the newlines come in
            // separated by empty writes for whatever reason.
            expectedOutput.push_back("\r");
            expectedOutput.push_back("\n");
            expectedOutput.push_back("");
        }

        hostSm.ProcessString(L"X\n");

        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Verify that we've printed height*2 lines of X's to the Terminal
    const auto termFirstView = term->GetViewport();
    for (til::CoordType y = 0; y < 2 * termFirstView.Height(); y++)
    {
        TestUtils::VerifyExpectedString(termTb, L"X  ", { 0, y });
    }

    // Write a Erase Scrollback VT sequence to the host, it should come through to the Terminal
    expectedOutput.push_back("\x1b[3J");
    hostSm.ProcessString(L"\x1b[3J");

    _checkConptyOutput = false;

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    const auto termSecondView = term->GetViewport();
    VERIFY_ARE_EQUAL(0, termSecondView.Top());

    // Verify the top of the Terminal viewport contains the contents of the old viewport
    for (til::CoordType y = 0; y < termSecondView.BottomInclusive(); y++)
    {
        TestUtils::VerifyExpectedString(termTb, L"X  ", { 0, y });
    }

    // Verify below the new viewport (the old viewport) has been cleared out
    for (auto y = termSecondView.BottomInclusive(); y < termFirstView.BottomInclusive(); y++)
    {
        TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y });
    }
}

void ConptyRoundtripTests::PassthroughClearAll()
{
    // see https://github.com/microsoft/terminal/issues/2832
    Log::Comment(L"This is a test to make sure that when the client emits a "
                 L"^[[2J, we actually forward the 2J to the terminal, to move "
                 L"the viewport. 2J importantly moves the viewport, so that "
                 L"all the _current_ buffer contents are moved to scrollback. "
                 L"We shouldn't just paint over the current viewport with spaces.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    auto& sm = si.GetStateMachine();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        if (i > 0)
        {
            sm.ProcessString(L"\r\n");
        }

        sm.ProcessString(L"~");
    }

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool afterClear = false) {
        const auto width = viewport.width();

        // "~" rows
        for (til::CoordType row = 0; row < viewport.bottom; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));
            VERIFY_IS_FALSE(tb.GetRowByOffset(row).WasWrapForced());
            auto iter = tb.GetCellDataAt({ 0, row });
            if (afterClear && row >= viewport.top)
            {
                TestUtils::VerifySpanOfText(L" ", iter, 0, width);
            }
            else
            {
                TestUtils::VerifySpanOfText(L"~", iter, 0, 1);
                TestUtils::VerifySpanOfText(L" ", iter, 0, width - 1);
            }
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());

    const til::rect originalTerminalView{ term->_mutableViewport.ToInclusive() };

    // Here, we'll emit the 2J to EraseAll, and move the viewport contents into
    // the scrollback.
    sm.ProcessString(L"\x1b[2J");

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Make sure that the terminal's new viewport is actually just lower than it
    // used to be.
    const til::rect newTerminalView{ term->_mutableViewport.ToInclusive() };
    VERIFY_ARE_EQUAL(end, newTerminalView.top);
    VERIFY_IS_GREATER_THAN(newTerminalView.top, originalTerminalView.top);

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, newTerminalView, true);
}

void ConptyRoundtripTests::PassthroughHardReset()
{
    // This test is highly similar to PassthroughClearScrollback.
    Log::Comment(NoThrowString().Format(
        L"Write more lines of output than there are lines in the viewport. Clear everything with ^[c"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    _logConpty = true;

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
        expectedOutput.push_back("X");
        if (i < hostView.BottomInclusive())
        {
            expectedOutput.push_back("\r\n");
        }
        else
        {
            // After we hit the bottom of the viewport, the newlines come in
            // separated by empty writes for whatever reason.
            expectedOutput.push_back("\r");
            expectedOutput.push_back("\n");
            expectedOutput.push_back("");
        }

        hostSm.ProcessString(L"X\n");

        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Verify that we've printed height*2 lines of X's to the Terminal
    const auto termFirstView = term->GetViewport();
    for (til::CoordType y = 0; y < 2 * termFirstView.Height(); y++)
    {
        TestUtils::VerifyExpectedString(termTb, L"X  ", { 0, y });
    }

    // Write a Hard Reset VT sequence to the host, it should come through to the Terminal
    // along with a DECSET sequence to re-enable win32 input and focus events.
    expectedOutput.push_back("\033c");
    expectedOutput.push_back("\033[?9001;1004h");
    hostSm.ProcessString(L"\033c");

    const auto termSecondView = term->GetViewport();
    VERIFY_ARE_EQUAL(0, termSecondView.Top());

    // Verify everything has been cleared out
    for (til::CoordType y = 0; y < termFirstView.BottomInclusive(); y++)
    {
        TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y });
    }
}

void ConptyRoundtripTests::PassthroughDECCTR()
{
    Log::Comment(L"Update the color table with DECCTR. This should immediately be flushed to the Terminal.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& termRenderSettings = term->GetRenderSettings();

    _flushFirstFrame();

    _logConpty = true;

    // We're going to update color table entries 101 through 103, so we start by
    // initializing those entries to black in the Terminal render settings.
    termRenderSettings.SetColorTableEntry(101, RGB(0, 0, 0));
    termRenderSettings.SetColorTableEntry(102, RGB(0, 0, 0));
    termRenderSettings.SetColorTableEntry(103, RGB(0, 0, 0));

    // DECCTR is expected to arrive in two parts. The control sequence comes
    // first, and the string content follows in the second packet.
    expectedOutput.push_back("\x1bP2$p");
    expectedOutput.push_back("101;2;100;0;0/102;2;0;100;0/103;2;0;0;100\x1b\\");

    // Send the control sequence to the host.
    hostSm.ProcessString(L"\x1bP2$p101;2;100;0;0/102;2;0;100;0/103;2;0;0;100\x1b\\");

    // Verify that the color table entries have been updated in the Terminal.
    VERIFY_ARE_EQUAL(RGB(255, 0, 0), termRenderSettings.GetColorTableEntry(101));
    VERIFY_ARE_EQUAL(RGB(0, 255, 0), termRenderSettings.GetColorTableEntry(102));
    VERIFY_ARE_EQUAL(RGB(0, 0, 255), termRenderSettings.GetColorTableEntry(103));

    termRenderSettings.ResetColorTable();
}

void ConptyRoundtripTests::OutputWrappedLinesAtTopOfBuffer()
{
    Log::Comment(
        L"Case 1: Write a wrapped line right at the start of the buffer, before any circling");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    const auto wrappedLineLength = TerminalViewWidth + 20;

    sm.ProcessString(std::wstring(wrappedLineLength, L'A'));

    auto verifyBuffer = [](const TextBuffer& tb) {
        // Buffer contents should look like the following: (80 wide)
        // (w) means we hard wrapped the line
        // (b) means the line is _not_ wrapped (it's broken, the default state.)
        // cursor is on the '_'
        //
        // |AAAAAAAA...AAAA| (w)
        // |AAAAA_  ...    | (b) (There are 20 'A's on this line.)
        // |        ...    | (b)

        VERIFY_IS_TRUE(tb.GetRowByOffset(0).WasWrapForced());
        VERIFY_IS_FALSE(tb.GetRowByOffset(1).WasWrapForced());
        auto iter0 = tb.GetCellDataAt({ 0, 0 });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, TerminalViewWidth);
        auto iter1 = tb.GetCellDataAt({ 0, 1 });
        TestUtils::VerifySpanOfText(L"A", iter1, 0, 20);
        auto iter2 = tb.GetCellDataAt({ 20, 1 });
        TestUtils::VerifySpanOfText(L" ", iter2, 0, TerminalViewWidth - 20);
    };

    verifyBuffer(hostTb);

    expectedOutput.push_back(std::string(TerminalViewWidth, 'A'));
    expectedOutput.push_back(std::string(20, 'A'));
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb);
}

void ConptyRoundtripTests::OutputWrappedLinesAtBottomOfBuffer()
{
    Log::Comment(
        L"Case 2: Write a wrapped line at the end of the buffer, once the conpty started circling");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    // First, fill the buffer with contents, so conpty starts circling

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
        expectedOutput.push_back("X");
        if (i < hostView.BottomInclusive())
        {
            expectedOutput.push_back("\r\n");
        }
        else
        {
            // After we hit the bottom of the viewport, the newlines come in
            // separated by empty writes for whatever reason.
            expectedOutput.push_back("\r");
            expectedOutput.push_back("\n");
            expectedOutput.push_back("");
        }

        hostSm.ProcessString(L"X\n");

        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    const auto wrappedLineLength = TerminalViewWidth + 20;

    // The following diagrams show the buffer contents after each string emitted
    // from conpty. For each of these diagrams:
    // (w) means we hard wrapped the line
    // (b) means the line is _not_ wrapped (it's broken, the default state.)
    // cursor is on the '_'

    // Initial state:
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |_              | (b)

    expectedOutput.push_back(std::string(TerminalViewWidth, 'A'));
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AAAA|_ (w) The cursor is actually on the last A here

    // TODO GH#5228 might break the "newline & repaint the wrapped char" checks here, that's okay.
    expectedOutput.push_back("\r"); // This \r\n is emitted by ScrollFrame to
    expectedOutput.push_back("\n"); // add a newline to the bottom of the buffer
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AAAA| (b)
    // |_              | (b)

    expectedOutput.push_back("\x1b[31;80H"); // Move the cursor BACK to the wrapped row
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AAAA| (b) The cursor is actually on the last A here
    // |               | (b)

    expectedOutput.push_back(std::string(1, 'A')); // Reprint the last character of the wrapped row
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AAAA|_ (w) The cursor is actually on the last A here
    // |               | (b)

    expectedOutput.push_back(std::string(20, 'A')); // Print the second line.
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AAAA| (w)
    // |AAAAA_         | (b) There are 20 'A's on this line.

    hostSm.ProcessString(std::wstring(wrappedLineLength, L'A'));

    auto verifyBuffer = [](const TextBuffer& tb, const til::CoordType wrappedRow) {
        // Buffer contents should look like the following: (80 wide)
        // (w) means we hard wrapped the line
        // (b) means the line is _not_ wrapped (it's broken, the default state.)
        // cursor is on the '_'
        //
        // |X              | (b)
        // |X              | (b)
        // ...
        // |X              | (b)
        // |AAAAAAAA...AAAA| (w)
        // |AAAAA_  ...    | (b) (There are 20 'A's on this line.)

        VERIFY_IS_TRUE(tb.GetRowByOffset(wrappedRow).WasWrapForced());
        VERIFY_IS_FALSE(tb.GetRowByOffset(wrappedRow + 1).WasWrapForced());

        auto iter0 = tb.GetCellDataAt({ 0, wrappedRow });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, TerminalViewWidth);
        auto iter1 = tb.GetCellDataAt({ 0, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L"A", iter1, 0, 20);
        auto iter2 = tb.GetCellDataAt({ 20, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L" ", iter2, 0, TerminalViewWidth - 20);
    };

    verifyBuffer(hostTb, hostView.BottomInclusive() - 1);

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    verifyBuffer(termTb, term->_mutableViewport.BottomInclusive() - 1);
}

void ConptyRoundtripTests::ScrollWithChangesInMiddle()
{
    Log::Comment(L"This test checks emitting a wrapped line at the bottom of the"
                 L" viewport while _also_ emitting other text elsewhere in the same frame. This"
                 L" output will cause us to scroll the viewport in one frame, but we need to"
                 L" make sure the wrapped line _stays_ wrapped, and the scrolled text appears in"
                 L" the right place.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    // First, fill the buffer with contents, so conpty starts circling

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
        expectedOutput.push_back("X");
        if (i < hostView.BottomInclusive())
        {
            expectedOutput.push_back("\r\n");
        }
        else
        {
            // After we hit the bottom of the viewport, the newlines come in
            // separated by empty writes for whatever reason.
            expectedOutput.push_back("\r");
            expectedOutput.push_back("\n");
            expectedOutput.push_back("");
        }

        hostSm.ProcessString(L"X\n");

        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    const auto wrappedLineLength = TerminalViewWidth + 20;

    // In the Terminal, we're going to expect:
    expectedOutput.push_back("\x1b[15;1H"); // Move the cursor to row 14, col 0
    expectedOutput.push_back("Y"); // Print a 'Y'
    expectedOutput.push_back("\x1b[32;1H"); // Move the cursor to the last row
    expectedOutput.push_back(std::string(TerminalViewWidth, 'A')); // Print the first 80 'A's
    // This is going to be the end of the first frame - b/c we moved the cursor
    // in the middle of the frame, we're going to hide/show the cursor during
    // this frame
    expectedOutput.push_back("\x1b[?25h"); // hide the cursor
    // On the subsequent frame:
    // TODO GH#5228 might break the "newline & repaint the wrapped char" checks here, that's okay.
    expectedOutput.push_back("\r"); // This \r\n is emitted by ScrollFrame to
    expectedOutput.push_back("\n"); // add a newline to the bottom of the buffer
    expectedOutput.push_back("\x1b[31;80H"); // Move the cursor BACK to the wrapped row
    expectedOutput.push_back(std::string(1, 'A')); // Reprint the last character of the wrapped row
    expectedOutput.push_back(std::string(20, 'A')); // Print the second line.

    _logConpty = true;

    // To the host, we'll do something very similar:
    hostSm.ProcessString(L"\x1b"
                         L"7"); // Save cursor
    hostSm.ProcessString(L"\x1b[15;1H"); // Move the cursor to row 14, col 0
    hostSm.ProcessString(L"Y"); // Print a 'Y'
    hostSm.ProcessString(L"\x1b"
                         L"8"); // Restore
    hostSm.ProcessString(std::wstring(wrappedLineLength, L'A')); // Print 100 'A's

    auto verifyBuffer = [](const TextBuffer& tb, const til::rect& viewport) {
        const auto wrappedRow = viewport.bottom - 2;
        const auto start = viewport.top;
        for (auto i = start; i < wrappedRow; i++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", i));
            TestUtils::VerifyExpectedString(tb, i == start + 13 ? L"Y" : L"X", { 0, i });
        }

        VERIFY_IS_TRUE(tb.GetRowByOffset(wrappedRow).WasWrapForced());
        VERIFY_IS_FALSE(tb.GetRowByOffset(wrappedRow + 1).WasWrapForced());

        auto iter0 = tb.GetCellDataAt({ 0, wrappedRow });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, TerminalViewWidth);

        auto iter1 = tb.GetCellDataAt({ 0, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L"A", iter1, 0, 20);
        auto iter2 = tb.GetCellDataAt({ 20, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L" ", iter2, 0, TerminalViewWidth - 20);
    };

    Log::Comment(NoThrowString().Format(L"Checking the host buffer..."));
    verifyBuffer(hostTb, hostView.ToExclusive());
    Log::Comment(NoThrowString().Format(L"... Done"));

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(NoThrowString().Format(L"Checking the terminal buffer..."));
    verifyBuffer(termTb, term->_mutableViewport.ToExclusive());
    Log::Comment(NoThrowString().Format(L"... Done"));
}

void ConptyRoundtripTests::ScrollWithMargins()
{
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;
    const auto initialTermView = term->GetViewport();

    Log::Comment(L"Flush first frame.");
    _flushFirstFrame();

    // Fill up the buffer with some text.
    // We're going to write something like this:
    // AAAA
    // BBBB
    // CCCC
    // ........
    // QQQQ
    // ****************
    // The letters represent the data in the TMUX pane.
    // The final *** line represents the mode line which we will
    // attempt to hold in place and not scroll.
    // Note that the last line will contain one '*' less than the width of the window.

    Log::Comment(L"Fill host with text pattern by feeding it into VT parser.");
    const auto rowsToWrite = initialTermView.Height() - 1;

    // For all lines but the last one, write out a few of a letter.
    for (auto i = 0; i < rowsToWrite; ++i)
    {
        const auto wch = static_cast<wchar_t>(L'A' + i);
        hostSm.ProcessCharacter(wch);
        hostSm.ProcessCharacter(wch);
        hostSm.ProcessCharacter(wch);
        hostSm.ProcessCharacter(wch);
        hostSm.ProcessCharacter('\n');
    }

    // For the last one, write out the asterisks for the mode line.
    for (auto i = 0; i < initialTermView.Width() - 1; ++i)
    {
        hostSm.ProcessCharacter('*');
    }

    // no newline in the bottom right corner or it will move unexpectedly.

    // Now set up the verification that the buffers are full of the pattern we expect.
    // This function will verify the text backing buffers.
    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor is waiting in the bottom right corner
        VERIFY_ARE_EQUAL(initialTermView.Height() - 1, cursor.GetPosition().y);
        VERIFY_ARE_EQUAL(initialTermView.Width() - 1, cursor.GetPosition().x);

        // For all rows except the last one, verify that we have a run of four letters.
        for (auto i = 0; i < rowsToWrite; ++i)
        {
            const std::wstring expectedString(4, static_cast<wchar_t>(L'A' + i));
            const til::point expectedPos{ 0, i };
            TestUtils::VerifyExpectedString(tb, expectedString, expectedPos);
        }

        // For the last row, verify we have an entire row of asterisks for the mode line.
        const std::wstring expectedModeLine(initialTermView.Width() - 1, L'*');
        const til::point expectedPos{ 0, rowsToWrite };
        TestUtils::VerifyExpectedString(tb, expectedModeLine, expectedPos);
    };

    // This will verify the text emitted from the PTY.
    for (auto i = 0; i < rowsToWrite; ++i)
    {
        const std::string expectedString(4, static_cast<char>('A' + i));
        expectedOutput.push_back(expectedString);
        expectedOutput.push_back("\r\n");
    }
    {
        const std::string expectedString(initialTermView.Width() - 1, '*');
        expectedOutput.push_back(expectedString);
    }

    Log::Comment(L"Verify host buffer contains pattern.");
    // Verify the host side.
    verifyBuffer(hostTb);

    Log::Comment(L"Emit PTY frame and validate it transmits the right data.");
    // Paint the frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Verify terminal buffer contains pattern.");
    // Verify the terminal side.
    verifyBuffer(termTb);

    Log::Comment(L"!!! OK. Set up the scroll region and let's get scrolling!");
    // This is a simulation of what TMUX does to scroll everything except the mode line.
    // First build up our VT strings...
    std::wstring reducedScrollRegion;
    {
        std::wstringstream wss;
        // For 20 tall buffer...
        // ESC[1;19r
        // Set scroll region to lines 1-19.
        wss << L"\x1b[1;" << initialTermView.Height() - 1 << L"r";
        reducedScrollRegion = wss.str();
    }
    std::wstring completeScrollRegion;
    {
        std::wstringstream wss;
        // For 20 tall buffer...
        // ESC[1;20r
        // Set scroll region to lines 1-20. (or the whole buffer)
        wss << L"\x1b[1;" << initialTermView.Height() << L"r";
        completeScrollRegion = wss.str();
    }
    std::wstring reducedCursorBottomRight;
    {
        std::wstringstream wss;
        // For 20 tall and 100 wide buffer
        // ESC[19;100H
        // Put cursor on line 19 (1 before last) and the right most column 100.
        // (Remember that in VT, we start counting from 1 not 0.)
        wss << L"\x1b[" << initialTermView.Height() - 1 << L";" << initialTermView.Width() << "H";
        reducedCursorBottomRight = wss.str();
    }
    std::wstring completeCursorAtPromptLine;
    {
        std::wstringstream wss;
        // For 20 tall and 100 wide buffer
        // ESC[19;1H
        // Put cursor on line 19 (1 before last) and the left most column 1.
        // (Remember that in VT, we start counting from 1 not 0.)
        wss << L"\x1b[" << initialTermView.Height() - 1 << L";1H";
        completeCursorAtPromptLine = wss.str();
    }

    Log::Comment(L"Perform all the operations on the buffer.");

    // OK this is what TMUX does.
    // 1. Mark off the scroll area as everything but the mode line.
    hostSm.ProcessString(reducedScrollRegion);
    // 2. Put the cursor in the bottom-right corner of the scroll region.
    hostSm.ProcessString(reducedCursorBottomRight);
    // 3. Send a single newline which should do the heavy lifting
    //    of pushing everything in the scroll region up by 1 line and
    //    leave everything outside the region alone.

    // This entire block is subject to change in the future with optimizations.
    {
        // Cursor gets redrawn in the bottom right of the scroll region with the repaint that is forced
        // early while the screen is rotated.
        std::stringstream ss;
        ss << "\x1b[" << initialTermView.Height() - 1 << ";" << initialTermView.Width() << "H";
        expectedOutput.push_back(ss.str());

        expectedOutput.push_back("\x1b[?25h"); // turn the cursor back on too.
    }

    hostSm.ProcessString(L"\n");
    // 4. Remove the scroll area by setting it to the entire size of the viewport.
    hostSm.ProcessString(completeScrollRegion);
    // 5. Put the cursor back at the beginning of the new line that was just revealed.
    hostSm.ProcessString(completeCursorAtPromptLine);

    // Set up the verifications like above.
    auto verifyBufferAfter = [&](const TextBuffer& tb, const auto panOffset) {
        auto& cursor = tb.GetCursor();
        // Verify the cursor is waiting on the freshly revealed line (1 above mode line)
        // and in the left most column.
        const auto bottomLine = initialTermView.BottomInclusive() + panOffset;
        VERIFY_ARE_EQUAL(bottomLine - 1, cursor.GetPosition().y);
        VERIFY_ARE_EQUAL(0, cursor.GetPosition().x);

        // For all rows except the last two, verify that we have a run of four letters.
        for (auto i = 0; i < rowsToWrite - 1; ++i)
        {
            // Start with B this time because the A line got scrolled off the top.
            const std::wstring expectedString(4, static_cast<wchar_t>(L'B' + i));
            const til::point expectedPos{ 0, panOffset + i };
            TestUtils::VerifyExpectedString(tb, expectedString, expectedPos);
        }

        // For the second to last row, verify that it is blank.
        {
            const std::wstring expectedBlankLine(initialTermView.Width(), L' ');
            const til::point blankLinePos{ 0, panOffset + rowsToWrite - 1 };
            TestUtils::VerifyExpectedString(tb, expectedBlankLine, blankLinePos);
        }

        // For the last row, verify we have an entire row of asterisks for the mode line.
        {
            const std::wstring expectedModeLine(initialTermView.Width() - 1, L'*');
            const til::point modeLinePos{ 0, panOffset + rowsToWrite };
            TestUtils::VerifyExpectedString(tb, expectedModeLine, modeLinePos);
        }
    };

    // This will verify the text emitted from the PTY.

    expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner
    expectedOutput.push_back("\n"); // linefeed pans the viewport down
    {
        // Cursor gets reset into second line from bottom, left most column
        std::stringstream ss;
        ss << "\x1b[" << initialTermView.Height() - 1 << ";1H";
        expectedOutput.push_back(ss.str());
    }
    {
        // Bottom of the scroll region is replaced with a blank line
        const std::string expectedString(initialTermView.Width(), ' ');
        expectedOutput.push_back(expectedString);
    }
    expectedOutput.push_back("\r\n"); // cursor moved to bottom left corner
    {
        // Mode line is redrawn at the bottom of the viewport
        const std::string expectedString(initialTermView.Width() - 1, '*');
        expectedOutput.push_back(expectedString);
        expectedOutput.push_back(" ");
    }
    {
        // Cursor gets reset into second line from bottom, left most column
        std::stringstream ss;
        ss << "\x1b[" << initialTermView.Height() - 1 << ";1H";
        expectedOutput.push_back(ss.str());
    }
    expectedOutput.push_back("\x1b[?25h"); // turn the cursor back on too.

    Log::Comment(L"Verify host buffer contains pattern moved up one and mode line still in place.");
    // Verify the host side.
    verifyBufferAfter(hostTb, 0);

    Log::Comment(L"Emit PTY frame and validate it transmits the right data.");
    // Paint the frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Verify terminal buffer contains pattern moved up one and mode line still in place.");
    // Verify the terminal side. Note the viewport has panned down a line.
    verifyBufferAfter(termTb, 1);
}

void ConptyRoundtripTests::DontWrapMoveCursorInSingleFrame()
{
    // See https://github.com/microsoft/terminal/pull/5181#issuecomment-607427840
    Log::Comment(L"This is a test for when a line of text exactly wrapped, but "
                 L"the cursor didn't end the frame at the end of line (waiting "
                 L"for more wrapped text). We should still move the cursor in "
                 L"this case.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    auto verifyBuffer = [](const TextBuffer& tb) {
        // Simple verification: Make sure the cursor is in the correct place,
        // and that it's visible. We don't care so much about the buffer
        // contents in this test.
        const til::point expectedCursor{ 8, 3 };
        VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        VERIFY_IS_TRUE(tb.GetCursor().IsVisible());
    };

    hostSm.ProcessString(L"\x1b[?25l");
    hostSm.ProcessString(L"\x1b[H");
    hostSm.ProcessString(L"\x1b[75C");
    hostSm.ProcessString(L"XXXXX");
    hostSm.ProcessString(L"\x1b[4;9H");
    hostSm.ProcessString(L"\x1b[?25h");

    Log::Comment(L"Checking the host buffer state");
    verifyBuffer(hostTb);

    expectedOutput.push_back("\x1b[75C");
    expectedOutput.push_back("XXXXX");
    expectedOutput.push_back("\x1b[4;9H");
    // We're _not_ expecting a cursor on here, because we didn't actually hide
    // the cursor during the course of this frame

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Checking the terminal buffer state");
    verifyBuffer(termTb);
}

void ConptyRoundtripTests::ClearHostTrickeryTest()
{
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:paintEachNewline", L"{0, 1, 2}")
        TEST_METHOD_PROPERTY(L"Data:cursorOnNextLine", L"{false, true}")
        TEST_METHOD_PROPERTY(L"Data:paintAfterDECALN", L"{false, true}")
        TEST_METHOD_PROPERTY(L"Data:changeAttributes", L"{false, true}")
        TEST_METHOD_PROPERTY(L"Data:useLongSpaces", L"{false, true}")
        TEST_METHOD_PROPERTY(L"Data:printTextAfterSpaces", L"{false, true}")
    END_TEST_METHOD_PROPERTIES();
    constexpr auto PaintEveryNewline = 0;
    constexpr auto PaintAfterAllNewlines = 1;

    INIT_TEST_PROPERTY(int, paintEachNewline, L"Any of: manually PaintFrame after each newline is emitted, once at the end of all newlines, or not at all");
    INIT_TEST_PROPERTY(bool, cursorOnNextLine, L"Either leave the cursor on the first line, or place it on the second line of the buffer");
    INIT_TEST_PROPERTY(bool, paintAfterDECALN, L"Controls whether we manually paint a frame after the DECALN sequence is emitted.");
    INIT_TEST_PROPERTY(bool, changeAttributes, L"If true, change the text attributes after the 'A's and spaces");
    INIT_TEST_PROPERTY(bool, useLongSpaces, L"If true, print 10 spaces instead of 5, longer than a CUF sequence.");
    INIT_TEST_PROPERTY(bool, printTextAfterSpaces, L"If true, print \"ZZZZZ\" after the spaces on the first line.");

    // See https://github.com/microsoft/terminal/issues/5039#issuecomment-606833841
    Log::Comment(L"This is a more than comprehensive test for GH#5039. We're "
                 L"going to print some text to the buffer, then fill the alt-"
                 L"buffer with text, then switch back to the main buffer. The "
                 L"text from the alt buffer should not pollute the main buffer.");

    // The text we're printing will look like one of the following, with the
    // cursor on the _
    //  * cursorOnNextLine=false, useLongSpaces=false:
    //    AAAAA     ZZZZZ_
    //  * cursorOnNextLine=false, useLongSpaces=true:
    //    AAAAA          ZZZZZ_
    //  * cursorOnNextLine=true, useLongSpaces=false:
    //    AAAAA     ZZZZZ
    //    BBBBB_
    //  * cursorOnNextLine=true, useLongSpaces=true:
    //    AAAAA          ZZZZZ
    //    BBBBB_
    //
    // If printTextAfterSpaces=false, then we won't print the "ZZZZZ"
    //
    // The interesting case that repros the bug in GH#5039 is
    //  - paintEachNewline=DontPaintAfterNewlines (2)
    //  - cursorOnNextLine=false
    //  - paintAfterDECALN=<any>
    //  - changeAttributes=true
    //  - useLongSpaces=<any>
    //  - printTextAfterSpaces=<any>
    //
    // All the possible cases are left here though, to catch potential future regressions.

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    auto verifyBuffer = [&cursorOnNextLine, &useLongSpaces, &printTextAfterSpaces](const TextBuffer& tb,
                                                                                   const til::rect viewport) {
        // We _would_ expect the Terminal's cursor to be on { 8, 0 }, but this
        // is currently broken due to #381/#4676. So we'll use the viewport
        // provided to find the actual Y position of the cursor.
        const auto viewTop = viewport.origin().y;
        const auto cursorRow = viewTop + (cursorOnNextLine ? 1 : 0);
        const auto cursorCol = (cursorOnNextLine ? 5 :
                                                   (10 + (useLongSpaces ? 5 : 0) + (printTextAfterSpaces ? 5 : 0)));
        const til::point expectedCursor{ cursorCol, cursorRow };

        VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        VERIFY_IS_TRUE(tb.GetCursor().IsVisible());
        auto iter = TestUtils::VerifyExpectedString(tb, L"AAAAA", { 0, viewTop });
        TestUtils::VerifyExpectedString(useLongSpaces ? L"          " : L"     ", iter);
        if (printTextAfterSpaces)
        {
            TestUtils::VerifyExpectedString(L"ZZZZZ", iter);
        }
        else
        {
            TestUtils::VerifyExpectedString(L"     ", iter);
        }
        TestUtils::VerifyExpectedString(L"     ", iter);

        if (cursorOnNextLine)
        {
            TestUtils::VerifyExpectedString(tb, L"BBBBB", { 0, cursorRow });
        }
    };

    // We're _not_ checking the conpty output during this test, only the side effects.
    _checkConptyOutput = false;

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });

    Log::Comment(L"Setting up the host buffer...");
    hostSm.ProcessString(L"AAAAA");
    hostSm.ProcessString(useLongSpaces ? L"          " : L"     ");
    if (changeAttributes)
    {
        hostSm.ProcessString(L"\x1b[44m");
    }
    if (printTextAfterSpaces)
    {
        hostSm.ProcessString(L"ZZZZZ");
    }
    hostSm.ProcessString(L"\x1b[0m");

    if (cursorOnNextLine)
    {
        hostSm.ProcessString(L"\n");
        hostSm.ProcessString(L"BBBBB");
    }
    Log::Comment(L"Painting after the initial setup.");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Switching to the alt buffer and using DECALN to fill it with 'E's");
    hostSm.ProcessString(L"\x1b[?1049h");
    hostSm.ProcessString(L"\x1b#8");
    if (paintAfterDECALN)
    {
        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    for (auto i = 0; i < si.GetViewport().Height(); i++)
    {
        hostSm.ProcessString(L"\n");
        if (paintEachNewline == PaintEveryNewline)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }
    }
    if (paintEachNewline == PaintAfterAllNewlines)
    {
        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }
    Log::Comment(L"Returning to the main buffer.");
    hostSm.ProcessString(L"\x1b[?1049l");

    Log::Comment(L"Checking the host buffer state");
    verifyBuffer(hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Checking the terminal buffer state");
    verifyBuffer(termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::OverstrikeAtBottomOfBuffer()
{
    // See https://github.com/microsoft/terminal/pull/5181#issuecomment-607545241
    Log::Comment(L"This test replicates the zsh menu-complete functionality. In"
                 L" the course of a single frame, we're going to both scroll "
                 L"the frame and print multiple lines of text above the bottom line.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    auto verifyBuffer = [](const TextBuffer& tb,
                           const til::rect viewport) {
        const auto lastRow = viewport.bottom - 1;
        const til::point expectedCursor{ 0, lastRow - 1 };
        VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        VERIFY_IS_TRUE(tb.GetCursor().IsVisible());

        TestUtils::VerifyExpectedString(tb, L"AAAAAAAAAA          DDDDDDDDDD", { 0, lastRow - 2 });
        TestUtils::VerifyExpectedString(tb, L"BBBBBBBBBB", { 0, lastRow - 1 });
        TestUtils::VerifyExpectedString(tb, L"FFFFFFFFFE", { 0, lastRow });
    };

    _logConpty = true;
    // We're _not_ checking the conpty output during this test, only the side effects.
    _checkConptyOutput = false;

    hostSm.ProcessString(L"\x1b#8");

    hostSm.ProcessString(L"\x1b[32;1H");

    hostSm.ProcessString(L"\x1b[J");
    hostSm.ProcessString(L"AAAAAAAAAA");
    hostSm.ProcessString(L"\x1b[K");
    hostSm.ProcessString(L"\r");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"BBBBBBBBBB");
    hostSm.ProcessString(L"\x1b[K");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"CCCCCCCCCC");
    hostSm.ProcessString(L"\x1b[2A");
    hostSm.ProcessString(L"\r");
    hostSm.ProcessString(L"\x1b[20C");
    hostSm.ProcessString(L"DDDDDDDDDD");
    hostSm.ProcessString(L"\x1b[K");
    hostSm.ProcessString(L"\r");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"\x1b[1B");
    hostSm.ProcessString(L"EEEEEEEEEE");
    hostSm.ProcessString(L"\r");
    hostSm.ProcessString(L"FFFFFFFFF");
    hostSm.ProcessString(L"\r");
    hostSm.ProcessString(L"\x1b[A");
    hostSm.ProcessString(L"\x1b[A");
    hostSm.ProcessString(L"\n");

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");

    verifyBuffer(termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::MarginsWithStatusLine()
{
    // See https://github.com/microsoft/terminal/issues/5161
    //
    // This test reproduces a case from the MSYS/cygwin (runtime < 3.1) vim.
    // From what I can tell, they implement scrolling by emitting a newline at
    // the bottom of the buffer (to create a new blank line), then they use
    // ScrollConsoleScreenBuffer to shift the status line(s) down a line, and
    // then they re-printing the status line.
    Log::Comment(L"Newline, and scroll the bottom lines of the buffer down with"
                 L" ScrollConsoleScreenBuffer to emulate how cygwin VIM works");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    auto verifyBuffer = [](const TextBuffer& tb,
                           const til::rect viewport) {
        const auto lastRow = viewport.bottom - 1;
        const til::point expectedCursor{ 1, lastRow };
        VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        VERIFY_IS_TRUE(tb.GetCursor().IsVisible());

        TestUtils::VerifyExpectedString(tb, L"EEEEEEEEEE", { 0, lastRow - 4 });
        TestUtils::VerifyExpectedString(tb, L"AAAAAAAAAA", { 0, lastRow - 3 });
        TestUtils::VerifyExpectedString(tb, L"          ", { 0, lastRow - 2 });
        TestUtils::VerifyExpectedString(tb, L"XBBBBBBBBB", { 0, lastRow - 1 });
        TestUtils::VerifyExpectedString(tb, L"YCCCCCCCCC", { 0, lastRow });
    };

    // We're _not_ checking the conpty output during this test, only the side effects.
    _checkConptyOutput = false;

    // Use DECALN to fill the buffer with 'E's.
    hostSm.ProcessString(L"\x1b#8");

    const auto originalBottom = si.GetViewport().BottomInclusive();
    // Print 3 lines into the bottom of the buffer:
    // AAAAAAAAAA
    // BBBBBBBBBB
    // CCCCCCCCCC
    // In this test, the 'B' and 'C' lines represent the status lines at the
    // bottom of vim, and the 'A' line is a buffer line.
    hostSm.ProcessString(L"\x1b[30;1H");

    hostSm.ProcessString(L"AAAAAAAAAA");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"BBBBBBBBBB");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"CCCCCCCCCC");

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // After printing the 'C' line, the cursor is on the bottom line of the viewport.
    // Emit a newline here to get a new line at the bottom of the viewport.
    hostSm.ProcessString(L"\n");
    const auto newBottom = si.GetViewport().BottomInclusive();

    {
        // Emulate calling ScrollConsoleScreenBuffer to scroll the B and C lines
        // down one line.
        til::inclusive_rect src;
        src.top = newBottom - 2;
        src.left = 0;
        src.right = si.GetViewport().Width();
        src.bottom = originalBottom;
        til::point tgt{ 0, newBottom - 1 };
        TextAttribute useThisAttr(0x07); // We don't terribly care about the attributes so this is arbitrary
        ScrollRegion(si, src, std::nullopt, tgt, L' ', useThisAttr);
    }

    // Move the cursor to the location of the B line
    hostSm.ProcessString(L"\x1b[31;1H");

    // Print an 'X' on the 'B' line, and a 'Y' on the 'C' line.
    hostSm.ProcessString(L"X");
    hostSm.ProcessString(L"\n");
    hostSm.ProcessString(L"Y");

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");

    verifyBuffer(termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::OutputWrappedLineWithSpace()
{
    // See https://github.com/microsoft/terminal/pull/5181#issuecomment-610110348
    Log::Comment(L"Ensures that a buffer line in conhost that wrapped _on a "
                 L"space_ will still be emitted as wrapped.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    const auto firstTextLength = TerminalViewWidth - 2;
    const auto spacesLength = 3;
    const auto secondTextLength = 1;

    sm.ProcessString(std::wstring(firstTextLength, L'A'));
    sm.ProcessString(std::wstring(spacesLength, L' '));
    sm.ProcessString(std::wstring(secondTextLength, L'B'));

    auto verifyBuffer = [&](const TextBuffer& tb) {
        // Buffer contents should look like the following: (80 wide)
        // (w) means we hard wrapped the line
        // (b) means the line is _not_ wrapped (it's broken, the default state.)
        //
        // |AAAA...AA  | (w)
        // | B_ ...    | (b) (cursor is on the '_')
        // |    ...    | (b)

        VERIFY_IS_TRUE(tb.GetRowByOffset(0).WasWrapForced());
        VERIFY_IS_FALSE(tb.GetRowByOffset(1).WasWrapForced());

        // First row
        auto iter0 = tb.GetCellDataAt({ 0, 0 });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, firstTextLength);
        TestUtils::VerifySpanOfText(L" ", iter0, 0, 2);

        // Second row
        auto iter1 = tb.GetCellDataAt({ 0, 1 });
        TestUtils::VerifySpanOfText(L" ", iter1, 0, 1);
        auto iter2 = tb.GetCellDataAt({ 1, 1 });
        TestUtils::VerifySpanOfText(L"B", iter2, 0, secondTextLength);
    };

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(hostTb);

    auto firstLine = std::string(firstTextLength, 'A');
    firstLine += "  ";
    std::string secondLine{ " B" };

    expectedOutput.push_back(firstLine);
    expectedOutput.push_back(secondLine);
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    verifyBuffer(termTb);
}

void ConptyRoundtripTests::OutputWrappedLineWithSpaceAtBottomOfBuffer()
{
    // See https://github.com/microsoft/terminal/pull/5181#issuecomment-610110348
    // This is the same test as OutputWrappedLineWithSpace, but at the bottom of
    // the buffer, so we get scrolling behavior as well.
    Log::Comment(L"Ensures that a buffer line in conhost that wrapped _on a "
                 L"space_ will still be emitted as wrapped.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    // First, fill the buffer with contents, so conpty starts circling
    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        Log::Comment(NoThrowString().Format(L"Writing line %d/%d", i, end));
        expectedOutput.push_back("X");
        if (i < hostView.BottomInclusive())
        {
            expectedOutput.push_back("\r\n");
        }
        else
        {
            // After we hit the bottom of the viewport, the newlines come in
            // separated by empty writes for whatever reason.
            expectedOutput.push_back("\r");
            expectedOutput.push_back("\n");
            expectedOutput.push_back("");
        }

        sm.ProcessString(L"X\n");

        VERIFY_SUCCEEDED(renderer.PaintFrame());
    }

    const auto firstTextLength = TerminalViewWidth - 2;
    const auto spacesLength = 3;
    const auto secondTextLength = 1;

    auto firstLine = std::string(firstTextLength, 'A');
    firstLine += "  ";
    std::string secondLine{ " B" };

    // The following diagrams show the buffer contents after each string emitted
    // from conpty. For each of these diagrams:
    // (w) means we hard wrapped the line
    // (b) means the line is _not_ wrapped (it's broken, the default state.)
    // cursor is on the '_'

    // Initial state:
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |_              | (b)

    expectedOutput.push_back(firstLine);
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AA _| (w) The cursor is actually on the last ' ' here

    // TODO GH#5228 might break the "newline & repaint the wrapped char" checks here, that's okay.
    expectedOutput.push_back("\r"); // This \r\n is emitted by ScrollFrame to
    expectedOutput.push_back("\n"); // add a newline to the bottom of the buffer
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AA | (b)
    // |_              | (b)

    expectedOutput.push_back("\x1b[31;80H"); // Move the cursor BACK to the wrapped row
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AA _| (b) The cursor is actually on the last ' ' here
    // |               | (b)

    expectedOutput.push_back(std::string(1, ' ')); // Reprint the last character of the wrapped row
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AA  |_ (w) The cursor is actually on the last ' ' here
    // |               | (b)

    expectedOutput.push_back(secondLine);
    // |X              | (b)
    // |X              | (b)
    // ...
    // |X              | (b)
    // |AAAAAAAA...AA  | (w)
    // | B_            | (b)

    sm.ProcessString(std::wstring(firstTextLength, L'A'));
    sm.ProcessString(std::wstring(spacesLength, L' '));
    sm.ProcessString(std::wstring(secondTextLength, L'B'));

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport) {
        // Buffer contents should look like the following: (80 wide)
        // (w) means we hard wrapped the line
        // (b) means the line is _not_ wrapped (it's broken, the default state.)
        //
        // |AAAA...AA  | (w)
        // | B_ ...    | (b) (cursor is on the '_')
        // |    ...    | (b)

        const auto wrappedRow = viewport.bottom - 2;
        VERIFY_IS_TRUE(tb.GetRowByOffset(wrappedRow).WasWrapForced());
        VERIFY_IS_FALSE(tb.GetRowByOffset(wrappedRow + 1).WasWrapForced());

        // First row
        auto iter0 = tb.GetCellDataAt({ 0, wrappedRow });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, firstTextLength);
        TestUtils::VerifySpanOfText(L" ", iter0, 0, 2);

        // Second row
        auto iter1 = tb.GetCellDataAt({ 0, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L" ", iter1, 0, 1);
        auto iter2 = tb.GetCellDataAt({ 1, wrappedRow + 1 });
        TestUtils::VerifySpanOfText(L"B", iter2, 0, secondTextLength);
    };

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(hostTb, hostView.ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    verifyBuffer(termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::BreakLinesOnCursorMovement()
{
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:cursorMovementMode", L"{0, 1, 2, 3, 4, 5, 6}")
    END_TEST_METHOD_PROPERTIES();
    constexpr auto MoveCursorWithCUP = 0;
    constexpr auto MoveCursorWithCR_LF = 1;
    constexpr auto MoveCursorWithLF_CR = 2;
    constexpr auto MoveCursorWithVPR_CR = 3;
    constexpr auto MoveCursorWithCUB_LF = 4;
    constexpr auto MoveCursorWithCUD_CR = 5;
    constexpr auto MoveCursorWithNothing = 6;
    INIT_TEST_PROPERTY(int, cursorMovementMode, L"Controls how we move the cursor, either with CUP, newline/carriage-return, or some other VT sequence");

    Log::Comment(L"This is a test for GH#5291. WSL vim uses spaces to clear the"
                 L" ends of blank lines, not EL. This test ensures we emit text"
                 L" from conpty such that the terminal re-creates the state of"
                 L" the host, which includes wrapped lines of lots of spaces.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    _flushFirstFrame();

    // Any of the cursor movements that use a LF will actually hard break the
    // line - everything else will leave it marked as wrapped.
    const auto expectHardBreak = (cursorMovementMode == MoveCursorWithLF_CR) ||
                                 (cursorMovementMode == MoveCursorWithCR_LF) ||
                                 (cursorMovementMode == MoveCursorWithCUB_LF);

    auto verifyBuffer = [&](const TextBuffer& tb,
                            const til::rect viewport) {
        const auto lastRow = viewport.bottom - 1;
        const til::point expectedCursor{ 5, lastRow };
        VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        VERIFY_IS_TRUE(tb.GetCursor().IsVisible());

        for (auto y = viewport.top; y < lastRow; y++)
        {
            // We're using CUP to move onto the status line _always_, so the
            // second-last row will always be marked as wrapped.
            const auto rowWrapped = (!expectHardBreak) || (y == lastRow - 1);
            VERIFY_ARE_EQUAL(rowWrapped, tb.GetRowByOffset(y).WasWrapForced());
            TestUtils::VerifyExpectedString(tb, L"~    ", { 0, y });
        }

        TestUtils::VerifyExpectedString(tb, L"AAAAA", { 0, lastRow });
    };

    // We're _not_ checking the conpty output during this test, only the side effects.
    _logConpty = true;
    _checkConptyOutput = false;

    // Lock must be taken to manipulate alt/main buffer state.
    gci.LockConsole();
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });

    // Use DECALN to fill the buffer with 'E's.
    hostSm.ProcessString(L"\x1b#8");

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Switching to the alt buffer");
    hostSm.ProcessString(L"\x1b[?1049h");
    auto restoreBuffer = wil::scope_exit([&] { hostSm.ProcessString(L"\x1b[?1049l"); });
    auto& altBuffer = gci.GetActiveOutputBuffer();
    auto& altTextBuffer = altBuffer.GetTextBuffer();

    // Go home and clear the screen.
    hostSm.ProcessString(L"\x1b[H");
    hostSm.ProcessString(L"\x1b[2J");

    // Write out lines of '~' followed by enough spaces to fill the line.
    hostSm.ProcessString(L"\x1b[94m");
    for (auto y = 0; y < altBuffer.GetViewport().BottomInclusive(); y++)
    {
        // Vim uses CUP to position the cursor on the first cell of each row, every row.
        if (cursorMovementMode == MoveCursorWithCUP)
        {
            std::wstringstream ss;
            ss << L"\x1b[";
            ss << y + 1;
            ss << L";1H";
            hostSm.ProcessString(ss.str());
        }
        // As an additional test, try breaking lines manually with \r\n
        else if (cursorMovementMode == MoveCursorWithCR_LF)
        {
            // Don't need to newline on the 0'th row
            if (y > 0)
            {
                hostSm.ProcessString(L"\r\n");
            }
        }
        // As an additional test, try breaking lines manually with \n\r
        else if (cursorMovementMode == MoveCursorWithLF_CR)
        {
            // Don't need to newline on the 0'th row
            if (y > 0)
            {
                hostSm.ProcessString(L"\n\r");
            }
        }
        // As an additional test, move the cursor down with VPR, then to the start of the line with CR
        else if (cursorMovementMode == MoveCursorWithVPR_CR)
        {
            // Don't need to newline on the 0'th row
            if (y > 0)
            {
                hostSm.ProcessString(L"\x1b[1e");
                hostSm.ProcessString(L"\r");
            }
        }
        // As an additional test, move the cursor back with CUB, then down with LF
        else if (cursorMovementMode == MoveCursorWithCUB_LF)
        {
            // Don't need to newline on the 0'th row
            if (y > 0)
            {
                hostSm.ProcessString(L"\x1b[80D");
                hostSm.ProcessString(L"\n");
            }
        }
        // As an additional test, move the cursor down with CUD, then to the start of the line with CR
        else if (cursorMovementMode == MoveCursorWithCUD_CR)
        {
            // Don't need to newline on the 0'th row
            if (y > 0)
            {
                hostSm.ProcessString(L"\x1b[B");
                hostSm.ProcessString(L"\r");
            }
        }
        // Win32 vim.exe will simply do _nothing_ in this scenario. It'll just
        // print the lines one after the other, without moving the cursor,
        // relying on us auto moving to the following line.
        else if (cursorMovementMode == MoveCursorWithNothing)
        {
        }

        // IMPORTANT! The way vim writes these blank lines is as '~' followed by
        // enough spaces to fill the line.
        // This bug (GH#5291 won't repro if you don't have the spaces).
        std::wstring line{ L"~" };
        line += std::wstring(79, L' ');
        hostSm.ProcessString(line);
    }

    // Print the "Status Line"
    hostSm.ProcessString(L"\x1b[32;1H");
    hostSm.ProcessString(L"\x1b[m");
    hostSm.ProcessString(L"AAAAA");

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(altTextBuffer, altBuffer.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    // GH#3492: Now that we support the alt buffer, make sure to validate the
    // _alt buffer's_ contents.
    verifyBuffer(*term->_altBuffer, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::TestCursorInDeferredEOLPositionOnNewLineWithSpaces()
{
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();

    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;
    const auto termView = term->GetViewport();

    _flushFirstFrame();
    _checkConptyOutput = false;

    // newline down to the bottom
    hostSm.ProcessString(std::wstring(gsl::narrow_cast<size_t>(TerminalViewHeight), L'\n'));
    // fill width-1 with "A", then add one space and another character..
    hostSm.ProcessString(std::wstring(gsl::narrow_cast<size_t>(TerminalViewWidth) - 1, L'A') + L" B");

    auto verifyBuffer = [&](const TextBuffer& tb, til::CoordType bottomRow) {
        // Buffer contents should look like the following: (80 wide)
        // (w) means we hard wrapped the line
        // (b) means the line is _not_ wrapped (it's broken, the default state.)
        // cursor is on the '_'
        //
        // | ^ ^ ^ ^ ^ ^ ^ | ( ) (entire buffer above us; contents do not matter)
        // |               | ( ) (entire buffer above us; contents do not matter)
        // |AAAAAAAA...AAA | (w) (79 'A's, one space)
        // |B_      ...    | (b) (There's only one 'B' on this line)

        til::point cursorPos{ tb.GetCursor().GetPosition() };
        // The cursor should be on the second char of the last line
        VERIFY_ARE_EQUAL(til::point(1, bottomRow), cursorPos);

        const auto& secondToLastRow = tb.GetRowByOffset(bottomRow - 1);
        const auto& lastRow = tb.GetRowByOffset(bottomRow);
        VERIFY_IS_TRUE(secondToLastRow.WasWrapForced());
        VERIFY_IS_FALSE(lastRow.WasWrapForced());

        auto expectedStringSecondToLastRow{ std::wstring(gsl::narrow_cast<size_t>(tb.GetSize().Width()) - 1, L'A') + L" " };
        TestUtils::VerifyExpectedString(tb, expectedStringSecondToLastRow, { 0, bottomRow - 1 });
        TestUtils::VerifyExpectedString(tb, L"B", { 0, bottomRow });
    };

    const auto hostView = si.GetViewport();
    verifyBuffer(hostTb, hostView.BottomInclusive());

    VERIFY_SUCCEEDED(renderer.PaintFrame());

    const auto newTermView = term->GetViewport();
    verifyBuffer(termTb, newTermView.BottomInclusive());
}

void ConptyRoundtripTests::ResizeRepaintVimExeBuffer()
{
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
    END_TEST_METHOD_PROPERTIES()

    // See https://github.com/microsoft/terminal/issues/5428
    Log::Comment(L"This test emulates what happens when you decrease the width "
                 L"of the window while running vim.exe.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // This is a helper that will recreate the way that vim redraws the buffer.
    auto drawVim = [&sm, &si]() {
        const auto hostView = si.GetViewport();
        const auto width = hostView.Width();

        // Write:
        // * AAA to the first line
        // * BBB to the second line
        // * a bunch of lines with a "~" followed by spaces (just the way vim.exe likes to render)
        // * A status line with a bunch of "X"s and _a single space in the last cell_.
        sm.ProcessString(L"AAA");
        sm.ProcessString(L"\r\n");
        sm.ProcessString(L"BBB");
        sm.ProcessString(L"\r\n");

        for (auto i = 2; i < hostView.BottomInclusive(); i++)
        {
            // IMPORTANT! The way vim writes these blank lines is as '~' followed by
            // enough spaces to fill the line.
            std::wstring line{ L"~" };
            line += std::wstring(width - 1, L' ');
            sm.ProcessString(line);
        }
        sm.ProcessString(std::wstring(width - 1, L'X'));
        sm.ProcessString(L" ");

        // Move the cursor back home, as if it's on the first "A". The bug won't
        // repro without this!
        sm.ProcessString(L"\x1b[H");
    };

    drawVim();

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport) {
        const auto firstRow = viewport.top;
        const auto width = viewport.width();

        // First row
        VERIFY_IS_FALSE(tb.GetRowByOffset(firstRow).WasWrapForced());
        auto iter0 = tb.GetCellDataAt({ 0, firstRow });
        TestUtils::VerifySpanOfText(L"A", iter0, 0, 3);
        TestUtils::VerifySpanOfText(L" ", iter0, 0, width - 3);

        // Second row
        VERIFY_IS_FALSE(tb.GetRowByOffset(firstRow + 1).WasWrapForced());
        auto iter1 = tb.GetCellDataAt({ 0, firstRow + 1 });
        TestUtils::VerifySpanOfText(L"B", iter1, 0, 3);
        TestUtils::VerifySpanOfText(L" ", iter1, 0, width - 3);

        // "~" rows
        for (auto row = firstRow + 2; row < viewport.bottom - 1; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));
            VERIFY_IS_TRUE(tb.GetRowByOffset(row).WasWrapForced());
            auto iter = tb.GetCellDataAt({ 0, row });
            TestUtils::VerifySpanOfText(L"~", iter, 0, 1);
            TestUtils::VerifySpanOfText(L" ", iter, 0, width - 1);
        }

        // Last row
        {
            const auto row = viewport.bottom - 1;
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));
            VERIFY_IS_TRUE(tb.GetRowByOffset(row).WasWrapForced());
            auto iter = tb.GetCellDataAt({ 0, row });
            TestUtils::VerifySpanOfText(L"X", iter, 0, width - 1);
            TestUtils::VerifySpanOfText(L" ", iter, 0, 1);
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());

    // After we resize, make sure to get the new textBuffers
    std::tie(hostTb, termTb) = _performResize({ TerminalViewWidth - 1,
                                                TerminalViewHeight });

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"Re-painting vim");
    // vim will redraw itself when it notices the buffer size change.
    drawVim();

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::ClsAndClearHostClearsScrollbackTest()
{
    // See https://github.com/microsoft/terminal/issues/3126#issuecomment-620677742

    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:clearBufferMethod", L"{0, 1, 2}")
    END_TEST_METHOD_PROPERTIES();
    constexpr auto ClearLikeCls = 0;
    constexpr auto ClearLikeClearHost = 1;
    constexpr auto ClearWithVT = 2;
    INIT_TEST_PROPERTY(int, clearBufferMethod, L"Controls whether we clear the buffer like cmd or like powershell");

    Log::Comment(L"This test checks the shims for cmd.exe and powershell.exe. "
                 L"Their build in commands for clearing the console buffer "
                 L"should work to clear the terminal buffer, not just the "
                 L"terminal viewport.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    auto& sm = si.GetStateMachine();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    const auto hostView = si.GetViewport();
    const auto end = 2 * hostView.Height();
    for (auto i = 0; i < end; i++)
    {
        if (i > 0)
        {
            sm.ProcessString(L"\r\n");
        }

        sm.ProcessString(L"~");
    }

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool afterClear = false) {
        const auto width = viewport.width();

        // "~" rows
        for (til::CoordType row = 0; row < viewport.bottom; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));
            VERIFY_IS_FALSE(tb.GetRowByOffset(row).WasWrapForced());
            auto iter = tb.GetCellDataAt({ 0, row });
            if (afterClear)
            {
                TestUtils::VerifySpanOfText(L" ", iter, 0, width);
            }
            else
            {
                TestUtils::VerifySpanOfText(L"~", iter, 0, 1);
                TestUtils::VerifySpanOfText(L" ", iter, 0, width - 1);
            }
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());

    VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions());
    VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize());

    if (clearBufferMethod == ClearLikeCls)
    {
        // Execute the cls, EXACTLY LIKE CMD.

        CONSOLE_SCREEN_BUFFER_INFOEX csbiex{ 0 };
        csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
        _apiRoutines.GetConsoleScreenBufferInfoExImpl(si, csbiex);

        til::inclusive_rect src{ 0 };
        src.top = 0;
        src.left = 0;
        src.right = csbiex.dwSize.X;
        src.bottom = csbiex.dwSize.Y;

        til::point tgt{ 0, -csbiex.dwSize.Y };
        VERIFY_SUCCEEDED(_apiRoutines.ScrollConsoleScreenBufferWImpl(si,
                                                                     src,
                                                                     tgt,
                                                                     std::nullopt, // no clip provided,
                                                                     L' ',
                                                                     csbiex.wAttributes,
                                                                     true));
    }
    else if (clearBufferMethod == ClearLikeClearHost)
    {
        CONSOLE_SCREEN_BUFFER_INFOEX csbiex{ 0 };
        csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
        _apiRoutines.GetConsoleScreenBufferInfoExImpl(si, csbiex);

        const auto totalCellsInBuffer = csbiex.dwSize.X * csbiex.dwSize.Y;
        size_t cellsWritten = 0;
        VERIFY_SUCCEEDED(_apiRoutines.FillConsoleOutputCharacterWImpl(si,
                                                                      L' ',
                                                                      totalCellsInBuffer,
                                                                      { 0, 0 },
                                                                      cellsWritten,
                                                                      true));
        VERIFY_SUCCEEDED(_apiRoutines.FillConsoleOutputAttributeImpl(si,
                                                                     csbiex.wAttributes,
                                                                     totalCellsInBuffer,
                                                                     { 0, 0 },
                                                                     cellsWritten));
    }
    else if (clearBufferMethod == ClearWithVT)
    {
        sm.ProcessString(L"\x1b[2J");
        VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions());
        VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize());

        sm.ProcessString(L"\x1b[3J");
    }

    VERIFY_ARE_EQUAL(si.GetViewport().Dimensions(), si.GetBufferSize().Dimensions());
    VERIFY_ARE_EQUAL(si.GetViewport(), si.GetBufferSize());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true);
}

void ConptyRoundtripTests::TestResizeWithCookedRead()
{
    // see https://github.com/microsoft/terminal/issues/1856
    Log::Comment(L"This test checks a crash in conpty where resizing the "
                 L"window with any data in a cooked read (like the input line "
                 L"in cmd.exe) would cause the conpty to crash.");

    // Resizing with a COOKED_READ used to cause a crash in
    // `Selection::s_GetInputLineBoundaries` north of
    // `Selection::GetValidAreaBoundaries`.
    //
    // If this test completes successfully, then we know that we didn't crash.

    // The specific cases that repro the original crash are:
    // * (0, -10)
    // * (0, -1)
    // * (0, 0)
    // the rest of the cases are added here for completeness.

    // Don't let the cooked read pollute other tests
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
        TEST_METHOD_PROPERTY(L"Data:dx", L"{-10, -1, 0, 1, 10}")
        TEST_METHOD_PROPERTY(L"Data:dy", L"{-10, -1, 0, 1, 10}")
    END_TEST_METHOD_PROPERTIES()

    INIT_TEST_PROPERTY(int, dx, L"The change in width of the buffer");
    INIT_TEST_PROPERTY(int, dy, L"The change in height of the buffer");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // Setup the cooked read data
    m_state->PrepareReadHandle();
    // TODO GH#5618: This string will get mangled, but we don't really care
    // about the buffer contents in this test, so it doesn't really matter.
    const std::string_view cookedReadContents{ "This is some cooked read data" };
    m_state->PrepareCookedReadData(cookedReadContents);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // After we resize, make sure to get the new textBuffers
    std::tie(hostTb, termTb) = _performResize({ TerminalViewWidth + dx,
                                                TerminalViewHeight + dy });

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // By simply reaching the end of this test, we know that we didn't crash. Hooray!
}

void ConptyRoundtripTests::ResizeInitializeBufferWithDefaultAttrs()
{
    // See https://github.com/microsoft/terminal/issues/3848
    Log::Comment(L"This test checks that the attributes in the text buffer are "
                 L"initialized to a sensible value during a resize. The entire "
                 L"buffer shouldn't be filled with _whatever the current "
                 L"attributes are_, it should be filled with the default "
                 L"attributes (however the application defines that). Then, "
                 L"after the resize, we should still be able to print to the "
                 L"buffer with the old \"current attributes\"");

    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
        TEST_METHOD_PROPERTY(L"Data:dx", L"{-1, 0, 1}")
        TEST_METHOD_PROPERTY(L"Data:dy", L"{-1, 0, 1}")
        TEST_METHOD_PROPERTY(L"Data:leaveTrailingChar", L"{false, true}")
    END_TEST_METHOD_PROPERTIES()

    INIT_TEST_PROPERTY(int, dx, L"The change in width of the buffer");
    INIT_TEST_PROPERTY(int, dy, L"The change in height of the buffer");
    INIT_TEST_PROPERTY(bool, leaveTrailingChar, L"If true, we'll print one additional '#' on row 3");

    // Do nothing if the resize would just be a no-op.
    if (dx == 0 && dy == 0)
    {
        return;
    }

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    auto defaultAttrs = si.GetAttributes();
    auto conhostGreenAttrs = TextAttribute();
    conhostGreenAttrs.SetIndexedBackground(TextColor::DARK_GREEN);
    auto terminalGreenAttrs = TextAttribute();
    terminalGreenAttrs.SetIndexedBackground(TextColor::DARK_GREEN);

    // Use an initial ^[[m to start printing with default-on-default
    sm.ProcessString(L"\x1b[m");

    // Print three lines with "# #", where the first "# " are in
    // default-on-green.
    for (auto i = 0; i < 3; i++)
    {
        sm.ProcessString(L"\x1b[42m");
        sm.ProcessString(L"# ");
        sm.ProcessString(L"\x1b[m");
        sm.ProcessString(L"#");
        sm.ProcessString(L"\r\n");
    }

    // Now, leave the active attributes as default-on-green. When we resize the
    // buffers, we don't want them initialized with default-on-green, we want
    // them to use whatever the set default attributes are.
    sm.ProcessString(L"\x1b[42m");

    // If leaveTrailingChar is true, we'll leave one default-on-green '#' on row
    // 3. This will force conpty to change the Terminal's colors to
    // default-on-green, so we can check that not only conhost initialize the
    // buffer colors correctly, but so does the Terminal.
    if (leaveTrailingChar)
    {
        sm.ProcessString(L"#");
    }

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool isTerminal, const bool afterResize) {
        const auto width = viewport.width();

        // Conhost and Terminal attributes are potentially different.
        const auto greenAttrs = isTerminal ? terminalGreenAttrs : conhostGreenAttrs;

        for (til::CoordType row = 0; row < tb.GetSize().Height(); row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d...", row));

            VERIFY_IS_FALSE(tb.GetRowByOffset(row).WasWrapForced());

            const auto hasChar = row < 3;
            const auto actualDefaultAttrs = isTerminal ? TextAttribute() : defaultAttrs;

            if (hasChar)
            {
                auto iter = TestUtils::VerifyLineContains(tb, { 0, row }, L'#', greenAttrs, 1u);
                TestUtils::VerifyLineContains(iter, L' ', greenAttrs, 1u);
                TestUtils::VerifyLineContains(iter, L'#', TextAttribute(), 1u);
                // After the resize, the default attrs of the last char will
                // extend to fill the rest of the row. This is GH#32. If that
                // bug ever gets fixed, this test will break, but that's
                // ABSOLUTELY OKAY.
                TestUtils::VerifyLineContains(iter, L' ', (afterResize ? TextAttribute() : actualDefaultAttrs), static_cast<size_t>(width - 3));
            }
            else if (leaveTrailingChar && row == 3)
            {
                auto iter = TestUtils::VerifyLineContains(tb, { 0, row }, L'#', greenAttrs, 1u);
                TestUtils::VerifyLineContains(iter, L' ', (actualDefaultAttrs), static_cast<size_t>(width - 1));
            }
            else
            {
                TestUtils::VerifyLineContains(tb, { 0, row }, L' ', actualDefaultAttrs, viewport.narrow_width<size_t>());
            }
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), false, false);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true, false);

    // After we resize, make sure to get the new textBuffers
    std::tie(hostTb, termTb) = _performResize({ TerminalViewWidth + dx,
                                                TerminalViewHeight + dy });

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), false, true);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true, true);
}

void ConptyRoundtripTests::NewLinesAtBottomWithBackground()
{
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:paintEachNewline", L"{false, true}")
        TEST_METHOD_PROPERTY(L"Data:spacesToPrint", L"{1, 7, 8, 9, 32}")
    END_TEST_METHOD_PROPERTIES();

    INIT_TEST_PROPERTY(bool, paintEachNewline, L"If true, call PaintFrame after each pair of lines.");
    INIT_TEST_PROPERTY(int, spacesToPrint, L"Controls the number of spaces printed after the first '#'");

    // See https://github.com/microsoft/terminal/issues/5502
    Log::Comment(L"Attempts to emit text to a new bottom line with spaces with "
                 L"a colored background. When that happens, we should make "
                 L"sure to still print the spaces, because the information "
                 L"about their background color is important.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    auto defaultAttrs = si.GetAttributes();
    auto conhostBlueAttrs = defaultAttrs;
    conhostBlueAttrs.SetIndexedForeground(TextColor::DARK_GREEN);
    conhostBlueAttrs.SetIndexedBackground(TextColor::DARK_BLUE);
    auto terminalBlueAttrs = TextAttribute();
    terminalBlueAttrs.SetIndexedForeground(TextColor::DARK_GREEN);
    terminalBlueAttrs.SetIndexedBackground(TextColor::DARK_BLUE);

    // We're going to print 4 more rows than the entire height of the viewport,
    // causing the buffer to circle 4 times. This is 2 extra iterations of the
    // two lines we're printing per iteration.
    const auto circledRows = 4;
    for (auto i = 0; i < (TerminalViewHeight + circledRows) / 2; i++)
    {
        // We're printing pairs of lines: ('_' is a space character)
        //
        // Line 1 chars: __________________ (break)
        // Line 1 attrs: DDDDDDDDDDDDDDDDDD (all default attrs)
        // Line 2 chars: ____#_________#___ (break)
        // Line 2 attrs: BBBBBBBBBBBBBBDDDD (First spacesToPrint+5 are blue BG, then default attrs)
        //                    [<----->]
        //                      This number of spaces controlled by spacesToPrint
        if (i > 0)
        {
            sm.ProcessString(L"\r\n");
        }

        // In WSL:
        // echo -e "\e[m\r\n\e[44;32m    #         \e[m#"

        sm.ProcessString(L"\x1b[m");
        sm.ProcessString(L"\r");
        sm.ProcessString(L"\n");
        sm.ProcessString(L"\x1b[44;32m");
        sm.ProcessString(L"    #");
        sm.ProcessString(std::wstring(spacesToPrint, L' '));
        sm.ProcessString(L"\x1b[m");
        sm.ProcessString(L"#");

        if (paintEachNewline)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }
    }

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport) {
        const auto width = viewport.width();
        const auto isTerminal = viewport.top != 0;

        // Conhost and Terminal attributes are potentially different.
        const auto blueAttrs = isTerminal ? terminalBlueAttrs : conhostBlueAttrs;

        for (til::CoordType row = 0; row < viewport.bottom - 2; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));
            VERIFY_IS_FALSE(tb.GetRowByOffset(row).WasWrapForced());

            const auto isBlank = (row % 2) == 0;
            const auto rowCircled = row > (viewport.bottom - 1 - circledRows);
            // When the buffer circles, new lines will be initialized using the
            // current text attributes. Those will be the default-on-default
            // attributes. All of the Terminal's buffer will use
            // default-on-default.
            const auto actualDefaultAttrs = rowCircled || isTerminal ? TextAttribute() : defaultAttrs;
            Log::Comment(NoThrowString().Format(L"isBlank=%d, rowCircled=%d", isBlank, rowCircled));

            if (isBlank)
            {
                TestUtils::VerifyLineContains(tb, { 0, row }, L' ', actualDefaultAttrs, viewport.narrow_width<size_t>());
            }
            else
            {
                auto iter = TestUtils::VerifyLineContains(tb, { 0, row }, L' ', blueAttrs, 4u);
                TestUtils::VerifyLineContains(iter, L'#', blueAttrs, 1u);
                TestUtils::VerifyLineContains(iter, L' ', blueAttrs, static_cast<size_t>(spacesToPrint));
                TestUtils::VerifyLineContains(iter, L'#', TextAttribute(), 1u);
                TestUtils::VerifyLineContains(iter, L' ', actualDefaultAttrs, static_cast<size_t>(width - 15));
            }
        }
    };

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());
}

void doWriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view string, DWORD flags = 0)
{
    auto dwNumBytes = string.size() * sizeof(wchar_t);
    VERIFY_NT_SUCCESS(WriteCharsLegacy(screenInfo,
                                       string.data(),
                                       string.data(),
                                       string.data(),
                                       &dwNumBytes,
                                       nullptr,
                                       screenInfo.GetTextBuffer().GetCursor().GetPosition().x,
                                       flags,
                                       nullptr));
}

void ConptyRoundtripTests::WrapNewLineAtBottom()
{
    // The actual bug case is
    // * paintEachNewline=2 (PaintEveryLine)
    // * writingMethod=1 (PrintWithWriteCharsLegacy)
    // * circledRows=4
    //
    // Though, mysteriously, the bug that this test caught _WASN'T_ the fix for
    // #5691

    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:paintEachNewline", L"{0, 1, 2}")
        TEST_METHOD_PROPERTY(L"Data:writingMethod", L"{0, 1}")
        TEST_METHOD_PROPERTY(L"Data:circledRows", L"{2, 4, 10}")
    END_TEST_METHOD_PROPERTIES();

    // By modifying how often we call PaintFrame, we can test if different
    // timings for the frame affect the results. In this test we'll be printing
    // a bunch of paired lines. These values control when the PaintFrame calls
    // will occur:
    constexpr auto PaintAfterBothLines = 1; // Paint after each pair of lines is output
    constexpr auto PaintEveryLine = 2; // Paint after each and every line is output.

    constexpr auto PrintWithPrintString = 0;
    constexpr auto PrintWithWriteCharsLegacy = 1;

    INIT_TEST_PROPERTY(int, writingMethod, L"Controls using either ProcessString or WriteCharsLegacy to write to the buffer");
    INIT_TEST_PROPERTY(int, circledRows, L"Controls the number of lines we output.");
    INIT_TEST_PROPERTY(int, paintEachNewline, L"Controls whether we should call PaintFrame every line of text or not.");

    // GH#5839 -
    // This test does expose a real bug when using WriteCharsLegacy to emit
    // wrapped lines in conpty without delayed EOL wrap. However, this fix has
    // not yet been made, so for now, we need to just skip the cases that cause
    // this.
    if (writingMethod == PrintWithWriteCharsLegacy && paintEachNewline == PaintEveryLine)
    {
        Log::Result(WEX::Logging::TestResults::Skipped);
        return;
    }

    // I've tested this with 0x0, 0x4, 0x80, 0x84, and 0-8, and none of these
    // flags seem to make a difference. So we're just assuming 0 here, so we
    // don't test a bunch of redundant cases.
    const auto writeCharsLegacyMode = 0;

    // This test was originally written for
    //   https://github.com/microsoft/terminal/issues/5691
    //
    // It does not _actually_ test #5691 however, it merely checks another issue
    // found during debugging.

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    const auto width = static_cast<size_t>(TerminalViewWidth);

    const auto charsInFirstLine = width;
    const auto numCharsFirstColor = 30;
    const auto numCharsSecondColor = charsInFirstLine - numCharsFirstColor;
    const auto charsInSecondLine = width / 2;

    const auto defaultAttrs = TextAttribute();
    const auto conhostDefaultAttrs = si.GetAttributes();

    // Helper to abstract calls into either StateMachine::ProcessString or
    // WriteCharsLegacy
    auto print = [&](const std::wstring_view str) {
        if (writingMethod == PrintWithPrintString)
        {
            sm.ProcessString(str);
        }
        else if (writingMethod == PrintWithWriteCharsLegacy)
        {
            doWriteCharsLegacy(si, str, writeCharsLegacyMode);
        }
    };

    // Each of the lines of text in the buffer will look like the following:
    //
    // Line 1 chars: ~~~~~~~~~~~~~~~~~~ <wrap>
    // Line 1 attrs: YYYYYYYDDDDDDDDDDD (First 30 are yellow FG, then default attrs)
    // Line 2 chars: ~~~~~~~~~~________ <break> (there are width/2 '~'s here)
    // Line 2 attrs: DDDDDDDDDDDDDDDDDD (all are default attrs)
    //

    for (auto i = 0; i < (TerminalViewHeight + circledRows) / 2; i++)
    {
        Log::Comment(NoThrowString().Format(L"writing pair of lines %d", i));

        if (i > 0)
        {
            sm.ProcessString(L"\r\n");
        }

        sm.ProcessString(L"\x1b[33m");

        print(std::wstring(numCharsFirstColor, L'~'));

        sm.ProcessString(L"\x1b[m");

        print(std::wstring(numCharsSecondColor, L'~'));

        // If we're painting every line, then paint now, while conpty is in a
        // deferred EOL state. Otherwise, we'll wait to paint till after more output.
        if (paintEachNewline == PaintEveryLine)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }

        // Print the rest of the string of text, which should continue from the
        // wrapped line before it.
        print(std::wstring(charsInSecondLine, L'~'));

        if (paintEachNewline == PaintEveryLine ||
            paintEachNewline == PaintAfterBothLines)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }
    }

    // At this point, the entire buffer looks like:
    //
    // row[0]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[1]: |~~~~~~             | <break>
    // row[2]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[3]: |~~~~~~             | <break>
    // row[4]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[5]: |~~~~~~             | <break>

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport) {
        const auto width = viewport.width();
        const auto isTerminal = viewport.top != 0;

        for (til::CoordType row = 0; row < viewport.bottom; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));

            // The first line wrapped, the second didn't, so on and so forth
            const auto isWrapped = (row % 2) == 0;
            const auto rowCircled = row >= (viewport.bottom - circledRows);

            const auto actualNonSpacesAttrs = defaultAttrs;
            const auto actualSpacesAttrs = rowCircled || isTerminal ? defaultAttrs : conhostDefaultAttrs;

            VERIFY_ARE_EQUAL(isWrapped, tb.GetRowByOffset(row).WasWrapForced());
            if (isWrapped)
            {
                TestUtils::VerifyExpectedString(tb, std::wstring(charsInFirstLine, L'~'), { 0, row });
            }
            else
            {
                auto iter = TestUtils::VerifyExpectedString(tb, std::wstring(charsInSecondLine, L'~'), { 0, row });
                TestUtils::VerifyExpectedString(std::wstring(width - charsInSecondLine, L' '), iter);
            }
        }
    };

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    VERIFY_ARE_EQUAL(circledRows, term->_mutableViewport.Top());
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS()
{
    // See https://github.com/microsoft/terminal/issues/5691
    Log::Comment(L"This test attempts to print text like the MSYS `less` pager "
                 L"does. When it prints a wrapped line, we should make sure to "
                 L"not break the line wrapping.");

    // The importantly valuable variable here ended up being
    // writingMethod=PrintWithWriteCharsLegacy. That was the one thing that
    // actually repro'd this bug. The other variables were introduced as part of
    // debugging, and are left for completeness.
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"Data:circledRows", L"{2, 4, 10}")
        TEST_METHOD_PROPERTY(L"Data:paintEachNewline", L"{0, 1, 2}")
        TEST_METHOD_PROPERTY(L"Data:writingMethod", L"{0, 1}")
    END_TEST_METHOD_PROPERTIES();

    // By modifying how often we call PaintFrame, we can test if different
    // timings for the frame affect the results. In this test we'll be printing
    // a bunch of paired lines. These values control when the PaintFrame calls
    // will occur:
    constexpr auto PaintAfterBothLines = 1; // Paint after each pair of lines is output
    constexpr auto PaintEveryLine = 2; // Paint after each and every line is output.

    constexpr auto PrintWithPrintString = 0;
    constexpr auto PrintWithWriteCharsLegacy = 1;

    INIT_TEST_PROPERTY(int, writingMethod, L"Controls using either ProcessString or WriteCharsLegacy to write to the buffer");
    INIT_TEST_PROPERTY(int, circledRows, L"Controls the number of lines we output.");
    INIT_TEST_PROPERTY(int, paintEachNewline, L"Controls whether we should call PaintFrame every line of text or not.");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    const auto width = static_cast<size_t>(TerminalViewWidth);

    const auto charsInFirstLine = width;
    const auto numCharsFirstColor = 30;
    const auto numCharsSecondColor = charsInFirstLine - numCharsFirstColor;
    const auto charsInSecondLine = width / 2;

    const auto defaultAttrs = TextAttribute();
    const auto conhostDefaultAttrs = si.GetAttributes();

    // Helper to abstract calls into either StateMachine::ProcessString or
    // WriteCharsLegacy
    auto print = [&](const std::wstring_view str) {
        if (writingMethod == PrintWithPrintString)
        {
            sm.ProcessString(str);
        }
        else if (writingMethod == PrintWithWriteCharsLegacy)
        {
            doWriteCharsLegacy(si, str, WC_LIMIT_BACKSPACE);
        }
    };

    // Each of the lines of text in the buffer will look like the following:
    //
    // Line 1 chars: ~~~~~~~~~~~~~~~~~~ <wrap>
    // Line 1 attrs: YYYYYYYDDDDDDDDDDD (First 30 are yellow FG, then default attrs)
    // Line 2 chars: ~~~~~~~~~~________ <break> (there are width/2 '~'s here)
    // Line 2 attrs: DDDDDDDDDDDDDDDDDD (all are default attrs)
    //
    // The last line of the buffer will be used as a "prompt" line, with a
    // single ':' in it. This is similar to the way `less` typically displays
    // its prompt at the bottom of the buffer.

    // First, print a whole viewport full of text.
    for (auto i = 0; i < (TerminalViewHeight) / 2; i++)
    {
        Log::Comment(NoThrowString().Format(L"writing pair of lines %d", i));

        sm.ProcessString(L"\x1b[33m");
        print(std::wstring(numCharsFirstColor, L'~'));
        sm.ProcessString(L"\x1b[m");

        if (paintEachNewline == PaintEveryLine)
        {
            print(std::wstring(numCharsSecondColor, L'~'));
            VERIFY_SUCCEEDED(renderer.PaintFrame());
            print(std::wstring(charsInSecondLine, L'~'));
        }
        else
        {
            // If we're not painting each and every line, then just print all of
            // the wrapped text in one go. These '~'s will wrap from the first
            // line onto the second line.
            print(std::wstring(numCharsSecondColor + charsInSecondLine, L'~'));
        }

        sm.ProcessString(L"\r\n");

        if (paintEachNewline == PaintEveryLine ||
            paintEachNewline == PaintAfterBothLines)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }
    }

    // Then print the trailing ':' on the last line.
    print(L":");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // At this point, the entire buffer looks like:
    //
    // row[25]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[26]: |~~~~~~             | <break>
    // row[27]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[28]: |~~~~~~             | <break>
    // row[29]: |~~~~~~~~~~~~~~~~~~~| <wrap>
    // row[30]: |~~~~~~             | <break>
    // row[31]: |:                  | <break>

    // Now, we'll print the lines that wrapped, after circling the buffer.
    for (auto i = 0; i < (circledRows) / 2; i++)
    {
        Log::Comment(NoThrowString().Format(L"writing pair of lines %d", i + (TerminalViewHeight / 2)));

        print(L"\r");
        sm.ProcessString(L"\x1b[33m");
        print(std::wstring(numCharsFirstColor, L'~'));
        sm.ProcessString(L"\x1b[m");

        if (paintEachNewline == PaintEveryLine)
        {
            // If we're painting every line, then we'll print the first line and
            // redraw the "prompt" line (with the ':'), paint the buffer, then
            // print the second line and reprint the prompt, as if the user was
            // slowly hitting the down arrow one line per frame.
            print(std::wstring(numCharsSecondColor, L'~'));
            print(L":");
            VERIFY_SUCCEEDED(renderer.PaintFrame());
            print(L"\r");
            print(std::wstring(charsInSecondLine, L'~'));
        }
        else
        {
            // Otherwise, we'll print the wrapped line all in one frame.
            print(std::wstring(numCharsSecondColor + charsInSecondLine, L'~'));
        }

        // Print the prompt at the bottom of the buffer
        print(L"\r\n");
        print(L":");

        if (paintEachNewline == PaintEveryLine ||
            paintEachNewline == PaintAfterBothLines)
        {
            VERIFY_SUCCEEDED(renderer.PaintFrame());
        }
    }

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport) {
        const auto width = viewport.width();
        const auto isTerminal = viewport.top != 0;
        auto lastRow = viewport.bottom - 1;
        for (til::CoordType row = 0; row < lastRow; row++)
        {
            Log::Comment(NoThrowString().Format(L"Checking row %d", row));

            // The first line wrapped, the second didn't, so on and so forth.
            // However, because conpty's buffer is only as tall as the viewport,
            // we're going to lose lines off the top of the buffer. Most
            // importantly, because we'll have the "prompt" line in the conpty
            // buffer, then the top line of the conpty will _not_ be wrapped,
            // when the 0th line of the terminal buffer _is_.
            const auto isWrapped = (row % 2) == (isTerminal ? 0 : 1);
            const auto rowCircled = row >= (viewport.bottom - circledRows);

            const auto actualNonSpacesAttrs = defaultAttrs;
            const auto actualSpacesAttrs = rowCircled || isTerminal ? defaultAttrs : conhostDefaultAttrs;

            VERIFY_ARE_EQUAL(isWrapped, tb.GetRowByOffset(row).WasWrapForced());
            if (isWrapped)
            {
                TestUtils::VerifyExpectedString(tb, std::wstring(charsInFirstLine, L'~'), { 0, row });
            }
            else
            {
                auto iter = TestUtils::VerifyExpectedString(tb, std::wstring(charsInSecondLine, L'~'), { 0, row });
                TestUtils::VerifyExpectedString(std::wstring(width - charsInSecondLine, L' '), iter);
            }
        }
        VERIFY_IS_FALSE(tb.GetRowByOffset(lastRow).WasWrapForced());
        auto iter = TestUtils::VerifyExpectedString(tb, std::wstring(1, L':'), { 0, lastRow });
        TestUtils::VerifyExpectedString(std::wstring(width - 1, L' '), iter);
    };

    Log::Comment(L"========== Checking the host buffer state ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive());

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state ==========");
    VERIFY_ARE_EQUAL(circledRows + 1, term->_mutableViewport.Top());
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive());
}

void ConptyRoundtripTests::DeleteWrappedWord()
{
    // See https://github.com/microsoft/terminal/issues/5839
    Log::Comment(L"This test ensures that when we print a empty row beneath a "
                 L"wrapped row, that we _actually_ clear it.");
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // Print two lines of text:
    // |AAAAAAAAAAAAA BBBBBB| <wrap>
    // |BBBBBBBB_           | <break>
    // (cursor on the '_')

    sm.ProcessString(L"\x1b[?25l");
    sm.ProcessString(std::wstring(50, L'A'));
    sm.ProcessString(L" ");
    sm.ProcessString(std::wstring(50, L'B'));
    sm.ProcessString(L"\x1b[?25h");

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool after) {
        const auto width = viewport.width();

        auto iter1 = tb.GetCellDataAt({ 0, 0 });
        TestUtils::VerifySpanOfText(L"A", iter1, 0, 50);
        TestUtils::VerifySpanOfText(L" ", iter1, 0, 1);
        if (after)
        {
            TestUtils::VerifySpanOfText(L" ", iter1, 0, 50);

            auto iter2 = tb.GetCellDataAt({ 0, 1 });
            TestUtils::VerifySpanOfText(L" ", iter2, 0, width);
        }
        else
        {
            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);

            auto iter2 = tb.GetCellDataAt({ 0, 1 });
            TestUtils::VerifySpanOfText(L"B", iter2, 0, 50 - (width - 51));
            TestUtils::VerifySpanOfText(L" ", iter2, 0, width);
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), false);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), false);

    // Now, go back and erase all the 'B's, as if the user executed a
    // backward-kill-word in PowerShell. Afterwards, the buffer will look like:
    //
    // |AAAAAAAAAAAAA_      |
    // |                    |
    //
    // We're doing this the same way PsReadline redraws the prompt - by just
    // reprinting all of it.

    sm.ProcessString(L"\x1b[?25l");
    sm.ProcessString(L"\x1b[H");
    sm.ProcessString(std::wstring(50, L'A'));
    sm.ProcessString(L" ");

    sm.ProcessString(std::wstring(TerminalViewWidth - 51, L' '));

    sm.ProcessString(L"\x1b[2;1H");
    sm.ProcessString(std::wstring(50 - (TerminalViewWidth - 51), L' '));
    sm.ProcessString(L"\x1b[1;50H");
    sm.ProcessString(L"\x1b[?25h");

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true);
}

// This test checks that upon conpty rendering again, terminal still maintains
// the same hyperlink IDs
void ConptyRoundtripTests::HyperlinkIdConsistency()
{
    Log::Comment(NoThrowString().Format(
        L"Write a link - the text will simply be 'Link' and the uri will be 'http://example.com'"));

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& hostSm = si.GetStateMachine();
    auto& hostTb = si.GetTextBuffer();
    auto& termTb = *term->_mainBuffer;

    _flushFirstFrame();

    hostSm.ProcessString(L"\x1b]8;;http://example.com\x1b\\Link\x1b]8;;\x1b\\");

    // For self-generated IDs, conpty will send a custom ID of the form
    // {sessionID}-{self-generated ID}
    // self-generated IDs begin at 1 and increment from there
    const std::string fmt{ "\x1b]8;id={}-1;http://example.com\x1b\\" };
    auto s = fmt::format(fmt, GetCurrentProcessId());
    expectedOutput.push_back(s);
    expectedOutput.push_back("Link");
    expectedOutput.push_back("\x1b]8;;\x1b\\");

    // Force a frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Move the cursor down
    hostSm.ProcessString(L"\x1b[2;1H");
    expectedOutput.push_back("\r\n");

    // Force a frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Move the cursor to somewhere in the link text
    hostSm.ProcessString(L"\x1b[1;2H");
    expectedOutput.push_back("\x1b[1;2H");
    expectedOutput.push_back("\x1b[?25h");

    // Force a frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    // Move the cursor off the link
    hostSm.ProcessString(L"\x1b[2;1H");
    expectedOutput.push_back("\r\n");

    // Force a frame
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    auto verifyData = [](TextBuffer& tb) {
        // Check that all the linked cells still have the same ID
        auto& row = tb.GetRowByOffset(0);
        auto id = row.GetAttrByColumn(0).GetHyperlinkId();
        for (uint16_t i = 1; i < 4; ++i)
        {
            VERIFY_ARE_EQUAL(id, row.GetAttrByColumn(i).GetHyperlinkId());
        }
    };

    verifyData(hostTb);
    verifyData(termTb);
}

void ConptyRoundtripTests::ClearBufferSignal()
{
    Log::Comment(L"Write some text to the conpty buffer. Send a ClearBuffer "
                 L"signal, and check that all but the cursor line is removed "
                 L"from the host and the terminal.");
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // Print two lines of text:
    // |AAAAAAAAAAAAA BBBBBB| <wrap>
    // |BBBBBBBB_           | <break>
    // (cursor on the '_')
    // A's are in blue-on-green,
    // B's are in red-on-yellow

    sm.ProcessString(L"\x1b[?25l");
    sm.ProcessString(L"\x1b[34;42m");
    sm.ProcessString(std::wstring(50, L'A'));
    sm.ProcessString(L" ");
    sm.ProcessString(L"\x1b[31;43m");
    sm.ProcessString(std::wstring(50, L'B'));
    sm.ProcessString(L"\x1b[?25h");

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const bool before) {
        const auto width = viewport.width();
        const auto numCharsOnSecondLine = 50 - (width - 51);
        auto iter1 = tb.GetCellDataAt({ 0, 0 });
        if (before)
        {
            TestUtils::VerifySpanOfText(L"A", iter1, 0, 50);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, 1);
            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);
            til::point expectedCursor{ numCharsOnSecondLine, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        }
        else
        {
            TestUtils::VerifySpanOfText(L"B", iter1, 0, numCharsOnSecondLine);
            til::point expectedCursor{ numCharsOnSecondLine, 0 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
        }
    };

    Log::Comment(L"========== Checking the host buffer state (before) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), true);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (before) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), true);

    Log::Comment(L"========== Clear the ConPTY buffer with the signal ==========");
    _clearConpty();

    Log::Comment(L"========== Checking the host buffer state (after) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), false);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (after) ==========");
    verifyBuffer(*termTb, term->_mutableViewport.ToExclusive(), false);
}

void ConptyRoundtripTests::SimpleAltBufferTest()
{
    Log::Comment(L"A test for entering and exiting the alt buffer, via conpty. "
                 L"Ensures cursor is in the right place, and contents are "
                 L"restored accordingly.");
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // Print two lines of text:
    // |AAAAAAAA            | <break>
    // |BBBBBBBB_           | <break>
    // (cursor on the '_')
    // A's are in blue-on-green,
    // B's are in red-on-yellow

    // A one line version: (more or less)
    //
    // printf "\x1b[2J\x1b[H" ; printf
    // "\x1b[?25l\x1b[34;42mAAAAA\n\x1b[31;43mBBBBB\x1b[?25h" ; sleep 2 ; printf
    // "\x1b[?1049h" ; sleep 2 ; printf "CCCCC" ; sleep 2 ; printf "\x1b[?1049l"
    // ; sleep 2

    sm.ProcessString(L"\x1b[?25l");
    sm.ProcessString(L"\x1b[34;42m");
    sm.ProcessString(std::wstring(50, L'A'));
    sm.ProcessString(L"\n");
    sm.ProcessString(L"\x1b[31;43m");
    sm.ProcessString(std::wstring(50, L'B'));
    sm.ProcessString(L"\x1b[?25h");

    // Four frames here:
    // * After text is printed to main buffer
    // * after we go to the alt buffer
    // * print more to the alt buffer
    // * after we go back to the buffer

    enum class Frame : int
    {
        InMainBufferBefore = 0,
        InAltBufferBefore,
        InAltBufferAfter,
        InMainBufferAfter
    };

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const Frame frame) {
        const auto width = viewport.width();
        auto iter0 = tb.GetCellDataAt({ 0, 0 });
        auto iter1 = tb.GetCellDataAt({ 0, 1 });
        switch (frame)
        {
        case Frame::InMainBufferBefore:
        case Frame::InMainBufferAfter:
        {
            TestUtils::VerifySpanOfText(L"A", iter0, 0, 50);
            TestUtils::VerifySpanOfText(L" ", iter0, 0, static_cast<size_t>(width - 50));

            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 50));
            til::point expectedCursor{ 50, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        case Frame::InAltBufferBefore:
        {
            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);

            til::point expectedCursor{ 50, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        case Frame::InAltBufferAfter:
        {
            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, 50u);
            TestUtils::VerifySpanOfText(L"C", iter1, 0, 5u);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 55));

            til::point expectedCursor{ 55, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        }
    };

    Log::Comment(L"========== Checking the host buffer state (InMainBufferBefore) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), Frame::InMainBufferBefore);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferBefore) ==========");
    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
    verifyBuffer(*termTb, term->_GetMutableViewport().ToExclusive(), Frame::InMainBufferBefore);

    Log::Comment(L"========== Switch to the alt buffer ==========");

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
    sm.ProcessString(L"\x1b[?1049h");
    // Don't leave ourselves in the alt buffer - that'll pollute other tests.
    auto leaveAltBuffer = wil::scope_exit([&] { sm.ProcessString(L"\x1b[?1049l"); });

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    auto& siAlt = gci.GetActiveOutputBuffer();
    auto* hostAltTb = &siAlt.GetTextBuffer();
    auto* termAltTb = &term->_activeBuffer();

    VERIFY_IS_TRUE(term->_inAltBuffer());
    VERIFY_ARE_NOT_EQUAL(termAltTb, termTb);

    Log::Comment(L"========== Checking the host buffer state (InAltBufferBefore) ==========");
    verifyBuffer(*hostAltTb, siAlt.GetViewport().ToExclusive(), Frame::InAltBufferBefore);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferBefore) ==========");
    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
    verifyBuffer(*termAltTb, term->_GetMutableViewport().ToExclusive(), Frame::InAltBufferBefore);

    Log::Comment(L"========== Add some text to the alt buffer ==========");

    sm.ProcessString(L"CCCCC");
    Log::Comment(L"========== Checking the host buffer state (InAltBufferAfter) ==========");
    verifyBuffer(*hostAltTb, siAlt.GetViewport().ToExclusive(), Frame::InAltBufferAfter);
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferAfter) ==========");
    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
    verifyBuffer(*termAltTb, term->_GetMutableViewport().ToExclusive(), Frame::InAltBufferAfter);

    Log::Comment(L"========== Back to the main buffer ==========");

    sm.ProcessString(L"\x1b[?1049l");
    Log::Comment(L"========== Checking the host buffer state (InMainBufferAfter) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), Frame::InMainBufferAfter);
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferAfter) ==========");
    VERIFY_ARE_EQUAL(0, term->_GetMutableViewport().Top());
    verifyBuffer(*termTb, term->_GetMutableViewport().ToExclusive(), Frame::InMainBufferAfter);
}

void ConptyRoundtripTests::AltBufferToAltBufferTest()
{
    Log::Comment(L"When we request the alt buffer when we're already in the alt buffer, we should still clear it out and replace it.");
    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();
    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _flushFirstFrame();

    _checkConptyOutput = false;
    _logConpty = true;

    // Print two lines of text:
    // |AAAAAAAA            | <break>
    // |BBBBBBBB_           | <break>
    // (cursor on the '_')
    // A's are in blue-on-green,
    // B's are in red-on-yellow

    // A one line version: (more or less)
    //
    // printf "\x1b[2J\x1b[H" ; printf
    // "\x1b[?25l\x1b[34;42mAAAAA\n\x1b[31;43mBBBBB\x1b[?25h" ; sleep 2 ; printf
    // "\x1b[?1049h" ; sleep 2 ; printf "CCCCC" ; sleep 2 ; printf "\x1b[?1049h"
    // ; sleep 2

    sm.ProcessString(L"\x1b[?25l");
    sm.ProcessString(L"\x1b[34;42m");
    sm.ProcessString(std::wstring(50, L'A'));
    sm.ProcessString(L"\n");
    sm.ProcessString(L"\x1b[31;43m");
    sm.ProcessString(std::wstring(50, L'B'));
    sm.ProcessString(L"\x1b[?25h");

    // Four frames here:
    // * After text is printed to main buffer
    // * after we go to the alt buffer
    // * print more to the alt buffer
    // * after we go back to the buffer

    enum class Frame : int
    {
        InMainBufferBefore = 0,
        InAltBufferBefore,
        InAltBufferAfter,
        StillInAltBuffer
    };

    auto verifyBuffer = [&](const TextBuffer& tb, const til::rect& viewport, const Frame frame) {
        const auto width = viewport.width();
        auto iter0 = tb.GetCellDataAt({ 0, 0 });
        auto iter1 = tb.GetCellDataAt({ 0, 1 });
        switch (frame)
        {
        case Frame::InMainBufferBefore:
        {
            TestUtils::VerifySpanOfText(L"A", iter0, 0, 50);
            TestUtils::VerifySpanOfText(L" ", iter0, 0, static_cast<size_t>(width - 50));

            TestUtils::VerifySpanOfText(L"B", iter1, 0, 50);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 50));
            til::point expectedCursor{ 50, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        case Frame::InAltBufferBefore:
        {
            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);

            til::point expectedCursor{ 50, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        case Frame::InAltBufferAfter:
        {
            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, 50u);
            TestUtils::VerifySpanOfText(L"C", iter1, 0, 5u);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, static_cast<size_t>(width - 55));

            til::point expectedCursor{ 55, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        case Frame::StillInAltBuffer:
        {
            TestUtils::VerifySpanOfText(L" ", iter0, 0, width);
            TestUtils::VerifySpanOfText(L" ", iter1, 0, width);

            til::point expectedCursor{ 55, 1 };
            VERIFY_ARE_EQUAL(expectedCursor, tb.GetCursor().GetPosition());
            break;
        }
        }
    };

    Log::Comment(L"========== Checking the host buffer state (InMainBufferBefore) ==========");
    verifyBuffer(*hostTb, si.GetViewport().ToExclusive(), Frame::InMainBufferBefore);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (InMainBufferBefore) ==========");
    verifyBuffer(*termTb, term->_GetMutableViewport().ToExclusive(), Frame::InMainBufferBefore);

    Log::Comment(L"========== Switch to the alt buffer ==========");

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });
    sm.ProcessString(L"\x1b[?1049h");
    // Don't leave ourselves in the alt buffer - that'll pollute other tests.
    auto leaveAltBuffer = wil::scope_exit([&] { sm.ProcessString(L"\x1b[?1049l"); });

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    auto* siAlt = &gci.GetActiveOutputBuffer();
    auto* hostAltTb = &siAlt->GetTextBuffer();
    auto* termAltTb = &term->_activeBuffer();

    VERIFY_IS_TRUE(term->_inAltBuffer());
    VERIFY_ARE_NOT_EQUAL(termAltTb, termTb);

    Log::Comment(L"========== Checking the host buffer state (InAltBufferBefore) ==========");
    verifyBuffer(*hostAltTb, siAlt->GetViewport().ToExclusive(), Frame::InAltBufferBefore);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferBefore) ==========");
    verifyBuffer(*termAltTb, term->_GetMutableViewport().ToExclusive(), Frame::InAltBufferBefore);

    Log::Comment(L"========== Add some text to the alt buffer ==========");

    sm.ProcessString(L"CCCCC");
    Log::Comment(L"========== Checking the host buffer state (InAltBufferAfter) ==========");
    verifyBuffer(*hostAltTb, siAlt->GetViewport().ToExclusive(), Frame::InAltBufferAfter);
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Checking the terminal buffer state (InAltBufferAfter) ==========");
    verifyBuffer(*termAltTb, term->_GetMutableViewport().ToExclusive(), Frame::InAltBufferAfter);

    Log::Comment(L"========== Stay in the alt buffer, what happens? ==========");

    sm.ProcessString(L"\x1b[?1049h");
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    siAlt = &gci.GetActiveOutputBuffer();
    hostAltTb = &siAlt->GetTextBuffer();
    termAltTb = &term->_activeBuffer();

    Log::Comment(L"========== Checking the host buffer state (StillInAltBuffer) ==========");
    verifyBuffer(*hostAltTb, siAlt->GetViewport().ToExclusive(), Frame::StillInAltBuffer);

    Log::Comment(L"========== Checking the terminal buffer state (StillInAltBuffer) ==========");
    verifyBuffer(*termAltTb, term->_GetMutableViewport().ToExclusive(), Frame::StillInAltBuffer);
}

void ConptyRoundtripTests::TestPowerLineFirstFrame()
{
    Log::Comment(L"This is a test for GH#8341. If we received colored spaces "
                 L"BEFORE the first frame, we should still emit them!");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();

    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    _checkConptyOutput = false;

    TextAttribute whiteOnGreen{};
    whiteOnGreen.SetIndexedForeground(TextColor::DARK_WHITE);
    whiteOnGreen.SetIndexedBackground(TextColor::BRIGHT_GREEN);

    TextAttribute greenOnBlack{};
    greenOnBlack.SetIndexedForeground(TextColor::BRIGHT_GREEN);
    greenOnBlack.SetIndexedBackground(TextColor::BRIGHT_BLACK);

    TextAttribute whiteOnBlack{};
    whiteOnBlack.SetIndexedForeground(TextColor::DARK_WHITE);
    whiteOnBlack.SetIndexedBackground(TextColor::BRIGHT_BLACK);

    TextAttribute blackOnDefault{};
    blackOnDefault.SetIndexedForeground(TextColor::BRIGHT_BLACK);

    TextAttribute defaultOnDefault{};

    Log::Comment(L"========== Fill test content ==========");

    // As a pwsh one-liner:
    //
    //  "`e[37m`e[102m foo\bar `e[92m`e[100m▶ `e[37mBar `e[90m`e[49m▶ `e[m"
    //
    // Generally taken from
    // https://github.com/microsoft/terminal/issues/8341#issuecomment-731310022,
    // but minimized for easier testing.

    sm.ProcessString(L"\x1b[37m\x1b[102m" // dark white on bright green
                     L" foo\\bar ");
    sm.ProcessString(L"\x1b[92m\x1b[100m" // bright green on bright black
                     L"▶ ");
    sm.ProcessString(L"\x1b[37m" // dark white on bright black
                     L"Bar ");
    sm.ProcessString(L"\x1b[90m\x1b[49m" // bright black on default
                     L"▶ ");
    sm.ProcessString(L"\x1b[m\n"); // default on default

    auto verifyBuffer = [&](const TextBuffer& tb) {
        // If this test fails on character 8, then it's because we didn't emit the space, we just moved ahead.
        auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, whiteOnGreen, 9u);
        TestUtils::VerifyLineContains(iter0, OutputCellIterator{ greenOnBlack, 2u });
        TestUtils::VerifyLineContains(iter0, OutputCellIterator{ whiteOnBlack, 4u });
        TestUtils::VerifyLineContains(iter0, OutputCellIterator{ blackOnDefault, 2u });
    };

    Log::Comment(L"========== Check host buffer ==========");
    verifyBuffer(*hostTb);

    Log::Comment(L"========== Paint first frame ==========");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Check terminal buffer ==========");
    verifyBuffer(*termTb);
}

void ConptyRoundtripTests::AltBufferResizeCrash()
{
    Log::Comment(L"During the review for GH#12719, it was noticed that this "
                 L"particular combination of resizing could crash the terminal."
                 L" This test makes sure we don't.");

    // Anything that resizes the buffer needs IsolationLevel:Method
    BEGIN_TEST_METHOD_PROPERTIES()
        TEST_METHOD_PROPERTY(L"IsolationLevel", L"Method")
    END_TEST_METHOD_PROPERTIES()

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });

    _flushFirstFrame();

    _checkConptyOutput = false;

    // Don't leave ourselves in the alt buffer - that'll pollute other tests.
    auto leaveAltBuffer = wil::scope_exit([&] { sm.ProcessString(L"\x1b[?1049l"); });

    // Note: we really don't care about the output in this test. We could, but
    // mostly we care to ensure we don't crash. If we make it through this test,
    // then it's a success.

    Log::Comment(L"========== Resize to 132x24 ==========");
    sm.ProcessString(L"\x1b[8;24;132t");
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Move cursor to (99,11) ==========");
    sm.ProcessString(L"\x1b[12;100H");
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Switch to the alt buffer ==========");
    sm.ProcessString(L"\x1b[?1049h");
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Resize to 80x10 ==========");
    sm.ProcessString(L"\x1b[8;10;80t");
    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());
}

void ConptyRoundtripTests::TestNoExtendedAttrsOptimization()
{
    Log::Comment(L"We don't want conpty to optimize out runs of spaces that DO "
                 L"have extended attrs, because EL / ECH don't fill space with "
                 L"those attributes");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();

    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });

    _flushFirstFrame();

    _checkConptyOutput = false;

    TextAttribute reverseAttrs{};
    reverseAttrs.SetReverseVideo(true);

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L' ', reverseAttrs, 9u);
        TestUtils::VerifyExpectedString(L"test", iter0);
        TestUtils::VerifyLineContains(iter0, L' ', reverseAttrs, 9u);

        TestUtils::VerifyLineContains(tb, { 0, 1 }, L' ', reverseAttrs, static_cast<uint32_t>(TerminalViewWidth));
    };

    Log::Comment(L"========== Fill test content ==========");
    sm.ProcessString(L"\x1b[7m         test         \x1b[m\n");
    sm.ProcessString(L"\x1b[7m");
    sm.ProcessString(std::wstring(TerminalViewWidth, L' '));
    sm.ProcessString(L"\x1b[m\n");

    Log::Comment(L"========== Check host buffer ==========");
    verifyBuffer(*hostTb);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Check terminal buffer ==========");
    verifyBuffer(*termTb);
}

void ConptyRoundtripTests::TestNoBackgroundAttrsOptimization()
{
    Log::Comment(L"Same as above, with BG attrs");

    auto& g = ServiceLocator::LocateGlobals();
    auto& renderer = *g.pRender;
    auto& gci = g.getConsoleInformation();
    auto& si = gci.GetActiveOutputBuffer();
    auto& sm = si.GetStateMachine();

    auto* hostTb = &si.GetTextBuffer();
    auto* termTb = term->_mainBuffer.get();

    gci.LockConsole(); // Lock must be taken to manipulate alt/main buffer state.
    auto unlock = wil::scope_exit([&] { gci.UnlockConsole(); });

    _flushFirstFrame();

    _checkConptyOutput = false;

    TextAttribute bgAttrs{};
    bgAttrs.SetIndexedBackground(TextColor::DARK_WHITE);

    auto verifyBuffer = [&](const TextBuffer& tb) {
        auto iter0 = TestUtils::VerifyLineContains(tb, { 0, 0 }, L' ', bgAttrs, 9u);
        TestUtils::VerifyExpectedString(L"test", iter0);
        TestUtils::VerifyLineContains(iter0, L' ', bgAttrs, 9u);

        TestUtils::VerifyLineContains(tb, { 0, 1 }, L' ', bgAttrs, static_cast<uint32_t>(TerminalViewWidth));
    };

    Log::Comment(L"========== Fill test content ==========");
    sm.ProcessString(L"\x1b[47m         test         \x1b[m\n");
    sm.ProcessString(L"\x1b[47m");
    sm.ProcessString(std::wstring(TerminalViewWidth, L' '));
    sm.ProcessString(L"\x1b[m\n");

    Log::Comment(L"========== Check host buffer ==========");
    verifyBuffer(*hostTb);

    Log::Comment(L"Painting the frame");
    VERIFY_SUCCEEDED(renderer.PaintFrame());

    Log::Comment(L"========== Check terminal buffer ==========");
    verifyBuffer(*termTb);
}
