/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#include <sctiledrenderingtest.hxx>

#include <boost/property_tree/json_parser.hpp>

#include <com/sun/star/datatransfer/clipboard/KitClipboard.hpp>
#include <com/sun/star/util/URLTransformer.hpp>

#include <test/helper/transferable.hxx>
#include <comphelper/processfactory.hxx>
#include <comphelper/propertysequence.hxx>
#include <comphelper/servicehelper.hxx>
#include <comphelper/SetFlagContextHelper.hxx>
#include <sfx2/dispatch.hxx>
#include <sfx2/viewfrm.hxx>
#include <svl/stritem.hxx>
#include <svl/numformat.hxx>
#include <svl/zformat.hxx>
#include <COKit/COKit.hxx>
#include <vcl/scheduler.hxx>
#include <comphelper/kit.hxx>
#include <comphelper/propertyvalue.hxx>
#include <comphelper/dispatchcommand.hxx>
#include <sfx2/msgpool.hxx>
#include <sfx2/childwin.hxx>
#include <sfx2/kit/helper.hxx>
#include <svx/svdpage.hxx>
#include <vcl/vclevent.hxx>
#include <vcl/virdev.hxx>
#include <tools/json_writer.hxx>
#include <unotools/syslocaleoptions.hxx>
#include <unotools/useroptions.hxx>

#include <com/sun/star/style/XStyleFamiliesSupplier.hpp>
#include <com/sun/star/container/XNameAccess.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>

#include <sc.hrc>
#include <postit.hxx>
#include <attrib.hxx>
#include <scitems.hxx>
#include <document.hxx>
#include <docuno.hxx>
#include <drwlayer.hxx>
#include <editutil.hxx>
#include <undomanager.hxx>
#include <docsh.hxx>
#include <tabvwsh.hxx>
#include <dbdocfun.hxx>
#include <dbdata.hxx>
#include <gridwin.hxx>
#include <sctestviewcallback.hxx>
#include <o3tl/unit_conversion.hxx>
#include <cstdlib>

using namespace com::sun::star;

static std::ostream& operator<<(std::ostream& os, ViewShellId const & id)
{
    os << static_cast<sal_Int32>(id); return os;
}

namespace {
// for passing data to testInvalidateOnTextEditWithDifferentZoomLevels
struct ColRowZoom
{
    SCCOL col;
    SCROW row;
    int zoom;
};
}

CPPUNIT_NS_BEGIN
namespace StringHelper
{
// used by CPPUNIT_TEST_PARAMETERIZED for testInvalidateOnTextEditWithDifferentZoomLevels
template<>
inline std::string toString(const ColRowZoom& item)
{
    std::ostringstream ss;
    ss << "zoom level: " << item.zoom << ", "
          "col: " << item.col << ", "
          "row: " << item.row;
    return ss.str();
}
}
CPPUNIT_NS_END

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowColumnSelections)
{
    ScModelObj* pModelObj = createDoc("select-row-cols.ods");

    // Select the 5th row with no modifier
    cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
            { "Row", cpo::uno::Any(sal_Int32(5 - 1)) },
            { "Modifier", cpo::uno::Any(sal_uInt16(0)) }
        }));
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    // Check if it is selected
    OString aResult = apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr);
    OString aExpected("1\t2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\n"_ostr);
    CPPUNIT_ASSERT_EQUAL(aExpected, aResult);

    // Select the 10th row with shift modifier
    aArgs = comphelper::InitPropertySequence({ { "Row", cpo::uno::Any(static_cast<sal_Int32>(10 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_SHIFT) } });
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    // Check if all the rows from 5th to 10th get selected
    aResult = apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr);
    aExpected = "1\t2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\n2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\n3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\n4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\n5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\n6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\t26\n"_ostr;
    CPPUNIT_ASSERT_EQUAL(aExpected, aResult);

    // Select the 10th row with ctrl modifier
    aArgs = comphelper::InitPropertySequence({ { "Row", cpo::uno::Any(static_cast<sal_Int32>(13 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_MOD1) } });
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    // Copying the non-contiguous selection (rows 5-10 plus row 13) now
    // serializes the selected rows, clamped to the used data area. The
    // unselected gap rows 11 and 12 inside the bounding box come out empty.
    aResult = apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr);
    aExpected = "1\t2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\n"
                "2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\n"
                "3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\n"
                "4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\n"
                "5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\n"
                "6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\t26\n"
                "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n"
                "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\n"
                "9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\t26\t27\t28\t29\n"_ostr;
    CPPUNIT_ASSERT_EQUAL(aExpected, aResult);

    // Select Column 5 with ctrl modifier
    aArgs = comphelper::InitPropertySequence({ { "Col", cpo::uno::Any(static_cast<sal_Int32>(5 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_MOD1) } });
    dispatchCommand(mxComponent, u".uno:SelectColumn"_ustr, aArgs);

    // Adding column 5 to the row selection yields a non-empty serialization
    // of the mixed selection (rows 5-10 and 13 in full, plus column 5's
    // cells for the rows in between) rather than the single cursor cell.
    // Assert it is populated and carries the fully selected rows rather than
    // pinning the exact bounding-box dump, which is dominated by column 5's
    // long tail of otherwise-empty rows.
    aResult = apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr);
    CPPUNIT_ASSERT(!aResult.isEmpty());
    CPPUNIT_ASSERT(aResult.indexOf("1\t2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21") >= 0);
    CPPUNIT_ASSERT(aResult.indexOf("9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\t22\t23\t24\t25\t26\t27\t28\t29") >= 0);

    // Test for deselection of already selected rows
    // First Deselect Row 13 because copy doesn't work for multiple selections
    aArgs = comphelper::InitPropertySequence({ { "Row", cpo::uno::Any(static_cast<sal_Int32>(13 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_MOD1) } });
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    // Deselect row 10
    aArgs = comphelper::InitPropertySequence({ { "Row", cpo::uno::Any(static_cast<sal_Int32>(10 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_MOD1) } });
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    // Click at row 6 holding shift
    aArgs = comphelper::InitPropertySequence({ { "Row", cpo::uno::Any(static_cast<sal_Int32>(6 - 1)) },
                                               { "Modifier", cpo::uno::Any(KEY_SHIFT) } });
    dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

    //  only row 5 should remain selected
    aResult = apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr);
    aExpected = "1\t2\t3\t4\t5\t6\t7\t8\t9\t10\t11\t12\t13\t14\t15\t16\t17\t18\t19\t20\t21\n"_ostr;
    CPPUNIT_ASSERT_EQUAL(aExpected, aResult);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testPartHash)
{
    ScModelObj* pModelObj = createDoc("sort-range.ods");

    int nParts = pModelObj->getParts();
    for (int it = 0; it < nParts; it++)
    {
        CPPUNIT_ASSERT(!pModelObj->getPartHash(it).isEmpty());
    }

    // check part that it does not exists
    CPPUNIT_ASSERT(pModelObj->getPartHash(100).isEmpty());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDocumentSize)
{
    ScModelObj* pModelObj = createDoc("sort-range.ods");
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);

    ScTabViewShell* pViewShell = pDocSh->GetBestViewShell(false);
    CPPUNIT_ASSERT(pViewShell);

    setupCOKitViewCallback(pViewShell);

    // check initial document size
    Size aDocSize = pModelObj->getDocumentSize();
    CPPUNIT_ASSERT(aDocSize.Width() > 0);
    CPPUNIT_ASSERT(aDocSize.Height() > 0);

    // Set cursor column
    pViewShell->SetCursor(100, 0);
    // 2 seconds
    osl::Condition::Result aResult = m_aDocSizeCondition.wait(std::chrono::seconds(2));
    CPPUNIT_ASSERT_EQUAL(osl::Condition::result_ok, aResult);

    // Set cursor row
    pViewShell->SetCursor(0, 100);
    // 2 seconds
    aResult = m_aDocSizeCondition.wait(std::chrono::seconds(2));
    CPPUNIT_ASSERT_EQUAL(osl::Condition::result_ok, aResult);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testEmptyColumnSelection)
{
    ScModelObj* pModelObj = createDoc("select-row-cols.ods");

    // Select empty column, 1000
    cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
                { "Col", cpo::uno::Any(sal_Int32(1000 - 1)) },
                { "Modifier", cpo::uno::Any(sal_uInt16(0)) }
        }));
    dispatchCommand(mxComponent, u".uno:SelectColumn"_ustr, aArgs);

    // should be an empty string
    CPPUNIT_ASSERT_EQUAL(OString(), apitest::helper::transferable::getTextSelection(pModelObj->getSelection(), "text/plain;charset=utf-8"_ostr));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testViewCursors)
{
    ScModelObj* pModelObj = createDoc("select-row-cols.ods");
    ScTestViewCallback aView1;
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2(/*bDeleteListenerOnDestruct*/false);
    // This was false, the new view did not get the view (cell) cursor of the old view.
    CPPUNIT_ASSERT(aView2.m_bViewCursorInvalidated);
    CPPUNIT_ASSERT(aView2.m_bOwnCursorInvalidated);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN);
    Scheduler::ProcessEventsToIdle();
    KitHelper::destroyView(KitHelper::getCurrentView());
    CPPUNIT_ASSERT(aView1.m_bViewCursorInvalidated);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSpellOnlineRenderParameter)
{
    ScModelObj* pModelObj = createDoc("empty.ods");

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    bool bSet = pView->IsAutoSpell();

    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues =
    {
        comphelper::makePropertyValue(u".uno:SpellOnline"_ustr, cpo::uno::Any(!bSet)),
    };
    pModelObj->initializeForTiledRendering(aPropertyValues);

    CPPUNIT_ASSERT_EQUAL(!bSet, pView->IsAutoSpell());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTextViewSelection)
{
    // Create two views, and leave the second one current.
    ScModelObj* pModelObj = createDoc("select-row-cols.ods");
    ScTestViewCallback aView1;
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // Create a selection on two cells in the second view, that's a text selection in COKit terms.
    aView1.m_bTextViewSelectionInvalidated = false;
    dispatchCommand(mxComponent, u".uno:GoRightSel"_ustr, {});
    // Make sure the first view got its notification.
    CPPUNIT_ASSERT(aView1.m_bTextViewSelectionInvalidated);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDocumentSizeChanged)
{
    // Load a document that doesn't have much content.
    createDoc("small.ods");
    setupCOKitViewCallback(SfxViewShell::Current());

    // Go to the A30 cell -- that will extend the document size.
    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues =
    {
        comphelper::makePropertyValue(u"ToPoint"_ustr, u"$A$30"_ustr),
    };
    dispatchCommand(mxComponent, u".uno:GoToCell"_ustr, aPropertyValues);
    // Assert that the size in the payload is not 0.
    CPPUNIT_ASSERT(m_aDocumentSize.getWidth() > 0);
    CPPUNIT_ASSERT(m_aDocumentSize.getHeight() > 0);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testViewLock)
{
    // Load a document that has a shape and create two views.
    ScModelObj* pModelObj = createDoc("shape.ods");
    ScTestViewCallback aView1;
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // Begin text edit in the second view and assert that the first gets a lock
    // notification.
    const ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScTabViewShell* pViewShell = pViewData->GetViewShell();
    CPPUNIT_ASSERT(pViewShell);
    SdrModel* pDrawModel = pViewData->GetDocument().GetDrawLayer();
    SdrPage* pDrawPage = pDrawModel->GetPage(0);
    SdrObject* pObject = pDrawPage->GetObj(0);
    SdrView* pView = pViewShell->GetScDrawView();
    aView1.m_bViewLock = false;
    pView->SdrBeginTextEdit(pObject);
    CPPUNIT_ASSERT(aView1.m_bViewLock);

    // End text edit in the second view, and assert that the lock is removed in
    // the first view.
    pView->SdrEndTextEdit();
    CPPUNIT_ASSERT(!aView1.m_bViewLock);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSheetSwitchViewLock)
{
    // Load a document with two sheets and create two views.
    ScModelObj* pModelObj = createDoc("two_sheets.ods");
    ScTestViewCallback aView1;
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // Reset counter after view creation (which may trigger callbacks).
    Scheduler::ProcessEventsToIdle();
    aView1.m_nViewLockCount = 0;

    // Switch to the second view and have it switch sheets.
    KitHelper::setView(aView2.getViewID());
    pModelObj->setPart(1);
    Scheduler::ProcessEventsToIdle();

    // The first view should not have received any VIEW_LOCK callbacks,
    // because no text editing was active.
    CPPUNIT_ASSERT_EQUAL(0, aView1.m_nViewLockCount);
}

namespace
{
void lcl_extractHandleParameters(std::string_view selection, sal_uInt32& id, sal_uInt32& x, sal_uInt32& y)
{
    OString extraInfo( selection.substr(selection.find("{")) );
    std::stringstream aStream((std::string(extraInfo)));
    boost::property_tree::ptree aTree;
    boost::property_tree::read_json(aStream, aTree);
    boost::property_tree::ptree
        handle0 = aTree
            .get_child("handles")
            .get_child("kinds")
            .get_child("rectangle")
            .get_child("1")
            .begin()->second;
    id = handle0.get_child("id").get_value<int>();
    x = handle0.get_child("point").get_child("x").get_value<int>();
    y = handle0.get_child("point").get_child("y").get_value<int>();
}
} //namespace

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testMoveShapeHandle)
{
    ScModelObj* pModelObj = createDoc("shape.ods");
    ScTestViewCallback aView1;
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, /*x=*/ 1,/*y=*/ 1,/*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, /*x=*/ 1, /*y=*/ 1, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
    {
        sal_uInt32 id, x, y;
        lcl_extractHandleParameters(aView1.m_ShapeSelection, id, x ,y);
        sal_uInt32 oldX = x;
        sal_uInt32 oldY = y;
        cpo::uno::Sequence<beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence(
        {
            {"HandleNum", cpo::uno::Any(id)},
            {"NewPosX", cpo::uno::Any(x+1)},
            {"NewPosY", cpo::uno::Any(y+1)}
        }));
        dispatchCommand(mxComponent, u".uno:MoveShapeHandle"_ustr, aPropertyValues);
        CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
        lcl_extractHandleParameters(aView1.m_ShapeSelection, id, x ,y);
        CPPUNIT_ASSERT_EQUAL(x-1, oldX);
        CPPUNIT_ASSERT_EQUAL(y-1, oldY);
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testMoveShapeHandleTextBox)
{
    ScModelObj* pModelObj = createDoc("shape-textbox.ods");
    ScTestViewCallback aView1;
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, /*x=*/ 1,/*y=*/ 1,/*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, /*x=*/ 1, /*y=*/ 1, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
    {
        sal_uInt32 id, x, y;
        lcl_extractHandleParameters(aView1.m_ShapeSelection, id, x ,y);
        sal_uInt32 oldX = x;
        sal_uInt32 oldY = y;
        cpo::uno::Sequence<beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence(
        {
            {"HandleNum", cpo::uno::Any(id)},
            {"NewPosX", cpo::uno::Any(x+1)},
            {"NewPosY", cpo::uno::Any(y+1)}
        }));
        dispatchCommand(mxComponent, u".uno:MoveShapeHandle"_ustr, aPropertyValues);
        CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
        lcl_extractHandleParameters(aView1.m_ShapeSelection, id, x ,y);
        CPPUNIT_ASSERT_EQUAL(x-1, oldX);
        CPPUNIT_ASSERT_EQUAL(y-1, oldY);
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testColRowResize)
{
    ScModelObj* pModelObj = createDoc("sort-range.ods");
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);

    ScTabViewShell* pViewShell = pDocSh->GetBestViewShell(false);
    CPPUNIT_ASSERT(pViewShell);

    setupCOKitViewCallback(pViewShell);

    ScDocument& rDoc = pDocSh->GetDocument();

    // Col 3, Tab 0
    cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
            { "ColumnWidth", cpo::uno::Any(sal_uInt16(4000)) }, // 4cm
            { "Column", cpo::uno::Any(sal_Int16(3)) }
        }));
    dispatchCommand(mxComponent, u".uno:ColumnWidth"_ustr, aArgs);

    sal_uInt16 nWidth = o3tl::convert(rDoc.GetColWidth(static_cast<SCCOL>(2), static_cast<SCTAB>(0), false), o3tl::Length::twip, o3tl::Length::mm100);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4001), nWidth);

    // Row 5, Tab 0
    cpo::uno::Sequence<beans::PropertyValue> aArgs2( comphelper::InitPropertySequence({
            { "RowHeight", cpo::uno::Any(sal_uInt16(2000)) },
            { "Row", cpo::uno::Any(sal_Int16(5)) },
        }));
    dispatchCommand(mxComponent, u".uno:RowHeight"_ustr, aArgs2);

    sal_uInt16 nHeight = o3tl::convert(rDoc.GetRowHeight(static_cast<SCROW>(4), static_cast<SCTAB>(0), false), o3tl::Length::twip, o3tl::Length::mm100);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2000), nHeight);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoShells)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    // Clear the currently selected cell.
    dispatchCommand(mxComponent, u".uno:ClearContents"_ustr, {});

    auto pDocShell = dynamic_cast<ScDocShell*>(pModelObj->GetEmbeddedObject());
    CPPUNIT_ASSERT(pDocShell);
    ScDocument& rDoc = pDocShell->GetDocument();
    ScUndoManager* pUndoManager = rDoc.GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetUndoActionCount());
    sal_Int32 nView1 = KitHelper::getCurrentView();
    // This was -1: ScSimpleUndo did not remember what view shell created it.
    CPPUNIT_ASSERT_EQUAL(ViewShellId(nView1), pUndoManager->GetUndoAction()->GetViewShellId());
}

namespace
{
bool lcl_hasEditView(const ScViewData& rViewData)
{
    bool bResult = false;
    for (unsigned int i=0; i<4; i++)
    {
        bResult = rViewData.HasEditView( static_cast<ScSplitPos>(i) );
        if (bResult) break;
    }
    return bResult;
}
} // namespace

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTextEditViews)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view #1
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    // text edit a cell in view #1
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(lcl_hasEditView(*pViewData));

    // view #2
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // move cell cursor i view #2
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN);
    Scheduler::ProcessEventsToIdle();

    // check that text edit view in view #1 has not be killed
    CPPUNIT_ASSERT(lcl_hasEditView(*pViewData));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTextEditViewInvalidations)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view #1
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    // view #2
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    aView2.m_bInvalidateTiles = false;
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(lcl_hasEditView(*pViewData));
    CPPUNIT_ASSERT(aView2.m_bInvalidateTiles);

    // text edit a cell in view #1 until
    // we can be sure we are out of the initial tile
    for (int i = 0; i < 40; ++i)
    {
        pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
        pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    }
    Scheduler::ProcessEventsToIdle();

    // text edit a cell in view #1 inside the new tile and
    // check that view #2 receive a tile invalidate message
    aView2.m_bInvalidateTiles = false;
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView2.m_bInvalidateTiles);

    // view #3
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView3;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    aView3.m_bInvalidateTiles = false;
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'y', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'y', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView3.m_bInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testNoRedundantSpellInvalidations)
{
    // Regression test: typing a long, mixed-language cell text/comment must
    // not cause idle spell-check to emit extra tile-invalidation messages on
    // either view.

    // Turn online spell-checking on for the document and current view #1.
    cpo::uno::Sequence<beans::PropertyValue> aSpellOnArgs = {
        comphelper::makePropertyValue(u".uno:SpellOnline"_ustr, cpo::uno::Any(true)),
    };
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    pModelObj->initializeForTiledRendering(aSpellOnArgs);
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    ScTabViewShell* pView1Sh = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView1Sh);
    CPPUNIT_ASSERT(pView1Sh->IsAutoSpell());

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    // view #2 - also auto-spell enabled via the same render parameter.
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(aSpellOnArgs);
    ScTabViewShell* pView2Sh = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView2Sh);
    CPPUNIT_ASSERT(pView2Sh->IsAutoSpell());
    ScTestViewCallback aView2;

    KitHelper::setView(nView1);

    // Type a long, mixed-language sequence into the active cell. Lots of
    // misspellings + language guesses are exactly what would have made
    // the old idle handler fire repeatedly and emit a stream of
    // invalidations after every batch of keystrokes.
    static const char* const kTypedText =
        "Thiss is ein kompliziertes exemple avec plusieurs languages "
        "und wronglyspeltwords throughout-the-text to ensure the "
        "spellcheker keepps rechecking stuff while we type.";
    for (const char* p = kTypedText; *p; ++p)
    {
        pModelObj->postKeyEvent(COKitKeyEventType::DOWN, *p, 0);
        Scheduler::ProcessEventsToIdle();
        pModelObj->postKeyEvent(COKitKeyEventType::UP, *p, 0);
        Scheduler::ProcessEventsToIdle();
    }

    // Now the typing has settled. From here on, no spell-check work
    // should produce any new tile invalidation on either view.
    aView1.ClearAllInvalids();
    aView2.ClearAllInvalids();

    // Pump idle several times. The editeng spell timer has a 100ms
    // timeout and could re-arm itself. Before this fix each fire would
    // produce a fresh InvalidateAtWindow / UpdateViews call.
    for (int i = 0; i < 5; ++i)
        Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT_EQUAL_MESSAGE(
        "Idle spell-check must not invalidate tiles in the typing view",
        size_t(0), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL_MESSAGE(
        "Idle spell-check must not invalidate tiles in the spectator view",
        size_t(0), aView2.m_aInvalidations.size());
    CPPUNIT_ASSERT_MESSAGE(
        "Idle spell-check must not flag a full-tile invalidation in the typing view",
        !aView1.m_bFullInvalidateTiles);
    CPPUNIT_ASSERT_MESSAGE(
        "Idle spell-check must not flag a full-tile invalidation in the spectator view",
        !aView2.m_bFullInvalidateTiles);
    CPPUNIT_ASSERT_MESSAGE(
        "Idle spell-check must not raise the tile-invalidate flag in the typing view",
        !aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_MESSAGE(
        "Idle spell-check must not raise the tile-invalidate flag in the spectator view",
        !aView2.m_bInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCreateViewGraphicSelection)
{
    // Load a document that has a shape and create two views.
    ScModelObj* pModelObj = createDoc("shape.ods");
    ScTestViewCallback aView1;

    // Mark the graphic in the first view.
    const ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScTabViewShell* pViewShell = pViewData->GetViewShell();
    CPPUNIT_ASSERT(pViewShell);
    SdrModel* pDrawModel = pViewData->GetDocument().GetDrawLayer();
    SdrPage* pDrawPage = pDrawModel->GetPage(0);
    SdrObject* pObject = pDrawPage->GetObj(0);
    SdrView* pView = pViewShell->GetScDrawView();
    aView1.m_bGraphicSelection = false;
    aView1.m_bGraphicViewSelection = false;
    pView->MarkObj(pObject, pView->GetSdrPageView());
    CPPUNIT_ASSERT(aView1.m_bGraphicSelection);

    // Create a second view.
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;
    CPPUNIT_ASSERT(aView2.m_bGraphicViewSelection);
    CPPUNIT_ASSERT(aView1.m_bGraphicViewSelection);

    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testGraphicInvalidate)
{
    // Load a document that has a shape and create two views.
    ScModelObj* pModelObj = createDoc("shape.ods");
    ScTestViewCallback aView;

    // Click to select graphic
    aView.m_bGraphicSelection = false;
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, /*x=*/ 1,/*y=*/ 1,/*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, /*x=*/ 1, /*y=*/ 1, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView.m_bGraphicSelection);

    // Drag Drop graphic
    aView.m_bGraphicSelection = false;
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, /*x=*/ 1,/*y=*/ 1,/*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::MOVE, /*x=*/ 1,/*y=*/ 10,/*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, /*x=*/ 1, /*y=*/ 10, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!aView.m_bFullInvalidateTiles);

    // Check again
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!aView.m_bFullInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testAutoSum)
{
    createDoc("small.ods");

    ScTestViewCallback aView;

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    dispatchCommand(mxComponent, u".uno:AutoSum"_ustr, aArgs);
    CPPUNIT_ASSERT(aView.m_sCellFormula.startsWith("=SUM("));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testHideColRow)
{
    createDoc("small.ods");
    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
                { "Col", cpo::uno::Any(sal_Int32(2 - 1)) },
                { "Modifier", cpo::uno::Any(KEY_SHIFT) }
            }));
        dispatchCommand(mxComponent, u".uno:SelectColumn"_ustr, aArgs);

        cpo::uno::Sequence<beans::PropertyValue> aArgs2( comphelper::InitPropertySequence({
                { "Col", cpo::uno::Any(sal_Int32(3 - 1)) },
                { "Modifier", cpo::uno::Any(sal_uInt16(0)) }
            }));

        dispatchCommand(mxComponent, u".uno:SelectColumn"_ustr, aArgs2);
    }

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    SCCOL nOldCurX = pViewData->GetCurX();
    SCROW nOldCurY = pViewData->GetCurY();
    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs;
        dispatchCommand(mxComponent, u".uno:HideColumn"_ustr, aArgs);
    }

    SCCOL nNewCurX = pViewData->GetCurX();
    SCROW nNewCurY = pViewData->GetCurY();
    CPPUNIT_ASSERT(nNewCurX > nOldCurX);
    CPPUNIT_ASSERT_EQUAL(nOldCurY, nNewCurY);
    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
                { "Row", cpo::uno::Any(sal_Int32(6 - 1)) },
                { "Modifier", cpo::uno::Any(KEY_SHIFT) }
            }));
        dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs);

        cpo::uno::Sequence<beans::PropertyValue> aArgs2( comphelper::InitPropertySequence({
                { "Row", cpo::uno::Any(sal_Int32(7 - 1)) },
                { "Modifier", cpo::uno::Any(sal_uInt16(0)) }
            }));
        dispatchCommand(mxComponent, u".uno:SelectRow"_ustr, aArgs2);
    }

    nOldCurX = pViewData->GetCurX();
    nOldCurY = pViewData->GetCurY();
    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs;
        dispatchCommand(mxComponent, u".uno:HideRow"_ustr, aArgs);
    }
    nNewCurX = pViewData->GetCurX();
    nNewCurY = pViewData->GetCurY();
    CPPUNIT_ASSERT(nNewCurY > nOldCurY);
    CPPUNIT_ASSERT_EQUAL(nOldCurX, nNewCurX);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTableResizeUndoKeepsCursor)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);

    auto pDocShell = dynamic_cast<ScDocShell*>(pModelObj->GetEmbeddedObject());
    CPPUNIT_ASSERT(pDocShell);
    ScDocument& rDoc = pDocShell->GetDocument();
    ScUndoManager* pUndoManager = rDoc.GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);

    // A styled table A1:C14 with auto-generated headers Column1|Column2|Column3.
    ScDBDocFunc aFunc(*pDocShell);
    CPPUNIT_ASSERT(aFunc.AddDBTable(u"Table1"_ustr, ScRange(0, 0, 0, 2, 13, 0),
                                    /*bHeader*/ true, /*bRecord*/ true, /*bApi*/ true,
                                    u"TableStyleMedium2"_ustr));

    // Put the cursor on B8, inside the table body.
    ScTabViewShell* pViewShell = ScTabViewShell::GetActiveViewShell();
    CPPUNIT_ASSERT(pViewShell);
    pViewShell->SetCursor(1, 7);

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    CPPUNIT_ASSERT_EQUAL(sal_Int16(1), pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pViewData->GetCurY());

    // Widen A1:C14 -> A1:L14 (drag-resize), generating Column4..Column12.
    ScDBData* pData = rDoc.GetDBCollection()->getNamedDBs().findByUpperName(u"TABLE1"_ustr);
    CPPUNIT_ASSERT(pData);
    ScDBData aWide(*pData);
    aWide.SetArea(0, 0, 0, 11, 13);
    aFunc.ModifyDBData(aWide);
    CPPUNIT_ASSERT_EQUAL(u"Column12"_ustr, rDoc.GetString(11, 0, 0));
    // The resize itself must not move the cursor.
    CPPUNIT_ASSERT_EQUAL(sal_Int16(1), pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL(sal_Int32(7), pViewData->GetCurY());

    // Undo: without the cursor guard the cursor jumped to D1 (first generated cell).
    pUndoManager->Undo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("undo must keep the cursor on B8", sal_Int16(1),
                                 pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("undo must keep the cursor on B8", sal_Int32(7),
                                 pViewData->GetCurY());

    // Redo: without the guard the cursor jumped to L1 (last generated cell).
    pUndoManager->Redo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("redo must keep the cursor on B8", sal_Int16(1),
                                 pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("redo must keep the cursor on B8", sal_Int32(7),
                                 pViewData->GetCurY());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testAutoExpandUndoKeepsCursor)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);

    auto pDocShell = dynamic_cast<ScDocShell*>(pModelObj->GetEmbeddedObject());
    CPPUNIT_ASSERT(pDocShell);
    ScDocument& rDoc = pDocShell->GetDocument();
    ScUndoManager* pUndoManager = rDoc.GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);

    // Styled table A1:C5 with auto-generated headers Column1|Column2|Column3.
    ScDBDocFunc aFunc(*pDocShell);
    CPPUNIT_ASSERT(aFunc.AddDBTable(u"Table1"_ustr, ScRange(0, 0, 0, 2, 4, 0),
                                    /*bHeader*/ true, /*bRecord*/ true, /*bApi*/ true,
                                    u"TableStyleMedium2"_ustr));

    // Type into D3 (the column band right of the table) — this is what the user
    // does, and it both flags the auto-expansion and leaves the cursor on D3.
    rDoc.SetString(ScAddress(3, 2, 0), u"x"_ustr);
    ScDBData* pData = rDoc.GetDBCollection()->getNamedDBs().findByUpperName(u"TABLE1"_ustr);
    CPPUNIT_ASSERT(pData);
    CPPUNIT_ASSERT(pData->HasPendingExpansion());

    ScTabViewShell* pViewShell = ScTabViewShell::GetActiveViewShell();
    CPPUNIT_ASSERT(pViewShell);
    pViewShell->SetCursor(3, 2);

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // Drain the pending expansion: the table grows to column D and a "Column4"
    // header is generated for it.
    pDocShell->ProcessPendingTableExpansions();
    CPPUNIT_ASSERT_EQUAL(u"Column4"_ustr, rDoc.GetString(3, 0, 0));
    CPPUNIT_ASSERT_EQUAL(sal_Int16(3), pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pViewData->GetCurY());

    // Undo: without the cursor guard this jumped to D1 (the generated header).
    pUndoManager->Undo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("undo must keep the cursor on D3", sal_Int16(3),
                                 pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("undo must keep the cursor on D3", sal_Int32(2),
                                 pViewData->GetCurY());

    // Redo: without the guard this jumped to D1 again.
    pUndoManager->Redo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("redo must keep the cursor on D3", sal_Int16(3),
                                 pViewData->GetCurX());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("redo must keep the cursor on D3", sal_Int32(2),
                                 pViewData->GetCurY());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidateOnCopyPasteCells)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);

    // view
    ScTestViewCallback aView;

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    // select and copy cells
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_HOME | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_HOME | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN | KEY_SHIFT);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN | KEY_SHIFT);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_RIGHT | KEY_SHIFT);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_RIGHT | KEY_SHIFT);
    Scheduler::ProcessEventsToIdle();
    dispatchCommand(mxComponent, u".uno:Copy"_ustr, aArgs);

    // move to destination cell
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_UP);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_UP);
    Scheduler::ProcessEventsToIdle();

    // paste cells
    aView.m_bInvalidateTiles = false;
    dispatchCommand(mxComponent, u".uno:Paste"_ustr, aArgs);
    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidateOnInserRowCol)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);

    // view
    ScTestViewCallback aView;

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    // move downward
    for (int i = 0; i < 200; ++i)
    {
        pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN);
        pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN);
    }
    Scheduler::ProcessEventsToIdle();

    // insert row
    aView.m_bInvalidateTiles = false;
    aView.m_aInvalidations.clear();
    dispatchCommand(mxComponent, u".uno:InsertRows"_ustr, aArgs);
    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(-75, 51240, 32212230, 63990), aView.m_aInvalidations[0]);

    // move on the right
    for (int i = 0; i < 200; ++i)
    {
        pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_RIGHT);
        pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_RIGHT);
    }
    Scheduler::ProcessEventsToIdle();

    // insert column
    aView.m_bInvalidateTiles = false;
    aView.m_aInvalidations.clear();
    dispatchCommand(mxComponent, u".uno:InsertColumns"_ustr, aArgs);
    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(254925, -15, 32212230, 63990), aView.m_aInvalidations[0]);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCommentCallback)
{
    // FIXME: Hack because previous tests do not destroy ScDocument(with annotations) on exit (?).
    ScPostIt::mnLastPostItId = 1;

    {
        ScModelObj* pModelObj = createDoc("small.ods");
        ScTestViewCallback aView1;
        int nView1 = KitHelper::getCurrentView();

        // Create a 2nd view
        KitHelper::createView();
        pModelObj->initializeForTiledRendering({});
        ScTestViewCallback aView2;

        KitHelper::setView(nView1);

        ScTabViewShell* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
        if (pTabViewShell)
            pTabViewShell->SetCursor(4, 4);

        // Add a new comment
        cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"Comment"_ustr)},
            {"Author", cpo::uno::Any(u"Kit User1"_ustr)},
        }));
        dispatchCommand(mxComponent, u".uno:InsertAnnotation"_ustr, aArgs);

        // We received a COKitCallbackType::COMMENT callback with comment 'Add' action
        CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView2.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("1"), aView1.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(std::string("1"), aView2.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(std::string("0"), aView1.m_aCommentCallbackResult.get<std::string>("tab"));
        CPPUNIT_ASSERT_EQUAL(std::string("0"), aView2.m_aCommentCallbackResult.get<std::string>("tab"));
        CPPUNIT_ASSERT_EQUAL(std::string("Kit User1"), aView1.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("Kit User1"), aView2.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("Comment"), aView1.m_aCommentCallbackResult.get<std::string>("text"));
        CPPUNIT_ASSERT_EQUAL(std::string("Comment"), aView2.m_aCommentCallbackResult.get<std::string>("text"));
        CPPUNIT_ASSERT_EQUAL(std::string("4 4 4 4"), aView1.m_aCommentCallbackResult.get<std::string>("cellRange"));
        CPPUNIT_ASSERT_EQUAL(std::string("4 4 4 4"), aView2.m_aCommentCallbackResult.get<std::string>("cellRange"));

        // Ensure deleting rows updates comments
        if (pTabViewShell)
            pTabViewShell->SetCursor(2, 2);

        dispatchCommand(mxComponent, u".uno:DeleteRows"_ustr, {});
        Scheduler::ProcessEventsToIdle();
        CPPUNIT_ASSERT_EQUAL(std::string("4 3 4 3"), aView1.m_aCommentCallbackResult.get<std::string>("cellRange"));
        CPPUNIT_ASSERT_EQUAL(std::string("4 3 4 3"), aView2.m_aCommentCallbackResult.get<std::string>("cellRange"));

        // Ensure deleting columns updates comments
        if (pTabViewShell)
            pTabViewShell->SetCursor(2, 2);

        dispatchCommand(mxComponent, u".uno:DeleteColumns"_ustr, {});
        Scheduler::ProcessEventsToIdle();
        CPPUNIT_ASSERT_EQUAL(std::string("3 3 3 3"), aView1.m_aCommentCallbackResult.get<std::string>("cellRange"));
        CPPUNIT_ASSERT_EQUAL(std::string("3 3 3 3"), aView2.m_aCommentCallbackResult.get<std::string>("cellRange"));

        std::string aCommentId = aView1.m_aCommentCallbackResult.get<std::string>("id");

        // Edit a comment
        // Select some random cell, we should be able to edit the cell note without
        // selecting the cell
        if (pTabViewShell)
            pTabViewShell->SetCursor(3, 100);
        aArgs = comphelper::InitPropertySequence(
        {
            {"Id", cpo::uno::Any(OUString::createFromAscii(aCommentId))},
            {"Text", cpo::uno::Any(u"Edited comment"_ustr)},
            {"Author", cpo::uno::Any(u"Kit User2"_ustr)},
        });
        dispatchCommand(mxComponent, u".uno:EditAnnotation"_ustr, aArgs);

        // We received a COKitCallbackType::COMMENT callback with comment 'Modify' action
        CPPUNIT_ASSERT_EQUAL(std::string("Modify"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("Modify"), aView2.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(aCommentId, aView1.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(aCommentId, aView2.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(std::string("Kit User2"), aView1.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("Kit User2"), aView2.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("Edited comment"), aView1.m_aCommentCallbackResult.get<std::string>("text"));
        CPPUNIT_ASSERT_EQUAL(std::string("Edited comment"), aView2.m_aCommentCallbackResult.get<std::string>("text"));
        CPPUNIT_ASSERT_EQUAL(std::string("3 3 3 3"), aView1.m_aCommentCallbackResult.get<std::string>("cellRange"));
        CPPUNIT_ASSERT_EQUAL(std::string("3 3 3 3"), aView2.m_aCommentCallbackResult.get<std::string>("cellRange"));

        // Delete the comment
        if (pTabViewShell)
            pTabViewShell->SetCursor(4, 43);
        aArgs = comphelper::InitPropertySequence(
        {
            {"Id", cpo::uno::Any(OUString::createFromAscii(aCommentId))}
        });
        dispatchCommand(mxComponent, u".uno:DeleteNote"_ustr, aArgs);

        // We received a COKitCallbackType::COMMENT callback with comment 'Remove' action
        CPPUNIT_ASSERT_EQUAL(std::string("Remove"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("Remove"), aView2.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(aCommentId, aView1.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(aCommentId, aView2.m_aCommentCallbackResult.get<std::string>("id"));
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoLimiting)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    ScUndoManager* pUndoManager = pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);

    // view #1
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();

    // check that undo action count in not 0
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #2
    KitHelper::setView(nView2);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // check that undo has not been executed on view #2
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #1
    KitHelper::setView(nView1);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // check that undo has been executed on view #1
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetUndoActionCount());

    // check that redo action count in not 0
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetRedoActionCount());

    // try to execute redo in view #2
    KitHelper::setView(nView2);
    dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
    // check that redo has not been executed on view #2
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetRedoActionCount());

    // try to execute redo in view #1
    KitHelper::setView(nView1);
    dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
    // check that redo has been executed on view #1
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetRedoActionCount());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoRepairDispatch)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    ScUndoManager* pUndoManager = pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);

    // view #1
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();

    // check that undo action count in not 0
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #2
    KitHelper::setView(nView2);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // check that undo has not been executed on view #2
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #2 in repair mode
    KitHelper::setView(nView2);
    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence(
    {
        {"Repair", cpo::uno::Any(true)}
    }));
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, aPropertyValues);
    // check that undo has been executed on view #2 in repair mode
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetUndoActionCount());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInsertGraphicInvalidations)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view
    ScTestViewCallback aView;

    // we need to paint a tile in the view for triggering the tile invalidation solution
    int nCanvasWidth = 256;
    int nCanvasHeight = 256;
    std::vector<unsigned char> aBuffer(nCanvasWidth * nCanvasHeight * 4);
    ScopedVclPtrInstance<VirtualDevice> pDevice(DeviceFormat::WITHOUT_ALPHA);
    pDevice->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasWidth, nCanvasHeight), 1.0, Point(), aBuffer.data());
    pModelObj->paintTile(*pDevice, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, /*nTilePosY=*/0, /*nTileWidth=*/3840, /*nTileHeight=*/3840);
    Scheduler::ProcessEventsToIdle();

    // insert an image in view and see if both views are invalidated
    aView.m_bInvalidateTiles = false;
    cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
            { "FileName", cpo::uno::Any(createFileURL(u"smile.png")) }
        }));
    dispatchCommand(mxComponent, u".uno:InsertGraphic"_ustr, aArgs);
    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);

    // undo image insertion in view and see if both views are invalidated
    aView.m_bInvalidateTiles = false;
    cpo::uno::Sequence<beans::PropertyValue> aArgs2;
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, aArgs2);
    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDocumentSizeWithTwoViews)
{
    // Open a document that has the cursor far away & paint a tile
    ScModelObj* pModelObj = createDoc("cursor-away.ods");

    // Set the visible area, and press page down
    pModelObj->setClientVisibleArea(tools::Rectangle(750, 1861, 20583, 6997));
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN);
    Scheduler::ProcessEventsToIdle();

    int nCanvasWidth = 256;
    int nCanvasHeight = 256;
    std::vector<unsigned char> aBuffer1(nCanvasWidth * nCanvasHeight * 4);
    ScopedVclPtrInstance<VirtualDevice> pDevice1(DeviceFormat::WITHOUT_ALPHA);
    pDevice1->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasWidth, nCanvasHeight), 1.0, Point(), aBuffer1.data());
    pModelObj->paintTile(*pDevice1, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, /*nTilePosY=*/291840, /*nTileWidth=*/3840, /*nTileHeight=*/3840);
    Scheduler::ProcessEventsToIdle();

    // Create a new view
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();

    std::vector<unsigned char> aBuffer2(nCanvasWidth * nCanvasHeight * 4);
    ScopedVclPtrInstance<VirtualDevice> pDevice2(DeviceFormat::WITHOUT_ALPHA);
    pDevice2->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasWidth, nCanvasHeight), 1.0, Point(), aBuffer2.data());
    pModelObj->paintTile(*pDevice2, nCanvasWidth, nCanvasHeight, /*nTilePosX=*/0, /*nTilePosY=*/291840, /*nTileWidth=*/3840, /*nTileHeight=*/3840);
    Scheduler::ProcessEventsToIdle();

    // Check that the tiles actually have the same content
    for (size_t i = 0; i < aBuffer1.size(); ++i)
        CPPUNIT_ASSERT_EQUAL(aBuffer1[i], aBuffer2[i]);

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowHeightChangeRendersFreshAfterUndo)
{
    // A row height change made outside the interactive resize path, like undo or
    // redo of typing that auto-grew the row, shifts every row below it. A view
    // that already resolved row positions for that area must render tiles with
    // the rows at their new places, the same as a freshly created view does.
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocShell* pDocSh = pViewData->GetDocShell();
    CPPUNIT_ASSERT(pDocSh);
    ScDocument& rDoc = pViewData->GetDocument();

    // A distant row lets a stale cached row position show up as a big offset.
    // The neighbouring rows carry text so a shifted render differs in pixels.
    const SCCOL nCol = 8;
    const SCROW nRow = 9864;
    for (SCROW nR = nRow - 3; nR <= nRow + 3; ++nR)
        if (nR != nRow)
            rDoc.SetString(ScAddress(nCol, nR, 0), "row " + OUString::number(nR + 1));

    const tools::Long nRowOffsetTw = rDoc.GetRowHeight(0, nRow - 1, 0);
    const tools::Rectangle aVisArea(0, nRowOffsetTw - 2000, 20000, nRowOffsetTw + 8000);
    pModelObj->setClientVisibleArea(aVisArea);

    // The header request is how a client viewport makes the view record row
    // position anchors for this area.
    {
        tools::JsonWriter aJsonWriter;
        pModelObj->getRowColumnHeaders(aVisArea, aJsonWriter);
        aJsonWriter.finishAndGetAsOString();
    }
    Scheduler::ProcessEventsToIdle();

    // Grow the row the way undo and redo of a data entry do: write the cell and
    // recompute the optimal height through the document shell.
    ScFieldEditEngine& rEngine = rDoc.GetEditEngine();
    rEngine.SetTextCurrentDefaults(u"one\ntwo\nthree\nfour\nfive"_ustr);
    rDoc.SetEditText(ScAddress(nCol, nRow, 0), rEngine.CreateTextObject());
    const sal_uInt16 nOldHeight = rDoc.GetRowHeight(nRow, 0);
    CPPUNIT_ASSERT(pDocSh->AdjustRowHeight(nRow, nRow, 0));
    CPPUNIT_ASSERT(rDoc.GetRowHeight(nRow, 0) > nOldHeight);
    Scheduler::ProcessEventsToIdle();

    // Render the rows below the grown one from the view that held old anchors.
    const int nCanvasWidth = 512;
    const int nCanvasHeight = 512;
    const tools::Long nTilePosX = 9000;
    const tools::Long nTilePosY = nRowOffsetTw + 1000;
    std::vector<unsigned char> aBuffer1(nCanvasWidth * nCanvasHeight * 4);
    ScopedVclPtrInstance<VirtualDevice> pDevice1(DeviceFormat::WITHOUT_ALPHA);
    pDevice1->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasWidth, nCanvasHeight), 1.0,
                                                        Point(), aBuffer1.data());
    pModelObj->paintTile(*pDevice1, nCanvasWidth, nCanvasHeight, nTilePosX, nTilePosY,
                         /*nTileWidth=*/7680, /*nTileHeight=*/7680);
    Scheduler::ProcessEventsToIdle();

    // A freshly created view resolves the row positions from the document.
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();
    pModelObj->setClientVisibleArea(aVisArea);
    {
        tools::JsonWriter aJsonWriter;
        pModelObj->getRowColumnHeaders(aVisArea, aJsonWriter);
        aJsonWriter.finishAndGetAsOString();
    }
    Scheduler::ProcessEventsToIdle();

    std::vector<unsigned char> aBuffer2(nCanvasWidth * nCanvasHeight * 4);
    ScopedVclPtrInstance<VirtualDevice> pDevice2(DeviceFormat::WITHOUT_ALPHA);
    pDevice2->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasWidth, nCanvasHeight), 1.0,
                                                        Point(), aBuffer2.data());
    pModelObj->paintTile(*pDevice2, nCanvasWidth, nCanvasHeight, nTilePosX, nTilePosY,
                         /*nTileWidth=*/7680, /*nTileHeight=*/7680);
    Scheduler::ProcessEventsToIdle();

    bool bAreBuffersMatching = aBuffer1 == aBuffer2;
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Buffers should match", true, bAreBuffersMatching);

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowsBelowKeepPositionAfterRowHeightChange)
{
    // A row height change made through the document shell, the path undo and
    // redo of a data entry take, shifts every row below it. A view that already
    // resolved row positions for that area must place those rows at their new
    // positions: both in the coordinates it reports and in the tiles it paints.
    // When it does not, tiles below the grown row show the neighbouring rows
    // shifted by the height delta, with text cut in half at the tile boundary.
    // The clients also have to be told that the row geometry they cache changed.
    comphelper::COKit::setCompatFlag(comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocShell* pDocSh = pViewData->GetDocShell();
    CPPUNIT_ASSERT(pDocSh);
    ScDocument& rDoc = pViewData->GetDocument();
    ScTestViewCallback aView;

    // A distant row makes a stale cached position visible as a large offset;
    // the rows around the grown one carry text so a shifted render differs in
    // pixels and shows the artifact.
    const SCCOL nCol = 8;
    const SCROW nRow = 9864;
    for (SCROW nR = nRow - 3; nR <= nRow + 3; ++nR)
        if (nR != nRow)
            rDoc.SetString(ScAddress(nCol, nR, 0), "row " + OUString::number(nR + 1));

    const tools::Long nRowOffsetTw = rDoc.GetRowHeight(0, nRow - 1, 0);
    const tools::Rectangle aVisArea(0, nRowOffsetTw - 2000, 20000, nRowOffsetTw + 8000);
    pModelObj->setClientVisibleArea(aVisArea);

    // The header request is how a client viewport makes the view record row
    // position anchors for this area.
    {
        tools::JsonWriter aJsonWriter;
        pModelObj->getRowColumnHeaders(aVisArea, aJsonWriter);
        aJsonWriter.finishAndGetAsOString();
    }
    Scheduler::ProcessEventsToIdle();

    // Grow the row the way ScUndoEnterData::DoChange does: write the multiline
    // cell, then recompute the optimal height through the document shell.
    ScFieldEditEngine& rEngine = rDoc.GetEditEngine();
    rEngine.SetTextCurrentDefaults(u"one\ntwo\nthree\nfour\nfive"_ustr);
    rDoc.SetEditText(ScAddress(nCol, nRow, 0), rEngine.CreateTextObject());
    const sal_uInt16 nOldHeight = rDoc.GetRowHeight(nRow, 0);
    aView.m_sInvalidateSheetGeometry = ""_ostr;
    CPPUNIT_ASSERT(pDocSh->AdjustRowHeight(nRow, nRow, 0));
    CPPUNIT_ASSERT(rDoc.GetRowHeight(nRow, 0) > nOldHeight);
    Scheduler::ProcessEventsToIdle();

    // The client re-reads the row geometry only when it is told that it changed.
    CPPUNIT_ASSERT_EQUAL("rows sizes"_ostr, aView.m_sInvalidateSheetGeometry);

    // The view's position for a row below the grown one has to match the
    // position accumulated from the document row heights.
    const double fPPTY = pViewData->GetPPTY();
    tools::Long nExpectedPixels = 0;
    for (SCROW nR = 0; nR < nRow + 2; ++nR)
        if (sal_uInt16 nSize = rDoc.GetRowHeight(nR, 0))
            nExpectedPixels += ScViewData::ToPixel(nSize, fPPTY);
    const Point aScrPos = pViewData->GetScrPos(nCol, nRow + 2, pViewData->GetActivePart());
    CPPUNIT_ASSERT_EQUAL(nExpectedPixels, aScrPos.Y());

    // Two stacked tiles have to show the same pixels as one tile covering both:
    // a stale position shifts the lower tile's content but not the lower half
    // of the taller render, which is the text-cut-in-half seam.
    const tools::Long nTileTw = 3840;
    const tools::Long nTilePosY = (nRowOffsetTw / nTileTw) * nTileTw;
    tools::Long nColOffsetTw = 0;
    for (SCCOL nC = 0; nC < nCol; ++nC)
        nColOffsetTw += rDoc.GetColWidth(nC, 0);
    const tools::Long nTilePosX = (nColOffsetTw / nTileTw) * nTileTw;
    const int nCanvasSize = 256;

    auto paintArea = [&](tools::Long nPosY, int nHeightPx,
                         tools::Long nHeightTw) -> std::vector<unsigned char>
    {
        std::vector<unsigned char> aBuffer(nCanvasSize * nHeightPx * 4);
        ScopedVclPtrInstance<VirtualDevice> pDevice(DeviceFormat::WITHOUT_ALPHA);
        pDevice->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasSize, nHeightPx), 1.0,
                                                           Point(), aBuffer.data());
        pModelObj->paintTile(*pDevice, nCanvasSize, nHeightPx, nTilePosX, nPosY,
                             /*nTileWidth=*/nTileTw, /*nTileHeight=*/nHeightTw);
        Scheduler::ProcessEventsToIdle();
        return aBuffer;
    };

    const std::vector<unsigned char> aUpperTile = paintArea(nTilePosY, nCanvasSize, nTileTw);
    const std::vector<unsigned char> aLowerTile
        = paintArea(nTilePosY + nTileTw, nCanvasSize, nTileTw);
    const std::vector<unsigned char> aBothTiles
        = paintArea(nTilePosY, 2 * nCanvasSize, 2 * nTileTw);
    const std::vector<unsigned char> aUpperRepaint = paintArea(nTilePosY, nCanvasSize, nTileTw);

    auto firstDiffPixelRow = [](const std::vector<unsigned char>& rTile,
                                 const unsigned char* pReference) -> int
    {
        for (size_t i = 0; i < rTile.size(); ++i)
            if (rTile[i] != pReference[i])
                return static_cast<int>(i / (nCanvasSize * 4));
        return -1;
    };
    const OString aDiffInfo = "first differing pixel row: upper vs big "
        + OString::number(firstDiffPixelRow(aUpperTile, aBothTiles.data()))
        + ", lower vs big "
        + OString::number(firstDiffPixelRow(aLowerTile, aBothTiles.data() + aUpperTile.size()))
        + ", upper repaint vs upper "
        + OString::number(firstDiffPixelRow(aUpperTile, aUpperRepaint.data()));

    // The bottommost pixel row of the upper tile carries the tile's own edge of
    // the boundary grid line; in the taller render the same y is interior, so
    // the comparison covers the rows above it.
    const size_t nUpperComparable = (nCanvasSize - 1) * nCanvasSize * 4;
    CPPUNIT_ASSERT_MESSAGE(aDiffInfo.getStr(),
        std::equal(aUpperTile.begin(), aUpperTile.begin() + nUpperComparable,
                   aBothTiles.begin()));
    CPPUNIT_ASSERT_MESSAGE(aDiffInfo.getStr(),
        std::equal(aLowerTile.begin(), aLowerTile.end(),
                   aBothTiles.begin() + aUpperTile.size()));
}

namespace
{

// Records row and column position anchors for the given area in the current view, the way a
// client viewport does when it shows that part of the sheet.
void requestRowColumnHeaders(ScModelObj* pModelObj, const tools::Rectangle& rArea)
{
    pModelObj->setClientVisibleArea(rArea);
    tools::JsonWriter aJsonWriter;
    pModelObj->getRowColumnHeaders(rArea, aJsonWriter);
    aJsonWriter.finishAndGetAsOString();
    Scheduler::ProcessEventsToIdle();
}

// The pixel position of the top of nRow accumulated from the document row heights, the same
// way a freshly created view resolves it. Hidden rows contribute nothing.
tools::Long expectedRowPosition(const ScDocument& rDoc, double fPPTY, SCROW nRow)
{
    tools::Long nPixels = 0;
    for (SCROW nR = 0; nR < nRow; ++nR)
        if (sal_uInt16 nSize = rDoc.GetRowHeight(nR, 0))
            nPixels += ScViewData::ToPixel(nSize, fPPTY);
    return nPixels;
}

}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowsKeepPositionAfterSortUndo)
{
    // Undo of a sort that was executed on a range with hidden rows moves the hidden flags
    // back to their previous rows. A view that already resolved row positions for that area
    // must place the rows at their restored positions, the same as a freshly created view,
    // and the clients have to be told that the row geometry they cache changed.
    comphelper::COKit::setCompatFlag(comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocument& rDoc = pViewData->GetDocument();
    ScTestViewCallback aView;

    // Descending values in a distant range make the ascending sort reverse the row order; a
    // stale cached row position then shows up as a large offset.
    const SCROW nRow = 9840;
    const int nRangeSize = 10;
    for (int i = 0; i < nRangeSize; ++i)
        rDoc.SetValue(ScAddress(0, nRow + i, 0), nRangeSize - i);

    // Two tall hidden rows: hiding them removes more height than hiding the default rows the
    // sort is going to hide in their place, so the total height above the rows below changes
    // with every move of the hidden flags.
    rDoc.SetRowHeightRange(nRow + 2, nRow + 3, 0, 800);
    rDoc.SetManualHeight(nRow + 2, nRow + 3, 0, true);
    rDoc.SetRowHidden(nRow + 2, nRow + 3, 0, true);

    dispatchCommand(mxComponent, u".uno:GoToCell"_ustr,
                    comphelper::InitPropertySequence(
                        { { "ToPoint", cpo::uno::Any(u"A9841:A9850"_ustr) } }));
    dispatchCommand(mxComponent, u".uno:SortAscending"_ustr, {});
    Scheduler::ProcessEventsToIdle();

    // The rows that carried the values 8 and 7 moved to the positions of the values in the
    // ascending order, and took their hidden flags with them; the tall rows became visible.
    CPPUNIT_ASSERT_EQUAL(1.0, rDoc.GetValue(ScAddress(0, nRow, 0)));
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 6, 0));
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 7, 0));
    CPPUNIT_ASSERT(!rDoc.RowHidden(nRow + 2, 0));

    const tools::Long nRowOffsetTw = rDoc.GetRowHeight(0, nRow - 1, 0);
    const tools::Rectangle aVisArea(0, nRowOffsetTw - 2000, 20000, nRowOffsetTw + 8000);
    requestRowColumnHeaders(pModelObj, aVisArea);

    const double fPPTY = pViewData->GetPPTY();
    CPPUNIT_ASSERT_EQUAL(expectedRowPosition(rDoc, fPPTY, nRow + 8),
                         pViewData->GetScrPos(0, nRow + 8, pViewData->GetActivePart()).Y());

    aView.m_sInvalidateSheetGeometry = ""_ostr;
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    Scheduler::ProcessEventsToIdle();

    // The undo moved the hidden flags back to the tall rows.
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 2, 0));
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 3, 0));
    CPPUNIT_ASSERT(!rDoc.RowHidden(nRow + 6, 0));

    CPPUNIT_ASSERT_EQUAL(expectedRowPosition(rDoc, fPPTY, nRow + 8),
                         pViewData->GetScrPos(0, nRow + 8, pViewData->GetActivePart()).Y());

    // The client re-reads the row geometry only when it is told that it changed.
    CPPUNIT_ASSERT_EQUAL("rows"_ostr, aView.m_sInvalidateSheetGeometry);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTilesBelowSortedRangeInvalidatedOnSort)
{
    // Sorting a range with hidden rows moves the hidden flags along with the rows, so every
    // row below the sorted range can sit at a new position. The clients keep the old pixels
    // of that area until they receive an invalidation for it.
    comphelper::COKit::setCompatFlag(comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocument& rDoc = pViewData->GetDocument();
    ScTestViewCallback aView;

    const SCROW nRow = 9840;
    const int nRangeSize = 10;
    for (int i = 0; i < nRangeSize; ++i)
        rDoc.SetValue(ScAddress(0, nRow + i, 0), nRangeSize - i);

    // Content below the sorted range extends the document there, so invalidations for that
    // area are not clipped away.
    for (SCROW nR = nRow + 12; nR <= nRow + 20; ++nR)
        rDoc.SetString(ScAddress(1, nR, 0), "row " + OUString::number(nR + 1));

    // Two tall hidden rows: every move of the hidden flags changes the total height of the
    // sorted range and with it the position of everything below.
    rDoc.SetRowHeightRange(nRow + 2, nRow + 3, 0, 800);
    rDoc.SetManualHeight(nRow + 2, nRow + 3, 0, true);
    rDoc.SetRowHidden(nRow + 2, nRow + 3, 0, true);

    dispatchCommand(mxComponent, u".uno:GoToCell"_ustr,
                    comphelper::InitPropertySequence(
                        { { "ToPoint", cpo::uno::Any(u"A9841:A9850"_ustr) } }));

    // The client viewport covers the sorted range and the rows below it; invalidations are
    // clipped to the area the client can show.
    const tools::Long nRowOffsetTw = rDoc.GetRowHeight(0, nRow - 1, 0);
    pModelObj->setClientVisibleArea(
        tools::Rectangle(0, nRowOffsetTw - 1000, 20000, nRowOffsetTw + 12000));
    Scheduler::ProcessEventsToIdle();

    auto anyInvalidationCoversY = [&aView](tools::Long nY) {
        for (const auto& rRect : aView.m_aInvalidations)
            if (rRect.Top() <= nY && rRect.Bottom() >= nY)
                return true;
        return false;
    };

    aView.ClearAllInvalids();
    dispatchCommand(mxComponent, u".uno:SortAscending"_ustr, {});
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 6, 0));

    // A row a few rows below the sorted range sits at a new position now, so its old pixels
    // are stale and the view has to receive an invalidation covering it.
    CPPUNIT_ASSERT(anyInvalidationCoversY(rDoc.GetRowHeight(0, nRow + 14, 0)));

    aView.ClearAllInvalids();
    dispatchCommand(mxComponent, u".uno:SortDescending"_ustr, {});
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(rDoc.RowHidden(nRow + 2, 0));

    CPPUNIT_ASSERT(anyInvalidationCoversY(rDoc.GetRowHeight(0, nRow + 14, 0)));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowsKeepPositionAfterCellStyleChange)
{
    // Changing a cell style through the style API, the way a macro or extension does,
    // recomputes the optimal height of every row that uses the style. A view that already
    // resolved row positions for that area must place the rows below at their new positions,
    // and the client has to be told that the row geometry it caches changed.
    comphelper::COKit::setCompatFlag(comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocument& rDoc = pViewData->GetDocument();
    ScTestViewCallback aView;

    const SCROW nRow = 9840;
    for (SCROW nR = nRow; nR <= nRow + 3; ++nR)
        rDoc.SetString(ScAddress(0, nR, 0), "row " + OUString::number(nR + 1));

    const tools::Long nRowOffsetTw = rDoc.GetRowHeight(0, nRow - 1, 0);
    const tools::Rectangle aVisArea(0, nRowOffsetTw - 2000, 20000, nRowOffsetTw + 8000);
    requestRowColumnHeaders(pModelObj, aVisArea);

    const double fPPTY = pViewData->GetPPTY();
    CPPUNIT_ASSERT_EQUAL(expectedRowPosition(rDoc, fPPTY, nRow + 8),
                         pViewData->GetScrPos(0, nRow + 8, pViewData->GetActivePart()).Y());

    // A bigger font in the default cell style makes every row with content taller.
    const sal_uInt16 nOldHeight = rDoc.GetRowHeight(nRow, 0);
    uno::Reference<style::XStyleFamiliesSupplier> xSupplier(mxComponent, uno::UNO_QUERY_THROW);
    uno::Reference<container::XNameAccess> xFamilies = xSupplier->getStyleFamilies();
    uno::Reference<container::XNameAccess> xCellStyles(xFamilies->getByName(u"CellStyles"_ustr),
                                                      uno::UNO_QUERY_THROW);
    uno::Reference<beans::XPropertySet> xStyle(xCellStyles->getByName(u"Default"_ustr),
                                               uno::UNO_QUERY_THROW);
    aView.m_sInvalidateSheetGeometry = ""_ostr;
    xStyle->setPropertyValue(u"CharHeight"_ustr, cpo::uno::Any(float(24)));
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(rDoc.GetRowHeight(nRow, 0) > nOldHeight);

    CPPUNIT_ASSERT_EQUAL(expectedRowPosition(rDoc, fPPTY, nRow + 8),
                         pViewData->GetScrPos(0, nRow + 8, pViewData->GetActivePart()).Y());

    // The client re-reads the row geometry only when it is told that it changed.
    CPPUNIT_ASSERT_EQUAL("rows sizes"_ostr, aView.m_sInvalidateSheetGeometry);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDisableUndoRepair)
{
    ScModelObj* pModelObj = createDoc("cursor-away.ods");
    CPPUNIT_ASSERT(pModelObj);

    // view #1
    int nView1 = KitHelper::getCurrentView();
    SfxViewShell* pView1 = SfxViewShell::Current();

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    CPPUNIT_ASSERT(pView1 != pView2);

    // both views have UNDO disabled
    {
        SfxItemSet aSet1(pView1->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        SfxItemSet aSet2(pView2->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        pView1->GetSlotState(SID_UNDO, nullptr, &aSet1);
        pView2->GetSlotState(SID_UNDO, nullptr, &aSet2);
        CPPUNIT_ASSERT_EQUAL(SfxItemState::DISABLED, aSet1.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT_EQUAL(SfxItemState::DISABLED, aSet2.GetItemState(SID_UNDO));
    }

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'h', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'h', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    // view1 has UNDO enabled, view2 is in UNDO-repair
    {
        SfxItemSet aSet1(pView1->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        SfxItemSet aSet2(pView2->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        pView1->GetSlotState(SID_UNDO, nullptr, &aSet1);
        pView2->GetSlotState(SID_UNDO, nullptr, &aSet2);
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet1.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxStringItem* >(aSet1.GetItem(SID_UNDO)));
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet2.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxUInt32Item* >(aSet2.GetItem(SID_UNDO)));
        const SfxUInt32Item* pUInt32Item = dynamic_cast<const SfxUInt32Item*>(aSet2.GetItem(SID_UNDO));
        CPPUNIT_ASSERT(pUInt32Item);
        CPPUNIT_ASSERT_EQUAL(static_cast< sal_uInt32 >(SID_REPAIRPACKAGE), pUInt32Item->GetValue());
    }

    // text edit a cell in view #2
    KitHelper::setView(nView2);
    pModelObj->setPart(1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'c', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'c', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    // both views have UNDO enabled
    Scheduler::ProcessEventsToIdle();
    {
        SfxItemSet aSet1(pView1->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        SfxItemSet aSet2(pView2->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        pView1->GetSlotState(SID_UNDO, nullptr, &aSet1);
        pView2->GetSlotState(SID_UNDO, nullptr, &aSet2);
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet1.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxStringItem* >(aSet1.GetItem(SID_UNDO)));
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet2.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxStringItem* >(aSet2.GetItem(SID_UNDO)));
    }

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDocumentRepair)
{
    // Create two views.
    ScModelObj* pModelObj = createDoc("cursor-away.ods");
    CPPUNIT_ASSERT(pModelObj);

    // view #1
    SfxViewShell* pView1 = SfxViewShell::Current();

    // view #2
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    int nView2 = KitHelper::getCurrentView();
    CPPUNIT_ASSERT(pView1 != pView2);
    {
        std::unique_ptr<SfxBoolItem> pItem1;
        std::unique_ptr<SfxBoolItem> pItem2;
        pView1->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem1);
        pView2->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem2);
        CPPUNIT_ASSERT(pItem1);
        CPPUNIT_ASSERT(pItem2);
        CPPUNIT_ASSERT_EQUAL(false, pItem1->GetValue());
        CPPUNIT_ASSERT_EQUAL(false, pItem2->GetValue());
    }

    // Insert a character in the second view.
    KitHelper::setView(nView2);
    pModelObj->setPart(1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'c', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'c', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    {
        std::unique_ptr<SfxBoolItem> pItem1;
        std::unique_ptr<SfxBoolItem> pItem2;
        pView1->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem1);
        pView2->GetViewFrame().GetBindings().QueryState(SID_DOC_REPAIR, pItem2);
        CPPUNIT_ASSERT(pItem1);
        CPPUNIT_ASSERT(pItem2);
        CPPUNIT_ASSERT_EQUAL(true, pItem1->GetValue());
        CPPUNIT_ASSERT_EQUAL(true, pItem2->GetValue());
    }

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testLanguageStatus)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);

    // view #1
    SfxViewShell* pView1 = SfxViewShell::Current();

    // view #2
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    CPPUNIT_ASSERT(pView1 != pView2);
    {
        std::unique_ptr<SfxPoolItem> xItem1;
        std::unique_ptr<SfxPoolItem> xItem2;
        pView1->GetViewFrame().GetBindings().QueryState(SID_LANGUAGE_STATUS, xItem1);
        pView2->GetViewFrame().GetBindings().QueryState(SID_LANGUAGE_STATUS, xItem2);
        const SfxStringItem* pItem1 = dynamic_cast<const SfxStringItem*>(xItem1.get());
        const SfxStringItem* pItem2 = dynamic_cast<const SfxStringItem*>(xItem2.get());
        CPPUNIT_ASSERT(pItem1);
        CPPUNIT_ASSERT(pItem2);
        CPPUNIT_ASSERT(!pItem1->GetValue().isEmpty());
        CPPUNIT_ASSERT(!pItem2->GetValue().isEmpty());
    }

    {
        SfxStringItem aLangString(SID_LANGUAGE_STATUS, u"Default_Spanish (Bolivia)"_ustr);
        pView1->GetViewFrame().GetDispatcher()->ExecuteList(SID_LANGUAGE_STATUS,
            SfxCallMode::SYNCHRON, { &aLangString });
    }

    {
        std::unique_ptr<SfxPoolItem> xItem1;
        std::unique_ptr<SfxPoolItem> xItem2;
        pView1->GetViewFrame().GetBindings().QueryState(SID_LANGUAGE_STATUS, xItem1);
        pView2->GetViewFrame().GetBindings().QueryState(SID_LANGUAGE_STATUS, xItem2);
        const SfxStringItem* pItem1 = dynamic_cast<const SfxStringItem*>(xItem1.get());
        const SfxStringItem* pItem2 = dynamic_cast<const SfxStringItem*>(xItem2.get());
        CPPUNIT_ASSERT(pItem1);
        CPPUNIT_ASSERT(pItem2);
        static constexpr OUString aLangBolivia(u"Spanish (Bolivia);es-BO"_ustr);
        CPPUNIT_ASSERT_EQUAL(aLangBolivia, pItem1->GetValue());
        CPPUNIT_ASSERT_EQUAL(aLangBolivia, pItem2->GetValue());
    }

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testMultiViewCopyPaste)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);

    pDoc->SetString(ScAddress(0, 0, 0), u"TestCopy1"_ustr);
    pDoc->SetString(ScAddress(1, 0, 0), u"TestCopy2"_ustr);

    // view #1
    ScTabViewShell* pView1 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView1);
    // emulate clipboard
    pView1->GetViewData().GetActiveWin()->SetClipboard(css::datatransfer::clipboard::KitClipboard::create(comphelper::getProcessComponentContext()));

    // view #2
    int nView1 = KitHelper::getCurrentView();
    KitHelper::createView();
    ScTabViewShell* pView2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    // emulate clipboard
    pView2->GetViewData().GetActiveWin()->SetClipboard(css::datatransfer::clipboard::KitClipboard::create(comphelper::getProcessComponentContext()));
    CPPUNIT_ASSERT(pView2);
    CPPUNIT_ASSERT(pView1 != pView2);
    CPPUNIT_ASSERT(pView1->GetViewData().GetActiveWin()->GetClipboard() != pView2->GetViewData().GetActiveWin()->GetClipboard());

    // copy text view 1
    pView1->SetCursor(0, 0);
    pView1->GetViewFrame().GetBindings().Execute(SID_COPY);

    // copy text view 2
    pView2->SetCursor(1, 0);
    pView2->GetViewFrame().GetBindings().Execute(SID_COPY);

     // paste text view 1
    pView1->SetCursor(0, 1);
    pView1->GetViewFrame().GetBindings().Execute(SID_PASTE);

    // paste text view 2
    pView2->SetCursor(1, 1);
    pView2->GetViewFrame().GetBindings().Execute(SID_PASTE);

    CPPUNIT_ASSERT_EQUAL(u"TestCopy1"_ustr, pDoc->GetString(ScAddress(0, 1, 0)));
    CPPUNIT_ASSERT_EQUAL(u"TestCopy2"_ustr, pDoc->GetString(ScAddress(1, 1, 0)));

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testIMESupport)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    VclPtr<vcl::Window> pDocWindow = pModelObj->getDocWindow();
    ScDocument* pDoc = pModelObj->GetDocument();

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    pView->SetCursor(0, 0);
    // sequence of chinese IME compositions when 'nihao' is typed in an IME
    const std::vector<OString> aUtf8Inputs{ "年"_ostr, "你"_ostr, "你好"_ostr, "你哈"_ostr, "你好"_ostr, "你好"_ostr };
    std::vector<OUString> aInputs;
    std::transform(aUtf8Inputs.begin(), aUtf8Inputs.end(),
                   std::back_inserter(aInputs), [](OString aInput) {
                       return OUString::fromUtf8(aInput);
                   });
    for (const auto& aInput: aInputs)
    {
        pDocWindow->PostExtTextInputEvent(VclEventId::ExtTextInput, aInput);
    }
    pDocWindow->PostExtTextInputEvent(VclEventId::EndExtTextInput, u""_ustr);

    // commit the string to the cell
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT_EQUAL(aInputs[aInputs.size() - 1], pDoc->GetString(ScAddress(0, 0, 0)));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testFilterDlg)
{
    createDoc("empty.ods");

    // view #1
    SfxViewShell* pView1 = SfxViewShell::Current();
    int nView1 = KitHelper::getCurrentView();

    // view #2
    KitHelper::createView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    CPPUNIT_ASSERT(pView1 != pView2);
    {
        pView2->GetViewFrame().GetDispatcher()->Execute(SID_FILTER,
            SfxCallMode::SLOT|SfxCallMode::RECORD);
    }

    Scheduler::ProcessEventsToIdle();
    SfxChildWindow* pRefWindow = pView2->GetViewFrame().GetChildWindow(SID_FILTER);
    CPPUNIT_ASSERT(pRefWindow);

    // switch to view 1
    KitHelper::setView(nView1);
    CPPUNIT_ASSERT_EQUAL(true, pView2->GetViewFrame().GetDispatcher()->IsLocked());
    CPPUNIT_ASSERT_EQUAL(false, pView1->GetViewFrame().GetDispatcher()->IsLocked());

    pRefWindow->GetController()->response(RET_CANCEL);

    CPPUNIT_ASSERT_EQUAL(false, pView2->GetViewFrame().GetDispatcher()->IsLocked());
    CPPUNIT_ASSERT_EQUAL(false, pView1->GetViewFrame().GetDispatcher()->IsLocked());

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testFunctionDlg)
{
    createDoc("empty.ods");

    // view #1
    SfxViewShell* pView1 = SfxViewShell::Current();
    int nView1 = KitHelper::getCurrentView();
    {
        pView1->GetViewFrame().GetDispatcher()->Execute(SID_OPENDLG_FUNCTION,
            SfxCallMode::SLOT|SfxCallMode::RECORD);
    }
    Scheduler::ProcessEventsToIdle();
    SfxChildWindow* pRefWindow = pView1->GetViewFrame().GetChildWindow(SID_OPENDLG_FUNCTION);
    CPPUNIT_ASSERT(pRefWindow);

    // view #2
    int nView2 = KitHelper::createView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    CPPUNIT_ASSERT(pView1 != pView2);

    // check locking
    CPPUNIT_ASSERT_EQUAL(true, pView1->GetViewFrame().GetDispatcher()->IsLocked());
    CPPUNIT_ASSERT_EQUAL(false, pView2->GetViewFrame().GetDispatcher()->IsLocked());

    KitHelper::setView(nView1);
    pRefWindow->GetController()->response(RET_CANCEL);

    CPPUNIT_ASSERT_EQUAL(false, pView1->GetViewFrame().GetDispatcher()->IsLocked());
    CPPUNIT_ASSERT_EQUAL(false, pView2->GetViewFrame().GetDispatcher()->IsLocked());

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView2);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSpellOnlineParameter)
{
    createDoc("empty.ods");

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    bool bSet = pView->IsAutoSpell();

    cpo::uno::Sequence<beans::PropertyValue> params =
    {
        comphelper::makePropertyValue(u"Enable"_ustr, cpo::uno::Any(!bSet)),
    };
    dispatchCommand(mxComponent, u".uno:SpellOnline"_ustr, params);
    CPPUNIT_ASSERT_EQUAL(!bSet, pView->IsAutoSpell());

    // set the same state as now and we don't expect any change (no-toggle)
    params =
    {
        comphelper::makePropertyValue(u"Enable"_ustr, cpo::uno::Any(!bSet)),
    };
    dispatchCommand(mxComponent, u".uno:SpellOnline"_ustr, params);
    CPPUNIT_ASSERT_EQUAL(!bSet, pView->IsAutoSpell());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testVbaRangeCopyPaste)
{
    ScModelObj* pModelObj = createDoc("RangeCopyPaste.ods");
    ScDocShell* pDocShell = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocShell);

    cpo::uno::Any aRet;
    cpo::uno::Sequence< cpo::uno::Any > aOutParam;
    cpo::uno::Sequence< cpo::uno::Any > aParams;
    cpo::uno::Sequence< sal_Int16 > aOutParamIndex;

    SfxObjectShell::CallXScript(
        mxComponent,
        u"vnd.sun.Star.script:Standard.Module1.Test_RangeCopyPaste?language=Basic&location=document"_ustr,
        aParams, aRet, aOutParamIndex, aOutParam);

    CPPUNIT_ASSERT(!pDocShell->GetClipData().is());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidationLoop)
{
    // Load the document with a form control.
    createDoc("invalidation-loop.fods");
    // Without the accompanying fix in place, this test would have never returned due to an infinite
    // invalidation loop between ScGridWindow::Paint() and vcl::Window::ImplPosSizeWindow().
    Scheduler::ProcessEventsToIdle();
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testPageDownInvalidation)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, awt::Key::PAGEDOWN, 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, awt::Key::PAGEDOWN, 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(3), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(15, 15, 1230, 225), aView1.m_aInvalidations[0]);
}

static Bitmap getTile(ScModelObj* pModelObj, int nTilePosX, int nTilePosY, tools::Long nTileWidth, tools::Long nTileHeight)
{
    size_t nCanvasSize = 1024;
    size_t nTileSize = 256;
    // BGRA format data
    std::vector<unsigned char> aPixmap(nCanvasSize * nCanvasSize * 4, 0);
    ScopedVclPtrInstance<VirtualDevice> xDevice(DeviceFormat::WITHOUT_ALPHA);
    xDevice->SetBackground(Wallpaper(COL_TRANSPARENT));
    xDevice->SetOutputSizePixelScaleOffsetAndKitBuffer(Size(nCanvasSize, nCanvasSize),
            1.0, Point(), aPixmap.data());
    pModelObj->paintTile(*xDevice, nCanvasSize, nCanvasSize, nTilePosX, nTilePosY, nTileWidth, nTileHeight);
    xDevice->EnableMapMode(false);
    return xDevice->GetBitmap(Point(0, 0), Size(nTileSize, nTileSize));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSheetChangeNoInvalidation)
{
    const bool oldPartInInvalidation = comphelper::COKit::isPartInInvalidation();
    comphelper::COKit::setPartInInvalidation(true);

    ScModelObj* pModelObj = createDoc("two_sheets.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // Set View to initial 100%
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 28050, 10605));
    pModelObj->setClientZoom(256, 256, 1920, 1920);

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);

    aView1.ClearAllInvalids();

    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    // switching sheets should trigger no unnecessary invalidations
    CPPUNIT_ASSERT(!aView1.m_bInvalidateTiles);

    // Get the known columns/rows of this sheet 2 now we have switched to it so
    // it knows what range to broadcast invalidations for if it knows cells need
    // to be redrawn.
    tools::JsonWriter aJsonWriter1;
    pModelObj->getRowColumnHeaders(tools::Rectangle(0, 15, 19650, 5400), aJsonWriter1);
    aJsonWriter1.finishAndGetAsOString();
    Scheduler::ProcessEventsToIdle();
    aView1.ClearAllInvalids();

    // switching back should also trigger no unnecessary invalidations
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEUP | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEUP | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!aView1.m_bInvalidateTiles);

    // The 2nd sheet has formulas that depend on B1 in the first sheet. So if
    // we change B1 there should be an invalidation in the second sheet for the
    // range that depends on it. Because this is a single user document with no
    // active view on the 2nd sheet this will happen on switching back to sheet 2
    typeCharsInCell("101", 1, 0, pView, pModelObj); // Type '101' in B1
    aView1.ClearAllInvalids();

    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    aView1.ClearAllInvalids();

    // Paint it to make it valid again
    getTile(pModelObj, 0, 0, 3840, 3840);

    // switching back to sheet 1 should trigger no unnecessary invalidations
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEUP | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEUP | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!aView1.m_bInvalidateTiles);

    // switching to sheet 2 should trigger no unnecessary invalidations this time
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!aView1.m_bInvalidateTiles);

    comphelper::COKit::setPartInInvalidation(oldPartInInvalidation);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInsertDeletePageInvalidation)
{
    ScModelObj* pModelObj = createDoc("insert_delete_sheet.ods");
    // the document has 1 sheet
    CPPUNIT_ASSERT_EQUAL(1, pModelObj->getParts());
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();

    cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
            { "Name", cpo::uno::Any(u""_ustr) },
            { "Index", cpo::uno::Any(sal_Int32(1)) }
        }));
    dispatchCommand(mxComponent, u".uno:Insert"_ustr, aArgs);
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(2), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(0, 0, 1000000000, 1000000000), aView1.m_aInvalidations[0]);
    CPPUNIT_ASSERT_EQUAL(2, pModelObj->getParts());

    // Delete sheet
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    cpo::uno::Sequence<beans::PropertyValue> aArgs2( comphelper::InitPropertySequence({
            { "Index", cpo::uno::Any(sal_Int32(1)) }
        }));
    dispatchCommand(mxComponent, u".uno:Remove"_ustr, aArgs2);
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(0, 0, 1000000000, 1000000000), aView1.m_aInvalidations[0]);
    CPPUNIT_ASSERT_EQUAL(1, pModelObj->getParts());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testGetRowColumnHeadersInvalidation)
{
    // NOTE NOTE NOTE
    // If you run this test in isolation using CPPUNIT_TEST_NAME=, it will fail because the invalidations
    // will be different.

    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    tools::JsonWriter aJsonWriter1;
    pModelObj->getRowColumnHeaders(tools::Rectangle(0, 15, 19650, 5400), aJsonWriter1);
    aJsonWriter1.finishAndGetAsOString();
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(Point(26775, 0), Size(22950, 13005)), aView1.m_aInvalidations[0]);

    // Extend area top-to-bottom
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    tools::JsonWriter aJsonWriter2;
    pModelObj->getRowColumnHeaders(tools::Rectangle(0, 5400, 19650, 9800), aJsonWriter2);
    aJsonWriter2.finishAndGetAsOString();
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(Point(0, 13005), Size(49725, 6375)), aView1.m_aInvalidations[0]);

    // Extend area left-to-right
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    tools::JsonWriter aJsonWriter3;
    pModelObj->getRowColumnHeaders(tools::Rectangle(5400, 5400, 25050, 9800), aJsonWriter3);
    aJsonWriter3.finishAndGetAsOString();
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(Point(49725, 0), Size(25500, 19380)), aView1.m_aInvalidations[0]);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testJumpHorizontallyInvalidation)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD2);
    Scheduler::ProcessEventsToIdle();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD2);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(26775, 0, 39525, 13005), aView1.m_aInvalidations[0]);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testJumpToLastRowInvalidation)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    KitHelper::setView(nView1);
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    // 261375 because we limit how far we jump into empty space in online, 267386880 if we don't limit
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(0, 13005, 26775, 261375), aView1.m_aInvalidations[0]);
}

// We need to ensure that views are not perturbed by rendering (!?) hmm ...
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testRowColumnHeaders)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view #1
    ScTestViewCallback aView1;
    int nView1 = KitHelper::getCurrentView();
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    ScTestViewCallback aView2;
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    // ViewRowColumnHeaders test
    KitHelper::setView(nView1);
    tools::JsonWriter aJsonWriter1;
    pModelObj->getRowColumnHeaders(tools::Rectangle(65,723,10410,4695), aJsonWriter1);
    OString aHeaders1 = aJsonWriter1.finishAndGetAsOString();

    KitHelper::setView(nView2);
    // 50% zoom
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 22474, 47333));
    pModelObj->setClientZoom(256, 256, 6636, 6636);
    tools::JsonWriter aJsonWriter2;
    pModelObj->getRowColumnHeaders(tools::Rectangle(65,723,10410,4695), aJsonWriter2);
    OString aHeaders2 = aJsonWriter2.finishAndGetAsOString();

    // Check vs. view #1
    KitHelper::setView(nView1);
    tools::JsonWriter aJsonWriter3;
    pModelObj->getRowColumnHeaders(tools::Rectangle(65,723,10410,4695), aJsonWriter3);
    OString aHeaders1_2 = aJsonWriter3.finishAndGetAsOString();
    CPPUNIT_ASSERT_EQUAL(aHeaders1, aHeaders1_2);

    // Check vs. view #2
    KitHelper::setView(nView2);
    tools::JsonWriter aJsonWriter4;
    pModelObj->getRowColumnHeaders(tools::Rectangle(65,723,10410,4695), aJsonWriter4);
    OString aHeaders2_2 = aJsonWriter4.finishAndGetAsOString();
    CPPUNIT_ASSERT_EQUAL(aHeaders2, aHeaders2_2);

    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView2);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

namespace
{
// Helper structs for setup and testing of ScModelObj::getSheetGeometryData()
struct SpanEntry
{
    size_t nVal;
    SCCOLROW nEnd;
};

struct SheetDimData
{
    typedef std::vector<SpanEntry> SpanList;
    SpanList aSizes;
    SpanList aHidden;
    SpanList aFiltered;
    // TODO: Add group info too to test.

    void setDataToDoc(ScDocument* pDoc, bool bCol) const
    {
        SCCOLROW nStart = 0;
        // sizes
        for (const auto& rSpan : aSizes)
        {
            if (bCol)
            {
                for (SCCOLROW nIdx = nStart; nIdx <= rSpan.nEnd; ++nIdx)
                    pDoc->SetColWidthOnly(nIdx, 0, rSpan.nVal);
            }
            else
                pDoc->SetRowHeightOnly(nStart, rSpan.nEnd, 0, rSpan.nVal);

            nStart = rSpan.nEnd + 1;
        }

        nStart = 0;
        // hidden
        for (const auto& rSpan : aHidden)
        {
            if (bCol)
                pDoc->SetColHidden(nStart, rSpan.nEnd, 0, !!rSpan.nVal);
            else
                pDoc->SetRowHidden(nStart, rSpan.nEnd, 0, !!rSpan.nVal);

            nStart = rSpan.nEnd + 1;
        }

        // There is no ScDocument interface to set ScTable::mpFilteredCols
        // It seems ScTable::mpFilteredCols is not really used !?
        if (bCol)
            return;

        nStart = 0;
        // filtered
        for (const auto& rSpan : aFiltered)
        {
            pDoc->SetRowFiltered(nStart, rSpan.nEnd, 0, !!rSpan.nVal);
            nStart = rSpan.nEnd + 1;
        }
    }

    void testPropertyTree(const boost::property_tree::ptree& rTree, bool bCol) const
    {
        struct SpanListWithKey
        {
            OString aKey;
            const SpanList& rSpanList;
        };

        const SpanListWithKey aPairList[] = {
            { "sizes"_ostr,    aSizes    },
            { "hidden"_ostr,   aHidden   },
            { "filtered"_ostr, aFiltered }
        };

        for (const auto& rEntry : aPairList)
        {
            // There is no ScDocument interface to set ScTable::mpFilteredCols
            // It seems ScTable::mpFilteredCols is not really used !?
            if (bCol && rEntry.aKey == "filtered")
                continue;

            bool bBooleanValue = rEntry.aKey != "sizes";
            OStringBuffer aExpectedEncoding;
            bool bFirst = true;
            for (const auto& rSpan : rEntry.rSpanList)
            {
                size_t nVal = rSpan.nVal;
                if (bBooleanValue && bFirst)
                    nVal = static_cast<size_t>(!!nVal);
                if (!bBooleanValue || bFirst)
                    aExpectedEncoding.append(OString::number(nVal) + ":");
                aExpectedEncoding.append(OString::number(rSpan.nEnd) + " ");
                bFirst = false;
            }

            // Get the tree's value for the property key ("sizes"/"hidden"/"filtered").
            OString aTreeValue(rTree.get<std::string>(rEntry.aKey.getStr()));

            CPPUNIT_ASSERT_EQUAL(aExpectedEncoding.toString(), aTreeValue);
        }
    }
};

class SheetGeometryData
{
    SheetDimData aCols;
    SheetDimData aRows;

public:

    SheetGeometryData(const SheetDimData& rCols, const SheetDimData& rRows) :
        aCols(rCols), aRows(rRows)
    {}

    void setDataToDoc(ScDocument* pDoc) const
    {
        aCols.setDataToDoc(pDoc, true);
        aRows.setDataToDoc(pDoc, false);
    }

    void parseTest(const OString& rJSON) const
    {
        // Assumes all flags passed to getSheetGeometryData() are true.
        boost::property_tree::ptree aTree;
        std::stringstream aStream((std::string(rJSON)));
        boost::property_tree::read_json(aStream, aTree);

        CPPUNIT_ASSERT_EQUAL(".uno:SheetGeometryData"_ostr, OString(aTree.get<std::string>("commandName")));

        aCols.testPropertyTree(aTree.get_child("columns"), true);
        aRows.testPropertyTree(aTree.get_child("rows"), false);
    }
};
} //namespace

// getSheetGeometryData() should return the exact same message
// irrespective of client zoom and view-area. Switching views
// should also not alter it.
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSheetGeometryDataInvariance)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScDocument* pDoc = pModelObj->GetDocument();
    const SheetGeometryData aSGData(
        // cols
        {
            // width spans
            {
                { STD_COL_WIDTH,   20     },
                { 2*STD_COL_WIDTH, 26     },
                { STD_COL_WIDTH,   pDoc->MaxCol() }
            },

            // hidden spans
            {
                { 0, 5      },
                { 1, 12     },
                { 0, pDoc->MaxCol() }
            },

            // filtered spans
            {
                { 0, 50     },
                { 1, 59     },
                { 0, pDoc->MaxCol() }
            }
        },

        // rows
        {
            // height spans
            {
                { 300,  50     },
                { 600,  65     },
                { 300,  pDoc->MaxRow() }
            },

            // hidden spans
            {
                { 1, 100    },
                { 0, 500    },
                { 1, 578    },
                { 0, pDoc->MaxRow() }
            },

            // filtered spans
            {
                { 0, 150    },
                { 1, 159    },
                { 0, pDoc->MaxRow() }
            }
        }
    );

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view #1
    ScTestViewCallback aView1;
    int nView1 = KitHelper::getCurrentView();

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    ScTestViewCallback aView2;
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    // Try with the default empty document once (nIdx = 0) and then with sheet geometry settings (nIdx = 1)
    for (size_t nIdx = 0; nIdx < 2; ++nIdx)
    {
        if (nIdx)
            aSGData.setDataToDoc(pDoc);

        KitHelper::setView(nView1);
        OString aGeomStr1 = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
                /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);

        KitHelper::setView(nView2);
        pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 22474, 47333));
        pModelObj->setClientZoom(256, 256, 6636, 6636);
        OString aGeomStr2 = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
                /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);

        // Check vs. view #1
        KitHelper::setView(nView1);
        OString aGeomStr1_2 = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
                /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);
        CPPUNIT_ASSERT_EQUAL(aGeomStr1, aGeomStr1_2);

        // Check vs. view #2
        KitHelper::setView(nView2);
        OString aGeomStr2_2 = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
                /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);
        CPPUNIT_ASSERT_EQUAL(aGeomStr2, aGeomStr2_2);
    }

    KitHelper::setView(nView1);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
    KitHelper::setView(nView2);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSheetGeometryDataCorrectness)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScDocument* pDoc = pModelObj->GetDocument();
    const SheetGeometryData aDefaultSGData(
        // cols
        {
            // width spans
            { { STD_COL_WIDTH, pDoc->MaxCol() } },
            // hidden spans
            { { 0,             pDoc->MaxCol() } },
            // filtered spans
            { { 0,             pDoc->MaxCol() } }
        },
        // rows
        {
            // height spans
            { { ScGlobal::nStdRowHeight, pDoc->MaxRow() } },
            // hidden spans
            { { 0,                       pDoc->MaxRow() } },
            // filtered spans
            { { 0,                       pDoc->MaxRow() } }
        }
    );

    const SheetGeometryData aSGData(
        // cols
        {
            // width spans
            {
                { STD_COL_WIDTH,   20     },
                { 2*STD_COL_WIDTH, 26     },
                { STD_COL_WIDTH,   pDoc->MaxCol() }
            },

            // hidden spans
            {
                { 0, 5      },
                { 1, 12     },
                { 0, pDoc->MaxCol() }
            },

            // filtered spans
            {
                { 0, 50     },
                { 1, 59     },
                { 0, pDoc->MaxCol() }
            }
        },

        // rows
        {
            // height spans
            {
                { 300,  50     },
                { 600,  65     },
                { 300,  pDoc->MaxRow() }
            },

            // hidden spans
            {
                { 1, 100    },
                { 0, 500    },
                { 1, 578    },
                { 0, pDoc->MaxRow() }
            },

            // filtered spans
            {
                { 0, 150    },
                { 1, 159    },
                { 0, pDoc->MaxRow() }
            }
        }
    );

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    // view #1
    ScTestViewCallback aView1;

    // with the default empty sheet and test the JSON encoding.
    OString aGeomDefaultStr = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
            /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);
    aDefaultSGData.parseTest(aGeomDefaultStr);

    // Apply geometry settings to the sheet and then test the resulting JSON encoding.
    aSGData.setDataToDoc(pDoc);
    OString aGeomStr = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
            /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);
    aSGData.parseTest(aGeomStr);

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testDeleteCellMultilineContent)
{
    ScModelObj* pModelObj = createDoc("multiline.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);

    // view #1
    ScTestViewCallback aView1;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    aView1.m_sInvalidateHeader = ""_ostr;
    ScDocument& rDoc = pDocSh->GetDocument();
    sal_uInt16 nRow1Height = rDoc.GetRowHeight(static_cast<SCROW>(0), static_cast<SCTAB>(0), false);

    // delete multiline cell content in view #1
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DELETE);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DELETE);
    Scheduler::ProcessEventsToIdle();

    // check if the row header has been invalidated and if the involved row is of the expected height
    CPPUNIT_ASSERT_EQUAL("row"_ostr, aView1.m_sInvalidateHeader);
    sal_uInt16 nRow2Height = rDoc.GetRowHeight(static_cast<SCROW>(0), static_cast<SCTAB>(0), false);
    CPPUNIT_ASSERT_EQUAL(nRow1Height, nRow2Height);
    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testPasteIntoWrapTextCell)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);

    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();

    // Set Wrap text in A3
    pDoc->ApplyAttr(0, 2, 0, ScLineBreakCell(true));
    const ScLineBreakCell& rItem = pDoc->GetAttr(0, 2, 0, ATTR_LINEBREAK);
    CPPUNIT_ASSERT(rItem.GetValue());

    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);

    ScTestViewCallback aView;
    CPPUNIT_ASSERT(!lcl_hasEditView(*pViewData));

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    // create source text in A1
    OUString sCopyContent(u"Very long text to copy"_ustr);
    pDoc->SetString(0, 0, 0, sCopyContent);

    // copy A1
    pView->SetCursor(0, 0);
    Scheduler::ProcessEventsToIdle();
    pView->GetViewFrame().GetBindings().Execute(SID_COPY);
    Scheduler::ProcessEventsToIdle();

    // verify clipboard
    uno::Reference<datatransfer::clipboard::XClipboard> xClipboard1 = pView->GetViewData().GetActiveWin()->GetClipboard();
    uno::Reference< datatransfer::XTransferable > xDataObj =
        xClipboard1->getContents();
    datatransfer::DataFlavor aFlavor;
    SotExchange::GetFormatDataFlavor(SotClipboardFormatId::STRING, aFlavor);
    cpo::uno::Any aData = xDataObj->getTransferData(aFlavor);
    OUString aTmpText;
    aData >>= aTmpText;
    CPPUNIT_ASSERT_EQUAL(sCopyContent, aTmpText.trim());

    // Go to A2 and paste.
    pView->SetCursor(0, 1);
    Scheduler::ProcessEventsToIdle();
    aView.m_sInvalidateSheetGeometry = ""_ostr;
    pView->GetViewFrame().GetBindings().Execute(SID_PASTE);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT_EQUAL(sCopyContent, pDoc->GetString(0, 1, 0));
    CPPUNIT_ASSERT_EQUAL("rows sizes"_ostr, aView.m_sInvalidateSheetGeometry);

    // create new source text in A2
    OUString sCopyContent2(u"Very long text to copy 2"_ustr);
    pDoc->SetString(0, 1, 0, sCopyContent2);
    Scheduler::ProcessEventsToIdle();

    // cut from A2
    pView->GetViewFrame().GetBindings().Execute(SID_CUT);
    Scheduler::ProcessEventsToIdle();

    // verify clipboard
    uno::Reference<datatransfer::clipboard::XClipboard> xClipboard2
        = pView->GetViewData().GetActiveWin()->GetClipboard();
    xDataObj = xClipboard2->getContents();
    SotExchange::GetFormatDataFlavor(SotClipboardFormatId::STRING, aFlavor);
    aData = xDataObj->getTransferData(aFlavor);
    aData >>= aTmpText;
    CPPUNIT_ASSERT_EQUAL(xClipboard1, xClipboard2);
    CPPUNIT_ASSERT_EQUAL(sCopyContent2, aTmpText.trim());

    // Go to A3 and paste.
    pView->SetCursor(0, 2);
    Scheduler::ProcessEventsToIdle();
    aView.m_sInvalidateSheetGeometry = ""_ostr;
    pView->GetViewFrame().GetBindings().Execute(SID_PASTE);
    Scheduler::ProcessEventsToIdle();

    // SG invalidations for all
    CPPUNIT_ASSERT_EQUAL(sCopyContent2, pDoc->GetString(0, 1, 0));
    CPPUNIT_ASSERT_EQUAL("all"_ostr, aView.m_sInvalidateSheetGeometry);

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSortAscendingDescending)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("sort-range.ods");
    ScDocument* pDoc = pModelObj->GetDocument();

    ScTestViewCallback aView;

    // select the values in the first column
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, 551, 129, 1, MOUSE_LEFT, 0);
    pModelObj->postMouseEvent(COKitMouseEventType::MOVE, 820, 1336, 1, MOUSE_LEFT, 0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, 820, 1359, 1, MOUSE_LEFT, 0);
    Scheduler::ProcessEventsToIdle();
    aView.m_sInvalidateSheetGeometry = ""_ostr;

    // sort ascending
    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    dispatchCommand(mxComponent, u".uno:SortAscending"_ustr, aArgs);

    // check it's sorted
    for (SCROW r = 0; r < 6; ++r)
    {
        CPPUNIT_ASSERT_EQUAL(double(r + 1), pDoc->GetValue(ScAddress(0, r, 0)));
    }

    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL("rows"_ostr, aView.m_sInvalidateSheetGeometry);

    aView.m_sInvalidateSheetGeometry = ""_ostr;
    // sort descending
    dispatchCommand(mxComponent, u".uno:SortDescending"_ustr, aArgs);

    // check it's sorted
    for (SCROW r = 0; r < 6; ++r)
    {
        CPPUNIT_ASSERT_EQUAL(double(6 - r), pDoc->GetValue(ScAddress(0, r, 0)));
    }

    // nothing else was sorted
    CPPUNIT_ASSERT_EQUAL(double(1), pDoc->GetValue(ScAddress(1, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(double(3), pDoc->GetValue(ScAddress(1, 1, 0)));
    CPPUNIT_ASSERT_EQUAL(double(2), pDoc->GetValue(ScAddress(1, 2, 0)));

    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL("rows"_ostr, aView.m_sInvalidateSheetGeometry);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testAutoInputStringBlock)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);
    ScDocument* pDoc = pModelObj->GetDocument();

    pDoc->SetString(ScAddress(0, 3, 0), u"ABC"_ustr);  // A4
    pDoc->SetString(ScAddress(0, 4, 0), u"BAC"_ustr);  // A5
    ScFieldEditEngine& rEE = pDoc->GetEditEngine();
    rEE.SetText(u"XYZ"_ustr);
    pDoc->SetEditText(ScAddress(0, 5, 0), rEE.CreateTextObject()); // A6
    pDoc->SetValue(ScAddress(0, 6, 0), 123);
    pDoc->SetString(ScAddress(0, 7, 0), u"ZZZ"_ustr);  // A8

    ScAddress aA1(0, 0, 0);
    typeCharsInCell("X", aA1.Col(), aA1.Row(), pView, pModelObj); // Type 'X' in A1
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A1 should autocomplete", u"XYZ"_ustr, pDoc->GetString(aA1));

    ScAddress aA3(0, 2, 0); // Adjacent to the string "superblock" A4:A8
    typeCharsInCell("X", aA3.Col(), aA3.Row(), pView, pModelObj); // Type 'X' in A3
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A3 should autocomplete", u"XYZ"_ustr, pDoc->GetString(aA3));

    ScAddress aA9(0, 8, 0); // Adjacent to the string "superblock" A4:A8
    typeCharsInCell("X", aA9.Col(), aA9.Row(), pView, pModelObj); // Type 'X' in A9
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A9 should autocomplete", u"XYZ"_ustr, pDoc->GetString(aA9));

    ScAddress aA11(0, 10, 0);
    typeCharsInCell("X", aA11.Col(), aA11.Row(), pView, pModelObj); // Type 'X' in A11
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A11 should autocomplete", u"XYZ"_ustr, pDoc->GetString(aA11));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testAutoInputExactMatch)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);
    ScDocument* pDoc = pModelObj->GetDocument();

    pDoc->SetString(ScAddress(0, 1, 0), u"Simple"_ustr);  // A2
    pDoc->SetString(ScAddress(0, 2, 0), u"Simple"_ustr);  // A3
    pDoc->SetString(ScAddress(0, 3, 0), u"Sing"_ustr);  // A4
    ScFieldEditEngine& rEE = pDoc->GetEditEngine();
    rEE.SetText(u"Case"_ustr);
    pDoc->SetEditText(ScAddress(0, 4, 0), rEE.CreateTextObject()); // A5
    pDoc->SetString(ScAddress(0, 5, 0), u"Time"_ustr);  // A6
    pDoc->SetString(ScAddress(0, 6, 0), u"Castle"_ustr);  // A7

    ScAddress aA8(0, 7, 0);
    typeCharsInCell("S", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "S" in A8
    // Should show the partial completion "i".
    CPPUNIT_ASSERT_EQUAL_MESSAGE("1: A8 should have partial completion Si", u"Si"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("Si", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "Si" in A8
    // Should not show any suggestions.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("2: A8 should not show suggestions", u"Si"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("Sim", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "Sim" in A8
    // Should autocomplete to "Simple" which is the only match.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("3: A8 should autocomplete", u"Simple"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("Sin", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "Sin" in A8
    // Should autocomplete to "Sing" which is the only match.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("4: A8 should autocomplete", u"Sing"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("C", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "C" in A8
    // Should show the partial completion "as".
    CPPUNIT_ASSERT_EQUAL_MESSAGE("5: A8 should have partial completion Cas", u"Cas"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("Cast", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "Cast" in A8
    // Should autocomplete to "Castle" which is the only match.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("6: A8 should autocomplete", u"Castle"_ustr, pDoc->GetString(aA8));

    typeCharsInCell("T", aA8.Col(), aA8.Row(), pView, pModelObj); // Type "T" in A8
    // Should autocomplete to "Time" which is the only match.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("7: A8 should autocomplete", u"Time"_ustr, pDoc->GetString(aA8));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testEditCursorBounds)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScDocument* pDoc = pModelObj->GetDocument();

    ScTestViewCallback aView;
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);
    comphelper::COKit::setViewIdForVisCursorInvalidation(true);

    // ~170% zoom.
    pModelObj->setClientZoom(256, 256, 2222, 2222);
    pModelObj->setClientVisibleArea(tools::Rectangle(7725, 379832, 16240, 6449));
    Scheduler::ProcessEventsToIdle();

    constexpr SCCOL nCol = 5;
    constexpr SCROW nRow = 2048;
    pDoc->SetValue(ScAddress(nCol, nRow, 0), 123);

    aView.m_bOwnCursorInvalidated = false;
    // Obtain the cell bounds via cursor.
    pView->SetCursor(nCol, nRow);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(aView.m_bOwnCursorInvalidated);
    CPPUNIT_ASSERT(!aView.m_aCellCursorBounds.IsEmpty());
    tools::Rectangle aCellBounds(aView.m_aCellCursorBounds);

    aView.m_aInvalidateCursorResult.clear();
    // Enter edit mode in the same cell.
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView.m_aInvalidateCursorResult.empty());
    CPPUNIT_ASSERT_MESSAGE("Edit cursor must be in cell bounds!",
        aCellBounds.Contains(aView.m_aInvalidateCursorResult.getBounds()));

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTextSelectionBounds)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScDocument* pDoc = pModelObj->GetDocument();

    ScTestViewCallback aView;
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);
    comphelper::COKit::setViewIdForVisCursorInvalidation(true);

    // ~170% zoom.
    pModelObj->setClientZoom(256, 256, 2222, 2222);
    pModelObj->setClientVisibleArea(tools::Rectangle(7725, 379832, 16240, 6449));
    Scheduler::ProcessEventsToIdle();

    constexpr SCCOL nCol = 5;
    constexpr SCROW nRow = 2048;
    pDoc->SetValue(ScAddress(nCol, nRow, 0), 123);

    aView.m_bOwnCursorInvalidated = false;
    // Obtain the cell bounds via cursor.
    pView->SetCursor(nCol, nRow);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(aView.m_bOwnCursorInvalidated);
    CPPUNIT_ASSERT(!aView.m_aCellCursorBounds.IsEmpty());
    tools::Rectangle aCellBounds(aView.m_aCellCursorBounds);

    aView.m_aTextSelectionResult.clear();
    // Enter edit mode in the same cell and select all text.
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);
    Scheduler::ProcessEventsToIdle();

    // CTRL + A
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_MOD1 | awt::Key::A);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_MOD1 | awt::Key::A);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView.m_aTextSelectionResult.empty());
    CPPUNIT_ASSERT_MESSAGE("Text selections must be in cell bounds!",
        !aCellBounds.Intersection(aView.m_aTextSelectionResult.getBounds(0)).IsEmpty());

    SfxViewShell::Current()->setCOKitViewCallback(nullptr);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testSheetViewDataCrash)
{
    ScModelObj* pModelObj = createDoc("empty.ods");

    // view #1
    int nView1 = KitHelper::getCurrentView();
    KitHelper::setView(nView1);

    // Imitate online while creating a new sheet on empty.ods.
    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({
            { "Name",  cpo::uno::Any(u"NewSheet"_ustr) },
            { "Index", cpo::uno::Any(sal_Int32(2)) }
        }));
    dispatchCommand(mxComponent, u".uno:Insert"_ustr, aArgs);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();
    ScTabViewShell* pView1 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView1);

    // view #2
    KitHelper::createView();
    ScTabViewShell* pView2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView2);
    Scheduler::ProcessEventsToIdle();

    KitHelper::setView(nView1);
    // Delete a range.
    pView1->SetCursor(1, 1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN | KEY_SHIFT);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN | KEY_SHIFT);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DELETE);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DELETE);
    // It will crash at this point without the fix.
    Scheduler::ProcessEventsToIdle();
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testTextBoxInsert)
{
    createDoc("empty.ods");
    ScTestViewCallback aView1;

    // insert textbox
    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({
            { "CreateDirectly",  cpo::uno::Any(true) }
        }));
    dispatchCommand(mxComponent, u".uno:DrawText"_ustr, aArgs);

    // check if we have textbox selected
    CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
    CPPUNIT_ASSERT(aView1.m_ShapeSelection != "EMPTY");

    Scheduler::ProcessEventsToIdle();
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCommentCellCopyPaste)
{
    // FIXME: Hack because previous tests do not destroy ScDocument(with annotations) on exit (?).
    ScPostIt::mnLastPostItId = 1;

    {
        ScModelObj* pModelObj = createDoc("empty.ods");
        ScTestViewCallback aView;
        int nView = KitHelper::getCurrentView();

        KitHelper::setView(nView);

        ScTabViewShell* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
        CPPUNIT_ASSERT(pTabViewShell);

        typeCharsInCell("ABC", 0, 0, pTabViewShell, pModelObj); // Type "ABC" in A1

        pTabViewShell->SetCursor(1, 1);

        // Add a new comment
        cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"COKit Comment Cell B2"_ustr)},
            {"Author", cpo::uno::Any(u"COKit Client"_ustr)},
        }));
        dispatchCommand(mxComponent, u".uno:InsertAnnotation"_ustr, aArgs);

        // We received a COKitCallbackType::COMMENT callback with comment 'Add' action
        CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("1"), aView.m_aCommentCallbackResult.get<std::string>("id"));
        CPPUNIT_ASSERT_EQUAL(std::string("0"), aView.m_aCommentCallbackResult.get<std::string>("tab"));
        CPPUNIT_ASSERT_EQUAL(std::string("COKit Client"), aView.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("COKit Comment Cell B2"), aView.m_aCommentCallbackResult.get<std::string>("text"));

        cpo::uno::Sequence<beans::PropertyValue> aCopyPasteArgs;

        // We need separate tests for single cell copy-paste and cell-range copy-paste
        // since they hit different code paths in ScColumn methods.

        // Single cell(with comment) copy paste test
        {
            dispatchCommand(mxComponent, u".uno:Copy"_ustr, aCopyPasteArgs);

            pTabViewShell->SetCursor(1, 49);
            Scheduler::ProcessEventsToIdle();
            dispatchCommand(mxComponent, u".uno:Paste"_ustr, aCopyPasteArgs); // Paste to cell B50

            // We received a COKitCallbackType::COMMENT callback with comment 'Add' action
            CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView.m_aCommentCallbackResult.get<std::string>("action"));
            // Without the fix the id will be "1".
            CPPUNIT_ASSERT_EQUAL(std::string("2"), aView.m_aCommentCallbackResult.get<std::string>("id"));
            CPPUNIT_ASSERT_EQUAL(std::string("0"), aView.m_aCommentCallbackResult.get<std::string>("tab"));
            CPPUNIT_ASSERT_EQUAL(std::string("COKit Client"), aView.m_aCommentCallbackResult.get<std::string>("author"));
            CPPUNIT_ASSERT_EQUAL(std::string("COKit Comment Cell B2"), aView.m_aCommentCallbackResult.get<std::string>("text"));
        }

        // Cell range (with a comment) copy paste test
        {
            // Select range A1:C3
            pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_HOME | KEY_MOD1);
            pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_HOME | KEY_MOD1);
            pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_RIGHT | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_RIGHT | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_RIGHT | KEY_SHIFT);
            pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_RIGHT | KEY_SHIFT);
            Scheduler::ProcessEventsToIdle();

            dispatchCommand(mxComponent, u".uno:Copy"_ustr, aCopyPasteArgs);

            pTabViewShell->SetCursor(3, 49);
            Scheduler::ProcessEventsToIdle();
            dispatchCommand(mxComponent, u".uno:Paste"_ustr, aCopyPasteArgs); // Paste to cell D50

            // We received a COKitCallbackType::COMMENT callback with comment 'Add' action
            CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView.m_aCommentCallbackResult.get<std::string>("action"));
            // Without the fix the id will be "1".
            CPPUNIT_ASSERT_EQUAL(std::string("3"), aView.m_aCommentCallbackResult.get<std::string>("id"));
            CPPUNIT_ASSERT_EQUAL(std::string("0"), aView.m_aCommentCallbackResult.get<std::string>("tab"));
            CPPUNIT_ASSERT_EQUAL(std::string("COKit Client"), aView.m_aCommentCallbackResult.get<std::string>("author"));
            CPPUNIT_ASSERT_EQUAL(std::string("COKit Comment Cell B2"), aView.m_aCommentCallbackResult.get<std::string>("text"));
        }
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidEntrySave)
{
    loadFromFile(u"validity.xlsx");

    // .uno:Save modifies the original file, make a copy first
    saveAndReload(TestFilter::XLSX);
    ScModelObj* pModelObj = comphelper::getFromUnoTunnel<ScModelObj>(mxComponent);
    CPPUNIT_ASSERT(pModelObj);
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    const ScDocument* pDoc = pModelObj->GetDocument();
    ScTestViewCallback aView;
    int nView = KitHelper::getCurrentView();

    KitHelper::setView(nView);

    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    ScTabViewShell* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pTabViewShell);

    // Type partial date "7/8" of "7/8/2013" that
    // the validation cell at A8 can accept
    typeCharsInCell("7/8", 0, 7, pTabViewShell, pModelObj,
        false /* bInEdit */, false /* bCommit */); // Type "7/8" in A8

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    dispatchCommand(mxComponent, u".uno:Save"_ustr, aArgs);

    CPPUNIT_ASSERT_MESSAGE("Should not be marked modified after save", !pDocSh->IsModified());

    // Complete the date in A8 by appending "/2013" and commit.
    typeCharsInCell("/2013", 0, 7, pTabViewShell, pModelObj,
        true /* bInEdit */, true /* bCommit */);

    // This would hang if the date entered "7/8/2013" is not acceptable.
    Scheduler::ProcessEventsToIdle();

    // Ensure that the correct date is recorded in the document.
    CPPUNIT_ASSERT_EQUAL(double(41463), pDoc->GetValue(ScAddress(0, 7, 0)));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoReordering)
{
    ScModelObj* pModelObj = createDoc("small.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    ScUndoManager* pUndoManager = pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);

    // view #1
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();

    // check that undo action count is not 0
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // text edit a different cell in view #2
    KitHelper::setView(nView2);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN);
    Scheduler::ProcessEventsToIdle();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_DOWN);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();

    // check that undo action count is not 1
    CPPUNIT_ASSERT_EQUAL(std::size_t(2), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #1
    KitHelper::setView(nView1);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // check that undo has been executed on view #1
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // try to execute undo in view #2
    KitHelper::setView(nView2);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // check that undo has been executed on view #2
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetUndoActionCount());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoReorderingRedo)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    ScUndoManager* pUndoManager = pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetUndoActionCount());

    // view #1
    int nView1 = KitHelper::getCurrentView();
    SfxViewShell* pView1 = SfxViewShell::Current();
    ScTestViewCallback aView1;

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    SfxViewShell* pView2 = SfxViewShell::Current();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // text edit another cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'y', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'y', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'y', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'y', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(2), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u"xx"_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u"yy"_ustr, pDoc->GetString(ScAddress(0, 1, 0)));

    // text edit a different cell in view #2
    KitHelper::setView(nView2);
    ScTabViewShell* pViewShell2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    pViewShell2->SetCursor(0, 2);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(3), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u"xx"_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u"yy"_ustr, pDoc->GetString(ScAddress(0, 1, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));

    // View 1 presses undo, and the second cell is erased
    KitHelper::setView(nView1);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(std::size_t(2), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u"xx"_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u""_ustr, pDoc->GetString(ScAddress(0, 1, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));

    // Verify that the UNDO buttons/actions are still enabled
    {
        SfxItemSet aSet1(pView1->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        SfxItemSet aSet2(pView2->GetPool(), svl::Items<SID_UNDO, SID_UNDO>);
        pView1->GetSlotState(SID_UNDO, nullptr, &aSet1);
        pView2->GetSlotState(SID_UNDO, nullptr, &aSet2);
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet1.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxStringItem* >(aSet1.GetItem(SID_UNDO)));
        CPPUNIT_ASSERT_EQUAL(SfxItemState::SET, aSet2.GetItemState(SID_UNDO));
        CPPUNIT_ASSERT(dynamic_cast< const SfxStringItem* >(aSet2.GetItem(SID_UNDO)));
    }

    // View 1 presses undo again, and the first cell is erased
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u""_ustr, pDoc->GetString(ScAddress(0, 1, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testUndoReorderingMulti)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    ScUndoManager* pUndoManager = pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoManager);
    CPPUNIT_ASSERT_EQUAL(std::size_t(0), pUndoManager->GetUndoActionCount());

    // view #1
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    // view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;

    // text edit a cell in view #1
    KitHelper::setView(nView1);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(1), pUndoManager->GetUndoActionCount());

    // text edit a different cell in view #2
    KitHelper::setView(nView2);
    ScTabViewShell* pView2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    pView2->SetCursor(0, 2);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'C', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(2), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u"xx"_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));

    // and another cell in view #2
    pView2->SetCursor(0, 3);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'D', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'D', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'D', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'D', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::RETURN);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::RETURN);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT_EQUAL(std::size_t(3), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u"xx"_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));
    CPPUNIT_ASSERT_EQUAL(u"DD"_ustr, pDoc->GetString(ScAddress(0, 3, 0)));

    // View 1 presses undo
    KitHelper::setView(nView1);
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(std::size_t(2), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(u""_ustr, pDoc->GetString(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(u"CC"_ustr, pDoc->GetString(ScAddress(0, 2, 0)));
    CPPUNIT_ASSERT_EQUAL(u"DD"_ustr, pDoc->GetString(ScAddress(0, 3, 0)));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testGetViewRenderState)
{
    // Add a pair of schemes, last added is the default
    svtools::EditableColorConfig aColorConfig;
    aColorConfig.AddScheme(u"Dark"_ustr);
    aColorConfig.AddScheme(u"Light"_ustr);

    ScModelObj* pModelObj = createDoc("empty.ods");
    int nFirstViewId = KitHelper::getCurrentView();
    ScTestViewCallback aView1;

    CPPUNIT_ASSERT_EQUAL("S;Default"_ostr, pModelObj->getViewRenderState());
    // Create a second view
    KitHelper::createView();
    ScTestViewCallback aView2;
    CPPUNIT_ASSERT_EQUAL("S;Default"_ostr, pModelObj->getViewRenderState());
    // Set second view to dark scheme
    {
        cpo::uno::Sequence<beans::PropertyValue> aPropertyValues = comphelper::InitPropertySequence(
            {
                { "NewTheme", cpo::uno::Any(u"Dark"_ustr) },
            }
        );
        dispatchCommand(mxComponent, u".uno:ChangeTheme"_ustr, aPropertyValues);
    }
    CPPUNIT_ASSERT_EQUAL("S;Dark"_ostr, pModelObj->getViewRenderState());

    // Switch back to first view and make sure it's the same
    KitHelper::setView(nFirstViewId);
    CPPUNIT_ASSERT_EQUAL("S;Default"_ostr, pModelObj->getViewRenderState());
}

/*
 * testInvalidateOnTextEditWithDifferentZoomLevels
 * steps:
 * set view 1 zoom to the passed zoom level
 * in view 1 type a char at the passed cell address
 * store invalidation rectangle
 * exit from in place editing (press esc)
 * create view 2 (keep 100% zoom)
 * go to the same cell address used in view 1
 * type a char into the cell
 * get invalidation rectangle for view 1
 * check if the invalidation rectangle is equal to the one stored previously
*/
class testInvalidateOnTextEditWithDifferentZoomLevels : public ScTiledRenderingTest
{
public:
    void TestBody(const ColRowZoom& rData);
    CPPUNIT_TEST_SUITE(testInvalidateOnTextEditWithDifferentZoomLevels);
    CPPUNIT_TEST_PARAMETERIZED(TestBody,
                               std::initializer_list<ColRowZoom>
                               {
                                   // zoom level 120%
                                   {0, 999, 1}, {99, 0, 1},
                                   // zoom level 40%
                                   {0, 999, -5}, {99, 0, -5}
                               });
    CPPUNIT_TEST_SUITE_END();
};
CPPUNIT_TEST_SUITE_REGISTRATION(testInvalidateOnTextEditWithDifferentZoomLevels);

void testInvalidateOnTextEditWithDifferentZoomLevels::TestBody(const ColRowZoom& rData)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);
    OUString sZoomUnoCmd = u".uno:ZoomPlus"_ustr;
    int nZoomLevel = rData.zoom;
    if (nZoomLevel < 0)
    {
        nZoomLevel = -nZoomLevel;
        sZoomUnoCmd = u".uno:ZoomMinus"_ustr;
    }
    // view #1
    ScTestViewCallback aView1;
    // set zoom level
    for (int i = 0; i < nZoomLevel; ++i)
        dispatchCommand(mxComponent, sZoomUnoCmd, {});
    Scheduler::ProcessEventsToIdle();
    auto* pTabViewShell1 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pTabViewShell1);
    // enable in place editing in view 1
    auto& rInvalidations = aView1.m_aInvalidations;
    pTabViewShell1->SetCursor(rData.col, rData.row);
    Scheduler::ProcessEventsToIdle();
    aView1.m_bInvalidateTiles = false;
    rInvalidations.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT(!rInvalidations.empty());
    tools::Rectangle aInvRect1 = rInvalidations[0];
    // end editing
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::ESCAPE);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::ESCAPE);
    Scheduler::ProcessEventsToIdle();
    // view #2
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTestViewCallback aView2;
    Scheduler::ProcessEventsToIdle();
    auto* pTabViewShell2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pTabViewShell2);
    pTabViewShell2->SetCursor(rData.col, rData.row);
    Scheduler::ProcessEventsToIdle();
    // text edit in view #2
    aView1.m_bInvalidateTiles = false;
    rInvalidations.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'x', 0);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'x', 0);
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(aView1.m_bInvalidateTiles);
    CPPUNIT_ASSERT(!rInvalidations.empty());
    tools::Rectangle aInvRect2 = rInvalidations[0];
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Invalidation rectangle is wrong.", aInvRect1, aInvRect2);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testForeignEditOutputAreaDifferentZoom)
{
    // When a foreign EditView fits exactly inside its cell (no text growth),
    // the OutputArea computed for rendering it into a painter at a different
    // zoom must equal the painter's pixel cell rect, regardless of the zoom
    // combination. With the prior code the editing view's zoom was multiplied
    // instead of divided, so the relation only held when zoomOther == 1.0,
    // which made in-progress edits invisible to other views unless the editor
    // happened to be at 100%.

    // 1000x200 twip cell:
    //   real mm100   : 1764 x 353
    //   px at 100%   :   67 x  13
    //   px at 150%   :  100 x  20
    //   px at  50%   :   33 x   7
    // px-as-mm100 (96 DPI fixed: 2540/96 ~= 26.458):
    //   100% : 67 * 26.458 ~= 1773
    //   150% : 100 * 26.458 ~= 2645
    //    50% : 33 * 26.458 ~=  873

    const tools::Rectangle aOrigOA(0, 0, 1764, 353);
    const Point aOriginAbsPx(0, 0);

    struct Case
    {
        const char* sLabel;
        double fThis, fOther;
        tools::Rectangle aEditPx;
        tools::Rectangle aOtherEdit;
    };
    const Case cases[] = {
        // painter 100%, editor 150% - the broken direction prior to the fix
        { "painter=1.0 editor=1.5", 1.0, 1.5,
          tools::Rectangle(0, 0,  67, 13), tools::Rectangle(0, 0, 2645, 529) },
        // painter 150%, editor 100% - worked even with the prior code
        { "painter=1.5 editor=1.0", 1.5, 1.0,
          tools::Rectangle(0, 0, 100, 20), tools::Rectangle(0, 0, 1773, 344) },
        // painter 100%, editor 50% - also broken prior to the fix
        { "painter=1.0 editor=0.5", 1.0, 0.5,
          tools::Rectangle(0, 0,  67, 13), tools::Rectangle(0, 0,  873, 172) },
    };

    for (const auto& c : cases)
    {
        const tools::Rectangle aResult = ScGridWindow::ComputeForeignEditOutputArea(
            aOrigOA, o3tl::Length::mm100, c.aOtherEdit, c.aEditPx, aOriginAbsPx,
            c.fThis, c.fThis, c.fOther, c.fOther);

        // Allow +/-2 px rounding slack from the 96 DPI px<->mm100 conversion.
        CPPUNIT_ASSERT_MESSAGE(c.sLabel, std::abs(aResult.Left()   - c.aEditPx.Left())   <= 2);
        CPPUNIT_ASSERT_MESSAGE(c.sLabel, std::abs(aResult.Top()    - c.aEditPx.Top())    <= 2);
        CPPUNIT_ASSERT_MESSAGE(c.sLabel, std::abs(aResult.Right()  - c.aEditPx.Right())  <= 2);
        CPPUNIT_ASSERT_MESSAGE(c.sLabel, std::abs(aResult.Bottom() - c.aEditPx.Bottom()) <= 2);
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testExtendEditInvalidateRectForText)
{
    // A foreign view sees another view's in-progress edit only through tiles, so
    // its invalidation must cover the whole text. The cell rectangle is widened
    // to the text width; text that fits the cell leaves it unchanged so a short
    // edit is not over-invalidated. Before the fix the invalidation stayed at the
    // cell and grown text was left stale in never-repainted tiles.
    const tools::Rectangle aCell(100, 200, 1316, 405); // 1217 x 206

    // Text wider than the cell: the rectangle grows rightwards to the text width,
    // keeping the cell's top, bottom and left.
    const tools::Rectangle aGrown = ScGridWindow::ExtendEditInvalidateRectForText(aCell, 4000);
    CPPUNIT_ASSERT_EQUAL(aCell.Left(), aGrown.Left());
    CPPUNIT_ASSERT_EQUAL(aCell.Top(), aGrown.Top());
    CPPUNIT_ASSERT_EQUAL(aCell.Bottom(), aGrown.Bottom());
    CPPUNIT_ASSERT(aGrown.GetWidth() >= 4000);

    // Text that fits inside the cell: the rectangle is left unchanged.
    CPPUNIT_ASSERT_EQUAL(aCell, ScGridWindow::ExtendEditInvalidateRectForText(aCell, 50));
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testOpenURL)
{
    // Given a document that has 2 views:
    createDoc("empty.ods");
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    KitHelper::createView();
    ScTestViewCallback aView2;

    // When clicking on a link in view 2, but switching to view 1 before processing async events:
    ScGlobal::OpenURL(/*aUrl=*/u"http://www.example.com/"_ustr, /*aTarget=*/u""_ustr,
                      /*bIgnoreSettings=*/true);
    KitHelper::setView(nView1);
    Scheduler::ProcessEventsToIdle();

    // Then make sure view 2 gets the callback, not view 1:
    // Without the accompanying fix in place, this test would have failed, view 1 got the hyperlink
    // callback.
    CPPUNIT_ASSERT(aView1.m_aHyperlinkClicked.isEmpty());
    CPPUNIT_ASSERT(!aView2.m_aHyperlinkClicked.isEmpty());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidateForSplitPanes)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);

    ScModelObj* pModelObj = createDoc("split.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    // view
    ScTestViewCallback aView;

    // move way over to the right where BP:20 exists, enough so that rows A and B
    // would scroll off the page and not be visible, if they were not frozen
    pModelObj->setClientVisibleArea(tools::Rectangle(73050, 0, 94019, 7034));
    Scheduler::ProcessEventsToIdle();

    ScAddress aBP20(67, 19, 0); // BP:20

    pView->SetCursor(aBP20.Col(), aBP20.Row());
    Scheduler::ProcessEventsToIdle();

    aView.m_bInvalidateTiles = false;
    aView.m_aInvalidations.clear();

    typeCharsInCell("X", aBP20.Col(), aBP20.Row(), pView, pModelObj); // Type 'X' in A1

    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);

    // missing before fix
    tools::Rectangle aTopLeftPane(0, 500, 3817, 742);
    bool bFoundTopLeftPane =
        std::find(aView.m_aInvalidations.begin(), aView.m_aInvalidations.end(), aTopLeftPane) != aView.m_aInvalidations.end();
    CPPUNIT_ASSERT_MESSAGE("The cell visible in the top left pane should be redrawn", bFoundTopLeftPane);

    // missing before fix
    tools::Rectangle aBottomLeftPane(0, 500, 3817, 3242);
    bool bFoundBottomLeftPane =
        std::find(aView.m_aInvalidations.begin(), aView.m_aInvalidations.end(), aBottomLeftPane) != aView.m_aInvalidations.end();
    CPPUNIT_ASSERT_MESSAGE("The cell visible in the bottom left pane should be redrawn", bFoundBottomLeftPane);
}

// Saving shouldn't trigger an invalidation
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testNoInvalidateOnSave)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);

    loadFromFile(u"invalidate-on-save.ods");

    // .uno:Save modifies the original file, make a copy first
    saveAndReload(TestFilter::ODS);
    ScModelObj* pModelObj = comphelper::getFromUnoTunnel<ScModelObj>(mxComponent);
    CPPUNIT_ASSERT(pModelObj);
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    Scheduler::ProcessEventsToIdle();

    // track invalidations
    ScTestViewCallback aView;

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    dispatchCommand(mxComponent, u".uno:Save"_ustr, aArgs);

    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView.m_bInvalidateTiles);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCellMinimalInvalidations)
{
    ScAddress aA8(0, 7, 0);
    ScAddress aD4(3, 7, 0);
    ScAddress aD13(3, 12, 0);
    ScAddress aD17(3, 16, 0);

    ScModelObj* pModelObj = createDoc("cell-invalidations.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    // Changed: Minimized invalidations (bFullRow: false)

    // Common case, LTR, default cell formatting
    cellInvalidationHelper(pModelObj, pView, aA8, true, false);
    cellInvalidationHelper(pModelObj, pView, aD4, true, false);
    // Left-aligned merged cells
    cellInvalidationHelper(pModelObj, pView, aD17, true, false);
    // Delete single cell text case
    cellInvalidationHelper(pModelObj, pView, aA8, false, false);
    // Paste into a single cell
    {
        pView->SetCursor(aD4.Col(), aD4.Row());
        cpo::uno::Sequence<beans::PropertyValue> aArgs;
        dispatchCommand(mxComponent, u".uno:Copy"_ustr, aArgs);
        pView->SetCursor(aA8.Col(), aA8.Row());
        Scheduler::ProcessEventsToIdle();

        ScTestViewCallback aView;
        dispatchCommand(mxComponent, u".uno:Paste"_ustr, aArgs);
        Scheduler::ProcessEventsToIdle();

        checkSampleInvalidation(aView, false);
    }

    // Unchanged: Non-minimized invalidations (bFullRow: true)

    // Centered merged cells;
    cellInvalidationHelper(pModelObj, pView, aD13, true, true);

    // switch to RTL sheet
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::PAGEDOWN | KEY_MOD1);
    Scheduler::ProcessEventsToIdle();

    cellInvalidationHelper(pModelObj, pView, aA8, true, true);
    cellInvalidationHelper(pModelObj, pView, aD4, true, true);
    // Delete Text
    cellInvalidationHelper(pModelObj, pView, aA8, false, true);
}

// That we don't end up with two views on different zooms that invalidate different
// rectangles, each should invalidate the same rectangle
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCellInvalidationDocWithExistingZoom)
{
    ScAddress aB7(1, 6, 0);
    ScopedVclPtrInstance<VirtualDevice> xDevice(DeviceFormat::WITHOUT_ALPHA);

    ScModelObj* pModelObj = createDoc("cell-invalidations-200zoom-settings.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    // Set View #1 to initial 100% and generate a paint
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 19845, 6405));
    pModelObj->setClientZoom(256, 256, 1536, 1536);
    pModelObj->paintTile(*xDevice, 3328, 512, 0, 0, 19968, 3072);

    Scheduler::ProcessEventsToIdle();

    int nView1 = KitHelper::getCurrentView();
    // register to track View #1 invalidations
    ScTestViewCallback aView1;

    // Create a View #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    // register to track View #1 invalidations
    ScTestViewCallback aView2;

    // Set View #2 to initial 100% and generate a paint
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 19845, 6405));
    pModelObj->setClientZoom(256, 256, 1536, 1536);
    pModelObj->paintTile(*xDevice, 3328, 512, 0, 0, 19968, 3072);

    // Set View #1 to 50% zoom and generate a paint
    KitHelper::setView(nView1);
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 41150, 13250));
    pModelObj->setClientZoom(256, 256, 3185, 3185);
    pModelObj->paintTile(*xDevice, 3328, 512, 0, 0, 41405, 6370);

    Scheduler::ProcessEventsToIdle();

    // Set View #2 to 200% zoom and generate a paint
    KitHelper::setView(nView2);
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 9574, 3090));
    pModelObj->setClientZoom(256, 256, 741, 741);
    pModelObj->paintTile(*xDevice, 3328, 512, 0, 0, 19968, 3072);

    Scheduler::ProcessEventsToIdle();
    aView1.m_bInvalidateTiles = false;
    aView1.m_aInvalidations.clear();
    aView2.m_bInvalidateTiles = false;
    aView2.m_aInvalidations.clear();

    ScTabViewShell* pView2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView2);
    pView2->SetCursor(aB7.Col(), aB7.Row());

    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DELETE);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DELETE);
    Scheduler::ProcessEventsToIdle();

    // The problem tested for here is with two views at different zooms then a
    // single cell invalidation resulted in the same rectangle reported as two
    // different invalidations rectangles of different scales. While we should
    // get the same invalidation rectangle reported.
    //
    // (B7 is a good choice to use in the real world to see the effect, to both
    // avoid getting the two rects combined into one bigger one, or to have the
    // two separated by so much space the 2nd is off-screen and not seen
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView1.m_aInvalidations.size());
    CPPUNIT_ASSERT_EQUAL(size_t(1), aView2.m_aInvalidations.size());

    // That they don't exactly match doesn't matter, we're not checking rounding issues,
    // what matters is that they are not utterly different rectangles
    // Without fix result is originally:
    // Comparing invalidation rectangles Width expected 6214742 actual 26716502 Tolerance 50
    CPPUNIT_ASSERT_RECTANGLE_EQUAL_WITH_TOLERANCE(aView2.m_aInvalidations[0],
                                                  aView1.m_aInvalidations[0],
                                                  50);
    // cool#4250: zoom values
    pModelObj->setExportZoom(150);

    {
        cpo::uno::ContextLayer aLayer(comphelper::NewFlagContext(u"IsKitExport"_ustr));
        save(TestFilter::ODS); // .ODS
    }
    xmlDocUniquePtr pSettingsXml = parseExport(u"settings.xml"_ustr);
    // Multi-user export: don't save every user's view into the exported file
    // assertXPath(pSettingsXml, "//config:config-item[@config:name='ViewId']", 1);
    // Use view's logic (not technical) zoom level for export
    assertXPathContent(pSettingsXml, "//config:config-item[@config:name='ZoomValue'][1]", u"150");

    save(TestFilter::XLSX); // .XLSX
    xmlDocUniquePtr pSheet1Xml = parseExport(u"xl/worksheets/sheet1.xml"_ustr);
    // Use view's logic (not technical) zoom level for export
    assertXPath(pSheet1Xml, "//x:sheetViews/x:sheetView", "zoomScale", u"150");
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInputHandlerSyncedZoom)
{
    ScModelObj* pModelObj = createDoc("cell-edit-300zoom-settings.ods");

    // Set View #1 to initial 150%
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 17933, 4853));
    // Before the fix, this zoom would leave the EditEngine reference device
    // at the zoom level stored in the document, so normal rendering and
    // editing rendering happened with different MapModes
    pModelObj->setClientZoom(256, 256, 1333, 1333);

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);
    pView->SetCursor(0, 4); // A5

    Scheduler::ProcessEventsToIdle();

    // Activate edit mode in that A5 cell
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);
    Scheduler::ProcessEventsToIdle();

    const ScViewData* pViewData1 = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData1);

    // Get that active EditView
    EditView* pEditView1 = pViewData1->GetEditView(SC_SPLIT_BOTTOMLEFT);
    CPPUNIT_ASSERT(pEditView1);
    EditEngine& rEditEngine1 = pEditView1->getEditEngine();
    // These must match, if they don't then text will have a different width in edit and view modes
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("EditEngine Ref Dev Zoom and ViewData Zoom should match",
                                 pViewData1->GetZoomX(), rEditEngine1.GetRefMapMode().GetScaleX(), 0.000001);
    CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE("EditEngine Ref Dev Zoom and ViewData Zoom should match",
                                 pViewData1->GetZoomY(), rEditEngine1.GetRefMapMode().GetScaleY(), 0.000001);

    // Create a View #2
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    // Set View #2 to the same zoom as View #1
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 17933, 4853));
    pModelObj->setClientZoom(256, 256, 1333, 1333);

    ScTabViewShell* pView2 = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView2);
    pView2->SetCursor(0, 5); // A6

    Scheduler::ProcessEventsToIdle();

    // Activate edit mode in that A6 cell
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);
    Scheduler::ProcessEventsToIdle();

    const ScViewData* pViewData2 = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData2);

    // Get the View #2 EditView
    EditView* pEditView2 = pViewData2->GetEditView(SC_SPLIT_BOTTOMLEFT);
    CPPUNIT_ASSERT(pEditView2);
    EditEngine& rEditEngine2 = pEditView2->getEditEngine();
    CPPUNIT_ASSERT(&rEditEngine1 != &rEditEngine2);
    // Before the fix, these had different settings, resulting in the text
    // dancing for the second user as they toggle in and out of edit mode, but
    // each user should have the same settings.
    CPPUNIT_ASSERT_EQUAL(rEditEngine1.GetControlWord(), rEditEngine2.GetControlWord());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testStatusBarLocale)
{
    // Given 2 views, the second's locale is set to German:
    createDoc("empty.ods");
    int nView1 = KitHelper::getCurrentView();
    ScTestViewCallback aView1;
    KitHelper::createView();
    ScTestViewCallback aView2;
    SfxViewShell* pView2 = SfxViewShell::Current();
    pView2->SetKitLocale(u"de-DE"_ustr);
    {
        SfxViewFrame& rFrame = pView2->GetViewFrame();
        SfxSlotPool& rSlotPool = SfxSlotPool::GetSlotPool(&rFrame);
        uno::Reference<util::XURLTransformer> xParser(util::URLTransformer::create(m_xContext));
        util::URL aCommandURL;
        aCommandURL.Complete = u".uno:RowColSelCount"_ustr;
        xParser->parseStrict(aCommandURL);
        const SfxSlot* pSlot = rSlotPool.GetUnoSlot(aCommandURL.Path);
        rFrame.GetBindings().GetDispatch(pSlot, aCommandURL, false);
    }
    aView2.m_aStateChanges.clear();

    // When creating a cell selection in the 2nd view and processing jobs with the 1st view set to
    // active:
    comphelper::dispatchCommand(u".uno:GoDownSel"_ustr, {});
    KitHelper::setView(nView1);
    pView2->GetViewFrame().GetBindings().GetTimer().Invoke();
    // Once more to hit the pImpl->bMsgDirty = false case in SfxBindings::NextJob_Impl().
    pView2->GetViewFrame().GetBindings().GetTimer().Invoke();

    // Then make sure that the locale is taken into account while producing the state changed
    // callback:
    auto it = aView2.m_aStateChanges.find(".uno:RowColSelCount");
    CPPUNIT_ASSERT(it != aView2.m_aStateChanges.end());
    std::string aLocale = it->second.get<std::string>("locale");
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: de-DE
    // - Actual  : en-US
    // i.e. the 2nd view got its callback with the locale of the first view, which is buggy.
    CPPUNIT_ASSERT_EQUAL(std::string("de-DE"), aLocale);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testLongFirstColumnMouseClick)
{
    // Document has a long first column. We want to mouse-click on the column and
    // check the selection changed to this column.

    // The issue we want to reproduce is that the click on a cell in the first column that is
    // very long (longer than ~800px default size of GridWindow) triggers a code-path where the cell
    // selected is the neighbouring cell even when we clicked on the area of the first cell.

    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);

    ScModelObj* pModelObj = createDoc("DocumentWithLongFirstColumn.ods");
    CPPUNIT_ASSERT(pModelObj);
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    // Fetch current view data
    ScViewData* pViewData = ScDocShell::GetViewData();
    CPPUNIT_ASSERT(pViewData);
    double nPPTX = pViewData->GetPPTX();
    double nPPTY = pViewData->GetPPTX();

    // Set click position

    // Left side of the first cell
    int leftCellSideX = 1 / nPPTX; // convert pixels to logical units

    // Right side of the first cell. First cell is long so click somewhere more than 800px (default of GridWindow size).
    int rightCellSideX = 1000 / nPPTX; // convert pixels to logical units

    // Vertical position - doesn't matter - select the first row
    int y = 1 / nPPTY;

    // Setup view #1
    ScTestViewCallback aView1;
    // Set client rect to 2000 x 2000 pixels
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 2000 / nPPTX, 2000 / nPPTY));
    Scheduler::ProcessEventsToIdle();

    // Click at on the left side of A1 cell
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, leftCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, leftCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    // Check the A1 cell is selected in view #1
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), ScDocShell::GetViewData()->GetCurX());
    CPPUNIT_ASSERT_EQUAL(SCROW(0), ScDocShell::GetViewData()->GetCurY());

    // Click at on the right side of A1 cell
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, rightCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, rightCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    // Check the A1 cell is selected in view #1
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), ScDocShell::GetViewData()->GetCurX());
    CPPUNIT_ASSERT_EQUAL(SCROW(0), ScDocShell::GetViewData()->GetCurY());

    // Try to check the same scenario in a new view

    // Setup view #2
    KitHelper::createView();
    int nView2 = KitHelper::getCurrentView();
    ScTestViewCallback aView2;
    // Set client rect to 2000 x 2000 pixels
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 2000 / nPPTX, 2000 / nPPTY));

    // Let's make sure we are in view #2
    KitHelper::setView(nView2);
    Scheduler::ProcessEventsToIdle();

    // Click at on the left side of A1 cell
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, leftCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, leftCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    // Check the A1 cell is selected in view #2
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), ScDocShell::GetViewData()->GetCurX());
    CPPUNIT_ASSERT_EQUAL(SCROW(0), ScDocShell::GetViewData()->GetCurY());

    // Click at on the right side of A1 cell
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN, rightCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP, rightCellSideX, y, /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    // Check the A1 cell is selected in view #2
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), ScDocShell::GetViewData()->GetCurX());
    CPPUNIT_ASSERT_EQUAL(SCROW(0), ScDocShell::GetViewData()->GetCurY());
}

// if we extend the tiled area to the right and bottom we want two resulting area
// that don't overlap. If they overlap that typically creates an unnecessary full
// screen invalidation.
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testExtendedAreasDontOverlap)
{
    comphelper::COKit::setCompatFlag(
        comphelper::COKit::Compat::scPrintTwipsMsgs);

    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    // Set an arbitrary initial size smaller than the final size
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 1000, 1000));

    Scheduler::ProcessEventsToIdle();

    // register to track View #1 invalidations
    ScTestViewCallback aView1;

    // extend to the right and bottom
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 39750, 12780));

    Scheduler::ProcessEventsToIdle();

    // we should get two rectangles for the two new areas
    CPPUNIT_ASSERT_EQUAL(size_t(2), aView1.m_aInvalidations.size());

    // And those should not overlap, otherwise they would merge to form
    // a mega rectangle, which defeats the purpose of creating two rects
    // in the first place.
    CPPUNIT_ASSERT_MESSAGE("Invalidations should not overlap",
        !aView1.m_aInvalidations[0].Overlaps(aView1.m_aInvalidations[1]));

    // But they should be adjacent
    CPPUNIT_ASSERT_EQUAL(aView1.m_aInvalidations[0].Top() +
                         aView1.m_aInvalidations[0].GetSize().Height(),
                         aView1.m_aInvalidations[1].Top());
}

// Ensure that editing a shape not in the topleft tile has its text shown inside the shape
// center while editing
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testEditShapeText)
{
    ScModelObj* pModelObj = createDoc("edit-shape-text.ods");

    // Set View to initial 100%
    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, 28050, 10605));
    pModelObj->setClientZoom(256, 256, 1920, 1920);

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    CPPUNIT_ASSERT(pView);

    const bool bShapeSelected = pView->SelectObject(u"Shape 1");
    CPPUNIT_ASSERT(bShapeSelected);

    CPPUNIT_ASSERT(ScDocShell::GetViewData()->GetScDrawView()->GetMarkedObjectList().GetMarkCount() != 0);

    Scheduler::ProcessEventsToIdle();

    // Enter editing mode, shape start with no text
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);

    Scheduler::ProcessEventsToIdle();

    // Grab a snapshot of the center of the shape
    Bitmap aBitmapBefore = getTile(pModelObj, 4096, 3584, 15360, 7680);

    // reuse this to type into the active shape edit
    typeCharsInCell("MMMMMMM", 0, 0, pView, pModelObj, true, false);

    // Grab a new snapshot of the center of the shape
    Bitmap aBitmapAfter = getTile(pModelObj, 4096, 3584, 15360, 7680);

    // Without the fix, the text is not inside this tile and the before and
    // after are the same.
    CPPUNIT_ASSERT_MESSAGE("Text is not visible", aBitmapBefore != aBitmapAfter);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testNumberFormatLocaleMultiUser)
{
    {
        // setup core language to FR as it will be the first session
        SvtSysLocaleOptions aLocalOptions;
        aLocalOptions.SetLocaleConfigString(u"fr-FR"_ustr);
        aLocalOptions.SetUILocaleConfigString(u"fr-FR"_ustr);
        aLocalOptions.Commit();

        loadFromFile(u"numlocale.xlsx");

        ScModelObj* pModelObj = comphelper::getFromUnoTunnel<ScModelObj>(mxComponent);
        CPPUNIT_ASSERT(pModelObj);

        pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
        ScDocument* pDoc = pModelObj->GetDocument();

        int nViewFR = KitHelper::getCurrentView();
        ScTestViewCallback aView1;
        SfxViewShell* pViewFR = SfxViewShell::Current();
        pViewFR->SetKitLocale(u"fr-FR"_ustr);

        // modify G12 with FR and use French keywords in the format
        KitHelper::setView(nViewFR);

        sal_Int32 nCheckPos;
        SvNumFormatType nType;
        sal_uInt32 nFormat;
        OUString aNumberFormat(u"JJ/MM/AAAA"_ustr);
        SvNumberFormatter* pFormatter = pDoc->GetFormatTable();
        pFormatter->PutEntry(aNumberFormat, nCheckPos, nType, nFormat);
        ScAddress aCellPos1(/*nColP=*/6, /*nRowP=*/11, /*nTabP=*/0);
        pDoc->SetNumberFormat(aCellPos1, nFormat);

        Scheduler::ProcessEventsToIdle();
    }

    {
        // now setup DE language in core
        SvtSysLocaleOptions aLocalOptions;
        aLocalOptions.SetLocaleConfigString(u"de-DE"_ustr);
        aLocalOptions.SetUILocaleConfigString(u"de-DE"_ustr);
        aLocalOptions.Commit();

        // save and reopen
        // .uno:Save modifies the original file, make a copy first
        saveAndReload(TestFilter::XLSM);

        ScModelObj* pModelObj = comphelper::getFromUnoTunnel<ScModelObj>(mxComponent);
        CPPUNIT_ASSERT(pModelObj);

        ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
        CPPUNIT_ASSERT(pView);

        Scheduler::ProcessEventsToIdle();

        cpo::uno::Sequence<beans::PropertyValue> aArgs;
        dispatchCommand(mxComponent, u".uno:Save"_ustr, aArgs);

        Scheduler::ProcessEventsToIdle();

        ScDocument* pDoc = pModelObj->GetDocument();

        // verify that format is correct (German), doesn't have any "string" inside
        sal_uInt32 nNumberFormat = pDoc->GetNumberFormat(/*col=*/6, /*row=*/11, /*tab=*/0);
        const SvNumberformat* pNumberFormat = pDoc->GetFormatTable()->GetEntry(nNumberFormat);
        CPPUNIT_ASSERT_EQUAL(u"TT.MM.JJ"_ustr, pNumberFormat->GetFormatstring());
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testLeftOverflowEdit)
{
    comphelper::COKit::setCompatFlag(comphelper::COKit::Compat::scPrintTwipsMsgs);
    ScModelObj* pModelObj = createDoc("right-aligned-with-overflow.ods");
    ScTestViewCallback aView;

    // Go to Cell B5000
    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues = {
        comphelper::makePropertyValue(u"ToPoint"_ustr, u"$B$5000"_ustr),
    };
    dispatchCommand(mxComponent, u".uno:GoToCell"_ustr, aPropertyValues);

    // Enter edit mode and select all text.
    aView.m_aTextSelectionResult.clear();
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::F2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::F2);
    Scheduler::ProcessEventsToIdle();
    // CTRL + A
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_MOD1 | awt::Key::A);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, KEY_MOD1 | awt::Key::A);
    Scheduler::ProcessEventsToIdle();

    // Without the accompanying fix this would fail with
    // - Expected: 20
    // - Actual  : 1300
    CPPUNIT_ASSERT_EQUAL(tools::Long(20), aView.m_aTextSelectionResult.m_aRefPoint.getX());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testFreezeRowOrColumn)
{
    createDoc("empty.ods");
    ScTestViewCallback aView;
    SfxViewShell* pView = SfxViewShell::Current();

    // Freeze panes on a column and receive the proper state back
    aView.m_aStateChanges.clear();
    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues = {
        comphelper::makePropertyValue(u"Index"_ustr,  cpo::uno::Any(static_cast<sal_Int32>(8))),
    };
    comphelper::dispatchCommand(u".uno:FreezePanesColumn"_ustr, aPropertyValues);
    Scheduler::ProcessEventsToIdle();
    pView->GetViewFrame().GetBindings().GetTimer().Invoke();
    pView->GetViewFrame().GetBindings().GetTimer().Invoke();
    auto it = aView.m_aStateChanges.find(".uno:FreezePanesColumn");
    CPPUNIT_ASSERT(it != aView.m_aStateChanges.end());
    std::string values = it->second.get<std::string>("state");
    std::string index = values.substr(0, values.find(' '));
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 8
    // - Actual  : 1
    CPPUNIT_ASSERT_EQUAL(std::string("8"), index);

    // Freeze panes on a row and receive the proper state back
    aView.m_aStateChanges.clear();
    comphelper::dispatchCommand(u".uno:FreezePanesRow"_ustr, aPropertyValues);
    Scheduler::ProcessEventsToIdle();
    pView->GetViewFrame().GetBindings().GetTimer().Invoke();
    pView->GetViewFrame().GetBindings().GetTimer().Invoke();
    it = aView.m_aStateChanges.find(".uno:FreezePanesRow");
    CPPUNIT_ASSERT(it != aView.m_aStateChanges.end());
    values = it->second.get<std::string>("state");
    index = values.substr(0, values.find(' '));
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 8
    // - Actual  : 1
    CPPUNIT_ASSERT_EQUAL(std::string("8"), index);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testCursorVisibilityAfterPaste)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    ScTestViewCallback aView;
    KitHelper::createView();
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());

    // copy text view 1
    pView->SetCursor(0, 0); // Go to A1.

    Scheduler::ProcessEventsToIdle();

    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'B', 0); // Type B.
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'B', 0);
    Scheduler::ProcessEventsToIdle();


    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::TAB);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::TAB);
    Scheduler::ProcessEventsToIdle();

    pView->SetCursor(0, 0); // Go to A1.
    Scheduler::ProcessEventsToIdle();

    pView->GetViewFrame().GetBindings().Execute(SID_COPY); // Copy B.
    Scheduler::ProcessEventsToIdle();

    pView->SetCursor(0, 1); // Go to A2.
    Scheduler::ProcessEventsToIdle();

    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 'B', 0); // Type B.
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 'B', 0);
    Scheduler::ProcessEventsToIdle();

    cpo::uno::Sequence<beans::PropertyValue> aArgs;
    dispatchCommand(mxComponent, u".uno:Paste"_ustr, aArgs); // Paste B.
    Scheduler::ProcessEventsToIdle();

    // Text cursor should still be visible.
    CPPUNIT_ASSERT_EQUAL(true, aView.m_textCursorVisible);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testAutoFilterPosition)
{
    ScModelObj* pModelObj = createDoc("autofilter.ods");
    ScTestViewCallback aView;
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());

    pView->SetCursor(0, 0); // Go to A1.
    Scheduler::ProcessEventsToIdle();

    // Use autofilter button shortcut (ALT + DOWNARROW) to avoid coordinate based click.
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN | KEY_MOD2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN | KEY_MOD2);
    Scheduler::ProcessEventsToIdle();

    // We should have the autofilter position callback.
    auto it = aView.m_aStateChanges.find("AutoFilterInfo");
    CPPUNIT_ASSERT(it != aView.m_aStateChanges.end());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testPivotFilterPosition)
{
    ScModelObj* pModelObj = createDoc("pivotTableFilter.ods");
    ScTestViewCallback aView;
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());

    pView->SetCursor(1, 0); // Go to B1.
    Scheduler::ProcessEventsToIdle();

    // Use filter button shortcut (ALT + DOWNARROW) to avoid coordinate based click.
    pModelObj->postKeyEvent(COKitKeyEventType::DOWN, 0, awt::Key::DOWN | KEY_MOD2);
    pModelObj->postKeyEvent(COKitKeyEventType::UP, 0, awt::Key::DOWN | KEY_MOD2);
    Scheduler::ProcessEventsToIdle();

    // We should have the autofilter position callback.
    auto it = aView.m_aStateChanges.find("PivotTableFilterInfo");
    CPPUNIT_ASSERT(it != aView.m_aStateChanges.end());
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testPrintRanges)
{
    ScModelObj* pModelObj = createDoc("PrintRanges.ods");
    ScTestViewCallback aView;
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());

    /*
        Expected output: { \"printranges\": [ { \"sheet\": 0, \"ranges\": [ [ 2, 6, 2, 6]]}]}
    */
    OUString printRanges = pModelObj->getPrintRanges().replaceAll(" ", "");

    CPPUNIT_ASSERT_EQUAL(u"{\"printranges\":[{\"sheet\":0,\"ranges\":[[2,6,2,6]]}]}"_ustr, printRanges);
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testKitLanguageStatus)
{
    ScModelObj* pModelObj = createDoc("pivotfr.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);

    // view #1
    SfxViewShell* pView1 = SfxViewShell::Current();
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(pView1);
    CPPUNIT_ASSERT(pView);

    {
        // Pivot table is in the same sheet and first data row starts at A17.
        pView->SetCursor(0, 16);
        dispatchCommand(mxComponent, u".uno:RecalcPivotTable"_ustr, {});

        // Document language is French(France), which the view must inherit.
        CPPUNIT_ASSERT_EQUAL(u"janv."_ustr, pDoc->GetString(ScAddress(0, 16, 0)));
    }

    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
                { "Language", cpo::uno::Any(u"Default_Spanish (Spain)"_ustr) },
            }));
        dispatchCommand(mxComponent, u".uno:LanguageStatus"_ustr, aArgs);

        // Pivot table is in the same sheet and first data row starts at A17.
        pView->SetCursor(0, 16);
        dispatchCommand(mxComponent, u".uno:RecalcPivotTable"_ustr, {});

        // view language is now Spanish(Spain)
        CPPUNIT_ASSERT_EQUAL(u"ene"_ustr, pDoc->GetString(ScAddress(0, 16, 0)));
    }

    {
        cpo::uno::Sequence<beans::PropertyValue> aArgs( comphelper::InitPropertySequence({
                { "Language", cpo::uno::Any(u"Default_English (USA)"_ustr) },
            }));
        dispatchCommand(mxComponent, u".uno:LanguageStatus"_ustr, aArgs);

        // Pivot table is in the same sheet and first data row starts at A17.
        pView->SetCursor(0, 16);
        dispatchCommand(mxComponent, u".uno:RecalcPivotTable"_ustr, {});

        // view language is now English(USA).
        CPPUNIT_ASSERT_EQUAL(u"Jan"_ustr, pDoc->GetString(ScAddress(0, 16, 0)));
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testValidationCellCrash)
{
    ScModelObj* pModelObj = createDoc("validationcrash.xlsx");
    pModelObj->initializeForTiledRendering(cpo::uno::Sequence<beans::PropertyValue>());
    CPPUNIT_ASSERT(pModelObj);
    ScDocShell* pDocSh = dynamic_cast< ScDocShell* >( pModelObj->GetEmbeddedObject() );
    CPPUNIT_ASSERT(pDocSh);
    ScDocument* pDoc = pModelObj->GetDocument();
    CPPUNIT_ASSERT(pDoc);

    ScTestViewCallback aView1;
    SfxViewShell* pView1 = SfxViewShell::Current();
    ScTabViewShell* pView = dynamic_cast<ScTabViewShell*>(pView1);
    CPPUNIT_ASSERT(pView);
    typeCharsInCell("42", 0, 1, pView, pModelObj, false /* bInEdit */, true /* bCommit */); // Type "42" in A2.
    // Without the fix this will crash.
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testThreadedCommentInsertAndResolve)
{
    // Reset the static counter so the first note gets a predictable id.
    ScPostIt::mnLastPostItId = 1;

    {
        ScModelObj* pModelObj = createDoc("small.ods");
        ScTestViewCallback aView1;

        KitHelper::createView();
        pModelObj->initializeForTiledRendering({});
        ScTestViewCallback aView2;

        KitHelper::setView(aView1.getViewID());

        ScTabViewShell* pTabViewShell = aView1.getTabViewShell();
        CPPUNIT_ASSERT(pTabViewShell);
        pTabViewShell->SetCursor(2, 3);

        // 1) Insert a threaded comment via .uno:InsertThreadedComment
        cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"Threaded comment text"_ustr)},
            {"Author", cpo::uno::Any(u"Kit Author"_ustr)},
        }));
        dispatchCommand(mxComponent, u".uno:InsertThreadedComment"_ustr, aArgs);

        // Both views receive COKitCallbackType::COMMENT with action "Add"
        CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("Add"), aView2.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("1"), aView1.m_aCommentCallbackResult.get<std::string>("id"));

        // The callback must carry threaded-comment-specific fields
        CPPUNIT_ASSERT_EQUAL(std::string("Threaded comment text"), aView1.m_aCommentCallbackResult.get<std::string>("text"));
        CPPUNIT_ASSERT_EQUAL(std::string("Kit Author"), aView1.m_aCommentCallbackResult.get<std::string>("author"));
        CPPUNIT_ASSERT_EQUAL(std::string("true"), aView1.m_aCommentCallbackResult.get<std::string>("threaded"));
        CPPUNIT_ASSERT_EQUAL(std::string("false"), aView1.m_aCommentCallbackResult.get<std::string>("resolved"));
        CPPUNIT_ASSERT_EQUAL(std::string("2 3 2 3"), aView1.m_aCommentCallbackResult.get<std::string>("cellRange"));

        std::string aCommentId = aView1.m_aCommentCallbackResult.get<std::string>("id");

        // 2) Verify getPostIts() includes threaded/resolved fields
        {
            tools::JsonWriter aJsonWriter;
            pModelObj->getPostIts(aJsonWriter);
            std::string aJson(aJsonWriter.finishAndGetAsOString());
            std::stringstream aStream(aJson);
            boost::property_tree::ptree aTree;
            boost::property_tree::read_json(aStream, aTree);

            auto aComments = aTree.get_child("comments");
            CPPUNIT_ASSERT_EQUAL(size_t(1), aComments.size());
            const auto& rComment = aComments.begin()->second;
            CPPUNIT_ASSERT_EQUAL(std::string("Threaded comment text"), rComment.get<std::string>("text"));
            CPPUNIT_ASSERT_EQUAL(std::string("true"), rComment.get<std::string>("threaded"));
            CPPUNIT_ASSERT_EQUAL(std::string("false"), rComment.get<std::string>("resolved"));
        }

        // 3) Resolve the threaded comment via .uno:ResolveComment
        aArgs = comphelper::InitPropertySequence(
        {
            {"Id", cpo::uno::Any(OUString::createFromAscii(aCommentId))},
        });
        dispatchCommand(mxComponent, u".uno:ResolveComment"_ustr, aArgs);

        // Both views receive COKitCallbackType::COMMENT with action "Modify"
        CPPUNIT_ASSERT_EQUAL(std::string("Modify"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("Modify"), aView2.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("true"), aView1.m_aCommentCallbackResult.get<std::string>("resolved"));

        // 4) Verify getPostIts() now shows resolved=true
        {
            tools::JsonWriter aJsonWriter;
            pModelObj->getPostIts(aJsonWriter);
            std::string aJson(aJsonWriter.finishAndGetAsOString());
            std::stringstream aStream(aJson);
            boost::property_tree::ptree aTree;
            boost::property_tree::read_json(aStream, aTree);

            const auto& rComment = aTree.get_child("comments").begin()->second;
            CPPUNIT_ASSERT_EQUAL(std::string("true"), rComment.get<std::string>("resolved"));
        }

        // 5) Toggle resolve back (unresolve)
        dispatchCommand(mxComponent, u".uno:ResolveComment"_ustr, aArgs);

        CPPUNIT_ASSERT_EQUAL(std::string("Modify"), aView1.m_aCommentCallbackResult.get<std::string>("action"));
        CPPUNIT_ASSERT_EQUAL(std::string("false"), aView1.m_aCommentCallbackResult.get<std::string>("resolved"));
    }
}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testThreadedCommentDocModifiedAndUndo)
{
    // Before the fix, .uno:InsertThreadedComment manipulated the note model
    // directly and never set the document-modified flag or registered undo.
    ScPostIt::mnLastPostItId = 1;

    {
        ScModelObj* pModelObj = createDoc("small.ods");
        ScTestViewCallback aView;
        ScDocShell* pDocSh = dynamic_cast<ScDocShell*>(pModelObj->GetEmbeddedObject());
        CPPUNIT_ASSERT(pDocSh);
        ScDocument* pDoc = pModelObj->GetDocument();
        CPPUNIT_ASSERT(pDoc);

        ScTabViewShell* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
        CPPUNIT_ASSERT(pTabViewShell);
        pTabViewShell->SetCursor(2, 3);

        // Fresh document: not modified, no undo actions, no note at the target cell.
        CPPUNIT_ASSERT(!pDocSh->IsModified());
        SfxUndoManager* pUndoMgr = pDocSh->GetUndoManager();
        CPPUNIT_ASSERT(pUndoMgr);
        CPPUNIT_ASSERT_EQUAL(size_t(0), pUndoMgr->GetUndoActionCount());
        const ScAddress aPos(2, 3, 0);
        CPPUNIT_ASSERT(!pDoc->GetNote(aPos));

        // 1) Insert a threaded comment via .uno:InsertThreadedComment.
        cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"Kit thread"_ustr)},
            {"Author", cpo::uno::Any(u"Kit Author"_ustr)},
        }));
        aView.m_aStateChanges.clear();
        dispatchCommand(mxComponent, u".uno:InsertThreadedComment"_ustr, aArgs);

        CPPUNIT_ASSERT(pDocSh->IsModified());
        CPPUNIT_ASSERT_EQUAL(size_t(1), pUndoMgr->GetUndoActionCount());

        // The note exists with threaded data attached.
        const ScPostIt* pNote = pDoc->GetNote(aPos);
        CPPUNIT_ASSERT(pNote);
        const ScThreadedCommentData* pThreaded = pNote->GetThreadedCommentData();
        CPPUNIT_ASSERT(pThreaded);
        CPPUNIT_ASSERT_EQUAL(u"Kit thread"_ustr, pThreaded->maRoot.maText);

        // The bindings timer broadcasts the deferred .uno:ModifiedStatus state change.
        pTabViewShell->GetViewFrame().GetBindings().GetTimer().Invoke();
        pTabViewShell->GetViewFrame().GetBindings().GetTimer().Invoke();
        auto it = aView.m_aStateChanges.find(".uno:ModifiedStatus");
        CPPUNIT_ASSERT(it != aView.m_aStateChanges.end());
        CPPUNIT_ASSERT_EQUAL(std::string("true"), it->second.get<std::string>("state"));

        // 2) Undo removes the note.
        dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
        CPPUNIT_ASSERT(!pDoc->GetNote(aPos));
        CPPUNIT_ASSERT_EQUAL(size_t(0), pUndoMgr->GetUndoActionCount());
        CPPUNIT_ASSERT_EQUAL(size_t(1), pUndoMgr->GetRedoActionCount());

        // 3) Redo brings the note back, including the threaded payload. The undo
        //    action stores a clone of ScThreadedCommentData (it lives outside
        //    ScNoteData), so the redo path must reattach it.
        dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
        pNote = pDoc->GetNote(aPos);
        CPPUNIT_ASSERT(pNote);
        pThreaded = pNote->GetThreadedCommentData();
        CPPUNIT_ASSERT(pThreaded);
        CPPUNIT_ASSERT_EQUAL(u"Kit thread"_ustr, pThreaded->maRoot.maText);
    }

    // Sub-case: when Online dispatches the command with no Author arg or with
    // an Author arg that carries an empty string, the engine must fall back to
    // SvtUserOptions().GetFullName() (which carries the WOPI UserFriendlyName
    // in a Kit context) rather than landing on "Unknown Author". Before the
    // fix the slot's pAuthorItem ternary checked "was the arg sent" rather
    // than "is the value non-empty", so an empty Author short-circuited the
    // user-options fallback.
    {
        SvtUserOptions aUserOpt;
        const OUString aSavedFirstName = aUserOpt.GetFirstName();
        const OUString aSavedLastName = aUserOpt.GetLastName();
        aUserOpt.SetToken(UserOptToken::FirstName, u"WOPI"_ustr);
        aUserOpt.SetToken(UserOptToken::LastName, u"Friendly"_ustr);

        ScModelObj* pModelObj = createDoc("small.ods");
        ScDocument* pDoc = pModelObj->GetDocument();
        CPPUNIT_ASSERT(pDoc);
        ScTabViewShell* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
        CPPUNIT_ASSERT(pTabViewShell);

        const ScAddress aPos(4, 5, 0);
        pTabViewShell->SetCursor(aPos.Col(), aPos.Row());

        // 1) No Author arg at all: pAuthorItem == nullptr in the slot.
        cpo::uno::Sequence<beans::PropertyValue> aArgsNoAuthor(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"no author arg"_ustr)},
        }));
        dispatchCommand(mxComponent, u".uno:InsertThreadedComment"_ustr, aArgsNoAuthor);
        const ScPostIt* pNote = pDoc->GetNote(aPos);
        CPPUNIT_ASSERT(pNote);
        CPPUNIT_ASSERT_EQUAL(u"WOPI Friendly"_ustr, pNote->GetAuthor());

        // Roll back so the next dispatch operates on a fresh cell state.
        dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
        CPPUNIT_ASSERT(!pDoc->GetNote(aPos));

        // 2) Author arg present but empty: the regression case.
        cpo::uno::Sequence<beans::PropertyValue> aArgsEmptyAuthor(comphelper::InitPropertySequence(
        {
            {"Text", cpo::uno::Any(u"empty author arg"_ustr)},
            {"Author", cpo::uno::Any(u""_ustr)},
        }));
        dispatchCommand(mxComponent, u".uno:InsertThreadedComment"_ustr, aArgsEmptyAuthor);
        pNote = pDoc->GetNote(aPos);
        CPPUNIT_ASSERT(pNote);
        CPPUNIT_ASSERT_EQUAL(u"WOPI Friendly"_ustr, pNote->GetAuthor());

        aUserOpt.SetToken(UserOptToken::FirstName, aSavedFirstName);
        aUserOpt.SetToken(UserOptToken::LastName, aSavedLastName);
    }
}

namespace {

void lcl_getStartDimPos(const boost::property_tree::ptree& tree, double twipsPerPixel, size_t queryIndex, size_t& displayTwips, size_t& printTwips)
{
    std::stringstream ss(tree.get<std::string>("sizes"));
    size_t size = 0;
    size_t index = 0;
    size_t prevIndex = 0;
    char tmp;
    printTwips = 0;
    displayTwips = 0;
    while ((ss >> size) && (ss >> tmp) && (ss >> index))
    {
        CPPUNIT_ASSERT(index >= prevIndex);
        size_t pxOne = size_t(size / twipsPerPixel);
        if (queryIndex > index)
        {
            size_t numIndices = index - prevIndex + 1;
            printTwips += (numIndices * size);
            displayTwips += size_t(pxOne * numIndices * twipsPerPixel);
            prevIndex = index + 1;
            continue;
        }

        printTwips += ((queryIndex - prevIndex) * size);
        displayTwips += size_t((queryIndex - prevIndex) * pxOne * twipsPerPixel);
        return;
    }
    CPPUNIT_FAIL("Bad queryIndex passed to lcl_getStartDimPos()");
}

void lcl_getShapeRectangle(OString& selection, tools::Rectangle& rectangle)
{
    std::stringstream ss((std::string(selection)));
    size_t x = 0, y = 0, width = 0, height = 0;
    char tmp;
    CPPUNIT_ASSERT_MESSAGE("Parse of graphicselection message failed!", ss >> x >> tmp >> y >> tmp >> width >> tmp >> height);
    rectangle.SetPos(Point(x, y));
    rectangle.SetSize(Size(width, height));
}

}

CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testChartSelectionCoords)
{
    ScModelObj* pModelObj = createDoc("chartsel.ods");
    auto* pTabViewShell = dynamic_cast<ScTabViewShell*>(SfxViewShell::Current());
    ScTestViewCallback aView1;
    OString aJson = pModelObj->getSheetGeometryData(/*bColumns*/ true, /*bRows*/ true, /*bSizes*/ true,
                /*bHidden*/ true, /*bFiltered*/ true, /*bGroups*/ true);
    boost::property_tree::ptree aTree;
    std::stringstream ss((std::string(aJson)));
    boost::property_tree::read_json(ss, aTree);
    auto& aCols = aTree.get_child("columns");
    auto& aRows = aTree.get_child("rows");

    constexpr double zoomFraction = 1.5;
    constexpr size_t tilePx = 256;
    constexpr size_t tileTwips = tilePx * 15.0 / zoomFraction;
    constexpr double twipsPerPixel = tileTwips / static_cast<double>(tilePx);

    pModelObj->setClientVisibleArea(tools::Rectangle(0, 0, tileTwips * 5, tileTwips * 5));
    pModelObj->setClientZoom(tilePx, tilePx, tileTwips, tileTwips);

    size_t x1ptwips = 0, x1dtwips = 0;
    size_t y1ptwips = 0, y1dtwips = 0;
    size_t x2ptwips = 0, x2dtwips = 0;
    size_t y2ptwips = 0, y2dtwips = 0;

    // Top-left of cell AT17(45, 16) is nearly the top-left of the chart.
    // Top-left of cell BA37(52, 36) is nearly the bottom-right of the chart.
    lcl_getStartDimPos(aCols, twipsPerPixel, 45, x1dtwips, x1ptwips);
    lcl_getStartDimPos(aRows, twipsPerPixel, 16, y1dtwips, y1ptwips);
    lcl_getStartDimPos(aCols, twipsPerPixel, 52, x2dtwips, x2ptwips);
    lcl_getStartDimPos(aRows, twipsPerPixel, 36, y2dtwips, y2ptwips);
    // Chart area in print twips.
    tools::Rectangle aChartAreaPtwips(Point(x1ptwips, y1ptwips), Size(x2ptwips - x1ptwips, y2ptwips - y1ptwips));
    // Chart area in display twips.
    tools::Rectangle aChartAreaDtwips(Point(x1dtwips, y1dtwips), Size(x2dtwips - x1dtwips, y2dtwips - y1dtwips));

    pTabViewShell->SetCursor(53, 40);
    Scheduler::ProcessEventsToIdle();

    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN,
            /*x=*/ aChartAreaDtwips.Left() + 300,
            /*y=*/ aChartAreaDtwips.Top() + 300,
            /*count=*/ 2, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP,
            /*x=*/ aChartAreaDtwips.Left() + 300,
            /*y=*/ aChartAreaDtwips.Top() + 300,
            /*count=*/ 2, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
    tools::Rectangle aActualChartRect;
    lcl_getShapeRectangle(aView1.m_ShapeSelection, aActualChartRect);
    CPPUNIT_ASSERT_MESSAGE("Actual chart area spills out of the expected bounds", aChartAreaPtwips.Contains(aActualChartRect));

    // Click again for chart wall area.
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONDOWN,
            /*x=*/ aChartAreaDtwips.Left() + 300,
            /*y=*/ aChartAreaDtwips.Top() + 300,
            /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    pModelObj->postMouseEvent(COKitMouseEventType::BUTTONUP,
            /*x=*/ aChartAreaDtwips.Left() + 300,
            /*y=*/ aChartAreaDtwips.Top() + 300,
            /*count=*/ 1, /*buttons=*/ 1, /*modifier=*/0);
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!aView1.m_ShapeSelection.isEmpty());
    tools::Rectangle aActualChartWall;
    lcl_getShapeRectangle(aView1.m_ShapeSelection, aActualChartWall);
    CPPUNIT_ASSERT_MESSAGE("Actual chart wall area spills out of the expected bounds", aChartAreaPtwips.Contains(aActualChartWall));
}

// Filling a cell that a neighbour's text overflows across must invalidate the
// columns that overflow covered.
CPPUNIT_TEST_FIXTURE(ScTiledRenderingTest, testInvalidateForOverflowingText)
{
    ScModelObj* pModelObj = createDoc("empty.ods");
    CPPUNIT_ASSERT(pModelObj);
    ScDocShell* pDocSh = dynamic_cast<ScDocShell*>(pModelObj->GetEmbeddedObject());
    CPPUNIT_ASSERT(pDocSh);
    ScTabViewShell* pView = pDocSh->GetBestViewShell(false);
    CPPUNIT_ASSERT(pView);
    ScDocument& rDoc = pDocSh->GetDocument();

    // A long left aligned string in A1 spills across the empty cells to its right.
    typeCharsInCell("This is an extremely long line of text that should overflow "
                    "across very many empty columns to the right",
                    0, 0, pView, pModelObj, false, true);
    Scheduler::ProcessEventsToIdle();

    ScTestViewCallback aView;

    // Fill B1, which A1 overflows across, isolating the commit's invalidation.
    typeCharsInCell("x", 1, 0, pView, pModelObj, false, false);
    aView.m_bInvalidateTiles = false;
    aView.m_aInvalidations.clear();
    typeCharsInCell("", 1, 0, pView, pModelObj, true, true);

    CPPUNIT_ASSERT(aView.m_bInvalidateTiles);
    tools::Rectangle aInvalidation;
    for (const auto& rRect : aView.m_aInvalidations)
        aInvalidation.Union(rRect);

    // Without the fix the invalidation was about one column wide.
    const tools::Long nColWidth = rDoc.GetColWidth(1, 0);
    CPPUNIT_ASSERT_GREATER(nColWidth * 4, aInvalidation.GetWidth());

    // But not the whole row to the sheet edge, which the width hint avoids.
    CPPUNIT_ASSERT_LESS(tools::Long(32212230), aInvalidation.Right());
}

CPPUNIT_PLUGIN_IMPLEMENT();

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
