/* -*- 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 "helper/debughelper.hxx"
#include "helper/qahelper.hxx"

#include <svl/asiancfg.hxx>

#include <simpleformulacalc.hxx>
#include <stringutil.hxx>
#include <scmatrix.hxx>
#include <scitems.hxx>
#include <reffind.hxx>
#include <clipparam.hxx>
#include <undoblk.hxx>
#include <undotab.hxx>
#include <attrib.hxx>
#include <dbdata.hxx>
#include <reftokenhelper.hxx>
#include <userdat.hxx>
#include <refdata.hxx>

#include <docfunc.hxx>
#include <funcdesc.hxx>
#include <globstr.hrc>
#include <scresid.hxx>

#include <columniterator.hxx>
#include <scopetools.hxx>
#include <dociter.hxx>
#include <edittextiterator.hxx>
#include <editutil.hxx>
#include <asciiopt.hxx>
#include <impex.hxx>
#include <globalnames.hxx>
#include <columnspanset.hxx>

#include <editable.hxx>
#include <postit.hxx>
#include <tabprotection.hxx>
#include <undomanager.hxx>
#include <token.hxx>

#include <formula/IFunctionDescription.hxx>

#include <editeng/borderline.hxx>
#include <editeng/brushitem.hxx>
#include <editeng/eeitem.hxx>
#include <editeng/wghtitem.hxx>
#include <editeng/postitem.hxx>
#include <editeng/lineitem.hxx>

#include <o3tl/nonstaticstring.hxx>
#include <svx/svdpage.hxx>
#include <svx/svdocirc.hxx>
#include <svx/svdopath.hxx>
#include <svx/svdocapt.hxx>
#include <svl/numformat.hxx>
#include <svl/srchitem.hxx>
#include <svl/sharedstringpool.hxx>
#include <unotools/collatorwrapper.hxx>
#include <sfx2/IDocumentModelAccessor.hxx>

#include <sfx2/sfxsids.hrc>

class ScUndoPaste;
class ScUndoCut;

namespace {

struct HoriIterCheck
{
    SCCOL nCol;
    SCROW nRow;
    const char* pVal;
};

}

class Test : public ScUcalcTestBase
{
protected:
    void checkPrecisionAsShown(OUString& rCode, double fValue, double fExpectedRoundVal);

    /** Get a separate new ScDocShell with ScDocument that suits unit test needs. */
    void getNewDocShell(ScDocShellRef& rDocShellRef);

    bool checkHorizontalIterator(ScDocument& rDoc, const std::vector<std::vector<const char*>>& rData,
            const HoriIterCheck* pChecks, size_t nCheckCount);
};

void Test::getNewDocShell( ScDocShellRef& rDocShellRef )
{
    rDocShellRef = new ScDocShell(
        SfxModelFlags::EMBEDDED_OBJECT |
        SfxModelFlags::DISABLE_EMBEDDED_SCRIPTS |
        SfxModelFlags::DISABLE_DOCUMENT_RECOVERY);

    rDocShellRef->DoInitUnitTest();
}

CPPUNIT_TEST_FIXTURE(Test, testCollator)
{
    sal_Int32 nRes = ScGlobal::GetCollator().compareString(u"A"_ustr, u"B"_ustr);
    CPPUNIT_ASSERT_MESSAGE("these strings are supposed to be different!", nRes != 0);
}

CPPUNIT_TEST_FIXTURE(Test, testUndoBackgroundColorInsertRow)
{
    m_pDoc->InsertTab(0, u"Table1"_ustr);

    ScMarkData aMark(m_pDoc->GetSheetLimits());

    // Set Values to B1, C2, D5
    m_pDoc->SetValue(ScAddress(1, 0, 0), 1.0); // B1
    m_pDoc->SetValue(ScAddress(2, 1, 0), 2.0); // C2
    m_pDoc->SetValue(ScAddress(3, 4, 0), 3.0); // D5

    // Add patterns
    ScPatternAttr aCellBlueColor(m_pDoc->getCellAttributeHelper());
    aCellBlueColor.ItemSetPut(SvxBrushItem(COL_BLUE, ATTR_BACKGROUND));
    m_pDoc->ApplyPatternAreaTab(0, 3, m_pDoc->MaxCol(), 3, 0, aCellBlueColor);

    // Insert a new row at row 3
    ScRange aRowOne(0, 2, 0, m_pDoc->MaxCol(), 2, 0);
    aMark.SetMarkArea(aRowOne);
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    rFunc.InsertCells(aRowOne, &aMark, INS_INSROWS_BEFORE, true, true);

    // Check patterns
    const SfxPoolItem* pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(1000, 4, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    // Undo the new row
    m_pDoc->GetUndoManager()->Undo();

    // Check patterns
    // Failed if row 3 is not blue all the way through
    pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(1000, 3, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testUndoBackgroundColorInsertColumn)
{
    m_pDoc->InsertTab(0, u"Table1"_ustr);

    ScMarkData aMark(m_pDoc->GetSheetLimits());

    // Set Values to B1, C2, D5
    m_pDoc->SetValue(ScAddress(1, 0, 0), 1.0); // B1
    m_pDoc->SetValue(ScAddress(2, 1, 0), 2.0); // C2
    m_pDoc->SetValue(ScAddress(3, 4, 0), 3.0); // D5

    // Add patterns
    ScPatternAttr aCellBlueColor(m_pDoc->getCellAttributeHelper());
    aCellBlueColor.ItemSetPut(SvxBrushItem(COL_BLUE, ATTR_BACKGROUND));
    m_pDoc->ApplyPatternAreaTab(3, 0, 3, m_pDoc->MaxRow(), 0, aCellBlueColor);

    // Insert a new column at column 3
    ScRange aColumnOne(2, 0, 0, 2, m_pDoc->MaxRow(), 0);
    aMark.SetMarkArea(aColumnOne);
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    rFunc.InsertCells(aColumnOne, &aMark, INS_INSCOLS_BEFORE, true, true);

    // Check patterns
    const SfxPoolItem* pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(4, 1000, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    // Undo the new column
    m_pDoc->GetUndoManager()->Undo();

    // Check patterns
    // Failed if column 3 is not blue all the way through
    pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(3, 1000, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMergedHyperlink)
{
    m_pDoc->InsertTab(0, u"Table1"_ustr);
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScFieldEditEngine& pEE = m_pDoc->GetEditEngine();
    pEE.SetTextCurrentDefaults(u"https://libreoffice.org/"_ustr);
    m_pDoc->SetEditText(ScAddress(1, 0, 0), pEE.CreateTextObject()); // B1

    m_pDoc->DoMergeContents(0, 0, 1, 0, 0); // A1:B1

    CPPUNIT_ASSERT_EQUAL(CELLTYPE_EDIT, m_pDoc->GetCellType(ScAddress(0, 0, 0))); // A1
    const EditTextObject* pEditObj = m_pDoc->GetEditText(ScAddress(0, 0, 0)); // A1
    CPPUNIT_ASSERT(pEditObj);
    CPPUNIT_ASSERT_EQUAL(u"https://libreoffice.org/"_ustr, pEditObj->GetText(0));
}

CPPUNIT_TEST_FIXTURE(Test, testSharedStringPool)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    svl::SharedStringPool& rPool = m_pDoc->GetSharedStringPool();
    size_t extraCount = rPool.getCount(); // internal items such as SharedString::getEmptyString()
    size_t extraCountIgnoreCase = rPool.getCountIgnoreCase();

    // Strings that are identical.
    m_pDoc->SetString(ScAddress(0,0,0), o3tl::nonStaticString(u"Andy"));  // A1
    m_pDoc->SetString(ScAddress(0,1,0), o3tl::nonStaticString(u"Andy"));  // A2
    m_pDoc->SetString(ScAddress(0,2,0), o3tl::nonStaticString(u"Bruce")); // A3
    m_pDoc->SetString(ScAddress(0,3,0), o3tl::nonStaticString(u"andy"));  // A4
    m_pDoc->SetString(ScAddress(0,4,0), o3tl::nonStaticString(u"BRUCE")); // A5

    {
        // These two shared string objects must go out of scope before the purge test.
        svl::SharedString aSS1 = m_pDoc->GetSharedString(ScAddress(0,0,0));
        svl::SharedString aSS2 = m_pDoc->GetSharedString(ScAddress(0,1,0));
        CPPUNIT_ASSERT_MESSAGE("Failed to get a valid shared string.", aSS1.isValid());
        CPPUNIT_ASSERT_MESSAGE("Failed to get a valid shared string.", aSS2.isValid());
        CPPUNIT_ASSERT_EQUAL(aSS1.getData(), aSS2.getData());

        aSS2 = m_pDoc->GetSharedString(ScAddress(0,2,0));
        CPPUNIT_ASSERT_MESSAGE("They must differ", aSS1.getData() != aSS2.getData());

        aSS2 = m_pDoc->GetSharedString(ScAddress(0,3,0));
        CPPUNIT_ASSERT_MESSAGE("They must differ", aSS1.getData() != aSS2.getData());

        aSS2 = m_pDoc->GetSharedString(ScAddress(0,4,0));
        CPPUNIT_ASSERT_MESSAGE("They must differ", aSS1.getData() != aSS2.getData());

        // A3 and A5 should differ but should be equal case-insensitively.
        aSS1 = m_pDoc->GetSharedString(ScAddress(0,2,0));
        aSS2 = m_pDoc->GetSharedString(ScAddress(0,4,0));
        CPPUNIT_ASSERT_MESSAGE("They must differ", aSS1.getData() != aSS2.getData());
        CPPUNIT_ASSERT_EQUAL_MESSAGE("They must be equal when cases are ignored.", aSS1.getDataIgnoreCase(), aSS2.getDataIgnoreCase());

        // A2 and A4 should be equal when ignoring cases.
        aSS1 = m_pDoc->GetSharedString(ScAddress(0,1,0));
        aSS2 = m_pDoc->GetSharedString(ScAddress(0,3,0));
        CPPUNIT_ASSERT_EQUAL_MESSAGE("They must be equal when cases are ignored.", aSS1.getDataIgnoreCase(), aSS2.getDataIgnoreCase());
    }

    // Check the string counts after purging. Purging shouldn't remove any strings in this case.
    rPool.purge();
    CPPUNIT_ASSERT_EQUAL(5+extraCount, rPool.getCount());
    CPPUNIT_ASSERT_EQUAL(2+extraCountIgnoreCase, rPool.getCountIgnoreCase());

    // Clear A1
    clearRange(m_pDoc, ScRange(ScAddress(0,0,0)));
    // Clear A2
    clearRange(m_pDoc, ScRange(ScAddress(0,1,0)));
    // Clear A3
    clearRange(m_pDoc, ScRange(ScAddress(0,2,0)));
    // Clear A4
    clearRange(m_pDoc, ScRange(ScAddress(0,3,0)));
    // Clear A5 and the pool should be completely empty.
    clearRange(m_pDoc, ScRange(ScAddress(0,4,0)));
    rPool.purge();
    CPPUNIT_ASSERT_EQUAL(extraCount, rPool.getCount());
    CPPUNIT_ASSERT_EQUAL(extraCountIgnoreCase, rPool.getCountIgnoreCase());

    // Now, compare string and edit text cells.
    m_pDoc->SetString(ScAddress(0,0,0), u"Andy and Bruce"_ustr); // A1 // [-loplugin:ostr]
    ScFieldEditEngine& rEE = m_pDoc->GetEditEngine();
    rEE.SetTextCurrentDefaults(u"Andy and Bruce"_ustr);

    {
        // Set 'Andy' bold.
        SfxItemSet aItemSet = rEE.GetEmptyItemSet();
        SvxWeightItem aWeight(WEIGHT_BOLD, EE_CHAR_WEIGHT);
        aItemSet.Put(aWeight);
        rEE.QuickSetAttribs(aItemSet, ESelection(0, 0, 0, 4));
    }

    {
        // Set 'Bruce' italic.
        SfxItemSet aItemSet = rEE.GetEmptyItemSet();
        SvxPostureItem aItalic(ITALIC_NORMAL, EE_CHAR_ITALIC);
        aItemSet.Put(aItalic);
        rEE.QuickSetAttribs(aItemSet, ESelection(0, 9, 0, 14));
    }

    m_pDoc->SetEditText(ScAddress(1,0,0), rEE.CreateTextObject()); // B1

    // These two should be equal.
    svl::SharedString aSS1 = m_pDoc->GetSharedString(ScAddress(0,0,0));
    svl::SharedString aSS2 = m_pDoc->GetSharedString(ScAddress(1,0,0));
    CPPUNIT_ASSERT_MESSAGE("Failed to get a valid string ID.", aSS1.isValid());
    CPPUNIT_ASSERT_MESSAGE("Failed to get a valid string ID.", aSS2.isValid());
    CPPUNIT_ASSERT_EQUAL(aSS1.getData(), aSS2.getData());

    rEE.SetTextCurrentDefaults(u"ANDY and BRUCE"_ustr);
    m_pDoc->SetEditText(ScAddress(2,0,0), rEE.CreateTextObject()); // C1
    aSS2 = m_pDoc->GetSharedString(ScAddress(2,0,0));
    CPPUNIT_ASSERT_MESSAGE("Failed to get a valid string ID.", aSS2.isValid());
    CPPUNIT_ASSERT_MESSAGE("These two should be different when cases are considered.", aSS1.getData() != aSS2.getData());

    // But they should be considered equal when cases are ignored.
    aSS1 = m_pDoc->GetSharedString(ScAddress(0,0,0));
    aSS2 = m_pDoc->GetSharedString(ScAddress(2,0,0));
    CPPUNIT_ASSERT_MESSAGE("Failed to get a valid string ID.", aSS1.isValid());
    CPPUNIT_ASSERT_MESSAGE("Failed to get a valid string ID.", aSS2.isValid());
    CPPUNIT_ASSERT_EQUAL(aSS1.getDataIgnoreCase(), aSS2.getDataIgnoreCase());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testBackgroundColorDeleteColumn)
{
    m_pDoc->InsertTab(0, u"Table1"_ustr);

    ScMarkData aMark(m_pDoc->GetSheetLimits());

    // Set Values to B1, C2, D5
    m_pDoc->SetValue(ScAddress(1, 0, 0), 1.0); // B1
    m_pDoc->SetValue(ScAddress(2, 1, 0), 2.0); // C2
    m_pDoc->SetValue(ScAddress(3, 4, 0), 3.0); // D5

    // Add patterns
    ScPatternAttr aCellBlueColor(m_pDoc->getCellAttributeHelper());
    aCellBlueColor.ItemSetPut(SvxBrushItem(COL_BLUE, ATTR_BACKGROUND));
    m_pDoc->ApplyPatternAreaTab(3, 0, 3, m_pDoc->MaxRow(), 0, aCellBlueColor);

    // Delete column 10
    m_pDoc->DeleteCol(ScRange(9,0,0,9,m_pDoc->MaxRow(),0));

    // Check patterns
    const SfxPoolItem* pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(3, 1000, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    // Delete column 2
    m_pDoc->DeleteCol(ScRange(1,0,0,1,m_pDoc->MaxRow(),0));

    // Check patterns
    pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(2, 1000, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testBackgroundColorDeleteRow)
{
    m_pDoc->InsertTab(0, u"Table1"_ustr);

    ScMarkData aMark(m_pDoc->GetSheetLimits());

    // Set Values to B1, C2, D5
    m_pDoc->SetValue(ScAddress(1, 0, 0), 1.0); // B1
    m_pDoc->SetValue(ScAddress(2, 1, 0), 2.0); // C2
    m_pDoc->SetValue(ScAddress(3, 4, 0), 3.0); // D5

    // Add patterns
    ScPatternAttr aCellBlueColor(m_pDoc->getCellAttributeHelper());
    aCellBlueColor.ItemSetPut(SvxBrushItem(COL_BLUE, ATTR_BACKGROUND));
    m_pDoc->ApplyPatternAreaTab(0, 3, m_pDoc->MaxCol(), 3, 0, aCellBlueColor);

    // Delete row 10
    m_pDoc->DeleteRow(ScRange(0,9,0,m_pDoc->MaxCol(),9,0));

    // Check patterns
    const SfxPoolItem* pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(1000, 3, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    // Delete row 2
    m_pDoc->DeleteRow(ScRange(0,1,0,m_pDoc->MaxCol(),1,0));

    // Check patterns
    pItem = nullptr;
    m_pDoc->GetPattern(ScAddress(1000, 2, 0))->GetItemSet().HasItem(ATTR_BACKGROUND, &pItem);
    CPPUNIT_ASSERT(pItem);
    CPPUNIT_ASSERT_EQUAL(COL_BLUE, static_cast<const SvxBrushItem*>(pItem)->GetColor());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testSharedStringPoolUndoDoc)
{
    struct
    {
        bool check( const ScDocument& rSrcDoc, ScDocument& rCopyDoc )
        {
            // Copy A1:A4 to the undo document.
            for (SCROW i = 0; i <= 4; ++i)
            {
                ScAddress aPos(0,i,0);
                rCopyDoc.SetString(aPos, rSrcDoc.GetString(aPos));
            }

            // String values in A1:A4 should have identical hash.
            for (SCROW i = 0; i <= 4; ++i)
            {
                ScAddress aPos(0,i,0);
                svl::SharedString aSS1 = rSrcDoc.GetSharedString(aPos);
                svl::SharedString aSS2 = rCopyDoc.GetSharedString(aPos);
                if (aSS1.getDataIgnoreCase() != aSS2.getDataIgnoreCase())
                {
                    std::cerr << "String hash values are not equal at row " << (i+1)
                        << " for string '" << aSS1.getString() << "'" << std::endl;
                    return false;
                }
            }

            return true;
        }

    } aTest;

    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,0,0), u"Header"_ustr);
    m_pDoc->SetString(ScAddress(0,1,0), u"A1"_ustr);
    m_pDoc->SetString(ScAddress(0,2,0), u"A2"_ustr);
    m_pDoc->SetString(ScAddress(0,3,0), u"A3"_ustr);

    ScDocument aUndoDoc(SCDOCMODE_UNDO);
    aUndoDoc.InitUndo(*m_pDoc, 0, 0);

    bool bSuccess = aTest.check(*m_pDoc, aUndoDoc);
    CPPUNIT_ASSERT_MESSAGE("Check failed with undo document.", bSuccess);

    // Test the clip document as well.
    ScDocument aClipDoc(SCDOCMODE_CLIP);
    aClipDoc.ResetClip(m_pDoc, static_cast<SCTAB>(0));

    bSuccess = aTest.check(*m_pDoc, aClipDoc);
    CPPUNIT_ASSERT_MESSAGE("Check failed with clip document.", bSuccess);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testRangeList)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    ScRangeList aRL;
    aRL.push_back(ScRange(1,1,0,3,10,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("List should have one range.", size_t(1), aRL.size());
    const ScRange* p = &aRL[0];
    CPPUNIT_ASSERT_MESSAGE("Failed to get the range object.", p);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong range.", ScAddress(1,1,0), p->aStart);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong range.", ScAddress(3,10,0), p->aEnd);

    // TODO: Add more tests here.

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMarkData)
{
    ScMarkData aMarkData(m_pDoc->GetSheetLimits());

    // Empty mark. Nothing is selected.
    std::vector<sc::ColRowSpan> aSpans = aMarkData.GetMarkedRowSpans();
    CPPUNIT_ASSERT_MESSAGE("Span should be empty.", aSpans.empty());
    aSpans = aMarkData.GetMarkedColSpans();
    CPPUNIT_ASSERT_MESSAGE("Span should be empty.", aSpans.empty());

    // Select B3:F7.
    aMarkData.SetMarkArea(ScRange(1,2,0,5,6,0));
    aSpans = aMarkData.GetMarkedRowSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one selected row span.", size_t(1), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(2), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(6), aSpans[0].mnEnd);

    aSpans = aMarkData.GetMarkedColSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one selected column span.", size_t(1), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(1), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(5), aSpans[0].mnEnd);

    // Select A11:B13.
    aMarkData.SetMultiMarkArea(ScRange(0,10,0,1,12,0));
    aSpans = aMarkData.GetMarkedRowSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be 2 selected row spans.", size_t(2), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(2), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(6), aSpans[0].mnEnd);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(10), aSpans[1].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(12), aSpans[1].mnEnd);

    aSpans = aMarkData.GetMarkedColSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one selected column span.", size_t(1), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(0), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(5), aSpans[0].mnEnd);

    // Select C8:C10.
    aMarkData.SetMultiMarkArea(ScRange(2,7,0,2,9,0));
    aSpans = aMarkData.GetMarkedRowSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one selected row span.", size_t(1), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(2), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(12), aSpans[0].mnEnd);

    aSpans = aMarkData.GetMarkedColSpans();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one selected column span.", size_t(1), aSpans.size());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(0), aSpans[0].mnStart);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOLROW>(5), aSpans[0].mnEnd);
}

CPPUNIT_TEST_FIXTURE(Test, testInput)
{

    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet",
                            m_pDoc->InsertTab (0, u"foo"_ustr));
    OUString test;

    m_pDoc->SetString(0, 0, 0, u"'10.5"_ustr);
    test = m_pDoc->GetString(0, 0, 0);
    bool bTest = test == "10.5";
    CPPUNIT_ASSERT_MESSAGE("String number should have the first apostrophe stripped.", bTest);
    m_pDoc->SetString(0, 0, 0, u"'apple'"_ustr);
    test = m_pDoc->GetString(0, 0, 0);
    bTest = test == "apple'";
    CPPUNIT_ASSERT_MESSAGE("Text content should have the first apostrophe stripped.", bTest);

    // Customized string handling policy.
    ScSetStringParam aParam;
    aParam.setTextInput();
    m_pDoc->SetString(0, 0, 0, u"000123"_ustr, &aParam);
    test = m_pDoc->GetString(0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Text content should have been treated as string, not number.", u"000123"_ustr, test);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testColumnIterator) // tdf#118620
{
    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet",
                            m_pDoc->InsertTab (0, u"foo"_ustr));

    m_pDoc->SetString(0, 0, 0, u"'10.5"_ustr);
    m_pDoc->SetString(0, m_pDoc->MaxRow()-5, 0, u"42.0"_ustr);
    std::optional<sc::ColumnIterator> it = m_pDoc->GetColumnIterator(0, 0, m_pDoc->MaxRow() - 10, m_pDoc->MaxRow());
    while (it->hasCell())
    {
        it->getCell();
        it->next();
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf66613)
{
    // Create different print ranges and col/row repetitions for two tabs
    const SCTAB nFirstTab = 0;
    CPPUNIT_ASSERT(m_pDoc->InsertTab(nFirstTab, u"FirstPrintRange"_ustr));
    ScRange aFirstPrintRange(0, 0, nFirstTab, 2, 2, nFirstTab);
    m_pDoc->AddPrintRange(nFirstTab, aFirstPrintRange);
    ScRange aFirstRepeatColRange(0, 0, nFirstTab, 0, 0, nFirstTab);
    m_pDoc->SetRepeatColRange(nFirstTab, aFirstRepeatColRange);
    ScRange aFirstRepeatRowRange(1, 1, nFirstTab, 1, 1, nFirstTab);
    m_pDoc->SetRepeatRowRange(nFirstTab, aFirstRepeatRowRange);

    const SCTAB nSecondTab = 1;
    CPPUNIT_ASSERT(m_pDoc->InsertTab(nSecondTab, u"SecondPrintRange"_ustr));
    ScRange aSecondPrintRange(0, 0, nSecondTab, 3, 3, nSecondTab);
    m_pDoc->AddPrintRange(nSecondTab, aSecondPrintRange);
    ScRange aSecondRepeatColRange(1, 1, nSecondTab, 1, 1, nSecondTab);
    m_pDoc->SetRepeatColRange(nSecondTab, aSecondRepeatColRange);
    ScRange aSecondRepeatRowRange(2, 2, nSecondTab, 2, 2, nSecondTab);
    m_pDoc->SetRepeatRowRange(nSecondTab, aSecondRepeatRowRange);

    // Transfer generated tabs to a new document with different order
    ScDocument aScDocument;
    aScDocument.TransferTab(*m_pDoc, nSecondTab, nFirstTab);
    aScDocument.TransferTab(*m_pDoc, nFirstTab, nSecondTab);

    // Check the number of print ranges in both documents
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), m_pDoc->GetPrintRangeCount(nFirstTab));
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), m_pDoc->GetPrintRangeCount(nSecondTab));
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aScDocument.GetPrintRangeCount(nFirstTab));
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), aScDocument.GetPrintRangeCount(nSecondTab));

    // Check the print ranges and col/row repetitions in both documents
    CPPUNIT_ASSERT_EQUAL(aFirstPrintRange, *m_pDoc->GetPrintRange(nFirstTab, 0));
    CPPUNIT_ASSERT_EQUAL(aFirstRepeatColRange, *m_pDoc->GetRepeatColRange(nFirstTab));
    CPPUNIT_ASSERT_EQUAL(aFirstRepeatRowRange, *m_pDoc->GetRepeatRowRange(nFirstTab));
    CPPUNIT_ASSERT_EQUAL(aSecondPrintRange, *m_pDoc->GetPrintRange(nSecondTab, 0));
    CPPUNIT_ASSERT_EQUAL(aSecondRepeatColRange, *m_pDoc->GetRepeatColRange(nSecondTab));
    CPPUNIT_ASSERT_EQUAL(aSecondRepeatRowRange, *m_pDoc->GetRepeatRowRange(nSecondTab));

    // Tabs have to be adjusted since the order of the tabs is inverted in the new document
    std::vector<ScRange*> aScRanges
        = { &aFirstPrintRange,  &aFirstRepeatColRange,  &aFirstRepeatRowRange,
            &aSecondPrintRange, &aSecondRepeatColRange, &aSecondRepeatRowRange };
    for (size_t i = 0; i < aScRanges.size(); i++)
    {
        const SCTAB nTab = i >= 3 ? nFirstTab : nSecondTab;
        aScRanges[i]->aStart.SetTab(nTab);
        aScRanges[i]->aEnd.SetTab(nTab);
    }

    // Without the fix in place, no print ranges and col/row repetitions would be present
    CPPUNIT_ASSERT_EQUAL(aFirstPrintRange, *aScDocument.GetPrintRange(nSecondTab, 0));
    CPPUNIT_ASSERT_EQUAL(aFirstRepeatColRange, *aScDocument.GetRepeatColRange(nSecondTab));
    CPPUNIT_ASSERT_EQUAL(aFirstRepeatRowRange, *aScDocument.GetRepeatRowRange(nSecondTab));
    CPPUNIT_ASSERT_EQUAL(aSecondPrintRange, *aScDocument.GetPrintRange(nFirstTab, 0));
    CPPUNIT_ASSERT_EQUAL(aSecondRepeatColRange, *aScDocument.GetRepeatColRange(nFirstTab));
    CPPUNIT_ASSERT_EQUAL(aSecondRepeatRowRange, *aScDocument.GetRepeatRowRange(nFirstTab));

    m_pDoc->DeleteTab(nFirstTab);
    m_pDoc->DeleteTab(nSecondTab);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf113027)
{
    // Insert some sheets including a whitespace in their name and switch the grammar to R1C1
    CPPUNIT_ASSERT(m_pDoc->InsertTab(0, u"Sheet 1"_ustr));
    CPPUNIT_ASSERT(m_pDoc->InsertTab(1, u"Sheet 2"_ustr));
    FormulaGrammarSwitch aFGSwitch(m_pDoc, formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1);

    // Add a formula containing a remote reference, i.e., to another sheet
    const ScAddress aScAddress(0, 0, 0);
    static constexpr OUString aFormula = u"='Sheet 2'!RC"_ustr;
    m_pDoc->SetString(aScAddress, aFormula);

    // Switch from relative to absolute cell reference
    ScRefFinder aFinder(aFormula, aScAddress, *m_pDoc, m_pDoc->GetAddressConvention());
    aFinder.ToggleRel(0, aFormula.getLength());

    // Without the fix in place, this test would have failed with
    // - Expected: ='Sheet 2'!R1C1
    // - Actual  : ='Sheet 2'!RC
    // i.e. the cell reference was not changed from relative to absolute
    CPPUNIT_ASSERT_EQUAL(u"='Sheet 2'!R1C1"_ustr, aFinder.GetText());

    m_pDoc->DeleteTab(0);
    m_pDoc->DeleteTab(1);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf90698)
{
    CPPUNIT_ASSERT(m_pDoc->InsertTab (0, u"Test"_ustr));
    m_pDoc->SetString(ScAddress(0,0,0), u"=(1;2)"_ustr);

    // Without the fix in place, this would have failed with
    // - Expected: =(1;2)
    // - Actual  : =(1~2)
    OUString aFormula = m_pDoc->GetFormula(0,0,0);
    CPPUNIT_ASSERT_EQUAL(u"=(1;2)"_ustr, aFormula);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf114406)
{
    CPPUNIT_ASSERT(m_pDoc->InsertTab (0, u"Test"_ustr));
    m_pDoc->SetString(ScAddress(0,0,0), u"5"_ustr);
    m_pDoc->SetString(ScAddress(1,0,0), u"=A1/100%"_ustr);

    // Without the fix in place, this would have failed with
    // - Expected: =A1/100%
    // - Actual  : =A1/1
    OUString aFormula = m_pDoc->GetFormula(1,0,0);
    CPPUNIT_ASSERT_EQUAL(u"=A1/100%"_ustr, aFormula);

    CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(1,0,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf93951)
{
    CPPUNIT_ASSERT(m_pDoc->InsertTab (0, u"Test"_ustr));
    m_pDoc->SetString(ScAddress(0,0,0), u"=2*§*2"_ustr);

    OUString aFormula = m_pDoc->GetFormula(0,0,0);

    // Without the fix in place, this test would have failed with
    // - Expected: =2*§*2
    // - Actual  : =2*
    CPPUNIT_ASSERT_EQUAL(u"=2*§*2"_ustr, aFormula);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf134490)
{
    CPPUNIT_ASSERT(m_pDoc->InsertTab (0, u"Test"_ustr));

    m_pDoc->SetString(ScAddress(0,0,0), u"--1"_ustr);
    m_pDoc->SetString(ScAddress(0,1,0), u"---1"_ustr);
    m_pDoc->SetString(ScAddress(0,2,0), u"+-1"_ustr);
    m_pDoc->SetString(ScAddress(0,3,0), u"+--1"_ustr);

    // Without the fix in place, this test would have failed with
    // - Expected: --1
    // - Actual  : -1
    CPPUNIT_ASSERT_EQUAL(u"--1"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"---1"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(u"+-1"_ustr, m_pDoc->GetString(ScAddress(0,2,0)));
    CPPUNIT_ASSERT_EQUAL(u"+--1"_ustr, m_pDoc->GetString(ScAddress(0,3,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf135249)
{
    CPPUNIT_ASSERT(m_pDoc->InsertTab (0, u"Test"_ustr));

    m_pDoc->SetString(ScAddress(0,0,0), u"1:60"_ustr);
    m_pDoc->SetString(ScAddress(0,1,0), u"1:123"_ustr);
    m_pDoc->SetString(ScAddress(0,2,0), u"1:1:123"_ustr);
    m_pDoc->SetString(ScAddress(0,3,0), u"0:123"_ustr);
    m_pDoc->SetString(ScAddress(0,4,0), u"0:0:123"_ustr);
    m_pDoc->SetString(ScAddress(0,5,0), u"0:123:59"_ustr);

    // These are not valid duration inputs
    CPPUNIT_ASSERT_EQUAL(u"1:60"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"1:123"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(u"1:1:123"_ustr, m_pDoc->GetString(ScAddress(0,2,0)));

    // These are valid duration inputs
    // Without the fix in place, this test would have failed with
    // - Expected: 02:03:00 AM
    // - Actual  : 0:123
    CPPUNIT_ASSERT_EQUAL(u"02:03:00 AM"_ustr, m_pDoc->GetString(ScAddress(0,3,0)));
    CPPUNIT_ASSERT_EQUAL(u"12:02:03 AM"_ustr, m_pDoc->GetString(ScAddress(0,4,0)));
    CPPUNIT_ASSERT_EQUAL(u"02:03:59 AM"_ustr, m_pDoc->GetString(ScAddress(0,5,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDocStatistics)
{
    SCTAB nStartTabs = m_pDoc->GetTableCount();
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Failed to increment sheet count.",
                               static_cast<SCTAB>(nStartTabs+1), m_pDoc->GetTableCount());
    m_pDoc->InsertTab(1, u"Sheet2"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Failed to increment sheet count.",
                               static_cast<SCTAB>(nStartTabs+2), m_pDoc->GetTableCount());

    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(0), m_pDoc->GetCellCount());
    m_pDoc->SetValue(ScAddress(0,0,0), 2.0);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(1), m_pDoc->GetCellCount());
    m_pDoc->SetValue(ScAddress(2,2,0), 2.5);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(2), m_pDoc->GetCellCount());
    m_pDoc->SetString(ScAddress(1,1,1), u"Test"_ustr);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(3), m_pDoc->GetCellCount());

    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(0), m_pDoc->GetFormulaGroupCount());
    m_pDoc->SetString(ScAddress(3,0,1), u"=A1"_ustr);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(1), m_pDoc->GetFormulaGroupCount());
    m_pDoc->SetString(ScAddress(3,1,1), u"=A2"_ustr);
    m_pDoc->SetString(ScAddress(3,2,1), u"=A3"_ustr);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(1), m_pDoc->GetFormulaGroupCount());
    m_pDoc->SetString(ScAddress(3,3,1), u"=A5"_ustr);
    m_pDoc->SetString(ScAddress(3,4,1), u"=A6"_ustr);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(2), m_pDoc->GetFormulaGroupCount());
    m_pDoc->SetString(ScAddress(3,1,1), u"=A3"_ustr);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt64>(4), m_pDoc->GetFormulaGroupCount());

    m_pDoc->DeleteTab(1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Failed to decrement sheet count.",
                               static_cast<SCTAB>(nStartTabs+1), m_pDoc->GetTableCount());
    m_pDoc->DeleteTab(0); // This may fail in case there is only one sheet in the document.
}

CPPUNIT_TEST_FIXTURE(Test, testRowForHeight)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->SetRowHeightRange( 0,  9, 0, 100);
    m_pDoc->SetRowHeightRange(10, 19, 0, 200);
    m_pDoc->SetRowHeightRange(20, 29, 0, 300);

    // Hide some rows.
    m_pDoc->SetRowHidden(3,  5, 0, true);
    m_pDoc->SetRowHidden(8, 12, 0, true);

    struct Check
    {
        tools::Long nHeight;
        SCROW nRow;
    };

    std::vector<Check> aChecks = {
        { -2000,  0 },
        { -1000,  0 },
        {    -1,  0 },
        {     0,  0 }, // row 0 begins
        {     1,  0 },
        {    99,  0 }, // row 0 ends
        {   100,  1 }, // row 1 begins
        {   101,  1 },
        {   120,  1 },
        {   199,  1 }, // row 1 ends
        {   200,  2 }, // row 2 begins
        {   201,  2 },
        {   299,  2 }, // row 2 ends
        {   300,  6 }, // row 6 begins, because 3-5 are hidden
        {   330,  6 },
        {   399,  6 }, // row 6 ends
        {   400,  7 }, // row 7 begins
        {   401,  7 },
        {   420,  7 },
        {   499,  7 }, // row 7 ends
        {   500, 13 }, // row 13 begins, because 8-12 are hidden
        {   501, 13 },
        {   599, 13 },
        {   600, 13 },
        {   699, 13 }, // row 13 ends (row 13 is 200 pixels high)
        {   700, 14 }, // row 14 begins
        {   780, 14 },
        {   899, 14 }, // row 14 ends (row 14 is 200 pixels high)
        {   900, 15 }, // row 15 begins
        {  1860, 19 },
        {  4020, 27 },
    };

    for (const Check& rCheck : aChecks)
    {
        SCROW nRow = m_pDoc->GetRowForHeight(0, rCheck.nHeight);
        OString sMessage = "for height " + OString::number(rCheck.nHeight) + " we expect row " + OString::number(rCheck.nRow);
        CPPUNIT_ASSERT_EQUAL_MESSAGE(sMessage.getStr(), rCheck.nRow, nRow);
    }
}

CPPUNIT_TEST_FIXTURE(Test, testDataEntries)
{
    /**
     * The 'data entries' data is a list of strings used for suggestions as
     * the user types in new cell value.
     */
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,5,0), u"Andy"_ustr);
    m_pDoc->SetString(ScAddress(0,6,0), u"Bruce"_ustr);
    m_pDoc->SetString(ScAddress(0,7,0), u"Charlie"_ustr);
    m_pDoc->SetString(ScAddress(0,10,0), u"Andy"_ustr);

    std::vector<ScTypedStrData> aEntries;
    m_pDoc->GetDataEntries(0, 0, 0, aEntries); // Try at the very top.

    // Entries are supposed to be sorted in ascending order, and are all unique.
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aEntries.size());
    std::vector<ScTypedStrData>::const_iterator it = aEntries.begin();
    CPPUNIT_ASSERT_EQUAL(u"Andy"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_EQUAL(u"Bruce"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_EQUAL(u"Charlie"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_MESSAGE("The entries should have ended here.", bool(it == aEntries.end()));

    aEntries.clear();
    m_pDoc->GetDataEntries(0, m_pDoc->MaxRow(), 0, aEntries); // Try at the very bottom.
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aEntries.size());

    // Make sure we get the same set of suggestions.
    it = aEntries.begin();
    CPPUNIT_ASSERT_EQUAL(u"Andy"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_EQUAL(u"Bruce"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_EQUAL(u"Charlie"_ustr, it->GetString());
    ++it;
    CPPUNIT_ASSERT_MESSAGE("The entries should have ended here.", bool(it == aEntries.end()));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testSelectionFunction)
{
    /**
     * Selection function is responsible for displaying quick calculation
     * results in the status bar.
     */
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Insert values into B2:B4.
    m_pDoc->SetString(ScAddress(1,1,0), u"=1"_ustr); // formula
    m_pDoc->SetValue(ScAddress(1,2,0), 2.0);
    m_pDoc->SetValue(ScAddress(1,3,0), 3.0);

    // Insert strings into B5:B8.
    m_pDoc->SetString(ScAddress(1,4,0), u"A"_ustr);
    m_pDoc->SetString(ScAddress(1,5,0), u"B"_ustr);
    m_pDoc->SetString(ScAddress(1,6,0), u"=\"C\""_ustr); // formula
    m_pDoc->SetString(ScAddress(1,7,0), u"D"_ustr);

    // Insert values into D2:D4.
    m_pDoc->SetValue(ScAddress(3,1,0), 4.0);
    m_pDoc->SetValue(ScAddress(3,2,0), 5.0);
    m_pDoc->SetValue(ScAddress(3,3,0), 6.0);

    // Insert edit text into D5.
    ScFieldEditEngine& rEE = m_pDoc->GetEditEngine();
    rEE.SetTextCurrentDefaults(u"Rich Text"_ustr);
    m_pDoc->SetEditText(ScAddress(3,4,0), rEE.CreateTextObject());

    // Insert Another string into D6.
    m_pDoc->SetString(ScAddress(3,5,0), u"E"_ustr);

    // Select B2:B8 & D2:D8 disjoint region.
    ScRangeList aRanges;
    aRanges.push_back(ScRange(1,1,0,1,7,0)); // B2:B8
    aRanges.push_back(ScRange(3,1,0,3,7,0)); // D2:D8
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.MarkFromRangeList(aRanges, true);

    struct Check
    {
        ScSubTotalFunc meFunc;
        double mfExpected;
    };

    {
        static const Check aChecks[] =
        {
            { SUBTOTAL_FUNC_AVE,              3.5 },
            { SUBTOTAL_FUNC_CNT2,            12.0 },
            { SUBTOTAL_FUNC_CNT,              6.0 },
            { SUBTOTAL_FUNC_MAX,              6.0 },
            { SUBTOTAL_FUNC_MIN,              1.0 },
            { SUBTOTAL_FUNC_SUM,             21.0 },
            { SUBTOTAL_FUNC_SELECTION_COUNT, 14.0 }
        };

        for (const auto& rCheck : aChecks)
        {
            double fRes = 0.0;
            bool bRes = m_pDoc->GetSelectionFunction(rCheck.meFunc, ScAddress(), aMark, fRes);
            CPPUNIT_ASSERT_MESSAGE("Failed to fetch selection function result.", bRes);
            CPPUNIT_ASSERT_EQUAL(rCheck.mfExpected, fRes);
        }
    }

    // Hide rows 4 and 6 and check the results again.

    m_pDoc->SetRowHidden(3, 3, 0, true);
    m_pDoc->SetRowHidden(5, 5, 0, true);
    CPPUNIT_ASSERT_MESSAGE("This row should be hidden.", m_pDoc->RowHidden(3, 0));
    CPPUNIT_ASSERT_MESSAGE("This row should be hidden.", m_pDoc->RowHidden(5, 0));

    {
        static const Check aChecks[] =
        {
            { SUBTOTAL_FUNC_AVE,              3.0 },
            { SUBTOTAL_FUNC_CNT2,             8.0 },
            { SUBTOTAL_FUNC_CNT,              4.0 },
            { SUBTOTAL_FUNC_MAX,              5.0 },
            { SUBTOTAL_FUNC_MIN,              1.0 },
            { SUBTOTAL_FUNC_SUM,             12.0 },
            { SUBTOTAL_FUNC_SELECTION_COUNT, 10.0 }
        };

        for (const auto& rCheck : aChecks)
        {
            double fRes = 0.0;
            bool bRes = m_pDoc->GetSelectionFunction(rCheck.meFunc, ScAddress(), aMark, fRes);
            CPPUNIT_ASSERT_MESSAGE("Failed to fetch selection function result.", bRes);
            CPPUNIT_ASSERT_EQUAL(rCheck.mfExpected, fRes);
        }
    }

    // Make sure that when no selection is present, use the current cursor position.
    ScMarkData aEmpty(m_pDoc->GetSheetLimits());

    {
        // D3 (numeric cell containing 5.)
        ScAddress aPos(3, 2, 0);

        static const Check aChecks[] =
        {
            { SUBTOTAL_FUNC_AVE,             5.0 },
            { SUBTOTAL_FUNC_CNT2,            1.0 },
            { SUBTOTAL_FUNC_CNT,             1.0 },
            { SUBTOTAL_FUNC_MAX,             5.0 },
            { SUBTOTAL_FUNC_MIN,             5.0 },
            { SUBTOTAL_FUNC_SUM,             5.0 },
            { SUBTOTAL_FUNC_SELECTION_COUNT, 1.0 }
        };

        for (const auto& rCheck : aChecks)
        {
            double fRes = 0.0;
            bool bRes = m_pDoc->GetSelectionFunction(rCheck.meFunc, aPos, aEmpty, fRes);
            CPPUNIT_ASSERT_MESSAGE("Failed to fetch selection function result.", bRes);
            CPPUNIT_ASSERT_EQUAL(rCheck.mfExpected, fRes);
        }
    }

    {
        // B7 (string formula cell containing ="C".)
        ScAddress aPos(1, 6, 0);

        static const Check aChecks[] =
        {
            { SUBTOTAL_FUNC_CNT2,            1.0 },
            { SUBTOTAL_FUNC_SELECTION_COUNT, 1.0 }
        };

        for (const auto& rCheck : aChecks)
        {
            double fRes = 0.0;
            bool bRes = m_pDoc->GetSelectionFunction(rCheck.meFunc, aPos, aEmpty, fRes);
            CPPUNIT_ASSERT_MESSAGE("Failed to fetch selection function result.", bRes);
            CPPUNIT_ASSERT_EQUAL(rCheck.mfExpected, fRes);
        }
    }

    // Calculate function across selected sheets.
    clearSheet(m_pDoc, 0);
    m_pDoc->InsertTab(1, u"Test2"_ustr);
    m_pDoc->InsertTab(2, u"Test3"_ustr);

    // Set values at B2 and C3 on each sheet.
    m_pDoc->SetValue(ScAddress(1,1,0), 1.0);
    m_pDoc->SetValue(ScAddress(2,2,0), 2.0);
    m_pDoc->SetValue(ScAddress(1,1,1), 4.0);
    m_pDoc->SetValue(ScAddress(2,2,1), 8.0);
    m_pDoc->SetValue(ScAddress(1,1,2), 16.0);
    m_pDoc->SetValue(ScAddress(2,2,2), 32.0);

    // Mark B2 and C3 on first sheet.
    aRanges.RemoveAll();
    aRanges.push_back(ScRange(1,1,0)); // B2
    aRanges.push_back(ScRange(2,2,0)); // C3
    aMark.MarkFromRangeList(aRanges, true);
    // Additionally select third sheet.
    aMark.SelectTable(2, true);

    {
        double fRes = 0.0;
        bool bRes = m_pDoc->GetSelectionFunction( SUBTOTAL_FUNC_SUM, ScAddress(), aMark, fRes);
        CPPUNIT_ASSERT_MESSAGE("Failed to fetch selection function result.", bRes);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("1+2+16+32=", 51.0, fRes);
    }

    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMarkedCellIteration)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Insert cells to A1, A5, B2 and C3.
    m_pDoc->SetString(ScAddress(0,0,0), u"California"_ustr);
    m_pDoc->SetValue(ScAddress(0,4,0), 1.2);
    m_pDoc->SetEditText(ScAddress(1,1,0), u"Boston"_ustr);
    m_pDoc->SetFormula(ScAddress(2,2,0), u"=SUM(1,2,3)"_ustr, m_pDoc->GetGrammar());

    // Select A1:C5.
    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SetMarkArea(ScRange(0,0,0,2,4,0));
    aMarkData.MarkToMulti(); // TODO : we shouldn't have to do this.

    struct Check
    {
        SCCOL mnCol;
        SCROW mnRow;
    };

    const std::vector<Check> aChecks = {
        { 0, 0 }, // A1
        { 0, 4 }, // A5
        { 1, 1 }, // B2
        { 2, 2 }, // C3
    };

    SCROW nRow = -1; // Start from the imaginary row before A1.
    SCCOL nCol = 0;

    for (const Check& rCheck : aChecks)
    {
        bool bFound = m_pDoc->GetNextMarkedCell(nCol, nRow, 0, aMarkData);
        if (!bFound)
        {
            std::ostringstream os;
            os << ScAddress(rCheck.mnCol, rCheck.mnRow, 0).GetColRowString() << " was expected, but not found.";
            CPPUNIT_FAIL(os.str());
        }

        CPPUNIT_ASSERT_EQUAL(rCheck.mnRow, nRow);
        CPPUNIT_ASSERT_EQUAL(rCheck.mnCol, nCol);
    }

    // No more marked cells on this sheet.
    bool bFound = m_pDoc->GetNextMarkedCell(nCol, nRow, 0, aMarkData);
    CPPUNIT_ASSERT(!bFound);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testCopyToDocument)
{
    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet", m_pDoc->InsertTab (0, u"src"_ustr));

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    m_pDoc->SetString(0, 0, 0, u"Header"_ustr);
    m_pDoc->SetString(0, 1, 0, u"1"_ustr);
    m_pDoc->SetString(0, 2, 0, u"2"_ustr);
    m_pDoc->SetString(0, 3, 0, u"3"_ustr);
    m_pDoc->SetString(0, 4, 0, u"=4/2"_ustr);
    m_pDoc->CalcAll();

    //note on A1
    ScAddress aAdrA1 (0, 0, 0); // numerical cell content
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aAdrA1);
    pNote->SetText(aAdrA1, u"Hello world in A1"_ustr);

    // Copy statically to another document.

    ScDocShellRef xDocSh2;
    getNewDocShell(xDocSh2);
    ScDocument* pDestDoc = &xDocSh2->GetDocument();
    pDestDoc->InsertTab(0, u"src"_ustr);
    pDestDoc->InitDrawLayer(xDocSh2.get());     // for note caption objects

    m_pDoc->CopyStaticToDocument(ScRange(0,1,0,0,3,0), 0, *pDestDoc); // Copy A2:A4
    m_pDoc->CopyStaticToDocument(ScRange(ScAddress(0,0,0)), 0,     *pDestDoc); // Copy A1
    m_pDoc->CopyStaticToDocument(ScRange(0,4,0,0,7,0), 0, *pDestDoc); // Copy A5:A8

    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,0,0), pDestDoc->GetString(0,0,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,1,0), pDestDoc->GetString(0,1,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,2,0), pDestDoc->GetString(0,2,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,3,0), pDestDoc->GetString(0,3,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,4,0), pDestDoc->GetString(0,4,0));

    // verify note
    CPPUNIT_ASSERT_MESSAGE("There should be a note in A1 destDocument", pDestDoc->HasNote(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("The notes content should be the same on both documents",
            m_pDoc->GetNote(ScAddress(0, 0, 0))->GetText(), pDestDoc->GetNote(ScAddress(0, 0, 0))->GetText());

    pDestDoc->DeleteTab(0);
    xDocSh2->DoClose();
    xDocSh2.clear();

    m_pDoc->DeleteTab(0);
}

bool Test::checkHorizontalIterator(ScDocument& rDoc, const std::vector<std::vector<const char*>>& rData, const HoriIterCheck* pChecks, size_t nCheckCount)
{
    ScAddress aPos(0,0,0);
    insertRangeData(&rDoc, aPos, rData);
    ScHorizontalCellIterator aIter(rDoc, 0, 0, 0, 1, rData.size() - 1);

    SCCOL nCol;
    SCROW nRow;
    size_t i = 0;
    for (ScRefCellValue* pCell = aIter.GetNext(nCol, nRow); pCell; pCell = aIter.GetNext(nCol, nRow), ++i)
    {
        if (i >= nCheckCount)
        {
            std::cerr << "hit invalid check " << i << " of " << nCheckCount << std::endl;
            CPPUNIT_FAIL("Iterator claims there is more data than there should be.");
            return false;
        }

        if (pChecks[i].nCol != nCol)
        {
            std::cerr << "Column mismatch " << pChecks[i].nCol << " vs. " << nCol << std::endl;
            return false;
        }

        if (pChecks[i].nRow != nRow)
        {
            std::cerr << "Row mismatch " << pChecks[i].nRow << " vs. " << nRow << std::endl;
            return false;
        }

        if (OUString::createFromAscii(pChecks[i].pVal) != pCell->getString(rDoc))
        {
            std::cerr << "String mismatch " << pChecks[i].pVal << " vs. " <<
                pCell->getString(rDoc) << std::endl;
            return false;
        }
    }

    return true;
}

CPPUNIT_TEST_FIXTURE(Test, testHorizontalIterator)
{
    m_pDoc->InsertTab(0, u"test"_ustr);

    {
        // Raw data - mixed types
        std::vector<std::vector<const char*>> aData = {
            { "A", "B" },
            { "C", "1" },
            { "D", "2" },
            { "E", "3" }
        };

        static const HoriIterCheck aChecks[] = {
            { 0, 0, "A" },
            { 1, 0, "B" },
            { 0, 1, "C" },
            { 1, 1, "1" },
            { 0, 2, "D" },
            { 1, 2, "2" },
            { 0, 3, "E" },
            { 1, 3, "3" },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, aChecks, std::size(aChecks));

        if (!bRes)
            CPPUNIT_FAIL("Failed on test mixed.");
    }

    {
        // Raw data - 'hole' data
        std::vector<std::vector<const char*>> aData = {
            { "A", "B" },
            { "C",  nullptr  },
            { "D", "E" },
        };

        static const HoriIterCheck aChecks[] = {
            { 0, 0, "A" },
            { 1, 0, "B" },
            { 0, 1, "C" },
            { 0, 2, "D" },
            { 1, 2, "E" },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, aChecks, std::size(aChecks));

        if (!bRes)
            CPPUNIT_FAIL("Failed on test hole.");
    }

    {
        // Very holy data
        std::vector<std::vector<const char*>> aData = {
            {  nullptr,  "A" },
            {  nullptr,   nullptr  },
            {  nullptr,  "1" },
            { "B",  nullptr  },
            { "C", "2" },
            { "D", "3" },
            { "E",  nullptr  },
            {  nullptr,  "G" },
            {  nullptr,   nullptr  },
        };

        static const HoriIterCheck aChecks[] = {
            { 1, 0, "A" },
            { 1, 2, "1" },
            { 0, 3, "B" },
            { 0, 4, "C" },
            { 1, 4, "2" },
            { 0, 5, "D" },
            { 1, 5, "3" },
            { 0, 6, "E" },
            { 1, 7, "G" },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, aChecks, std::size(aChecks));

        if (!bRes)
            CPPUNIT_FAIL("Failed on test holy.");
    }

    {
        // Degenerate case
        std::vector<std::vector<const char*>> aData = {
            {  nullptr,   nullptr },
            {  nullptr,   nullptr },
            {  nullptr,   nullptr },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, nullptr, 0);

        if (!bRes)
            CPPUNIT_FAIL("Failed on test degenerate.");
    }

    {
        // Data at end
        std::vector<std::vector<const char*>> aData = {
            {  nullptr,   nullptr },
            {  nullptr,   nullptr },
            {  nullptr,  "A" },
        };

        static const HoriIterCheck aChecks[] = {
            { 1, 2, "A" },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, aChecks, std::size(aChecks));

        if (!bRes)
            CPPUNIT_FAIL("Failed on test at end.");
    }

    {
        // Data in middle
        std::vector<std::vector<const char*>> aData = {
            {  nullptr,   nullptr  },
            {  nullptr,   nullptr  },
            {  nullptr,  "A" },
            {  nullptr,  "1" },
            {  nullptr,   nullptr  },
        };

        static const HoriIterCheck aChecks[] = {
            { 1, 2, "A" },
            { 1, 3, "1" },
        };

        bool bRes = checkHorizontalIterator(
            *m_pDoc, aData, aChecks, std::size(aChecks));

        if (!bRes)
            CPPUNIT_FAIL("Failed on test in middle.");
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testValueIterator)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Turn on "precision as shown" option.
    ScDocOptions aOpt = m_pDoc->GetDocOptions();
    aOpt.SetCalcAsShown(true);
    m_pDoc->SetDocOptions(aOpt);

    ScInterpreterContext aContext(*m_pDoc, m_pDoc->GetFormatTable());

    // Purely horizontal data layout with numeric data.
    for (SCCOL i = 1; i <= 3; ++i)
        m_pDoc->SetValue(ScAddress(i,2,0), i);

    {
        const double aChecks[] = { 1.0, 2.0, 3.0 };
        size_t const nCheckLen = std::size(aChecks);
        ScValueIterator aIter(aContext, ScRange(1,2,0,3,2,0));
        bool bHas = false;
        size_t nCheckPos = 0;
        double fVal;
        FormulaError nErr;
        for (bHas = aIter.GetFirst(fVal, nErr); bHas; bHas = aIter.GetNext(fVal, nErr), ++nCheckPos)
        {
            CPPUNIT_ASSERT_MESSAGE("Iteration longer than expected.", nCheckPos < nCheckLen);
            CPPUNIT_ASSERT_EQUAL(aChecks[nCheckPos], fVal);
            CPPUNIT_ASSERT_EQUAL(0, static_cast<int>(nErr));
        }
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testHorizontalAttrIterator)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Set the background color of B2:C3,D2,E3,C4:D4,B5:D5 to blue
    ScPatternAttr aCellBackColor(m_pDoc->getCellAttributeHelper());
    aCellBackColor.ItemSetPut(SvxBrushItem(COL_BLUE, ATTR_BACKGROUND));
    m_pDoc->ApplyPatternAreaTab(1, 1, 2, 2, 0, aCellBackColor);
    m_pDoc->ApplyPatternAreaTab(3, 1, 3, 1, 0, aCellBackColor);
    m_pDoc->ApplyPatternAreaTab(4, 2, 4, 2, 0, aCellBackColor);
    m_pDoc->ApplyPatternAreaTab(2, 3, 3, 3, 0, aCellBackColor);
    m_pDoc->ApplyPatternAreaTab(1, 4, 4, 4, 0, aCellBackColor);

    // some numeric data
    for (SCCOL i = 1; i <= 4; ++i)
        for (SCROW j = 1; j <= 4; ++j)
            m_pDoc->SetValue(ScAddress(i,j,0), i*10+j);

    {
        const int aChecks[][3] = { {1, 3, 1}, {1, 2, 2}, {4, 4, 2}, {2, 3, 3}, {1, 4, 4} };
        const size_t nCheckLen = std::size(aChecks);

        ScHorizontalAttrIterator aIter(*m_pDoc, 0, 0, 0, 5, 5);
        SCCOL nCol1, nCol2;
        SCROW nRow;
        size_t nCheckPos = 0;
        for (const ScPatternAttr* pAttr = aIter.GetNext(nCol1, nCol2, nRow); pAttr; pAttr = aIter.GetNext(nCol1, nCol2, nRow))
        {
            if (pAttr->isDefault())
                continue;
            CPPUNIT_ASSERT_MESSAGE("Iteration longer than expected.", nCheckPos < nCheckLen);
            CPPUNIT_ASSERT_EQUAL(aChecks[nCheckPos][0], static_cast<int>(nCol1));
            CPPUNIT_ASSERT_EQUAL(aChecks[nCheckPos][1], static_cast<int>(nCol2));
            CPPUNIT_ASSERT_EQUAL(aChecks[nCheckPos][2], static_cast<int>(nRow));
            ++nCheckPos;
        }
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testIteratorsUnallocatedColumnsAttributes)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);

    // Set values in first two columns, to ensure allocation of those columns.
    m_pDoc->SetValue(ScAddress(0,1,0), 1);
    m_pDoc->SetValue(ScAddress(1,1,0), 2);
    constexpr SCCOL allocatedColsCount = 2;
    assert( allocatedColsCount >= INITIALCOLCOUNT );
    CPPUNIT_ASSERT_EQUAL(allocatedColsCount, m_pDoc->GetAllocatedColumnsCount(0));

    // Make entire second row and third row bold.
    ScPatternAttr boldAttr(m_pDoc->getCellAttributeHelper());
    boldAttr.ItemSetPut(SvxWeightItem(WEIGHT_BOLD, ATTR_FONT_WEIGHT));
    m_pDoc->ApplyPatternAreaTab(0, 1, m_pDoc->MaxCol(), 2, 0, boldAttr);

    // That shouldn't need allocating more columns, just changing the default attribute.
    CPPUNIT_ASSERT_EQUAL(allocatedColsCount, m_pDoc->GetAllocatedColumnsCount(0));
    vcl::Font aFont;
    const ScPatternAttr* pattern = m_pDoc->GetPattern(m_pDoc->MaxCol(), 1, 0);
    pattern->fillFontOnly(aFont);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("font should be bold", WEIGHT_BOLD, aFont.GetWeightMaybeAskConfig());

    // Test iterators.
    ScDocAttrIterator docit( *m_pDoc, 0, allocatedColsCount - 1, 1, allocatedColsCount, 2 );
    SCCOL col1, col2;
    SCROW row1, row2;
    CPPUNIT_ASSERT_EQUAL( pattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL( SCCOL(allocatedColsCount - 1), col1 );
    CPPUNIT_ASSERT_EQUAL( SCROW(1), row1 );
    CPPUNIT_ASSERT_EQUAL( SCROW(2), row2 );
    CPPUNIT_ASSERT_EQUAL( pattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL( allocatedColsCount, col1 );
    CPPUNIT_ASSERT_EQUAL( SCROW(1), row1 );
    CPPUNIT_ASSERT_EQUAL( SCROW(2), row2 );
    CPPUNIT_ASSERT( docit.GetNext( col1, row1, row2 ) == nullptr );

    ScAttrRectIterator rectit( *m_pDoc, 0, allocatedColsCount - 1, 1, allocatedColsCount, 2 );
    CPPUNIT_ASSERT_EQUAL( pattern, rectit.GetNext( col1, col2, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL( SCCOL(allocatedColsCount - 1), col1 );
    CPPUNIT_ASSERT_EQUAL( allocatedColsCount, col2 );
    CPPUNIT_ASSERT_EQUAL( SCROW(1), row1 );
    CPPUNIT_ASSERT_EQUAL( SCROW(2), row2 );
    CPPUNIT_ASSERT( rectit.GetNext( col1, col2, row1, row2 ) == nullptr );

    ScHorizontalAttrIterator horit( *m_pDoc, 0, allocatedColsCount - 1, 1, allocatedColsCount, 2 );
    CPPUNIT_ASSERT_EQUAL( pattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT_EQUAL( SCCOL(allocatedColsCount - 1), col1 );
    CPPUNIT_ASSERT_EQUAL( allocatedColsCount, col2 );
    CPPUNIT_ASSERT_EQUAL( SCROW(1), row1 );
    CPPUNIT_ASSERT_EQUAL( pattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT_EQUAL( SCCOL(allocatedColsCount - 1), col1 );
    CPPUNIT_ASSERT_EQUAL( allocatedColsCount, col2 );
    CPPUNIT_ASSERT_EQUAL( SCROW(2), row1 );
    CPPUNIT_ASSERT( horit.GetNext( col1, col2, row1 ) == nullptr );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testIteratorsDefPattern)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);

    // The default pattern is the default style, which can be edited by the user.
    // As such iterators should not ignore it by default, because it might contain
    // some attributes set.

    // Set cells as bold, default allocated, bold, default unallocated.
    SCCOL firstCol = 100;
    SCCOL lastCol = 103;
    ScPatternAttr boldAttr(m_pDoc->getCellAttributeHelper());
    boldAttr.ItemSetPut(SvxWeightItem(WEIGHT_BOLD, ATTR_FONT_WEIGHT));
    m_pDoc->ApplyPattern(100, 0, 0, boldAttr);
    m_pDoc->ApplyPattern(102, 0, 0, boldAttr);

    CPPUNIT_ASSERT_EQUAL(SCCOL(102 + 1), m_pDoc->GetAllocatedColumnsCount(0));
    const ScPatternAttr* pattern = m_pDoc->GetPattern(100, 0, 0);
    const ScPatternAttr* defPattern(&m_pDoc->getCellAttributeHelper().getDefaultCellAttribute()); //GetDefPattern();
    CPPUNIT_ASSERT(!ScPatternAttr::areSame(pattern, defPattern));
    CPPUNIT_ASSERT_EQUAL(pattern, m_pDoc->GetPattern(102, 0, 0));
    CPPUNIT_ASSERT_EQUAL(defPattern, m_pDoc->GetPattern(101, 0, 0));
    CPPUNIT_ASSERT_EQUAL(defPattern, m_pDoc->GetPattern(103, 0, 0));

    // Test iterators.
    ScDocAttrIterator docit( *m_pDoc, 0, firstCol, 0, lastCol, 0 );
    SCCOL col1, col2;
    SCROW row1, row2;
    CPPUNIT_ASSERT_EQUAL(pattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(pattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, docit.GetNext( col1, row1, row2 ));
    CPPUNIT_ASSERT(docit.GetNext( col1, row1, row2 ) == nullptr );

    ScAttrRectIterator rectit( *m_pDoc, 0, firstCol, 0, lastCol, 0 );
    CPPUNIT_ASSERT_EQUAL(pattern, rectit.GetNext( col1, col2, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, rectit.GetNext( col1, col2, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(pattern, rectit.GetNext( col1, col2, row1, row2 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, rectit.GetNext( col1, col2, row1, row2 ));
    CPPUNIT_ASSERT(rectit.GetNext( col1, col2, row1, row2 ) == nullptr );

    ScHorizontalAttrIterator horit( *m_pDoc, 0, firstCol, 0, lastCol, 0 );
    CPPUNIT_ASSERT_EQUAL(pattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT_EQUAL(pattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT_EQUAL(defPattern, horit.GetNext( col1, col2, row1 ));
    CPPUNIT_ASSERT(horit.GetNext( col1, col2, row1 ) == nullptr );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testLastChangedColFlagsWidth)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);

    constexpr SCCOL firstChangedCol = 100;
    assert( firstChangedCol > m_pDoc->GetAllocatedColumnsCount(0));
    CPPUNIT_ASSERT_EQUAL(INITIALCOLCOUNT, m_pDoc->GetAllocatedColumnsCount(0));
    for( SCCOL col = firstChangedCol; col <= m_pDoc->MaxCol(); ++col )
        m_pDoc->SetColWidth( col, 0, 10 );

    // That shouldn't need allocating more columns, just changing column flags.
    CPPUNIT_ASSERT_EQUAL(INITIALCOLCOUNT, m_pDoc->GetAllocatedColumnsCount(0));
    // But the flags are changed.
    CPPUNIT_ASSERT_EQUAL(m_pDoc->MaxCol(), m_pDoc->GetLastChangedColFlagsWidth(0));

    m_pDoc->DeleteTab(0);
}

namespace {

bool broadcasterShifted(const ScDocument& rDoc, const ScAddress& rFrom, const ScAddress& rTo)
{
    const SvtBroadcaster* pBC = rDoc.GetBroadcaster(rFrom);
    if (pBC)
    {
        std::cerr << "Broadcaster shouldn't be here." << std::endl;
        return false;
    }

    pBC = rDoc.GetBroadcaster(rTo);
    if (!pBC)
    {
        std::cerr << "Broadcaster should be here." << std::endl;
        return false;
    }
    return true;
}

ScSingleRefToken* getSingleRefToken(ScDocument& rDoc, const ScAddress& rPos)
{
    ScFormulaCell* pFC = rDoc.GetFormulaCell(rPos);
    if (!pFC)
    {
        std::cerr << "Formula cell expected, but not found." << std::endl;
        return nullptr;
    }

    ScTokenArray* pTokens = pFC->GetCode();
    if (!pTokens)
    {
        std::cerr << "Token array is not present." << std::endl;
        return nullptr;
    }

    formula::FormulaToken* pToken = pTokens->FirstToken();
    if (!pToken || pToken->GetType() != formula::svSingleRef)
    {
        std::cerr << "Not a single reference token." << std::endl;
        return nullptr;
    }

    return static_cast<ScSingleRefToken*>(pToken);
}

bool checkRelativeRefToken(ScDocument& rDoc, const ScAddress& rPos, SCCOL nRelCol, SCROW nRelRow)
{
    auto pToken = getSingleRefToken(rDoc, rPos);
    if (!pToken)
        return false;

    ScSingleRefData& rRef = pToken->GetSingleRef();
    if (!rRef.IsColRel() || rRef.Col() != nRelCol)
    {
        std::cerr << "Unexpected relative column address." << std::endl;
        return false;
    }

    if (!rRef.IsRowRel() || rRef.Row() != nRelRow)
    {
        std::cerr << "Unexpected relative row address." << std::endl;
        return false;
    }

    return true;
}

bool checkDeletedRefToken(ScDocument& rDoc, const ScAddress& rPos)
{
    auto pToken = getSingleRefToken(rDoc, rPos);
    if (!pToken)
        return false;

    ScSingleRefData& rRef = pToken->GetSingleRef();
    if (!rRef.IsDeleted())
    {
        std::cerr << "Deleted reference is expected, but it's still a valid reference." << std::endl;
        return false;
    }

    return true;
}

}

CPPUNIT_TEST_FIXTURE(Test, testCellBroadcaster)
{
    /**
     * More direct test for cell broadcaster management, used to track formula
     * dependencies.
     */
    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet", m_pDoc->InsertTab (0, u"foo"_ustr));

    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true); // turn on auto calculation.
    m_pDoc->SetString(ScAddress(1,0,0), u"=A1"_ustr); // B1 depends on A1.
    double val = m_pDoc->GetValue(ScAddress(1,0,0)); // A1 is empty, so the result should be 0.
    CPPUNIT_ASSERT_EQUAL(0.0, val);

    const SvtBroadcaster* pBC = m_pDoc->GetBroadcaster(ScAddress(0,0,0));
    CPPUNIT_ASSERT_MESSAGE("Cell A1 should have a broadcaster.", pBC);

    // Change the value of A1 and make sure that B1 follows.
    m_pDoc->SetValue(ScAddress(0,0,0), 1.23);
    val = m_pDoc->GetValue(ScAddress(1,0,0));
    CPPUNIT_ASSERT_EQUAL(1.23, val);

    // Move column A down 5 cells. Make sure B1 now references A6, not A1.
    m_pDoc->InsertRow(0, 0, 0, 0, 0, 5);
    CPPUNIT_ASSERT_MESSAGE("Relative reference check failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,0,0), -1, 5));

    // Make sure the broadcaster has also moved.
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,0,0), ScAddress(0,5,0)));

    // Set new value to A6 and make sure B1 gets updated.
    m_pDoc->SetValue(ScAddress(0,5,0), 45.6);
    val = m_pDoc->GetValue(ScAddress(1,0,0));
    CPPUNIT_ASSERT_EQUAL(45.6, val);

    // Move column A up 3 cells, and make sure B1 now references A3, not A6.
    m_pDoc->DeleteRow(0, 0, 0, 0, 0, 3);
    CPPUNIT_ASSERT_MESSAGE("Relative reference check failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,0,0), -1, 2));

    // The broadcaster should also have been relocated from A6 to A3.
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,5,0), ScAddress(0,2,0)));

    // Insert cells over A1:A10 and shift cells to right.
    m_pDoc->InsertCol(ScRange(0, 0, 0, 0, 10, 0));
    CPPUNIT_ASSERT_MESSAGE("Relative reference check failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(2,0,0), -1, 2));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,2,0), ScAddress(1,2,0)));

    // Delete formula in C2, which should remove the broadcaster in B3.
    pBC = m_pDoc->GetBroadcaster(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster in B3 should still exist.", pBC);
    clearRange(m_pDoc, ScRange(ScAddress(2,0,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_NONE, m_pDoc->GetCellType(ScAddress(2,0,0))); // C2 should be empty.
    pBC = m_pDoc->GetBroadcaster(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster in B3 should have been removed.", !pBC);

    // Clear everything and start over.
    clearRange(m_pDoc, ScRange(0,0,0,10,100,0));

    m_pDoc->SetString(ScAddress(1,0,0), u"=A1"_ustr); // B1 depends on A1.
    pBC = m_pDoc->GetBroadcaster(ScAddress(0,0,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist in A1.", pBC);

    // While column A is still empty, move column A down 2 cells. This should
    // move the broadcaster from A1 to A3.
    m_pDoc->InsertRow(0, 0, 0, 0, 0, 2);
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,0,0), ScAddress(0,2,0)));

    // Move it back while column A is still empty.
    m_pDoc->DeleteRow(0, 0, 0, 0, 0, 2);
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,2,0), ScAddress(0,0,0)));

    // Clear everything again
    clearRange(m_pDoc, ScRange(0,0,0,10,100,0));

    // B1:B3 depends on A1:A3
    m_pDoc->SetString(ScAddress(1,0,0), u"=A1"_ustr);
    m_pDoc->SetString(ScAddress(1,1,0), u"=A2"_ustr);
    m_pDoc->SetString(ScAddress(1,2,0), u"=A3"_ustr);
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B1 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,0,0), -1, 0));
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B2 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,1,0), -1, 0));
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B3 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,2,0), -1, 0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist in A1.", m_pDoc->GetBroadcaster(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist in A2.", m_pDoc->GetBroadcaster(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist in A3.", m_pDoc->GetBroadcaster(ScAddress(0,2,0)));

    // Insert Rows at row 2, down 5 rows.
    m_pDoc->InsertRow(0, 0, 0, 0, 1, 5);
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist in A1.", m_pDoc->GetBroadcaster(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B1 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,0,0), -1, 0));

    // Broadcasters in A2 and A3 should shift down by 5 rows.
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,1,0), ScAddress(0,6,0)));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster relocation failed.",
                           broadcasterShifted(*m_pDoc, ScAddress(0,2,0), ScAddress(0,7,0)));

    // B2 and B3 should reference shifted cells.
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B2 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,1,0), -1, 5));
    CPPUNIT_ASSERT_MESSAGE("Relative reference check in B2 failed.",
                           checkRelativeRefToken(*m_pDoc, ScAddress(1,2,0), -1, 5));

    // Delete cells with broadcasters.
    m_pDoc->DeleteRow(0, 0, 0, 0, 4, 6);
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should NOT exist in A7.", !m_pDoc->GetBroadcaster(ScAddress(0,6,0)));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should NOT exist in A8.", !m_pDoc->GetBroadcaster(ScAddress(0,7,0)));

    // References in B2 and B3 should be invalid.
    CPPUNIT_ASSERT_MESSAGE("Deleted reference check in B2 failed.",
                           checkDeletedRefToken(*m_pDoc, ScAddress(1,1,0)));
    CPPUNIT_ASSERT_MESSAGE("Deleted reference check in B3 failed.",
                           checkDeletedRefToken(*m_pDoc, ScAddress(1,2,0)));

    // Clear everything again
    clearRange(m_pDoc, ScRange(0,0,0,10,100,0));

    {
        // Switch to R1C1 to make it easier to input relative references in multiple cells.
        FormulaGrammarSwitch aFGSwitch(m_pDoc, formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1);

        // Have B1:B20 reference A1:A20.
        val = 0.0;
        for (SCROW i = 0; i < 20; ++i)
        {
            m_pDoc->SetValue(ScAddress(0,i,0), val++);
            m_pDoc->SetString(ScAddress(1,i,0), u"=RC[-1]"_ustr);
        }
    }

    // Ensure that the formula cells show correct values, and the referenced
    // cells have broadcasters.
    val = 0.0;
    for (SCROW i = 0; i < 20; ++i, ++val)
    {
        CPPUNIT_ASSERT_EQUAL(val, m_pDoc->GetValue(ScAddress(1,i,0)));
        pBC = m_pDoc->GetBroadcaster(ScAddress(0,i,0));
        CPPUNIT_ASSERT_MESSAGE("Broadcast should exist here.", pBC);
    }

    // Delete formula cells in B2:B19.
    clearRange(m_pDoc, ScRange(1,1,0,1,18,0));
    // Ensure that A2:A19 no longer have broadcasters, but A1 and A20 still do.
    CPPUNIT_ASSERT_MESSAGE("A1 should still have broadcaster.", m_pDoc->GetBroadcaster(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_MESSAGE("A20 should still have broadcaster.", m_pDoc->GetBroadcaster(ScAddress(0,19,0)));
    for (SCROW i = 1; i <= 18; ++i)
    {
        pBC = m_pDoc->GetBroadcaster(ScAddress(0,i,0));
        CPPUNIT_ASSERT_MESSAGE("Broadcaster should have been deleted.", !pBC);
    }

    // Clear everything again
    clearRange(m_pDoc, ScRange(0,0,0,10,100,0));

    m_pDoc->SetValue(ScAddress(0,0,0), 2.0);
    m_pDoc->SetString(ScAddress(1,0,0), u"=A1"_ustr);
    m_pDoc->SetString(ScAddress(2,0,0), u"=B1"_ustr);
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(0,0,0));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(1,0,0));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(2,0,0));

    pBC = m_pDoc->GetBroadcaster(ScAddress(0,0,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist here.", pBC);
    pBC = m_pDoc->GetBroadcaster(ScAddress(1,0,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist here.", pBC);

    // Change the value of A1 and make sure everyone follows suit.
    m_pDoc->SetValue(ScAddress(0,0,0), 3.5);
    CPPUNIT_ASSERT_EQUAL(3.5, m_pDoc->GetValue(0,0,0));
    CPPUNIT_ASSERT_EQUAL(3.5, m_pDoc->GetValue(1,0,0));
    CPPUNIT_ASSERT_EQUAL(3.5, m_pDoc->GetValue(2,0,0));

    // Insert a column at column B.
    m_pDoc->InsertCol(ScRange(1,0,0,1,m_pDoc->MaxRow(),0));
    pBC = m_pDoc->GetBroadcaster(ScAddress(0,0,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist here.", pBC);
    pBC = m_pDoc->GetBroadcaster(ScAddress(2,0,0));
    CPPUNIT_ASSERT_MESSAGE("Broadcaster should exist here.", pBC);

    // Change the value of A1 again.
    m_pDoc->SetValue(ScAddress(0,0,0), 5.5);
    CPPUNIT_ASSERT_EQUAL(5.5, m_pDoc->GetValue(0,0,0));
    CPPUNIT_ASSERT_EQUAL(5.5, m_pDoc->GetValue(2,0,0));
    CPPUNIT_ASSERT_EQUAL(5.5, m_pDoc->GetValue(3,0,0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFuncParam)
{

    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet",
                            m_pDoc->InsertTab (0, u"foo"_ustr));

    // First, the normal case, with no missing parameters.
    m_pDoc->SetString(0, 0, 0, u"=AVERAGE(1;2;3)"_ustr);
    m_pDoc->CalcFormulaTree(false, false);
    double val = m_pDoc->GetValue(0, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 2.0, val);

    // Now function with missing parameters.  Missing values should be treated
    // as zeros.
    m_pDoc->SetString(0, 0, 0, u"=AVERAGE(1;;;)"_ustr);
    m_pDoc->CalcFormulaTree(false, false);
    val = m_pDoc->GetValue(0, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 0.25, val);

    // Conversion of string to numeric argument.
    m_pDoc->SetString(0, 0, 0, u"=\"\"+3"_ustr);    // empty string
    m_pDoc->SetString(0, 1, 0, u"=\" \"+3"_ustr);   // only blank
    m_pDoc->SetString(0, 2, 0, u"=\" 4 \"+3"_ustr); // number in blanks
    m_pDoc->SetString(0, 3, 0, u"=\" x \"+3"_ustr); // non-numeric
    m_pDoc->SetString(0, 4, 0, u"=\"4.4\"+3"_ustr); // locale dependent

    OUString aVal;
    ScCalcConfig aConfig;

    // With "Convert also locale dependent" and "Empty string as zero"=True option.
    aConfig.meStringConversion = ScCalcConfig::StringConversion::LOCALE;
    aConfig.mbEmptyStringAsZero = true;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    val = m_pDoc->GetValue(0, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 1, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 2, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.0, val);
    aVal = m_pDoc->GetString( 0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    val = m_pDoc->GetValue(0, 4, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.4, val);

    // With "Convert also locale dependent" and "Empty string as zero"=False option.
    aConfig.meStringConversion = ScCalcConfig::StringConversion::LOCALE;
    aConfig.mbEmptyStringAsZero = false;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    aVal = m_pDoc->GetString( 0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 1, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    val = m_pDoc->GetValue(0, 2, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.0, val);
    aVal = m_pDoc->GetString( 0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    val = m_pDoc->GetValue(0, 4, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.4, val);

    // With "Convert only unambiguous" and "Empty string as zero"=True option.
    aConfig.meStringConversion = ScCalcConfig::StringConversion::UNAMBIGUOUS;
    aConfig.mbEmptyStringAsZero = true;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    val = m_pDoc->GetValue(0, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 1, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 2, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.0, val);
    aVal = m_pDoc->GetString( 0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 4, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);

    // With "Convert only unambiguous" and "Empty string as zero"=False option.
    aConfig.meStringConversion = ScCalcConfig::StringConversion::UNAMBIGUOUS;
    aConfig.mbEmptyStringAsZero = false;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    aVal = m_pDoc->GetString( 0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 1, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    m_pDoc->GetValue(0, 2, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 7.0, val);
    aVal = m_pDoc->GetString( 0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 4, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);

    // With "Treat as zero" ("Empty string as zero" is ignored).
    aConfig.meStringConversion = ScCalcConfig::StringConversion::ZERO;
    aConfig.mbEmptyStringAsZero = true;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    val = m_pDoc->GetValue(0, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 1, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 2, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 3, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);
    val = m_pDoc->GetValue(0, 4, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("incorrect result", 3.0, val);

    // With "Generate #VALUE! error" ("Empty string as zero" is ignored).
    aConfig.meStringConversion = ScCalcConfig::StringConversion::ILLEGAL;
    aConfig.mbEmptyStringAsZero = false;
    m_pDoc->SetCalcConfig(aConfig);
    m_pDoc->CalcAll();
    aVal = m_pDoc->GetString( 0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 1, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 2, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);
    aVal = m_pDoc->GetString( 0, 4, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("incorrect result", u"#VALUE!"_ustr, aVal);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNamedRange)
{
    static const RangeNameDef aNames[] = {
        { "Divisor",  "$Sheet1.$A$1:$A$1048576", 1 },
        { "MyRange1", "$Sheet1.$A$1:$A$100",     2 },
        { "MyRange2", "$Sheet1.$B$1:$B$100",     3 },
        { "MyRange3", "$Sheet1.$C$1:$C$100",     4 }
    };

    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet", m_pDoc->InsertTab (0, u"Sheet1"_ustr));

    m_pDoc->SetValue (0, 0, 0, 101);

    std::unique_ptr<ScRangeName> pNames(new ScRangeName);
    bool bSuccess = insertRangeNames(m_pDoc, pNames.get(), aNames, aNames + std::size(aNames));
    CPPUNIT_ASSERT_MESSAGE("Failed to insert range names.", bSuccess);
    m_pDoc->SetRangeName(std::move(pNames));

    ScRangeName& rNewRanges = m_pDoc->GetRangeName();

    // Make sure the index lookup does the right thing.
    for (const auto& rName : aNames)
    {
        const ScRangeData* p = rNewRanges.findByIndex(rName.mnIndex);
        CPPUNIT_ASSERT_MESSAGE("lookup of range name by index failed.", p);
        OUString aName = p->GetName();
        CPPUNIT_ASSERT_MESSAGE("wrong range name is retrieved.", aName.equalsAscii(rName.mpName));
    }

    // Test usage in formula expression.
    m_pDoc->SetString (1, 0, 0, u"=A1/Divisor"_ustr);
    m_pDoc->CalcAll();

    double result = m_pDoc->GetValue (1, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE ("calculation failed", 1.0, result);

    // Test copy-ability of range names.
    std::unique_ptr<ScRangeName> pCopiedRanges(new ScRangeName(rNewRanges));
    m_pDoc->SetRangeName(std::move(pCopiedRanges));
    // Make sure the index lookup still works.
    for (const auto& rName : aNames)
    {
        const ScRangeData* p = m_pDoc->GetRangeName().findByIndex(rName.mnIndex);
        CPPUNIT_ASSERT_MESSAGE("lookup of range name by index failed with the copied instance.", p);
        OUString aName = p->GetName();
        CPPUNIT_ASSERT_MESSAGE("wrong range name is retrieved with the copied instance.", aName.equalsAscii(rName.mpName));
    }

    // Test using another-sheet-local name, scope Sheet1.
    ScRangeData* pLocal1 = new ScRangeData( *m_pDoc, u"local1"_ustr, ScAddress(0,0,0));
    ScRangeData* pLocal2 = new ScRangeData( *m_pDoc, u"local2"_ustr, u"$Sheet1.$A$1"_ustr);
    ScRangeData* pLocal3 = new ScRangeData( *m_pDoc, u"local3"_ustr, u"Sheet1.$A$1"_ustr);
    ScRangeData* pLocal4 = new ScRangeData( *m_pDoc, u"local4"_ustr, u"$A$1"_ustr); // implicit relative sheet reference
    std::unique_ptr<ScRangeName> pLocalRangeName1(new ScRangeName);
    pLocalRangeName1->insert(pLocal1);
    pLocalRangeName1->insert(pLocal2);
    pLocalRangeName1->insert(pLocal3);
    pLocalRangeName1->insert(pLocal4);
    m_pDoc->SetRangeName(0, std::move(pLocalRangeName1));

    CPPUNIT_ASSERT_MESSAGE ("failed to insert sheet", m_pDoc->InsertTab (1, u"Sheet2"_ustr));

    // Use other-sheet-local name of Sheet1 on Sheet2.
    ScAddress aPos(1,0,1);
    OUString aFormula(u"=Sheet1.local1+Sheet1.local2+Sheet1.local3+Sheet1.local4"_ustr);
    m_pDoc->SetString(aPos, aFormula);
    OUString aString = m_pDoc->GetFormula(1,0,1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("formula string should be equal", aFormula, aString);
    double fValue = m_pDoc->GetValue(aPos);
    ASSERT_DOUBLES_EQUAL_MESSAGE("value should be 4 times Sheet1.A1", 404.0, fValue);

    m_pDoc->DeleteTab(1);
    m_pDoc->SetRangeName(0,nullptr); // Delete the names.
    m_pDoc->SetRangeName(nullptr); // Delete the names.
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testInsertNameList)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    static const RangeNameDef aNames[] = {
        { "MyRange1", "$Test.$A$1:$A$100", 1 },
        { "MyRange2", "$Test.$B$1:$B$100", 2 },
        { "MyRange3", "$Test.$C$1:$C$100", 3 }
    };

    std::unique_ptr<ScRangeName> pNames(new ScRangeName);
    bool bSuccess = insertRangeNames(m_pDoc, pNames.get(), aNames, aNames + std::size(aNames));
    CPPUNIT_ASSERT_MESSAGE("Failed to insert range names.", bSuccess);
    m_pDoc->SetRangeName(std::move(pNames));

    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    ScAddress aPos(1,1,0);
    rDocFunc.InsertNameList(aPos, true);

    for (auto const& rName : aNames)
    {
        OUString aName = m_pDoc->GetString(aPos);
        CPPUNIT_ASSERT_EQUAL(OUString::createFromAscii(rName.mpName), aName);
        ScAddress aExprPos = aPos;
        aExprPos.IncCol();
        OUString aExpr = m_pDoc->GetString(aExprPos);
        OUString aExpected = "=" + OUString::createFromAscii(rName.mpExpr);
        CPPUNIT_ASSERT_EQUAL(aExpected, aExpr);
        aPos.IncRow();
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testCSV)
{
    const int English = 0, European = 1;
    struct {
        const char *pStr; int eSep; bool bResult; double nValue;
    } aTests[] = {
        { "foo",       English,  false, 0.0 },
        { "1.0",       English,  true,  1.0 },
        { "1,0",       English,  false, 0.0 },
        { "1.0",       European, false, 0.0 },
        { "1.000",     European, true,  1000.0 },
        { "1,000",     European, true,  1.0 },
        { "1.000",     English,  true,  1.0 },
        { "1,000",     English,  true,  1000.0 },
        { " 1.0",      English,  true,  1.0 },
        { " 1.0  ",    English,  true,  1.0 },
        { "1.0 ",      European, false, 0.0 },
        { "1.000",     European, true,  1000.0 },
        { "1137.999",  English,  true,  1137.999 },
        { "1.000.00",  European, false, 0.0 },
        { "+,123",     English,  false, 0.0 },
        { "-,123",     English,  false, 0.0 }
    };
    for (const auto& rTest : aTests) {
        OUString aStr(rTest.pStr, strlen (rTest.pStr), RTL_TEXTENCODING_UTF8);
        double nValue = 0.0;
        bool bResult = ScStringUtil::parseSimpleNumber
                (aStr, rTest.eSep == English ? '.' : ',',
                 rTest.eSep == English ? ',' : '.',
                 0,
                 nValue);
        CPPUNIT_ASSERT_EQUAL_MESSAGE ("CSV numeric detection failure", rTest.bResult, bResult);
        CPPUNIT_ASSERT_EQUAL_MESSAGE ("CSV numeric value failure", rTest.nValue, nValue);
    }
}

template<typename Evaluator>
static void checkMatrixElements(const ScMatrix& rMat)
{
    SCSIZE nC, nR;
    rMat.GetDimensions(nC, nR);
    Evaluator aEval;
    for (SCSIZE i = 0; i < nC; ++i)
    {
        for (SCSIZE j = 0; j < nR; ++j)
        {
            aEval(i, j, rMat.Get(i, j));
        }
    }
}

namespace {

struct AllZeroMatrix
{
    void operator() (SCSIZE /*nCol*/, SCSIZE /*nRow*/, const ScMatrixValue& rVal) const
    {
        CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of numeric type", int(ScMatValType::Value), static_cast<int>(rVal.nType));
        ASSERT_DOUBLES_EQUAL_MESSAGE("element value must be zero", 0.0, rVal.fVal);
    }
};

struct PartiallyFilledZeroMatrix
{
    void operator() (SCSIZE nCol, SCSIZE nRow, const ScMatrixValue& rVal) const
    {
        CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of numeric type", int(ScMatValType::Value), static_cast<int>(rVal.nType));
        if (1 <= nCol && nCol <= 2 && 2 <= nRow && nRow <= 8)
        {
            ASSERT_DOUBLES_EQUAL_MESSAGE("element value must be 3.0", 3.0, rVal.fVal);
        }
        else
        {
            ASSERT_DOUBLES_EQUAL_MESSAGE("element value must be zero", 0.0, rVal.fVal);
        }
    }
};

struct AllEmptyMatrix
{
    void operator() (SCSIZE /*nCol*/, SCSIZE /*nRow*/, const ScMatrixValue& rVal) const
    {
        CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of empty type", int(ScMatValType::Empty), static_cast<int>(rVal.nType));
        ASSERT_DOUBLES_EQUAL_MESSAGE("value of \"empty\" element is expected to be zero", 0.0, rVal.fVal);
    }
};

struct PartiallyFilledEmptyMatrix
{
    void operator() (SCSIZE nCol, SCSIZE nRow, const ScMatrixValue& rVal) const
    {
        if (nCol == 1 && nRow == 1)
        {
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of boolean type", int(ScMatValType::Boolean), static_cast<int>(rVal.nType));
            ASSERT_DOUBLES_EQUAL_MESSAGE("element value is not what is expected", 1.0, rVal.fVal);
        }
        else if (nCol == 4 && nRow == 5)
        {
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of value type", int(ScMatValType::Value), static_cast<int>(rVal.nType));
            ASSERT_DOUBLES_EQUAL_MESSAGE("element value is not what is expected", -12.5, rVal.fVal);
        }
        else if (nCol == 8 && nRow == 2)
        {
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of value type", int(ScMatValType::String), static_cast<int>(rVal.nType));
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element value is not what is expected", u"Test"_ustr, rVal.aStr.getString());
        }
        else if (nCol == 8 && nRow == 11)
        {
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of empty path type", int(ScMatValType::EmptyPath), static_cast<int>(rVal.nType));
            ASSERT_DOUBLES_EQUAL_MESSAGE("value of \"empty\" element is expected to be zero", 0.0, rVal.fVal);
        }
        else
        {
            CPPUNIT_ASSERT_EQUAL_MESSAGE("element is not of empty type", int(ScMatValType::Empty), static_cast<int>(rVal.nType));
            ASSERT_DOUBLES_EQUAL_MESSAGE("value of \"empty\" element is expected to be zero", 0.0, rVal.fVal);
        }
    }
};

}

CPPUNIT_TEST_FIXTURE(Test, testMatrix)
{
    svl::SharedStringPool& rPool = m_pDoc->GetSharedStringPool();
    ScMatrixRef pMat, pMat2;

    // First, test the zero matrix type.
    pMat = new ScMatrix(0, 0, 0.0);
    SCSIZE nC, nR;
    pMat->GetDimensions(nC, nR);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix is not empty", SCSIZE(0), nC);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix is not empty", SCSIZE(0), nR);
    pMat->Resize(4, 10, 0.0);
    pMat->GetDimensions(nC, nR);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix size is not as expected", SCSIZE(4), nC);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix size is not as expected", SCSIZE(10), nR);
    CPPUNIT_ASSERT_MESSAGE("both 'and' and 'or' should evaluate to false",
                           !pMat->And());
    CPPUNIT_ASSERT_MESSAGE("both 'and' and 'or' should evaluate to false",
                           !pMat->Or());

    // Resizing into a larger matrix should fill the void space with zeros.
    checkMatrixElements<AllZeroMatrix>(*pMat);

    pMat->FillDouble(3.0, 1, 2, 2, 8);
    checkMatrixElements<PartiallyFilledZeroMatrix>(*pMat);
    CPPUNIT_ASSERT_MESSAGE("matrix is expected to be numeric", pMat->IsNumeric());
    CPPUNIT_ASSERT_MESSAGE("partially non-zero matrix should evaluate false on 'and' and true on 'or",
                           !pMat->And());
    CPPUNIT_ASSERT_MESSAGE("partially non-zero matrix should evaluate false on 'and' and true on 'or",
                           pMat->Or());
    pMat->FillDouble(5.0, 0, 0, nC-1, nR-1);
    CPPUNIT_ASSERT_MESSAGE("fully non-zero matrix should evaluate true both on 'and' and 'or",
                           pMat->And());
    CPPUNIT_ASSERT_MESSAGE("fully non-zero matrix should evaluate true both on 'and' and 'or",
                           pMat->Or());

    // Test the AND and OR evaluations.
    pMat = new ScMatrix(2, 2, 0.0);

    // Only some of the elements are non-zero.
    pMat->PutBoolean(true, 0, 0);
    pMat->PutDouble(1.0, 1, 1);
    CPPUNIT_ASSERT_MESSAGE("incorrect OR result", pMat->Or());
    CPPUNIT_ASSERT_MESSAGE("incorrect AND result", !pMat->And());

    // All of the elements are non-zero.
    pMat->PutBoolean(true, 0, 1);
    pMat->PutDouble(2.3, 1, 0);
    CPPUNIT_ASSERT_MESSAGE("incorrect OR result", pMat->Or());
    CPPUNIT_ASSERT_MESSAGE("incorrect AND result", pMat->And());

    // Now test the empty matrix type.
    pMat = new ScMatrix(10, 20);
    pMat->GetDimensions(nC, nR);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix size is not as expected", SCSIZE(10), nC);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("matrix size is not as expected", SCSIZE(20), nR);
    checkMatrixElements<AllEmptyMatrix>(*pMat);

    pMat->PutBoolean(true, 1, 1);
    pMat->PutDouble(-12.5, 4, 5);
    pMat->PutString(rPool.intern(u"Test"_ustr), 8, 2);
    pMat->PutEmptyPath(8, 11);
    checkMatrixElements<PartiallyFilledEmptyMatrix>(*pMat);

    // Test resizing.
    pMat = new ScMatrix(0, 0);
    pMat->Resize(2, 2, 1.5);
    pMat->PutEmpty(1, 1);

    CPPUNIT_ASSERT_EQUAL(1.5, pMat->GetDouble(0, 0));
    CPPUNIT_ASSERT_EQUAL(1.5, pMat->GetDouble(0, 1));
    CPPUNIT_ASSERT_EQUAL(1.5, pMat->GetDouble(1, 0));
    CPPUNIT_ASSERT_MESSAGE("PutEmpty() call failed.", pMat->IsEmpty(1, 1));

    // Max and min values.
    pMat = new ScMatrix(2, 2, 0.0);
    pMat->PutDouble(-10, 0, 0);
    pMat->PutDouble(-12, 0, 1);
    pMat->PutDouble(-8, 1, 0);
    pMat->PutDouble(-25, 1, 1);
    CPPUNIT_ASSERT_EQUAL(-25.0, pMat->GetMinValue(false));
    CPPUNIT_ASSERT_EQUAL(-8.0, pMat->GetMaxValue(false));
    pMat->PutString(rPool.intern(u"Test"_ustr), 0, 0);
    CPPUNIT_ASSERT_EQUAL(0.0, pMat->GetMaxValue(true)); // text as zero.
    CPPUNIT_ASSERT_EQUAL(-8.0, pMat->GetMaxValue(false)); // ignore text.
    pMat->PutBoolean(true, 0, 0);
    CPPUNIT_ASSERT_EQUAL(1.0, pMat->GetMaxValue(false));
    pMat = new ScMatrix(2, 2, 10.0);
    pMat->PutBoolean(false, 0, 0);
    pMat->PutDouble(12.5, 1, 1);
    CPPUNIT_ASSERT_EQUAL(0.0, pMat->GetMinValue(false));
    CPPUNIT_ASSERT_EQUAL(12.5, pMat->GetMaxValue(false));

    // Convert matrix into a linear double array. String elements become NaN
    // and empty elements become 0.
    pMat = new ScMatrix(3, 3);
    pMat->PutDouble(2.5, 0, 0);
    pMat->PutDouble(1.2, 0, 1);
    pMat->PutString(rPool.intern(u"A"_ustr), 1, 1);
    pMat->PutDouble(2.3, 2, 1);
    pMat->PutDouble(-20, 2, 2);

    static const double fNaN = std::numeric_limits<double>::quiet_NaN();

    std::vector<double> aDoubles;
    pMat->GetDoubleArray(aDoubles);

    {
        const double pChecks[] = { 2.5, 1.2, 0, 0, fNaN, 0, 0, 2.3, -20 };
        CPPUNIT_ASSERT_EQUAL(SAL_N_ELEMENTS(pChecks), aDoubles.size());
        for (size_t i = 0, n = aDoubles.size(); i < n; ++i)
        {
            if (std::isnan(pChecks[i]))
                CPPUNIT_ASSERT_MESSAGE("NaN is expected, but it's not.", std::isnan(aDoubles[i]));
            else
                CPPUNIT_ASSERT_EQUAL(pChecks[i], aDoubles[i]);
        }
    }

    pMat2 = new ScMatrix(3, 3, 10.0);
    pMat2->PutString(rPool.intern(u"B"_ustr), 1, 0);
    pMat2->MergeDoubleArrayMultiply(aDoubles);

    {
        const double pChecks[] = { 25, 12, 0, fNaN, fNaN, 0, 0, 23, -200 };
        CPPUNIT_ASSERT_EQUAL(SAL_N_ELEMENTS(pChecks), aDoubles.size());
        for (size_t i = 0, n = aDoubles.size(); i < n; ++i)
        {
            if (std::isnan(pChecks[i]))
                CPPUNIT_ASSERT_MESSAGE("NaN is expected, but it's not.", std::isnan(aDoubles[i]));
            else
                CPPUNIT_ASSERT_EQUAL(pChecks[i], aDoubles[i]);
        }
    }
}

CPPUNIT_TEST_FIXTURE(Test, testMatrixComparisonWithErrors)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    // Insert the source values in A1:A2.
    m_pDoc->SetString(0, 0, 0, u"=1/0"_ustr);
    m_pDoc->SetValue( 0, 1, 0, 1.0);

    // Create a matrix formula in B3:B4 referencing A1:A2 and doing a greater
    // than comparison on it's values. Error value must be propagated.
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    m_pDoc->InsertMatrixFormula(1, 2, 1, 3, aMark, u"=A1:A2>0"_ustr);

    CPPUNIT_ASSERT_EQUAL(u"#DIV/0!"_ustr, m_pDoc->GetString(0,0,0));
    CPPUNIT_ASSERT_EQUAL(1.0,                 m_pDoc->GetValue( 0,1,0));
    CPPUNIT_ASSERT_EQUAL(u"#DIV/0!"_ustr, m_pDoc->GetString(1,2,0));
    CPPUNIT_ASSERT_EQUAL(u"TRUE"_ustr,    m_pDoc->GetString(1,3,0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMatrixConditionalBooleanResult)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    // Create matrix formulas in A1:B1,A2:B2,A3:B3,A4:B4 producing mixed
    // boolean and numeric results in an unformatted area.
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    m_pDoc->InsertMatrixFormula( 0,0, 1,0, aMark, u"=IF({1;0};TRUE();42)"_ustr);  // {TRUE,42}
    m_pDoc->InsertMatrixFormula( 0,1, 1,1, aMark, u"=IF({0;1};TRUE();42)"_ustr);  // {42,1} aim for {42,TRUE}
    m_pDoc->InsertMatrixFormula( 0,2, 1,2, aMark, u"=IF({1;0};42;FALSE())"_ustr); // {42,0} aim for {42,FALSE}
    m_pDoc->InsertMatrixFormula( 0,3, 1,3, aMark, u"=IF({0;1};42;FALSE())"_ustr); // {FALSE,42}

    CPPUNIT_ASSERT_EQUAL( u"TRUE"_ustr,  m_pDoc->GetString(0,0,0));
    CPPUNIT_ASSERT_EQUAL( u"42"_ustr,    m_pDoc->GetString(1,0,0));
    CPPUNIT_ASSERT_EQUAL( u"42"_ustr,    m_pDoc->GetString(0,1,0));
    //CPPUNIT_ASSERT_EQUAL( OUString("TRUE"),  m_pDoc->GetString(1,1,0));   // not yet
    CPPUNIT_ASSERT_EQUAL( u"42"_ustr,    m_pDoc->GetString(0,2,0));
    //CPPUNIT_ASSERT_EQUAL( OUString("FALSE"), m_pDoc->GetString(1,2,0));   // not yet
    CPPUNIT_ASSERT_EQUAL( u"FALSE"_ustr, m_pDoc->GetString(0,3,0));
    CPPUNIT_ASSERT_EQUAL( u"42"_ustr,    m_pDoc->GetString(1,3,0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testEnterMixedMatrix)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    // Insert the source values in A1:B2.
    m_pDoc->SetString(0, 0, 0, u"A"_ustr);
    m_pDoc->SetString(1, 0, 0, u"B"_ustr);
    double val = 1.0;
    m_pDoc->SetValue(0, 1, 0, val);
    val = 2.0;
    m_pDoc->SetValue(1, 1, 0, val);

    // Create a matrix range in A4:B5 referencing A1:B2.
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    m_pDoc->InsertMatrixFormula(0, 3, 1, 4, aMark, u"=A1:B2"_ustr);

    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(0,0,0), m_pDoc->GetString(0,3,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetString(1,0,0), m_pDoc->GetString(1,3,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetValue(0,1,0), m_pDoc->GetValue(0,4,0));
    CPPUNIT_ASSERT_EQUAL(m_pDoc->GetValue(1,1,0), m_pDoc->GetValue(1,4,0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMatrixEditable)
{
    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true); // turn auto calc on.

    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Values in A1:B1.
    m_pDoc->SetValue(ScAddress(0,0,0), 1.0);
    m_pDoc->SetValue(ScAddress(1,0,0), 2.0);

    // A2 is a normal formula.
    m_pDoc->SetString(ScAddress(0,1,0), u"=5"_ustr);

    // A3:A4 is a matrix.
    ScRange aMatRange(0,2,0,0,3,0);
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SetMarkArea(aMatRange);
    m_pDoc->InsertMatrixFormula(0, 2, 0, 3, aMark, u"=TRANSPOSE(A1:B1)"_ustr);

    // Check their values.
    CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,2,0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0,3,0)));

    // Make sure A3:A4 is a matrix.
    ScFormulaCell* pFC = m_pDoc->GetFormulaCell(ScAddress(0,2,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A3 should be matrix origin.",
                               ScMatrixMode::Formula, pFC->GetMatrixFlag());

    pFC = m_pDoc->GetFormulaCell(ScAddress(0,3,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("A4 should be matrix reference.",
                               ScMatrixMode::Reference, pFC->GetMatrixFlag());

    // Check to make sure A3:A4 combined is editable.
    ScEditableTester aTester;
    aTester.TestSelection(*m_pDoc, aMark);
    CPPUNIT_ASSERT(aTester.IsEditable());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testCellCopy)
{
    m_pDoc->InsertTab(0, u"TestTab"_ustr);
    ScAddress aSrc(0,0,0);
    ScAddress aDest(0,1,0);
    OUString aStr(u"please copy me"_ustr);
    m_pDoc->SetString(aSrc, u"please copy me"_ustr);
    CPPUNIT_ASSERT_EQUAL(aStr, m_pDoc->GetString(aSrc));
    // copy to self - why not ?
    m_pDoc->CopyCellToDocument(aSrc,aDest,*m_pDoc);
    CPPUNIT_ASSERT_EQUAL(aStr, m_pDoc->GetString(aDest));
}

CPPUNIT_TEST_FIXTURE(Test, testSheetCopy)
{
    m_pDoc->InsertTab(0, u"TestTab"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document should have one sheet to begin with.",
                               static_cast<SCTAB>(1), m_pDoc->GetTableCount());

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    // Insert text in A1.
    m_pDoc->SetString(ScAddress(0,0,0), u"copy me"_ustr);

    // Insert edit cells in B1:B3.
    ScFieldEditEngine& rEE = m_pDoc->GetEditEngine();
    rEE.SetTextCurrentDefaults(u"Edit 1"_ustr);
    m_pDoc->SetEditText(ScAddress(1,0,0), rEE.CreateTextObject());
    rEE.SetTextCurrentDefaults(u"Edit 2"_ustr);
    m_pDoc->SetEditText(ScAddress(1,1,0), rEE.CreateTextObject());
    rEE.SetTextCurrentDefaults(u"Edit 3"_ustr);
    m_pDoc->SetEditText(ScAddress(1,2,0), rEE.CreateTextObject());

    SCROW nRow1, nRow2;
    bool bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("new sheet should have all rows visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", m_pDoc->MaxRow(), nRow2);

    // insert a note
    ScAddress aAdrA1 (0,2,0); // empty cell content.
    ScPostIt *pNoteA1 = m_pDoc->GetOrCreateNote(aAdrA1);
    pNoteA1->SetText(aAdrA1, u"Hello world in A3"_ustr);

    // Copy and test the result.
    m_pDoc->CopyTab(0, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document now should have two sheets.",
                               static_cast<SCTAB>(2), m_pDoc->GetTableCount());

    bHidden = m_pDoc->RowHidden(0, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("copied sheet should also have all rows visible as the original.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("copied sheet should also have all rows visible as the original.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("copied sheet should also have all rows visible as the original.", m_pDoc->MaxRow(), nRow2);
    CPPUNIT_ASSERT_MESSAGE("There should be note on A3 in new sheet", m_pDoc->HasNote(ScAddress(0,2,1)));
    CPPUNIT_ASSERT_EQUAL(u"copy me"_ustr, m_pDoc->GetString(ScAddress(0,0,1)));

    // Check the copied edit cells.
    const EditTextObject* pEditObj = m_pDoc->GetEditText(ScAddress(1,0,1));
    CPPUNIT_ASSERT_MESSAGE("There should be an edit cell in B1.", pEditObj);
    CPPUNIT_ASSERT_EQUAL(u"Edit 1"_ustr, pEditObj->GetText(0));
    pEditObj = m_pDoc->GetEditText(ScAddress(1,1,1));
    CPPUNIT_ASSERT_MESSAGE("There should be an edit cell in B2.", pEditObj);
    CPPUNIT_ASSERT_EQUAL(u"Edit 2"_ustr, pEditObj->GetText(0));
    pEditObj = m_pDoc->GetEditText(ScAddress(1,2,1));
    CPPUNIT_ASSERT_MESSAGE("There should be an edit cell in B3.", pEditObj);
    CPPUNIT_ASSERT_EQUAL(u"Edit 3"_ustr, pEditObj->GetText(0));

    m_pDoc->DeleteTab(1);

    m_pDoc->SetRowHidden(5, 10, 0, true);
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 0 - 4 should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(4), nRow2);
    bHidden = m_pDoc->RowHidden(5, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 5 - 10 should be hidden", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(5), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(10), nRow2);
    bHidden = m_pDoc->RowHidden(11, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 11 - maxrow should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", SCROW(11), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", m_pDoc->MaxRow(), nRow2);

    // Copy the sheet once again.
    m_pDoc->CopyTab(0, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document now should have two sheets.",
                               static_cast<SCTAB>(2), m_pDoc->GetTableCount());
    bHidden = m_pDoc->RowHidden(0, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 0 - 4 should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(4), nRow2);
    bHidden = m_pDoc->RowHidden(5, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 5 - 10 should be hidden", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(5), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(10), nRow2);
    bHidden = m_pDoc->RowHidden(11, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 11 - maxrow should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", SCROW(11), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", m_pDoc->MaxRow(), nRow2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testSheetMove)
{
    m_pDoc->InsertTab(0, u"TestTab1"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document should have one sheet to begin with.", static_cast<SCTAB>(1), m_pDoc->GetTableCount());
    SCROW nRow1, nRow2;
    bool bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("new sheet should have all rows visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", m_pDoc->MaxRow(), nRow2);

    //test if inserting before another sheet works
    m_pDoc->InsertTab(0, u"TestTab2"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document should have two sheets", static_cast<SCTAB>(2), m_pDoc->GetTableCount());
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("new sheet should have all rows visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", m_pDoc->MaxRow(), nRow2);

    // Move and test the result.
    m_pDoc->MoveTab(0, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document now should have two sheets.", static_cast<SCTAB>(2), m_pDoc->GetTableCount());
    bHidden = m_pDoc->RowHidden(0, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("copied sheet should also have all rows visible as the original.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("copied sheet should also have all rows visible as the original.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("copied sheet should also have all rows visible as the original.", m_pDoc->MaxRow(), nRow2);
    OUString aName;
    m_pDoc->GetName(0, aName);
    CPPUNIT_ASSERT_EQUAL_MESSAGE( "sheets should have changed places", u"TestTab1"_ustr, aName);

    m_pDoc->SetRowHidden(5, 10, 0, true);
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 0 - 4 should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(4), nRow2);
    bHidden = m_pDoc->RowHidden(5, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 5 - 10 should be hidden", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(5), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(10), nRow2);
    bHidden = m_pDoc->RowHidden(11, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 11 - maxrow should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", SCROW(11), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", m_pDoc->MaxRow(), nRow2);

    // Move the sheet once again.
    m_pDoc->MoveTab(1, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document now should have two sheets.", static_cast<SCTAB>(2), m_pDoc->GetTableCount());
    bHidden = m_pDoc->RowHidden(0, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 0 - 4 should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 0 - 4 should be visible", SCROW(4), nRow2);
    bHidden = m_pDoc->RowHidden(5, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 5 - 10 should be hidden", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(5), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 5 - 10 should be hidden", SCROW(10), nRow2);
    bHidden = m_pDoc->RowHidden(11, 1, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 11 - maxrow should be visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", SCROW(11), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 11 - maxrow should be visible", m_pDoc->MaxRow(), nRow2);
    m_pDoc->GetName(0, aName);
    CPPUNIT_ASSERT_EQUAL_MESSAGE( "sheets should have changed places", u"TestTab2"_ustr, aName);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDataArea)
{
    m_pDoc->InsertTab(0, u"Data"_ustr);

    // Totally empty sheet should be rightfully considered empty in all accounts.
    CPPUNIT_ASSERT_MESSAGE("Sheet is expected to be empty.", m_pDoc->IsPrintEmpty(0, 0, 100, 100, 0));
    CPPUNIT_ASSERT_MESSAGE("Sheet is expected to be empty.", m_pDoc->IsBlockEmpty(0, 0, 100, 100, 0));

    // Now, set borders in some cells...
    ::editeng::SvxBorderLine aLine(nullptr, 50, SvxBorderLineStyle::SOLID);
    SvxBoxItem aBorderItem(ATTR_BORDER);
    aBorderItem.SetLine(&aLine, SvxBoxItemLine::LEFT);
    aBorderItem.SetLine(&aLine, SvxBoxItemLine::RIGHT);
    for (SCROW i = 0; i < 100; ++i)
        // Set borders from row 1 to 100.
        m_pDoc->ApplyAttr(0, i, 0, aBorderItem);

    // Now the sheet is considered non-empty for printing purposes, but still
    // be empty in all the other cases.
    CPPUNIT_ASSERT_MESSAGE("Empty sheet with borders should be printable.",
                           !m_pDoc->IsPrintEmpty(0, 0, 0, 100, 100));
    CPPUNIT_ASSERT_MESSAGE("But it should still be considered empty in all the other cases.",
                           m_pDoc->IsBlockEmpty(0, 0, 100, 100, 0));

    // Adding a real cell content should turn the block non-empty.
    m_pDoc->SetString(0, 0, 0, u"Some text"_ustr);
    CPPUNIT_ASSERT_MESSAGE("Now the block should not be empty with a real cell content.",
                           !m_pDoc->IsBlockEmpty(0, 0, 100, 100, 0));

    // TODO: Add more tests for normal data area calculation.

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testStreamValid)
{
    /**
     * Make sure the sheet streams are invalidated properly.
     */
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->InsertTab(1, u"Sheet2"_ustr);
    m_pDoc->InsertTab(2, u"Sheet3"_ustr);
    m_pDoc->InsertTab(3, u"Sheet4"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("We should have 4 sheet instances.", static_cast<SCTAB>(4), m_pDoc->GetTableCount());

    OUString a1(u"A1"_ustr);
    OUString a2(u"A2"_ustr);
    OUString test;

    // Put values into Sheet1.
    m_pDoc->SetString(0, 0, 0, a1);
    m_pDoc->SetString(0, 1, 0, a2);
    test = m_pDoc->GetString(0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet1.A1", test, a1);
    test = m_pDoc->GetString(0, 1, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet1.A2", test, a2);

    // Put formulas into Sheet2 to Sheet4 to reference values from Sheet1.
    m_pDoc->SetString(0, 0, 1, u"=Sheet1.A1"_ustr);
    m_pDoc->SetString(0, 1, 1, u"=Sheet1.A2"_ustr);
    m_pDoc->SetString(0, 0, 2, u"=Sheet1.A1"_ustr);
    m_pDoc->SetString(0, 0, 3, u"=Sheet1.A2"_ustr);

    test = m_pDoc->GetString(0, 0, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet2.A1", test, a1);
    test = m_pDoc->GetString(0, 1, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet2.A2", test, a2);
    test = m_pDoc->GetString(0, 0, 2);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet3.A1", test, a1);
    test = m_pDoc->GetString(0, 0, 3);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected value in Sheet3.A1", test, a2);

    // Set all sheet streams valid after all the initial cell values are in
    // place. In reality we need to have real XML streams stored in order to
    // claim they are valid, but we are just testing the flag values here.
    m_pDoc->SetStreamValid(0, true);
    m_pDoc->SetStreamValid(1, true);
    m_pDoc->SetStreamValid(2, true);
    m_pDoc->SetStreamValid(3, true);
    CPPUNIT_ASSERT_MESSAGE("Stream is expected to be valid.", m_pDoc->IsStreamValid(0));
    CPPUNIT_ASSERT_MESSAGE("Stream is expected to be valid.", m_pDoc->IsStreamValid(1));
    CPPUNIT_ASSERT_MESSAGE("Stream is expected to be valid.", m_pDoc->IsStreamValid(2));
    CPPUNIT_ASSERT_MESSAGE("Stream is expected to be valid.", m_pDoc->IsStreamValid(3));

    // Now, insert a new row at row 2 position on Sheet1.  This will move cell
    // A2 downward but cell A1 remains unmoved.
    m_pDoc->InsertRow(0, 0, m_pDoc->MaxCol(), 0, 1, 2);
    test = m_pDoc->GetString(0, 0, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Cell A1 should not have moved.", test, a1);
    test = m_pDoc->GetString(0, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the old cell A2 should now be at A4.", test, a2);
    ScRefCellValue aCell;
    aCell.assign(*m_pDoc, ScAddress(0,1,0));
    CPPUNIT_ASSERT_MESSAGE("Cell A2 should be empty.", aCell.isEmpty());
    aCell.assign(*m_pDoc, ScAddress(0,2,0));
    CPPUNIT_ASSERT_MESSAGE("Cell A3 should be empty.", aCell.isEmpty());

    // After the move, Sheet1, Sheet2, and Sheet4 should have their stream
    // invalidated, whereas Sheet3's stream should still be valid.
    CPPUNIT_ASSERT_MESSAGE("Stream should have been invalidated.", !m_pDoc->IsStreamValid(0));
    CPPUNIT_ASSERT_MESSAGE("Stream should have been invalidated.", !m_pDoc->IsStreamValid(1));
    CPPUNIT_ASSERT_MESSAGE("Stream should have been invalidated.", !m_pDoc->IsStreamValid(3));
    CPPUNIT_ASSERT_MESSAGE("Stream should still be valid.", m_pDoc->IsStreamValid(2));

    m_pDoc->DeleteTab(3);
    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFunctionLists)
{
    /**
     * Test built-in cell functions to make sure their categories and order
     * are correct.
     */
    const char* aDataBase[] = {
        "DAVERAGE",
        "DCOUNT",
        "DCOUNTA",
        "DGET",
        "DMAX",
        "DMIN",
        "DPRODUCT",
        "DSTDEV",
        "DSTDEVP",
        "DSUM",
        "DVAR",
        "DVARP",
        nullptr
    };

    const char* aDateTime[] = {
        "DATE",
        "DATEDIF",
        "DATEVALUE",
        "DAY",
        "DAYS",
        "DAYS360",
        "DAYSINMONTH",
        "DAYSINYEAR",
        "EASTERSUNDAY",
        "HOUR",
        "ISLEAPYEAR",
        "ISOWEEKNUM",
        "MINUTE",
        "MONTH",
        "MONTHS",
        "NETWORKDAYS",
        "NETWORKDAYS.INTL",
        "NOW",
        "SECOND",
        "TIME",
        "TIMEVALUE",
        "TODAY",
        "WEEKDAY",
        "WEEKNUM",
        "WEEKNUM_OOO",
        "WEEKS",
        "WEEKSINYEAR",
        "WORKDAY.INTL",
        "YEAR",
        "YEARS",
        nullptr
    };

    const char* aFinancial[] = {
        "CUMIPMT",
        "CUMPRINC",
        "DB",
        "DDB",
        "EFFECT",
        "FV",
        "IPMT",
        "IRR",
        "ISPMT",
        "MIRR",
        "NOMINAL",
        "NPER",
        "NPV",
        "OPT_BARRIER",
        "OPT_PROB_HIT",
        "OPT_PROB_INMONEY",
        "OPT_TOUCH",
        "PDURATION",
        "PMT",
        "PPMT",
        "PV",
        "RATE",
        "RRI",
        "SLN",
        "SYD",
        "VDB",
        nullptr
    };

    const char* aInformation[] = {
        "CELL",
        "CURRENT",
        "FORMULA",
        "INFO",
        "ISBLANK",
        "ISERR",
        "ISERROR",
        "ISEVEN",
        "ISFORMULA",
        "ISLOGICAL",
        "ISNA",
        "ISNONTEXT",
        "ISNUMBER",
        "ISODD",
        "ISOMITTED",
        "ISREF",
        "ISTEXT",
        "N",
        "NA",
        "TYPE",
        nullptr
    };

    const char* aLogical[] = {
        "AND",
        "FALSE",
        "IF",
        "IFERROR",
        "IFNA",
        "IFS",
        "NOT",
        "OR",
        "SWITCH",
        "TRUE",
        "XOR",
        nullptr
    };

    const char* aMathematical[] = {
        "ABS",
        "ACOS",
        "ACOSH",
        "ACOT",
        "ACOTH",
        "AGGREGATE",
        "ASIN",
        "ASINH",
        "ATAN",
        "ATAN2",
        "ATANH",
        "BITAND",
        "BITLSHIFT",
        "BITOR",
        "BITRSHIFT",
        "BITXOR",
        "CEILING",
        "CEILING.MATH",
        "CEILING.PRECISE",
        "CEILING.XCL",
        "COLOR",
        "COMBIN",
        "COMBINA",
        "CONVERT_OOO",
        "COS",
        "COSH",
        "COT",
        "COTH",
        "CSC",
        "CSCH",
        "DEGREES",
        "EUROCONVERT",
        "EVEN",
        "EXP",
        "FACT",
        "FLOOR",
        "FLOOR.MATH",
        "FLOOR.PRECISE",
        "FLOOR.XCL",
        "GCD",
        "INT",
        "ISO.CEILING",
        "LCM",
        "LN",
        "LOG",
        "LOG10",
        "MOD",
        "ODD",
        "PI",
        "POWER",
        "PRODUCT",
        "RADIANS",
        "RAND",
        "RAND.NV",
        "RANDARRAY",
        "RANDBETWEEN.NV",
        "RAWSUBTRACT",
        "ROUND",
        "ROUNDDOWN",
        "ROUNDSIG",
        "ROUNDUP",
        "SEC",
        "SECH",
        "SIGN",
        "SIN",
        "SINH",
        "SQRT",
        "SUBTOTAL",
        "SUM",
        "SUMIF",
        "SUMIFS",
        "SUMSQ",
        "TAN",
        "TANH",
        "TRUNC",
        nullptr
    };

    const char* aArray[] = {
        "BYCOL",
        "BYROW",
        "FOURIER",
        "FREQUENCY",
        "GROWTH",
        "LINEST",
        "LOGEST",
        "MAKEARRAY",
        "MAP",
        "MDETERM",
        "MINVERSE",
        "MMULT",
        "MSVD",
        "MUNIT",
        "REDUCE",
        "SCAN",
        "SEQUENCE",
        "SUMPRODUCT",
        "SUMX2MY2",
        "SUMX2PY2",
        "SUMXMY2",
        "TRANSPOSE",
        "TREND",
        nullptr
    };

    const char* aStatistical[] = {
        "AVEDEV",
        "AVERAGE",
        "AVERAGEA",
        "AVERAGEIF",
        "AVERAGEIFS",
        "B",
        "BETA.DIST",
        "BETA.INV",
        "BETADIST",
        "BETAINV",
        "BINOM.DIST",
        "BINOM.INV",
        "BINOMDIST",
        "CHIDIST",
        "CHIINV",
        "CHISQ.DIST",
        "CHISQ.DIST.RT",
        "CHISQ.INV",
        "CHISQ.INV.RT",
        "CHISQ.TEST",
        "CHISQDIST",
        "CHISQINV",
        "CHITEST",
        "CONFIDENCE",
        "CONFIDENCE.NORM",
        "CONFIDENCE.T",
        "CORREL",
        "COUNT",
        "COUNTA",
        "COUNTBLANK",
        "COUNTIF",
        "COUNTIFS",
        "COVAR",
        "COVARIANCE.P",
        "COVARIANCE.S",
        "CRITBINOM",
        "DEVSQ",
        "ERF.PRECISE",
        "ERFC.PRECISE",
        "EXPON.DIST",
        "EXPONDIST",
        "F.DIST",
        "F.DIST.RT",
        "F.INV",
        "F.INV.RT",
        "F.TEST",
        "FDIST",
        "FINV",
        "FISHER",
        "FISHERINV",
        "FORECAST",
        "FORECAST.ETS.ADD",
        "FORECAST.ETS.MULT",
        "FORECAST.ETS.PI.ADD",
        "FORECAST.ETS.PI.MULT",
        "FORECAST.ETS.SEASONALITY",
        "FORECAST.ETS.STAT.ADD",
        "FORECAST.ETS.STAT.MULT",
        "FORECAST.LINEAR",
        "FTEST",
        "GAMMA",
        "GAMMA.DIST",
        "GAMMA.INV",
        "GAMMADIST",
        "GAMMAINV",
        "GAMMALN",
        "GAMMALN.PRECISE",
        "GAUSS",
        "GEOMEAN",
        "HARMEAN",
        "HYPGEOM.DIST",
        "HYPGEOMDIST",
        "INTERCEPT",
        "KURT",
        "LARGE",
        "LOGINV",
        "LOGNORM.DIST",
        "LOGNORM.INV",
        "LOGNORMDIST",
        "MAX",
        "MAXA",
        "MAXIFS",
        "MEDIAN",
        "MIN",
        "MINA",
        "MINIFS",
        "MODE",
        "MODE.MULT",
        "MODE.SNGL",
        "NEGBINOM.DIST",
        "NEGBINOMDIST",
        "NORM.DIST",
        "NORM.INV",
        "NORM.S.DIST",
        "NORM.S.INV",
        "NORMDIST",
        "NORMINV",
        "NORMSDIST",
        "NORMSINV",
        "PEARSON",
        "PERCENTILE",
        "PERCENTILE.EXC",
        "PERCENTILE.INC",
        "PERCENTRANK",
        "PERCENTRANK.EXC",
        "PERCENTRANK.INC",
        "PERMUT",
        "PERMUTATIONA",
        "PHI",
        "POISSON",
        "POISSON.DIST",
        "PROB",
        "QUARTILE",
        "QUARTILE.EXC",
        "QUARTILE.INC",
        "RANK",
        "RANK.AVG",
        "RANK.EQ",
        "RSQ",
        "SKEW",
        "SKEWP",
        "SLOPE",
        "SMALL",
        "STANDARDIZE",
        "STDEV",
        "STDEV.P",
        "STDEV.S",
        "STDEVA",
        "STDEVP",
        "STDEVPA",
        "STEYX",
        "T.DIST",
        "T.DIST.2T",
        "T.DIST.RT",
        "T.INV",
        "T.INV.2T",
        "T.TEST",
        "TDIST",
        "TINV",
        "TRIMMEAN",
        "TTEST",
        "VAR",
        "VAR.P",
        "VAR.S",
        "VARA",
        "VARP",
        "VARPA",
        "WEIBULL",
        "WEIBULL.DIST",
        "Z.TEST",
        "ZTEST",
        nullptr
    };

    const char* aSpreadsheet[] = {
        "ADDRESS",
        "AREAS",
        "CHOOSE",
        "CHOOSECOLS",
        "CHOOSEROWS",
        "COLUMN",
        "COLUMNS",
        "DDE",
        "DROP",
        "ERROR.TYPE",
        "ERRORTYPE",
        "EXPAND",
        "FILTER",
        "GETPIVOTDATA",
        "HLOOKUP",
        "HSTACK",
        "HYPERLINK",
        "INDEX",
        "INDIRECT",
        "LAMBDA",
        "LET",
        "LOOKUP",
        "MATCH",
        "OFFSET",
        "ROW",
        "ROWS",
        "SHEET",
        "SHEETS",
        "SORT",
        "SORTBY",
        "STYLE",
        "TAKE",
        "TEXTSPLIT",
        "TOCOL",
        "TOROW",
        "UNIQUE",
        "VLOOKUP",
        "VSTACK",
        "WRAPCOLS",
        "WRAPROWS",
        "XLOOKUP",
        "XMATCH",
        nullptr
    };

    const char* aText[] = {
        "ARABIC",
        "ARRAYTOTEXT",
        "ASC",
        "BAHTTEXT",
        "BASE",
        "CHAR",
        "CLEAN",
        "CODE",
        "CONCAT",
        "CONCATENATE",
        "DECIMAL",
        "DOLLAR",
        "ENCODEURL",
        "EXACT",
        "FILTERXML",
        "FIND",
        "FINDB",
        "FIXED",
        "JIS",
        "LEFT",
        "LEFTB",
        "LEN",
        "LENB",
        "LOWER",
        "MID",
        "MIDB",
        "NUMBERVALUE",
        "PROPER",
        "REGEX",
        "REPLACE",
        "REPLACEB",
        "REPT",
        "RIGHT",
        "RIGHTB",
        "ROMAN",
        "ROT13",
        "SEARCH",
        "SEARCHB",
        "SUBSTITUTE",
        "T",
        "TEXT",
        "TEXTAFTER",
        "TEXTBEFORE",
        "TEXTJOIN",
        "TRIM",
        "UNICHAR",
        "UNICODE",
        "UPPER",
        "VALUE",
        "WEBSERVICE",
        nullptr
    };

    struct {
        const char* Category; const char** Functions;
    } aTests[] = {
        { "Database",     aDataBase },
        { "Date&Time",    aDateTime },
        { "Financial",    aFinancial },
        { "Information",  aInformation },
        { "Logical",      aLogical },
        { "Mathematical", aMathematical },
        { "Array",        aArray },
        { "Statistical",  aStatistical },
        { "Spreadsheet",  aSpreadsheet },
        { "Text",         aText },
        { "Add-in",       nullptr },
        { nullptr, nullptr }
    };

    ScFunctionMgr* pFuncMgr = ScGlobal::GetStarCalcFunctionMgr();
    sal_uInt32 n = pFuncMgr->getCount();
    for (sal_uInt32 i = 0; i < n; ++i)
    {
        const formula::IFunctionCategory* pCat = pFuncMgr->getCategory(i);
        CPPUNIT_ASSERT_MESSAGE("Unexpected category name", pCat->getName().equalsAscii(aTests[i].Category));
        sal_uInt32 nFuncCount = pCat->getCount();
        for (sal_uInt32 j = 0; j < nFuncCount; ++j)
        {
            const formula::IFunctionDescription* pFunc = pCat->getFunction(j);
            CPPUNIT_ASSERT_EQUAL_MESSAGE("Unexpected function name", OUString::createFromAscii(aTests[i].Functions[j]), pFunc->getFunctionName());
        }
    }
}

CPPUNIT_TEST_FIXTURE(Test, testGraphicsInGroup)
{
    m_pDoc->InsertTab(0, u"TestTab"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("document should have one sheet to begin with.",
                               static_cast<SCTAB>(1), m_pDoc->GetTableCount());
    SCROW nRow1, nRow2;
    bool bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("new sheet should have all rows visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", m_pDoc->MaxRow(), nRow2);

    m_pDoc->InitDrawLayer();
    ScDrawLayer *pDrawLayer = m_pDoc->GetDrawLayer();
    CPPUNIT_ASSERT_MESSAGE("must have a draw layer", pDrawLayer != nullptr);
    SdrPage* pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("must have a draw page", pPage != nullptr);

    {
        //Add a square
        tools::Rectangle aOrigRect(2,2,100,100);
        rtl::Reference<SdrRectObj> pObj = new SdrRectObj(*pDrawLayer, aOrigRect);
        pPage->InsertObject(pObj.get());
        const tools::Rectangle &rNewRect = pObj->GetLogicRect();
        CPPUNIT_ASSERT_EQUAL_MESSAGE("must have equal position and size", aOrigRect, rNewRect);

        ScDrawLayer::SetPageAnchored(*pObj);

        //Use a range of rows guaranteed to include all of the square
        m_pDoc->ShowRows(0, 100, 0, false);
        m_pDoc->SetDrawPageSize(0);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Should not change when page anchored", aOrigRect, rNewRect);
        m_pDoc->ShowRows(0, 100, 0, true);
        m_pDoc->SetDrawPageSize(0);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Should not change when page anchored", aOrigRect, rNewRect);

        ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, true);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("That shouldn't change size or positioning",
                               aOrigRect, rNewRect);

        m_pDoc->ShowRows(0, 100, 0, false);
        m_pDoc->SetDrawPageSize(0);

        CPPUNIT_ASSERT_EQUAL_MESSAGE("Hiding should not change the logic rectangle",
                               aOrigRect, rNewRect);
        CPPUNIT_ASSERT_MESSAGE("Hiding should make invisible", !pObj->IsVisible());

        m_pDoc->ShowRows(0, 100, 0, true);
        m_pDoc->SetDrawPageSize(0);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Should not change when cell anchored", aOrigRect, rNewRect);
        CPPUNIT_ASSERT_MESSAGE("Show should make visible", pObj->IsVisible());
    }

    {
        // Add a circle.
        tools::Rectangle aOrigRect(10,10,210,210); // 200 x 200
        rtl::Reference<SdrCircObj> pObj = new SdrCircObj(*pDrawLayer, SdrCircKind::Full, aOrigRect);
        pPage->InsertObject(pObj.get());
        const tools::Rectangle& rNewRect = pObj->GetLogicRect();
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Position and size of the circle shouldn't change when inserted into the page.",
                               aOrigRect, rNewRect);

        ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, false);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Size changed when cell anchored. Not good.",
                               aOrigRect, rNewRect);

        // Insert 2 rows at the top.  This should push the circle object down.
        m_pDoc->InsertRow(0, 0, m_pDoc->MaxCol(), 0, 0, 2);
        m_pDoc->SetDrawPageSize(0);

        // Make sure the size of the circle is still identical.
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Size of the circle has changed, but shouldn't!",
                               aOrigRect.GetSize(), rNewRect.GetSize());

        // Delete 2 rows at the top.  This should bring the circle object to its original position.
        m_pDoc->DeleteRow(0, 0, m_pDoc->MaxCol(), 0, 0, 2);
        m_pDoc->SetDrawPageSize(0);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Failed to move back to its original position.",
                               aOrigRect, rNewRect);
    }

    {
        // Add a line.
        basegfx::B2DPolygon aTempPoly;
        Point aStartPos(10,300), aEndPos(110,200); // bottom-left to top-right.
        tools::Rectangle aOrigRect(10,200,110,300); // 100 x 100
        aTempPoly.append(basegfx::B2DPoint(aStartPos.X(), aStartPos.Y()));
        aTempPoly.append(basegfx::B2DPoint(aEndPos.X(), aEndPos.Y()));
        rtl::Reference<SdrPathObj> pObj = new SdrPathObj(*pDrawLayer, SdrObjKind::Line, basegfx::B2DPolyPolygon(aTempPoly));
        pObj->NbcSetLogicRect(aOrigRect);
        pPage->InsertObject(pObj.get());
        const tools::Rectangle& rNewRect = pObj->GetLogicRect();
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Size differ.", aOrigRect, rNewRect);

        ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, false);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Size changed when cell-anchored. Not good.",
                               aOrigRect, rNewRect);

        // Insert 2 rows at the top and delete them immediately.
        m_pDoc->InsertRow(0, 0, m_pDoc->MaxCol(), 0, 0, 2);
        m_pDoc->DeleteRow(0, 0, m_pDoc->MaxCol(), 0, 0, 2);
        m_pDoc->SetDrawPageSize(0);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Size of a line object changed after row insertion and removal.",
                               aOrigRect, rNewRect);

        sal_Int32 n = pObj->GetPointCount();
        CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be exactly 2 points in a line object.", static_cast<sal_Int32>(2), n);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Line shape has changed.",
                               aStartPos, pObj->GetPoint(0));
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Line shape has changed.",
                               aEndPos, pObj->GetPoint(1));
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testGraphicsOnSheetMove)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);
    m_pDoc->InsertTab(1, u"Tab2"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be only 2 sheets to begin with", static_cast<SCTAB>(2), m_pDoc->GetTableCount());

    m_pDoc->InitDrawLayer();
    ScDrawLayer* pDrawLayer = m_pDoc->GetDrawLayer();
    CPPUNIT_ASSERT_MESSAGE("No drawing layer.", pDrawLayer);
    SdrPage* pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("No page instance for the 1st sheet.", pPage);

    // Insert an object.
    tools::Rectangle aObjRect(2,2,100,100);
    rtl::Reference<SdrObject> pObj = new SdrRectObj(*pDrawLayer, aObjRect);
    pPage->InsertObject(pObj.get());
    ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, false);

    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be one object on the 1st sheet.", static_cast<size_t>(1), pPage->GetObjCount());

    const ScDrawObjData* pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Object meta-data doesn't exist.", pData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(0), pData->maStart.Tab());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(0), pData->maEnd.Tab());

    pPage = pDrawLayer->GetPage(1);
    CPPUNIT_ASSERT_MESSAGE("No page instance for the 2nd sheet.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("2nd sheet shouldn't have any object.", static_cast<size_t>(0), pPage->GetObjCount());

    // Insert a new sheet at left-end, and make sure the object has moved to
    // the 2nd page.
    m_pDoc->InsertTab(0, u"NewTab"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be 3 sheets.", static_cast<SCTAB>(3), m_pDoc->GetTableCount());
    pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("1st sheet should have no object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("1st sheet should have no object.", size_t(0), pPage->GetObjCount());
    pPage = pDrawLayer->GetPage(1);
    CPPUNIT_ASSERT_MESSAGE("2nd sheet should have one object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("2nd sheet should have one object.", size_t(1), pPage->GetObjCount());
    pPage = pDrawLayer->GetPage(2);
    CPPUNIT_ASSERT_MESSAGE("3rd sheet should have no object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("3rd sheet should have no object.", size_t(0), pPage->GetObjCount());

    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(1), pData->maStart.Tab());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(1), pData->maEnd.Tab());

    // Now, delete the sheet that just got inserted. The object should be back
    // on the 1st sheet.
    m_pDoc->DeleteTab(0);
    pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("1st sheet should have one object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("1st sheet should have one object.", size_t(1), pPage->GetObjCount());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Size and position of the object shouldn't change.",
                           aObjRect, pObj->GetLogicRect());

    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(0), pData->maStart.Tab());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(0), pData->maEnd.Tab());

    // Move the 1st sheet to the last position.
    m_pDoc->MoveTab(0, 1);
    pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("1st sheet should have no object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("1st sheet should have no object.", size_t(0), pPage->GetObjCount());
    pPage = pDrawLayer->GetPage(1);
    CPPUNIT_ASSERT_MESSAGE("2nd sheet should have one object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("2nd sheet should have one object.", size_t(1), pPage->GetObjCount());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(1), pData->maStart.Tab());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(1), pData->maEnd.Tab());

    // Copy the 2nd sheet, which has one drawing object to the last position.
    m_pDoc->CopyTab(1, 2);
    pPage = pDrawLayer->GetPage(2);
    CPPUNIT_ASSERT_MESSAGE("Copied sheet should have one object.", pPage);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Copied sheet should have one object.", size_t(1), pPage->GetObjCount());
    pObj = pPage->GetObj(0);
    CPPUNIT_ASSERT_MESSAGE("Failed to get drawing object.", pObj);
    pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Failed to get drawing object meta-data.", pData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(2), pData->maStart.Tab());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Wrong sheet ID in cell anchor data!", SCTAB(2), pData->maEnd.Tab());

    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testToggleRefFlag)
{
    /**
     * Test toggling relative/absolute flag of cell and cell range references.
     * This corresponds with hitting Shift-F4 while the cursor is on a formula
     * cell.
     */
    // In this test, there is no need to insert formula string into a cell in
    // the document, as ScRefFinder does not depend on the content of the
    // document except for the sheet names.

    m_pDoc->InsertTab(0, u"Test"_ustr);

    {
        // Calc A1: basic 2D reference

        OUString aFormula(u"=B100"_ustr);
        ScAddress aPos(1, 5, 0);
        ScRefFinder aFinder(aFormula, aPos, *m_pDoc, formula::FormulaGrammar::CONV_OOO);

        // Original
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Does not equal the original text.", aFormula, aFinder.GetText());

        // column relative / row relative -> column absolute / row absolute
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong conversion.", u"=$B$100"_ustr, aFormula );

        // column absolute / row absolute -> column relative / row absolute
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong conversion.", u"=B$100"_ustr, aFormula );

        // column relative / row absolute -> column absolute / row relative
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong conversion.", u"=$B100"_ustr, aFormula );

        // column absolute / row relative -> column relative / row relative
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL_MESSAGE( "Wrong conversion.", u"=B100"_ustr, aFormula );
    }

    {
        // Excel R1C1: basic 2D reference

        OUString aFormula(u"=R2C1"_ustr);
        ScAddress aPos(3, 5, 0);
        ScRefFinder aFinder(aFormula, aPos, *m_pDoc, formula::FormulaGrammar::CONV_XL_R1C1);

        // Original
        CPPUNIT_ASSERT_EQUAL_MESSAGE("Does not equal the original text.", aFormula, aFinder.GetText());

        // column absolute / row absolute -> column relative / row absolute
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R2C[-3]"_ustr, aFormula);

        // column relative / row absolute - > column absolute / row relative
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R[-4]C1"_ustr, aFormula);

        // column absolute / row relative -> column relative / row relative
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R[-4]C[-3]"_ustr, aFormula);

        // column relative / row relative -> column absolute / row absolute
        aFinder.ToggleRel(0, aFormula.getLength());
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R2C1"_ustr, aFormula);
    }

    {
        // Excel R1C1: Selection at the end of the formula string and does not
        // overlap the formula string at all (inspired by fdo#39135).
        OUString aFormula(u"=R1C1"_ustr);
        ScAddress aPos(1, 1, 0);
        ScRefFinder aFinder(aFormula, aPos, *m_pDoc, formula::FormulaGrammar::CONV_XL_R1C1);

        // Original
        CPPUNIT_ASSERT_EQUAL(aFormula, aFinder.GetText());

        // Make the column relative.
        sal_Int32 n = aFormula.getLength();
        aFinder.ToggleRel(n, n);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R1C[-1]"_ustr, aFormula);

        // Make the row relative.
        n = aFormula.getLength();
        aFinder.ToggleRel(n, n);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R[-1]C1"_ustr, aFormula);

        // Make both relative.
        n = aFormula.getLength();
        aFinder.ToggleRel(n, n);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R[-1]C[-1]"_ustr, aFormula);

        // Back to the original.
        n = aFormula.getLength();
        aFinder.ToggleRel(n, n);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=R1C1"_ustr, aFormula);
    }

    {
        // Calc A1:
        OUString aFormula(u"=A1+4"_ustr);
        ScAddress aPos(1, 1, 0);
        ScRefFinder aFinder(aFormula, aPos, *m_pDoc, formula::FormulaGrammar::CONV_OOO);

        // Original
        CPPUNIT_ASSERT_EQUAL(aFormula, aFinder.GetText());

        // Set the cursor over the 'A1' part and toggle.
        aFinder.ToggleRel(2, 2);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=$A$1+4"_ustr, aFormula);

        aFinder.ToggleRel(2, 2);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=A$1+4"_ustr, aFormula);

        aFinder.ToggleRel(2, 2);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=$A1+4"_ustr, aFormula);

        aFinder.ToggleRel(2, 2);
        aFormula = aFinder.GetText();
        CPPUNIT_ASSERT_EQUAL(u"=A1+4"_ustr, aFormula);
    }

    {
        // Calc A1, with newlines in the formula:
        OUString aFormula(u"=SUM(\nA1:A10\n)"_ustr);
        ScAddress aPos(1, 1, 0);
        ScRefFinder aFinder(aFormula, aPos, *m_pDoc, formula::FormulaGrammar::CONV_OOO);

        // Original
        CPPUNIT_ASSERT_EQUAL(aFormula, aFinder.GetText());

        // Set the cursor at the 'A1' part and toggle
        aFinder.ToggleRel(6, 6);
        CPPUNIT_ASSERT_EQUAL(u"=SUM(\n$A$1:A10\n)"_ustr, aFinder.GetText());
        // Selected: "$A$1"
        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), aFinder.GetSelStart());
        CPPUNIT_ASSERT_EQUAL(sal_Int32(10), aFinder.GetSelEnd());

        // Set the cursor behind the 'A10' part and toggle
        aFinder.ToggleRel(14, 14);
        CPPUNIT_ASSERT_EQUAL(u"=SUM(\n$A$1:$A$10\n)"_ustr, aFinder.GetText());
        // Selected: "$A$10"
        CPPUNIT_ASSERT_EQUAL(sal_Int32(11), aFinder.GetSelStart());
        CPPUNIT_ASSERT_EQUAL(sal_Int32(16), aFinder.GetSelEnd());

        // Set the cursor around an area including a newline and toggle
        aFinder.ToggleRel(2, 6);
        CPPUNIT_ASSERT_EQUAL(u"=SUM(\nA$1:$A$10\n)"_ustr, aFinder.GetText());
        // Selected: "A$1"
        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), aFinder.GetSelStart());
        CPPUNIT_ASSERT_EQUAL(sal_Int32(9), aFinder.GetSelEnd());

        // Select all and toggle
        aFinder.ToggleRel(0, 16);
        CPPUNIT_ASSERT_EQUAL(u"=SUM(\n$A1:A$10\n)"_ustr, aFinder.GetText());
        // Selected: "$A1:A$10"
        CPPUNIT_ASSERT_EQUAL(sal_Int32(6), aFinder.GetSelStart());
        CPPUNIT_ASSERT_EQUAL(sal_Int32(14), aFinder.GetSelEnd());
    }

    // TODO: Add more test cases esp. for 3D references, Excel A1 syntax, and
    // partial selection within formula string.

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutofilter)
{
    m_pDoc->InsertTab( 0, u"Test"_ustr );

    // cell contents (0 = empty cell)
    const char* aData[][3] = {
        { "C1", "C2", "C3" },
        {  "0",  "1",  "A" },
        {  "1",  "2",    nullptr },
        {  "1",  "2",  "B" },
        {  "0",  "2",  "B" }
    };

    SCCOL nCols = std::size(aData[0]);
    SCROW nRows = std::size(aData);

    // Populate cells.
    for (SCROW i = 0; i < nRows; ++i)
        for (SCCOL j = 0; j < nCols; ++j)
            if (aData[i][j])
                m_pDoc->SetString(j, i, 0, OUString::createFromAscii(aData[i][j]));

    ScDBData* pDBData = new ScDBData(u"NONAME"_ustr, 0, 0, 0, nCols-1, nRows-1);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));

    pDBData->SetAutoFilter(true);
    ScRange aRange;
    pDBData->GetArea(aRange);
    m_pDoc->ApplyFlagsTab( aRange.aStart.Col(), aRange.aStart.Row(),
                           aRange.aEnd.Col(), aRange.aStart.Row(),
                           aRange.aStart.Tab(), ScMF::Auto);

    //create the query param
    ScQueryParam aParam;
    pDBData->GetQueryParam(aParam);
    ScQueryEntry& rEntry = aParam.GetEntry(0);
    rEntry.bDoQuery = true;
    rEntry.nField = 0;
    rEntry.eOp = SC_EQUAL;
    rEntry.GetQueryItem().mfVal = 0;
    // add queryParam to database range.
    pDBData->SetQueryParam(aParam);

    // perform the query.
    m_pDoc->Query(0, aParam, true);

    //control output
    SCROW nRow1, nRow2;
    bool bHidden = m_pDoc->RowHidden(2, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 2 & 3 should be hidden", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 2 & 3 should be hidden", SCROW(2), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 2 & 3 should be hidden", SCROW(3), nRow2);

    // Remove filtering.
    rEntry.Clear();
    m_pDoc->Query(0, aParam, true);
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("All rows should be shown.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("All rows should be shown.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("All rows should be shown.", m_pDoc->MaxRow(), nRow2);

    // Filter for non-empty cells by column C.
    rEntry.bDoQuery = true;
    rEntry.nField = 2;
    rEntry.SetQueryByNonEmpty();
    m_pDoc->Query(0, aParam, true);

    // only row 3 should be hidden.  The rest should be visible.
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 1 & 2 should be visible.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 1 & 2 should be visible.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 1 & 2 should be visible.", SCROW(1), nRow2);
    bHidden = m_pDoc->RowHidden(2, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("row 3 should be hidden.", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 3 should be hidden.", SCROW(2), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 3 should be hidden.", SCROW(2), nRow2);
    bHidden = m_pDoc->RowHidden(3, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("row 4 and down should be visible.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 4 and down should be visible.", SCROW(3), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 4 and down should be visible.", m_pDoc->MaxRow(), nRow2);

    // Now, filter for empty cells by column C.
    rEntry.SetQueryByEmpty();
    m_pDoc->Query(0, aParam, true);

    // Now, only row 1 and 3, and 6 and down should be visible.
    bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("row 1 should be visible.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 1 should be visible.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 1 should be visible.", SCROW(0), nRow2);
    bHidden = m_pDoc->RowHidden(1, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("row 2 should be hidden.", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 2 should be hidden.", SCROW(1), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 2 should be hidden.", SCROW(1), nRow2);
    bHidden = m_pDoc->RowHidden(2, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("row 3 should be visible.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 3 should be visible.", SCROW(2), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("row 3 should be visible.", SCROW(2), nRow2);
    bHidden = m_pDoc->RowHidden(3, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 4 & 5 should be hidden.", bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 4 & 5 should be hidden.", SCROW(3), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 4 & 5 should be hidden.", SCROW(4), nRow2);
    bHidden = m_pDoc->RowHidden(5, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 6 and down should be all visible.", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 6 and down should be all visible.", SCROW(5), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 6 and down should be all visible.", m_pDoc->MaxRow(), nRow2);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutoFilterTimeValue)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,0,0), u"Hours"_ustr);
    m_pDoc->SetValue(ScAddress(0,1,0), 72.3604166666671);
    m_pDoc->SetValue(ScAddress(0,2,0), 265);

    ScDBData* pDBData = new ScDBData(STR_DB_GLOBAL_NONAME, 0, 0, 0, 0, 2);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));

    // Apply the "hour:minute:second" format to A2:A3.
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    sal_uInt32 nFormat = pFormatter->GetFormatIndex(NF_TIME_HH_MMSS, LANGUAGE_ENGLISH_US);
    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));

    m_pDoc->ApplyPatternAreaTab(0, 1, 0, 2, 0, aNewAttrs); // apply it to A2:A3.

    printRange(m_pDoc, ScRange(0,0,0,0,2,0), "Data"); // A1:A3

    // Make sure the hour:minute:second format is really applied.
    CPPUNIT_ASSERT_EQUAL(u"1736:39:00"_ustr, m_pDoc->GetString(ScAddress(0,1,0))); // A2
    CPPUNIT_ASSERT_EQUAL(u"6360:00:00"_ustr, m_pDoc->GetString(ScAddress(0,2,0))); // A3

    // Filter by the A2 value.  Only A1 and A2 should be visible.
    ScQueryParam aParam;
    pDBData->GetQueryParam(aParam);
    ScQueryEntry& rEntry = aParam.GetEntry(0);
    rEntry.bDoQuery = true;
    rEntry.nField = 0;
    rEntry.eOp = SC_EQUAL;
    rEntry.GetQueryItem().maString = m_pDoc->GetSharedStringPool().intern(u"1736:39:00"_ustr);
    rEntry.GetQueryItem().meType = ScQueryEntry::ByString;

    pDBData->SetQueryParam(aParam);

    // perform the query.
    m_pDoc->Query(0, aParam, true);

    // A1:A2 should be visible while A3 should be filtered out.
    CPPUNIT_ASSERT_MESSAGE("A1 should be visible.", !m_pDoc->RowFiltered(0,0));
    CPPUNIT_ASSERT_MESSAGE("A2 should be visible.", !m_pDoc->RowFiltered(1,0));
    CPPUNIT_ASSERT_MESSAGE("A3 should be filtered out.", m_pDoc->RowFiltered(2,0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutofilterOptimizations)
{
    m_pDoc->InsertTab( 0, u"Test"_ustr );

    constexpr SCCOL nCols = 4;
    constexpr SCROW nRows = 200;
    m_pDoc->SetString(0, 0, 0, u"Column1"_ustr);
    m_pDoc->SetString(1, 0, 0, u"Column2"_ustr);
    m_pDoc->SetString(2, 0, 0, u"Column3"_ustr);
    m_pDoc->SetString(3, 0, 0, u"Column4"_ustr);

    // Fill 1st column with 0-199, 2nd with 1-200, 3rd with "1000"-"1199", 4th with "1001-1200"
    // (the pairs are off by one to each other to check filtering out a value filters out
    // only the relevant column).
    for(SCROW i = 0; i < nRows; ++i)
    {
        m_pDoc->SetValue(0, i + 1, 0, i);
        m_pDoc->SetValue(1, i + 1, 0, i+1);
        m_pDoc->SetString(2, i + 1, 0, "val" + OUString::number(i+1000));
        m_pDoc->SetString(3, i + 1, 0, "val" + OUString::number(i+1000+1));
    }

    ScDBData* pDBData = new ScDBData(u"NONAME"_ustr, 0, 0, 0, nCols, nRows);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));

    pDBData->SetAutoFilter(true);
    ScRange aRange;
    pDBData->GetArea(aRange);
    m_pDoc->ApplyFlagsTab( aRange.aStart.Col(), aRange.aStart.Row(),
                           aRange.aEnd.Col(), aRange.aStart.Row(),
                           aRange.aStart.Tab(), ScMF::Auto);

    //create the query param
    ScQueryParam aParam;
    pDBData->GetQueryParam(aParam);
    ScQueryEntry& rEntry0 = aParam.GetEntry(0);
    rEntry0.bDoQuery = true;
    rEntry0.nField = 0;
    rEntry0.eOp = SC_EQUAL;
    rEntry0.GetQueryItems().resize(nRows);
    ScQueryEntry& rEntry1 = aParam.GetEntry(1);
    rEntry1.bDoQuery = true;
    rEntry1.nField = 1;
    rEntry1.eOp = SC_EQUAL;
    rEntry1.GetQueryItems().resize(nRows);
    ScQueryEntry& rEntry2 = aParam.GetEntry(2);
    rEntry2.bDoQuery = true;
    rEntry2.nField = 2;
    rEntry2.eOp = SC_EQUAL;
    rEntry2.GetQueryItems().resize(nRows);
    ScQueryEntry& rEntry3 = aParam.GetEntry(3);
    rEntry3.bDoQuery = true;
    rEntry3.nField = 3;
    rEntry3.eOp = SC_EQUAL;
    rEntry3.GetQueryItems().resize(nRows);
    // Set up autofilter to select all values except one in each column.
    // This should only filter out 2nd, 3rd, 6th and 7th rows.
    for( int i = 0; i < nRows; ++i )
    {
        if(i!= 1)
            rEntry0.GetQueryItems()[i].mfVal = i;
        if(i!= 2)
            rEntry1.GetQueryItems()[i].mfVal = i + 1;
        if(i!= 5)
        {
            rEntry2.GetQueryItems()[i].maString = m_pDoc->GetSharedStringPool().intern("val" + OUString::number(i+1000));
            rEntry2.GetQueryItems()[i].meType = ScQueryEntry::ByString;
        }
        if(i!= 6)
        {
            rEntry3.GetQueryItems()[i].maString = m_pDoc->GetSharedStringPool().intern("val" + OUString::number(i+1000+1));
            rEntry3.GetQueryItems()[i].meType = ScQueryEntry::ByString;
        }
    }
    // add queryParam to database range.
    pDBData->SetQueryParam(aParam);

    // perform the query.
    m_pDoc->Query(0, aParam, true);

    // check that only rows with filtered out values are hidden, and not rows that share
    // a value in a different column
    SCROW nRow1, nRow2;
    CPPUNIT_ASSERT_MESSAGE("row 2 should be visible", !m_pDoc->RowHidden(1, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 3 should be hidden", m_pDoc->RowHidden(2, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 4 should be hidden", m_pDoc->RowHidden(3, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 5 should be visible", !m_pDoc->RowHidden(4, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 6 should be visible", !m_pDoc->RowHidden(5, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 7 should be hidden", m_pDoc->RowHidden(6, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 8 should be hidden", m_pDoc->RowHidden(7, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_MESSAGE("row 9 should be visible", !m_pDoc->RowHidden(8, 0, &nRow1, &nRow2));

    // Remove filtering.
    rEntry0.Clear();
    rEntry1.Clear();
    rEntry2.Clear();
    m_pDoc->Query(0, aParam, true);
    CPPUNIT_ASSERT_MESSAGE("All rows should be shown.", !m_pDoc->RowHidden(0, 0, &nRow1, &nRow2));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("All rows should be shown.", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("All rows should be shown.", m_pDoc->MaxRow(), nRow2);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf76441)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // The result will be different depending on whether the format is set before
    // or after inserting the string

    OUString aCode = u"MM:SS"_ustr;
    sal_Int32 nCheckPos;
    SvNumFormatType nType;
    sal_uInt32 nFormat;
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    pFormatter->PutEntry( aCode, nCheckPos, nType, nFormat );

    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
    {
        // First insert the string, then the format
        m_pDoc->SetString(ScAddress(0,0,0), u"01:20"_ustr);

        m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs);

        CPPUNIT_ASSERT_EQUAL(u"20:00"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    }

    {
        // First set the format, then insert the string
        m_pDoc->ApplyPattern(0, 1, 0, aNewAttrs);

        m_pDoc->SetString(ScAddress(0,1,0), u"01:20"_ustr);

        // Without the fix in place, this test would have failed with
        // - Expected: 01:20
        // - Actual  : 20:00
        CPPUNIT_ASSERT_EQUAL(u"01:20"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf76836)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    OUString aCode = u"\"192.168.0.\"@"_ustr;
    sal_Int32 nCheckPos;
    SvNumFormatType nType;
    sal_uInt32 nFormat;
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    pFormatter->PutEntry( aCode, nCheckPos, nType, nFormat );

    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));

    m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs);
    m_pDoc->SetValue(0,0,0, 10.0);

    // Without the fix in place, this test would have failed with
    // - Expected: 10
    // - Actual  : 192.168.0.10
    CPPUNIT_ASSERT_EQUAL(u"10"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));

    m_pDoc->ApplyPattern(0, 1, 0, aNewAttrs);
    m_pDoc->SetString(ScAddress(0,1,0), u"10"_ustr);
    CPPUNIT_ASSERT_EQUAL(u"192.168.0.10"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf151752)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,0,0), u"66000:00"_ustr);

    // Without the fix in place, this test would have failed with
    // - Expected: 66000:00:00
    // - Actual  : 464:00:00
    CPPUNIT_ASSERT_EQUAL(u"66000:00:00"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf142186)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // The result will be different depending on whether the format is set before
    // or after inserting the string

    OUString aCode = u"0\".\"0"_ustr;
    sal_Int32 nCheckPos;
    SvNumFormatType nType;
    sal_uInt32 nFormat;
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    pFormatter->PutEntry( aCode, nCheckPos, nType, nFormat );

    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
    {
        // First insert the string, then the format
        m_pDoc->SetString(ScAddress(0,0,0), u"123.45"_ustr);

        m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs);

        CPPUNIT_ASSERT_EQUAL(u"12.3"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    }

    {
        // First set the format, then insert the string
        m_pDoc->ApplyPattern(0, 1, 0, aNewAttrs);

        m_pDoc->SetString(ScAddress(0,1,0), u"123.45"_ustr);

        // Without the fix in place, this test would have failed with
        // - Expected: 12.3
        // - Actual  : 1234.5
        CPPUNIT_ASSERT_EQUAL(u"12.3"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf137063)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetValue(0,0,0, 0.000000006);
    m_pDoc->SetValue(0,1,0, 0.0000000006);

    // Without the fix in place, this test would have failed with
    // - Expected: 0.000000006
    // - Actual  : 6E-09
    CPPUNIT_ASSERT_EQUAL(u"0.000000006"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"6E-10"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf164124)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    OUString aCode = u"YYYY-MM-DD"_ustr;
    sal_Int32 nCheckPos;
    SvNumFormatType nType;
    sal_uInt32 nFormat;
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    pFormatter->PutEntry( aCode, nCheckPos, nType, nFormat );

    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
    m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs);

    m_pDoc->SetString(ScAddress(0,0,0), u"2021-6/1"_ustr);

    // Without the fix in place, this test would have failed with
    // - Expected: 2021-6/1
    // - Actual  : 1905-07-19
    CPPUNIT_ASSERT_EQUAL(u"2021-6/1"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf126342)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    OUString aCode = u"YYYY-MM-DD"_ustr;
    sal_Int32 nCheckPos;
    SvNumFormatType nType;
    sal_uInt32 nFormat;
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    pFormatter->PutEntry( aCode, nCheckPos, nType, nFormat );

    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
    m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs);

    m_pDoc->SetString(ScAddress(0,0,0), u"11/7/19"_ustr);

    CPPUNIT_ASSERT_EQUAL(u"2019-11-07"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));

    // Overwrite the existing date with the exact same input
    m_pDoc->SetString(ScAddress(0,0,0), u"11/7/19"_ustr);

    // Without the fix in place, this test would have failed with
    // - Expected: 2019-11-07
    // - Actual  : 2011-07-19
    CPPUNIT_ASSERT_EQUAL(u"2019-11-07"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAdvancedFilter)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // cell contents (nullptr = empty cell)
    std::vector<std::vector<const char*>> aData = {
        { "Value", "Tag" }, // A1:B11
        {  "1", "R" },
        {  "2", "R" },
        {  "3", "R" },
        {  "4", "C" },
        {  "5", "C" },
        {  "6", "C" },
        {  "7", "R" },
        {  "8", "R" },
        {  "9", "R" },
        { "10", "C" },
        { nullptr },
        { "Value", "Tag" }, // A13:B14
        { "> 5", "R" },
    };

    // Populate cells.
    for (size_t nRow = 0; nRow < aData.size(); ++nRow)
    {
        const std::vector<const char*>& rRowData = aData[nRow];
        for (size_t nCol = 0; nCol < rRowData.size(); ++nCol)
        {
            const char* pCell = rRowData[nCol];
            if (pCell)
                m_pDoc->SetString(nCol, nRow, 0, OUString::createFromAscii(pCell));
        }
    }

    ScDBData* pDBData = new ScDBData(STR_DB_GLOBAL_NONAME, 0, 0, 0, 1, 10);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));

    ScRange aDataRange(0,0,0,1,10,0);
    ScRange aFilterRuleRange(0,12,0,1,13,0);

    printRange(m_pDoc, aDataRange, "Data");
    printRange(m_pDoc, aFilterRuleRange, "Filter Rule");

    ScQueryParam aQueryParam;
    aQueryParam.bHasHeader = true;
    aQueryParam.nCol1 = aDataRange.aStart.Col();
    aQueryParam.nRow1 = aDataRange.aStart.Row();
    aQueryParam.nCol2 = aDataRange.aEnd.Col();
    aQueryParam.nRow2 = aDataRange.aEnd.Row();
    aQueryParam.nTab = aDataRange.aStart.Tab();

    bool bGood = m_pDoc->CreateQueryParam(aFilterRuleRange, aQueryParam);
    CPPUNIT_ASSERT_MESSAGE("failed to create query param.", bGood);

    // First entry is for the 'Value' field, and is greater than 5.
    ScQueryEntry aEntry = aQueryParam.GetEntry(0);
    CPPUNIT_ASSERT(aEntry.bDoQuery);
    CPPUNIT_ASSERT_EQUAL(SCCOLROW(0), aEntry.nField);
    CPPUNIT_ASSERT_EQUAL(SC_GREATER, aEntry.eOp);

    ScQueryEntry::QueryItemsType aItems = aEntry.GetQueryItems();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aItems.size());
    CPPUNIT_ASSERT_EQUAL(ScQueryEntry::ByValue, aItems[0].meType);
    CPPUNIT_ASSERT_EQUAL(5.0, aItems[0].mfVal);

    // Second entry is for the 'Tag' field, and is == 'R'.
    aEntry = aQueryParam.GetEntry(1);
    CPPUNIT_ASSERT(aEntry.bDoQuery);
    CPPUNIT_ASSERT_EQUAL(SCCOLROW(1), aEntry.nField);
    CPPUNIT_ASSERT_EQUAL(SC_EQUAL, aEntry.eOp);

    aItems = aEntry.GetQueryItems();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aItems.size());
    CPPUNIT_ASSERT_EQUAL(ScQueryEntry::ByString, aItems[0].meType);
    CPPUNIT_ASSERT_EQUAL(u"R"_ustr, aItems[0].maString.getString());

    // perform the query.
    m_pDoc->Query(0, aQueryParam, true);

    // Only rows 1,8-10 should be visible.
    bool bFiltered = m_pDoc->RowFiltered(0, 0);
    CPPUNIT_ASSERT_MESSAGE("row 1 (header row) should be visible", !bFiltered);

    SCROW nRow1 = -1, nRow2 = -1;
    bFiltered = m_pDoc->RowFiltered(1, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 2-7 should be filtered out.", bFiltered);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 2-7 should be filtered out.", SCROW(1), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 2-7 should be filtered out.", SCROW(6), nRow2);

    bFiltered = m_pDoc->RowFiltered(7, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("rows 8-10 should be visible.", !bFiltered);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 8-10 should be visible.", SCROW(7), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("rows 8-10 should be visible.", SCROW(9), nRow2);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDateFilterContains)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    constexpr SCCOL nCols = 1;
    constexpr SCROW nRows = 5;
    m_pDoc->SetString(0, 0, 0, u"Date"_ustr);
    m_pDoc->SetString(0, 1, 0, u"1/2/2021"_ustr);
    m_pDoc->SetString(0, 2, 0, u"2/1/1999"_ustr);
    m_pDoc->SetString(0, 3, 0, u"2/1/1997"_ustr);
    m_pDoc->SetString(0, 4, 0, u"3/3/2001"_ustr);
    m_pDoc->SetString(0, 5, 0, u"3/3/1996"_ustr);

    // Set the fields as dates.
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    sal_uInt32 nFormat = pFormatter->GetFormatIndex(NF_DATE_DIN_YYMMDD, LANGUAGE_ENGLISH_US);
    ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
    aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
    m_pDoc->ApplyPatternAreaTab(0, 1, 0, 5, 0, aNewAttrs); // apply it to A1:A6

    ScDBData* pDBData = new ScDBData(u"NONAME"_ustr, 0, 0, 0, nCols, nRows);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));

    pDBData->SetAutoFilter(true);
    ScRange aRange;
    pDBData->GetArea(aRange);
    m_pDoc->ApplyFlagsTab( aRange.aStart.Col(), aRange.aStart.Row(),
                           aRange.aEnd.Col(), aRange.aStart.Row(),
                           aRange.aStart.Tab(), ScMF::Auto);

    //create the query param
    ScQueryParam aParam;
    pDBData->GetQueryParam(aParam);
    ScQueryEntry& rEntry = aParam.GetEntry(0);
    rEntry.bDoQuery = true;
    rEntry.nField = 0;
    rEntry.eOp = SC_CONTAINS;
    rEntry.GetQueryItem().maString = m_pDoc->GetSharedStringPool().intern(u"2"_ustr);
    pDBData->SetQueryParam(aParam);

    // perform the query.
    m_pDoc->Query(0, aParam, true);

    // Dates in rows 2-4 contain '2', row 5 shows 2001 only as 01, and row 6 doesn't contain it at all.
    CPPUNIT_ASSERT_MESSAGE("row 2 should be visible", !m_pDoc->RowHidden(1, 0));
    CPPUNIT_ASSERT_MESSAGE("row 3 should be visible", !m_pDoc->RowHidden(2, 0));
    CPPUNIT_ASSERT_MESSAGE("row 4 should be visible", !m_pDoc->RowHidden(3, 0));
    CPPUNIT_ASSERT_MESSAGE("row 5 should be hidden", m_pDoc->RowHidden(4, 0));
    CPPUNIT_ASSERT_MESSAGE("row 6 should be hidden", m_pDoc->RowHidden(5, 0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf98642)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->SetString(0, 0, 0, u"test"_ustr);

    ScRangeData* pName1 = new ScRangeData( *m_pDoc, u"name1"_ustr, u"$Sheet1.$A$1"_ustr);
    ScRangeData* pName2 = new ScRangeData( *m_pDoc, u"name2"_ustr, u"$Sheet1.$A$1"_ustr);

    std::unique_ptr<ScRangeName> pGlobalRangeName(new ScRangeName());
    pGlobalRangeName->insert(pName1);
    pGlobalRangeName->insert(pName2);
    m_pDoc->SetRangeName(std::move(pGlobalRangeName));

    m_pDoc->SetString(1, 0, 0, u"=name1"_ustr);
    m_pDoc->SetString(1, 1, 0, u"=name2"_ustr);

    CPPUNIT_ASSERT_EQUAL(u"test"_ustr, m_pDoc->GetString(1, 0, 0));
    CPPUNIT_ASSERT_EQUAL(u"test"_ustr, m_pDoc->GetString(1, 1, 0));

    OUString aFormula = m_pDoc->GetFormula(1,0,0);
    CPPUNIT_ASSERT_EQUAL(u"=name1"_ustr, aFormula);
    aFormula = m_pDoc->GetFormula(1,1,0);

    // Without the fix in place, this test would have failed with
    // - Expected: =name2
    // - Actual  : =name1
    CPPUNIT_ASSERT_EQUAL(u"=name2"_ustr, aFormula);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMergedCells)
{
    //test merge and unmerge
    //TODO: an undo/redo test for this would be a good idea
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->DoMerge(1, 1, 3, 3, 0, false);
    SCCOL nEndCol = 1;
    SCROW nEndRow = 1;
    m_pDoc->ExtendMerge( 1, 1, nEndCol, nEndRow, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("did not merge cells", SCCOL(3), nEndCol);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("did not merge cells", SCROW(3), nEndRow);
    ScRange aRange(0,2,0,m_pDoc->MaxCol(),2,0);
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SetMarkArea(aRange);
    m_xDocShell->GetDocFunc().InsertCells(aRange, &aMark, INS_INSROWS_BEFORE, true, true);
    m_pDoc->ExtendMerge(1, 1, nEndCol, nEndRow, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("did not increase merge area", SCCOL(3), nEndCol);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("did not increase merge area", SCROW(4), nEndRow);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testRenameTable)
{
    //test set rename table
    //TODO: set name1 and name2 and do an undo to check if name 1 is set now
    //TODO: also check if new name for table is same as another table

    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->InsertTab(1, u"Sheet2"_ustr);

    //test case 1 , rename table2 to sheet 1, it should return error
    OUString nameToSet = u"Sheet1"_ustr;
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    CPPUNIT_ASSERT_MESSAGE("name same as another table is being set", !rDocFunc.RenameTable(1,nameToSet,false,true) );

    //test case 2 , simple rename to check name
    nameToSet = u"test1"_ustr;
    m_xDocShell->GetDocFunc().RenameTable(0,nameToSet,false,true);
    OUString nameJustSet;
    m_pDoc->GetName(0,nameJustSet);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("table not renamed", nameToSet, nameJustSet);

    //test case 3 , rename again
    OUString anOldName;
    m_pDoc->GetName(0,anOldName);

    nameToSet = u"test2"_ustr;
    rDocFunc.RenameTable(0,nameToSet,false,true);
    m_pDoc->GetName(0,nameJustSet);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("table not renamed", nameToSet, nameJustSet);

    //test case 4 , check if  undo works
    SfxUndoAction* pUndo = new ScUndoRenameTab(*m_xDocShell,0,anOldName,nameToSet);
    pUndo->Undo();
    m_pDoc->GetName(0,nameJustSet);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct name is not set after undo", nameJustSet, anOldName);

    pUndo->Redo();
    m_pDoc->GetName(0,nameJustSet);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct color is not set after redo", nameJustSet, nameToSet);
    delete pUndo;

    m_pDoc->DeleteTab(0);
    m_pDoc->DeleteTab(1);
}

CPPUNIT_TEST_FIXTURE(Test, testSetBackgroundColor)
{
    //test set background color
    //TODO: set color1 and set color2 and do an undo to check if color1 is set now.

    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    Color aColor;

     //test yellow
    aColor=COL_YELLOW;
    m_xDocShell->GetDocFunc().SetTabBgColor(0,aColor,false, true);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct color is not set",
                           aColor, m_pDoc->GetTabBgColor(0));

    Color aOldTabBgColor=m_pDoc->GetTabBgColor(0);
    aColor = COL_BLUE;
    m_xDocShell->GetDocFunc().SetTabBgColor(0,aColor,false, true);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct color is not set the second time",
                           aColor, m_pDoc->GetTabBgColor(0));

    //now check for undo
    SfxUndoAction* pUndo = new ScUndoTabColor(*m_xDocShell, 0, aOldTabBgColor, aColor);
    pUndo->Undo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct color is not set after undo", aOldTabBgColor, m_pDoc->GetTabBgColor(0));
    pUndo->Redo();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("the correct color is not set after undo", aColor, m_pDoc->GetTabBgColor(0));
    delete pUndo;
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testUpdateReference)
{
    //test that formulas are correctly updated during sheet delete
    //TODO: add tests for relative references, updating of named ranges, ...
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->InsertTab(1, u"Sheet2"_ustr);
    m_pDoc->InsertTab(2, u"Sheet3"_ustr);
    m_pDoc->InsertTab(3, u"Sheet4"_ustr);

    m_pDoc->SetValue(0,0,2, 1);
    m_pDoc->SetValue(1,0,2, 2);
    m_pDoc->SetValue(1,1,3, 4);
    m_pDoc->SetString(2,0,2, u"=A1+B1"_ustr);
    m_pDoc->SetString(2,1,2, u"=Sheet4.B2+A1"_ustr);

    double aValue;
    aValue = m_pDoc->GetValue(2,0,2);
    ASSERT_DOUBLES_EQUAL_MESSAGE("formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2,1,2);
    ASSERT_DOUBLES_EQUAL_MESSAGE("formula does not return correct result", 5, aValue);

    //test deleting both sheets: one is not directly before the sheet, the other one is
    m_pDoc->DeleteTab(0);
    aValue = m_pDoc->GetValue(2,0,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting first sheet formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2,1,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting first sheet formula does not return correct result", 5, aValue);

    m_pDoc->DeleteTab(0);
    aValue = m_pDoc->GetValue(2,0,0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting second sheet formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2,1,0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting second sheet formula does not return correct result", 5, aValue);

    //test adding two sheets
    m_pDoc->InsertTab(0, u"Sheet2"_ustr);
    aValue = m_pDoc->GetValue(2,0,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting first sheet formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2,1,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting first sheet formula does not return correct result", 5, aValue);

    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    aValue = m_pDoc->GetValue(2,0,2);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting second sheet formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2,1,2);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting second sheet formula does not return correct result", 5, aValue);

    //test new DeleteTabs/InsertTabs methods
    m_pDoc->DeleteTabs(0, 2);
    aValue = m_pDoc->GetValue(2, 0, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting sheets formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2, 1, 0);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after deleting sheets formula does not return correct result", 5, aValue);

    std::vector<OUString> aSheets;
    aSheets.emplace_back("Sheet1");
    aSheets.emplace_back("Sheet2");
    m_pDoc->InsertTabs(0, aSheets, true);
    aValue = m_pDoc->GetValue(2, 0, 2);

    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting sheets formula does not return correct result", 3, aValue);
    aValue = m_pDoc->GetValue(2, 1, 2);
    ASSERT_DOUBLES_EQUAL_MESSAGE("after inserting sheets formula does not return correct result", 5, aValue);

    m_pDoc->DeleteTab(3);
    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);

    // Test positional update and invalidation of lookup cache for insertion
    // and deletion within entire column reference.
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);
    m_pDoc->InsertTab(1, u"Sheet2"_ustr);
    m_pDoc->SetString(0,1,0, u"s1"_ustr);
    m_pDoc->SetString(0,0,1, u"=MATCH(\"s1\";Sheet1.A:A;0)"_ustr);
    aValue = m_pDoc->GetValue(0,0,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("unexpected MATCH result", 2, aValue);
    m_pDoc->InsertRow(0,0,m_pDoc->MaxCol(),0,0,1);    // insert 1 row before row 1 in Sheet1
    aValue = m_pDoc->GetValue(0,0,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("unexpected MATCH result", 3, aValue);
    m_pDoc->DeleteRow(0,0,m_pDoc->MaxCol(),0,0,1);    // delete row 1 in Sheet1
    aValue = m_pDoc->GetValue(0,0,1);
    ASSERT_DOUBLES_EQUAL_MESSAGE("unexpected MATCH result", 2, aValue);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testSearchCells)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,0,0), u"A"_ustr);
    m_pDoc->SetString(ScAddress(0,1,0), u"B"_ustr);
    m_pDoc->SetString(ScAddress(0,2,0), u"A"_ustr);
    // Leave A4 blank.
    m_pDoc->SetString(ScAddress(0,4,0), u"A"_ustr);
    m_pDoc->SetString(ScAddress(0,5,0), u"B"_ustr);
    m_pDoc->SetString(ScAddress(0,6,0), u"C"_ustr);

    SvxSearchItem aItem(SID_SEARCH_ITEM);
    aItem.SetSearchString(u"A"_ustr);
    aItem.SetCommand(SvxSearchCmd::FIND_ALL);
    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectOneTable(0);
    SCCOL nCol = 0;
    SCROW nRow = 0;
    SCTAB nTab = 0;
    ScRangeList aMatchedRanges;
    OUString aUndoStr;
    bool bMatchedRangesWereClamped = false;
    bool bSuccess = m_pDoc->SearchAndReplace(aItem, nCol, nRow, nTab, aMarkData, aMatchedRanges, aUndoStr, nullptr, bMatchedRangesWereClamped);

    CPPUNIT_ASSERT_MESSAGE("Search And Replace should succeed", bSuccess);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be exactly 3 matching cells.", size_t(3), aMatchedRanges.size());
    ScAddress aHit(0,0,0);
    CPPUNIT_ASSERT_MESSAGE("A1 should be inside the matched range.", aMatchedRanges.Contains(ScRange(aHit)));
    aHit.SetRow(2);
    CPPUNIT_ASSERT_MESSAGE("A3 should be inside the matched range.", aMatchedRanges.Contains(ScRange(aHit)));
    aHit.SetRow(4);
    CPPUNIT_ASSERT_MESSAGE("A5 should be inside the matched range.", aMatchedRanges.Contains(ScRange(aHit)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFormulaPosition)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    ScAddress aPos(0,0,0); // A1
    m_pDoc->SetString(aPos, u"=ROW()"_ustr);
    aPos.IncRow(); // A2
    m_pDoc->SetString(aPos, u"=ROW()"_ustr);
    aPos.SetRow(3); // A4;
    m_pDoc->SetString(aPos, u"=ROW()"_ustr);

    {
        SCROW aRows[] = { 0, 1, 3 };
        bool bRes = checkFormulaPositions(*m_pDoc, aPos.Tab(), aPos.Col(), aRows, std::size(aRows));
        CPPUNIT_ASSERT(bRes);
    }

    m_pDoc->InsertRow(0,0,0,0,1,5); // Insert 5 rows at A2.
    {
        SCROW aRows[] = { 0, 6, 8 };
        bool bRes = checkFormulaPositions(*m_pDoc, aPos.Tab(), aPos.Col(), aRows, std::size(aRows));
        CPPUNIT_ASSERT(bRes);
    }

    m_pDoc->DeleteTab(0);
}

namespace {

bool hasRange(const ScDocument* pDoc, const std::vector<ScTokenRef>& rRefTokens, const ScRange& rRange, const ScAddress& rPos)
{
    for (const ScTokenRef& p : rRefTokens)
    {
        if (!ScRefTokenHelper::isRef(p) || ScRefTokenHelper::isExternalRef(p))
            continue;

        switch (p->GetType())
        {
            case formula::svSingleRef:
            {
                ScSingleRefData aData = static_cast<ScSingleRefToken*>(p.get())->GetSingleRef();
                if (rRange.aStart != rRange.aEnd)
                    break;

                ScAddress aThis = aData.toAbs(*pDoc, rPos);
                if (aThis == rRange.aStart)
                    return true;
            }
            break;
            case formula::svDoubleRef:
            {
                ScComplexRefData aData = static_cast<ScDoubleRefToken*>(p.get())->GetDoubleRef();
                ScRange aThis = aData.toAbs(*pDoc, rPos);
                if (aThis == rRange)
                    return true;
            }
            break;
            default:
                ;
        }
    }
    return false;
}

}

CPPUNIT_TEST_FIXTURE(Test, testJumpToPrecedentsDependents)
{
    /**
     * Test to make sure correct precedent / dependent cells are obtained when
     * preparing to jump to them.
     */
    // Precedent is another cell that the cell references, while dependent is
    // another cell that references it.
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(2, 0, 0, u"=A1+A2+B3"_ustr); // C1
    m_pDoc->SetString(2, 1, 0, u"=A1"_ustr);       // C2
    m_pDoc->CalcAll();

    std::vector<ScTokenRef> aRefTokens;
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();

    {
        // C1's precedent should be A1:A2,B3.
        ScAddress aC1(2, 0, 0);
        ScRangeList aRange((ScRange(aC1)));
        rDocFunc.DetectiveCollectAllPreds(aRange, aRefTokens);
        CPPUNIT_ASSERT_MESSAGE("A1:A2 should be a precedent of C1.",
                               hasRange(m_pDoc, aRefTokens, ScRange(0, 0, 0, 0, 1, 0), aC1));
        CPPUNIT_ASSERT_MESSAGE("B3 should be a precedent of C1.",
                               hasRange(m_pDoc, aRefTokens, ScRange(1, 2, 0), aC1));
    }

    {
        // C2's precedent should be A1 only.
        ScAddress aC2(2, 1, 0);
        ScRangeList aRange((ScRange(aC2)));
        rDocFunc.DetectiveCollectAllPreds(aRange, aRefTokens);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("there should only be one reference token.",
                               static_cast<size_t>(1), aRefTokens.size());
        CPPUNIT_ASSERT_MESSAGE("A1 should be a precedent of C1.",
                               hasRange(m_pDoc, aRefTokens, ScRange(0, 0, 0), aC2));
    }

    {
        // A1's dependent should be C1:C2.
        ScAddress aA1(0, 0, 0);
        ScRangeList aRange((ScRange(aA1)));
        rDocFunc.DetectiveCollectAllSuccs(aRange, aRefTokens);
        CPPUNIT_ASSERT_EQUAL_MESSAGE("C1:C2 should be the only dependent of A1.",
                               std::vector<ScTokenRef>::size_type(1), aRefTokens.size());
        CPPUNIT_ASSERT_MESSAGE("C1:C2 should be the only dependent of A1.",
                               hasRange(m_pDoc, aRefTokens, ScRange(2, 0, 0, 2, 1, 0), aA1));
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf149665)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(0, 0, 0, u"''1"_ustr);

    // Without the fix in place, this test would have failed with
    // - Expected: '1
    // - Actual  : ''1
    CPPUNIT_ASSERT_EQUAL( u"'1"_ustr, m_pDoc->GetString( 0, 0, 0 ) );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf64001)
{
    m_pDoc->InsertTab(0, u"test"_ustr);

    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectTable(0, true);

    m_pDoc->SetString( 0, 0, 0, u"TRUE"_ustr );
    m_pDoc->Fill( 0, 0, 0, 0, nullptr, aMarkData, 9, FILL_TO_BOTTOM, FILL_AUTO );

    for (SCCOL i = 0; i < 10; ++i)
    {
        CPPUNIT_ASSERT_EQUAL( u"TRUE"_ustr, m_pDoc->GetString( 0, i, 0 ) );
    }

    m_pDoc->SetString( 0, 10, 0, u"FALSE"_ustr );

    m_pDoc->SetString( 1, 0, 0, u"=COUNTIF(A1:A11;TRUE)"_ustr );

    // Without the fix in place, this test would have failed with
    // - Expected: 10
    // - Actual  : 1
    CPPUNIT_ASSERT_EQUAL( 10.0, m_pDoc->GetValue( 1, 0, 0 ) );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutoFill)
{
    m_pDoc->InsertTab(0, u"test"_ustr);

    m_pDoc->SetValue(0,0,0,1);

    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectTable(0, true);

    m_pDoc->Fill( 0, 0, 0, 0, nullptr, aMarkData, 5);
    for (SCROW i = 0; i< 6; ++i)
        ASSERT_DOUBLES_EQUAL(static_cast<double>(i+1.0), m_pDoc->GetValue(0, i, 0));

    // check that hidden rows are not affected by autofill
    // set values for hidden rows
    m_pDoc->SetValue(0,1,0,10);
    m_pDoc->SetValue(0,2,0,10);

    m_pDoc->SetRowHidden(1, 2, 0, true);
    m_pDoc->Fill( 0, 0, 0, 0, nullptr, aMarkData, 8);

    ASSERT_DOUBLES_EQUAL(10.0, m_pDoc->GetValue(0,1,0));
    ASSERT_DOUBLES_EQUAL(10.0, m_pDoc->GetValue(0,2,0));
    for (SCROW i = 3; i< 8; ++i)
        ASSERT_DOUBLES_EQUAL(static_cast<double>(i-1.0), m_pDoc->GetValue(0, i, 0));

    m_pDoc->Fill( 0, 0, 0, 8, nullptr, aMarkData, 5, FILL_TO_RIGHT );
    for (SCCOL i = 0; i < 5; ++i)
    {
        for(SCROW j = 0; j < 8; ++j)
        {
            if (j > 2)
            {
                ASSERT_DOUBLES_EQUAL(static_cast<double>(j-1+i), m_pDoc->GetValue(i, j, 0));
            }
            else if (j == 0)
            {
                ASSERT_DOUBLES_EQUAL(static_cast<double>(i+1), m_pDoc->GetValue(i, 0, 0));
            }
            else // j == 1 || j == 2
            {
                if(i == 0)
                    ASSERT_DOUBLES_EQUAL(10.0, m_pDoc->GetValue(0,j,0));
                else
                    ASSERT_DOUBLES_EQUAL(0.0, m_pDoc->GetValue(i,j,0));
            }
        }
    }

    // test auto fill user data lists
    m_pDoc->SetString( 0, 100, 0, u"January"_ustr );
    m_pDoc->Fill( 0, 100, 0, 100, nullptr, aMarkData, 2, FILL_TO_BOTTOM, FILL_AUTO );
    OUString aTestValue = m_pDoc->GetString( 0, 101, 0 );
    CPPUNIT_ASSERT_EQUAL( u"February"_ustr, aTestValue );
    aTestValue = m_pDoc->GetString( 0, 102, 0 );
    CPPUNIT_ASSERT_EQUAL( u"March"_ustr, aTestValue );

    // test that two same user data list entries will not result in incremental fill
    m_pDoc->SetString( 0, 101, 0, u"January"_ustr );
    m_pDoc->Fill( 0, 100, 0, 101, nullptr, aMarkData, 2, FILL_TO_BOTTOM, FILL_AUTO );
    for ( SCROW i = 102; i <= 103; ++i )
    {
        aTestValue = m_pDoc->GetString( 0, i, 0 );
        CPPUNIT_ASSERT_EQUAL( u"January"_ustr, aTestValue );
    }

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    // Fill A1:A6 with 1,2,3,4,5,6.
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    m_pDoc->SetValue(ScAddress(0,0,0), 1.0);
    ScRange aRange(0,0,0,0,5,0);
    aMarkData.SetMarkArea(aRange);
    rFunc.FillSeries(aRange, &aMarkData, FILL_TO_BOTTOM, FILL_AUTO, FILL_DAY, MAXDOUBLE, 1.0, MAXDOUBLE, true);
    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0,2,0)));
    CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0,3,0)));
    CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0,4,0)));
    CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(0,5,0)));

    // Undo should clear the area except for the top cell.
    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoMgr);
    pUndoMgr->Undo();

    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    for (SCROW i = 1; i <= 5; ++i)
        CPPUNIT_ASSERT_EQUAL(CELLTYPE_NONE, m_pDoc->GetCellType(ScAddress(0,i,0)));

    // Redo should put the serial values back in.
    pUndoMgr->Redo();
    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0,2,0)));
    CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0,3,0)));
    CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0,4,0)));
    CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(0,5,0)));

    // test that filling formulas vertically up does the right thing
    for(SCROW nRow = 0; nRow < 10; ++nRow)
        m_pDoc->SetValue(100, 100 + nRow, 0, 1);

    m_pDoc->SetString(100, 110, 0, u"=A111"_ustr);

    m_pDoc->Fill(100, 110, 100, 110, nullptr, aMarkData, 10, FILL_TO_TOP, FILL_AUTO);
    for(SCROW nRow = 110; nRow >= 100; --nRow)
    {
        OUString aExpected = "=A" + OUString::number(nRow +1);
        OUString aFormula = m_pDoc->GetFormula(100, nRow, 0);
        CPPUNIT_ASSERT_EQUAL(aExpected, aFormula);
    }

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 100, 0, u"2012-10-31"_ustr );
    m_pDoc->SetString( 0, 101, 0, u"2012-10-31"_ustr );
    m_pDoc->Fill( 0, 100, 0, 101, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#89754, Without the fix in place, this test would have failed with
    // - Expected: 2012-10-31
    // - Actual  : 2012-11-01
    CPPUNIT_ASSERT_EQUAL( u"2012-10-31"_ustr, m_pDoc->GetString( 0, 102, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"2012-10-31"_ustr, m_pDoc->GetString( 0, 103, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"2012-10-31"_ustr, m_pDoc->GetString( 0, 104, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0, 0, 0, 0, m_pDoc->MaxRow(), 0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString(0, 100, 0, u"2019-10-31"_ustr);
    m_pDoc->SetString(0, 101, 0, u"2019-11-30"_ustr);
    m_pDoc->SetString(0, 102, 0, u"2019-12-31"_ustr);
    m_pDoc->Fill(0, 100, 0, 102, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO);

    // tdf#58745, Without the fix in place, this test would have failed with
    // - Expected: 2020-01-31
    // - Actual  : 2019-01-11
    CPPUNIT_ASSERT_EQUAL(u"2020-01-31"_ustr, m_pDoc->GetString(0, 103, 0));
    CPPUNIT_ASSERT_EQUAL(u"2020-02-29"_ustr, m_pDoc->GetString(0, 104, 0));
    CPPUNIT_ASSERT_EQUAL(u"2020-03-31"_ustr, m_pDoc->GetString(0, 105, 0));

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 50, 0, u"1.0"_ustr );
    m_pDoc->SetString( 0, 51, 0, u"1.1"_ustr );
    m_pDoc->SetString( 0, 52, 0, u"1.2"_ustr );
    m_pDoc->SetString( 0, 53, 0, u"1.3"_ustr );
    m_pDoc->Fill( 0, 50, 0, 53, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO );

    CPPUNIT_ASSERT_EQUAL( u"1.4"_ustr, m_pDoc->GetString( 0, 54, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"1.5"_ustr, m_pDoc->GetString( 0, 55, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"1.6"_ustr, m_pDoc->GetString( 0, 56, 0 ) );

    m_pDoc->SetString( 0, 60, 0, u"4.0"_ustr );
    m_pDoc->SetString( 0, 61, 0, u"4.1"_ustr );
    m_pDoc->SetString( 0, 62, 0, u"4.2"_ustr );
    m_pDoc->SetString( 0, 63, 0, u"4.3"_ustr );
    m_pDoc->Fill( 0, 60, 0, 63, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#37424: Without the fix in place, this test would have failed with
    // - Expected: 4.4
    // - Actual  : 5
    CPPUNIT_ASSERT_EQUAL( u"4.4"_ustr, m_pDoc->GetString( 0, 64, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"4.5"_ustr, m_pDoc->GetString( 0, 65, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"4.6"_ustr, m_pDoc->GetString( 0, 66, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 70, 0, u"001-001-001"_ustr );
    m_pDoc->Fill( 0, 70, 0, 70, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#105268: Without the fix in place, this test would have failed with
    // - Expected: 001-001-002
    // - Actual  : 001-001000
    CPPUNIT_ASSERT_EQUAL( u"001-001-002"_ustr, m_pDoc->GetString( 0, 71, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"001-001-003"_ustr, m_pDoc->GetString( 0, 72, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"001-001-004"_ustr, m_pDoc->GetString( 0, 73, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 80, 0, u"1%"_ustr );
    m_pDoc->Fill( 0, 80, 0, 80, nullptr, aMarkData, 3, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#89998: Without the fix in place, this test would have failed with
    // - Expected: 2.00%
    // - Actual  : 101.00%
    CPPUNIT_ASSERT_EQUAL( u"2.00%"_ustr, m_pDoc->GetString( 0, 81, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"3.00%"_ustr, m_pDoc->GetString( 0, 82, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"4.00%"_ustr, m_pDoc->GetString( 0, 83, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 0, 0, u"1"_ustr );
    m_pDoc->SetString( 0, 1, 0, u"1.1"_ustr );
    m_pDoc->Fill( 0, 0, 0, 1, nullptr, aMarkData, 60, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#129606: Without the fix in place, this test would have failed with
    // - Expected: 6
    // - Actual  : 6.00000000000001
    CPPUNIT_ASSERT_EQUAL( u"6"_ustr, m_pDoc->GetString( 0, 50, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 0, 0, u"2022-10-01 00:00:00.000"_ustr );
    m_pDoc->SetString( 0, 1, 0, u"2022-10-01 01:00:00.000"_ustr );
    m_pDoc->Fill( 0, 0, 0, 1, nullptr, aMarkData, 25, FILL_TO_BOTTOM, FILL_AUTO );

    // tdf#151460: Without the fix in place, this test would have failed with
    // - Expected: 2022-10-01 20:00:00.000
    // - Actual  : 2022-10-01 19:59:59.999
    CPPUNIT_ASSERT_EQUAL( u"2022-10-01 20:00:00.000"_ustr, m_pDoc->GetString( 0, 20, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 0, 0, u"1st"_ustr );

    m_pDoc->Fill( 0, 0, 0, 0, nullptr, aMarkData, 5, FILL_TO_BOTTOM, FILL_AUTO );

    CPPUNIT_ASSERT_EQUAL( u"1st"_ustr, m_pDoc->GetString( 0, 0, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"2nd"_ustr, m_pDoc->GetString( 0, 1, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"3rd"_ustr, m_pDoc->GetString( 0, 2, 0 ) );

    // Clear column A for a new test.
    clearRange(m_pDoc, ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    m_pDoc->SetRowHidden(0, m_pDoc->MaxRow(), 0, false); // Show all rows.

    m_pDoc->SetString( 0, 200, 0, u"15:00"_ustr );
    m_pDoc->SetString( 0, 201, 0, u"15:20"_ustr );
    m_pDoc->Fill( 0, 200, 0, 201, nullptr, aMarkData, 25, FILL_TO_BOTTOM, FILL_AUTO );

    CPPUNIT_ASSERT_EQUAL( u"03:00:00 PM"_ustr, m_pDoc->GetString( 0, 200, 0 ) );

    // tdf#153517: Without the fix in place, this test would have failed with
    // - Expected: 03:20:00 PM
    // - Actual  : 03:19:59 PM
    CPPUNIT_ASSERT_EQUAL( u"03:20:00 PM"_ustr, m_pDoc->GetString( 0, 201, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"03:40:00 PM"_ustr, m_pDoc->GetString( 0, 202, 0 ) );
    CPPUNIT_ASSERT_EQUAL( u"04:00:00 PM"_ustr, m_pDoc->GetString( 0, 203, 0 ) );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTdf55960_FillSeriesPrecision)
{
    m_pDoc->InsertTab(0, u"tdf55960"_ustr);

    m_pDoc->SetValue(0, 0, 0, -2.0);
    m_pDoc->SetValue(0, 1, 0, -1.95);

    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectTable(0, true);

    // Autofill should preserve the detected 0.05 step without accumulating
    // floating point artifacts around zero.
    m_pDoc->Fill(0, 0, 0, 1, nullptr, aMarkData, 80, FILL_TO_BOTTOM, FILL_AUTO);

    CPPUNIT_ASSERT_EQUAL(-1.0, m_pDoc->GetValue(ScAddress(0, 20, 0)));
    CPPUNIT_ASSERT_EQUAL( 0.0, m_pDoc->GetValue(ScAddress(0, 40, 0)));
    CPPUNIT_ASSERT_EQUAL( 1.0, m_pDoc->GetValue(ScAddress(0, 60, 0)));
    CPPUNIT_ASSERT_EQUAL( 2.0, m_pDoc->GetValue(ScAddress(0, 80, 0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutoFillSimple)
{
    m_pDoc->InsertTab(0, u"test"_ustr);

    m_pDoc->SetValue(0, 0, 0, 1);
    m_pDoc->SetString(0, 1, 0, u"=10"_ustr);

    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectTable(0, true);

    m_pDoc->Fill( 0, 0, 0, 1, nullptr, aMarkData, 6, FILL_TO_BOTTOM, FILL_AUTO);

    for(SCROW nRow = 0; nRow < 8; ++nRow)
    {
        if (nRow % 2 == 0)
        {
            double nVal = m_pDoc->GetValue(0, nRow, 0);
            CPPUNIT_ASSERT_EQUAL((nRow+2)/2.0, nVal);
        }
        else
        {
            OString aMsg = "wrong value in row: " + OString::number(nRow);
            double nVal = m_pDoc->GetValue(0, nRow, 0);
            CPPUNIT_ASSERT_EQUAL_MESSAGE(aMsg.getStr(), 10.0, nVal);
        }
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFindAreaPosVertical)
{
    std::vector<std::vector<const char*>> aData = {
        {   nullptr, "1", "1" },
        { "1",   nullptr, "1" },
        { "1", "1", "1" },
        {   nullptr, "1", "1" },
        { "1", "1", "1" },
        { "1",   nullptr, "1" },
        { "1", "1", "1" },
    };

    m_pDoc->InsertTab(0, u"Test1"_ustr);
    clearRange( m_pDoc, ScRange(0, 0, 0, 1, aData.size(), 0));
    ScAddress aPos(0,0,0);
    ScRange aDataRange = insertRangeData( m_pDoc, aPos, aData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("failed to insert range data at correct position", aPos, aDataRange.aStart);

    m_pDoc->SetRowHidden(4,4,0,true);
    bool bHidden = m_pDoc->RowHidden(4,0);
    CPPUNIT_ASSERT(bHidden);

    SCCOL nCol = 0;
    SCROW nRow = 0;
    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(1), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(0), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(0), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(5), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(0), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(6), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(0), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(m_pDoc->MaxRow(), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(0), nCol);

    nCol = 1;
    nRow = 2;

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(3), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(1), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_DOWN);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(6), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(1), nCol);

    nCol = 2;
    nRow = 6;
    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_UP);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(2), nCol);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFindAreaPosColRight)
{
    std::vector<std::vector<const char*>> aData = {
        { "", "1", "1", "", "1", "1", "1" },
        { "", "", "1", "1", "1", "", "1" },
    };

    m_pDoc->InsertTab(0, u"test1"_ustr);
    clearRange( m_pDoc, ScRange(0, 0, 0, 7, aData.size(), 0));
    ScAddress aPos(0,0,0);
    ScRange aDataRange = insertRangeData( m_pDoc, aPos, aData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("failed to insert range data at correct position", aPos, aDataRange.aStart);

    m_pDoc->SetColHidden(4,4,0,true);
    bool bHidden = m_pDoc->ColHidden(4,0);
    CPPUNIT_ASSERT(bHidden);

    SCCOL nCol = 0;
    SCROW nRow = 0;
    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(1), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(2), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(5), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(6), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(0), nRow);
    CPPUNIT_ASSERT_EQUAL(m_pDoc->MaxCol(), nCol);

    nCol = 2;
    nRow = 1;

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(1), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(3), nCol);

    m_pDoc->FindAreaPos(nCol, nRow, 0, SC_MOVE_RIGHT);

    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(1), nRow);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCCOL>(6), nCol);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testShiftCells)
{
    m_pDoc->InsertTab(0, u"foo"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    OUString aTestVal(u"Some Text"_ustr);

    // Text into cell E5.
    m_pDoc->SetString(4, 3, 0, aTestVal);

    // put a Note in cell E5
    ScAddress rAddr(4, 3, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(rAddr);
    pNote->SetText(rAddr, u"Hello"_ustr);

    CPPUNIT_ASSERT_MESSAGE("there should be a note", m_pDoc->HasNote(4, 3, 0));

    // Insert cell at D5. This should shift the string cell to right.
    m_pDoc->InsertCol(3, 0, 3, 0, 3, 1);
    OUString aStr = m_pDoc->GetString(5, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("We should have a string cell here.", aTestVal, aStr);
    CPPUNIT_ASSERT_MESSAGE("D5 is supposed to be blank.", m_pDoc->IsBlockEmpty(3, 4, 3, 4, 0));

    CPPUNIT_ASSERT_MESSAGE("there should be NO note", !m_pDoc->HasNote(4, 3, 0));
    CPPUNIT_ASSERT_MESSAGE("there should be a note", m_pDoc->HasNote(5, 3, 0));

    // Delete cell D5, to shift the text cell back into D5.
    m_pDoc->DeleteCol(3, 0, 3, 0, 3, 1);
    aStr = m_pDoc->GetString(4, 3, 0);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("We should have a string cell here.", aTestVal, aStr);
    CPPUNIT_ASSERT_MESSAGE("E5 is supposed to be blank.", m_pDoc->IsBlockEmpty(4, 4, 4, 4, 0));

    CPPUNIT_ASSERT_MESSAGE("there should be NO note", !m_pDoc->HasNote(5, 3, 0));
    CPPUNIT_ASSERT_MESSAGE("there should be a note", m_pDoc->HasNote(4, 3, 0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteDefaultStyle)
{
    m_pDoc->InsertTab(0, u"PostIts"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    auto pNote = m_pDoc->GetOrCreateNote({0, 0, 0});
    auto pCaption = pNote->GetCaption();

    CPPUNIT_ASSERT(pCaption);
    CPPUNIT_ASSERT_EQUAL(ScResId(STR_STYLENAME_NOTE), pCaption->GetStyleSheet()->GetName());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteThreadedCommentData)
{
    m_pDoc->InsertTab(0, u"PostIts"_ustr);
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress aPos(0, 0, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aPos);
    pNote->SetText(aPos, u"Hello"_ustr);
    pNote->SetAuthor(u"Author"_ustr);

    // Initially not resolved (no threaded data).
    CPPUNIT_ASSERT(!pNote->IsResolved());
    CPPUNIT_ASSERT(!pNote->GetThreadedCommentData());

    // SetResolved creates threaded data on demand.
    pNote->SetResolved(true);
    CPPUNIT_ASSERT(pNote->IsResolved());
    CPPUNIT_ASSERT(pNote->GetThreadedCommentData());
    CPPUNIT_ASSERT(pNote->GetThreadedCommentData()->mbDone);

    // SetResolved(false) clears the flag but keeps the data.
    pNote->SetResolved(false);
    CPPUNIT_ASSERT(!pNote->IsResolved());
    CPPUNIT_ASSERT(pNote->GetThreadedCommentData());
    CPPUNIT_ASSERT(!pNote->GetThreadedCommentData()->mbDone);

    // Set full threaded comment data.
    auto pData = std::make_unique<ScThreadedCommentData>();
    pData->maRoot.maGuid = u"{11111111-1111-1111-1111-111111111111}"_ustr;
    pData->maRoot.maPersonId = u"{AAAA}"_ustr;
    pData->maRoot.maDateTime = u"2026-04-07T12:00:00Z"_ustr;
    pData->mbDone = true;

    ScThreadedCommentEntry aReply;
    aReply.maGuid = u"{22222222-2222-2222-2222-222222222222}"_ustr;
    aReply.maPersonId = u"{BBBB}"_ustr;
    aReply.maText = u"Reply text"_ustr;
    pData->maReplies.push_back(aReply);

    pNote->SetThreadedCommentData(std::move(pData));
    CPPUNIT_ASSERT(pNote->IsResolved());
    CPPUNIT_ASSERT_EQUAL(size_t(1), pNote->GetThreadedCommentData()->maReplies.size());
    CPPUNIT_ASSERT_EQUAL(u"Reply text"_ustr, pNote->GetThreadedCommentData()->maReplies[0].maText);

    // Clone preserves threaded data.
    std::unique_ptr<ScPostIt> pClone = pNote->Clone(aPos, *m_pDoc, ScAddress(1, 0, 0), true);
    CPPUNIT_ASSERT(pClone->IsResolved());
    CPPUNIT_ASSERT(pClone->GetThreadedCommentData());
    CPPUNIT_ASSERT_EQUAL(u"{11111111-1111-1111-1111-111111111111}"_ustr,
                         pClone->GetThreadedCommentData()->maRoot.maGuid);
    CPPUNIT_ASSERT_EQUAL(size_t(1), pClone->GetThreadedCommentData()->maReplies.size());

    // Clone without caption also preserves threaded data.
    std::unique_ptr<ScPostIt> pClone2 = pNote->Clone(aPos, *m_pDoc, ScAddress(2, 0, 0), false);
    CPPUNIT_ASSERT(pClone2->IsResolved());
    CPPUNIT_ASSERT_EQUAL(u"{11111111-1111-1111-1111-111111111111}"_ustr,
                         pClone2->GetThreadedCommentData()->maRoot.maGuid);

    // Remove threaded data.
    pNote->SetThreadedCommentData(nullptr);
    CPPUNIT_ASSERT(!pNote->IsResolved());
    CPPUNIT_ASSERT(!pNote->GetThreadedCommentData());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNotePersonList)
{
    m_pDoc->InsertTab(0, u"PostIts"_ustr);

    // Initially empty.
    CPPUNIT_ASSERT(m_pDoc->GetPersonList().empty());

    ScPersonData aPerson1;
    aPerson1.maId = u"{AAAA}"_ustr;
    aPerson1.maDisplayName = u"Alice"_ustr;
    m_pDoc->AddPerson(aPerson1);
    CPPUNIT_ASSERT_EQUAL(size_t(1), m_pDoc->GetPersonList().size());

    // Duplicate by ID is ignored.
    ScPersonData aPerson1Dup;
    aPerson1Dup.maId = u"{AAAA}"_ustr;
    aPerson1Dup.maDisplayName = u"Alice duplicate"_ustr;
    m_pDoc->AddPerson(aPerson1Dup);
    CPPUNIT_ASSERT_EQUAL(size_t(1), m_pDoc->GetPersonList().size());
    CPPUNIT_ASSERT_EQUAL(u"Alice"_ustr, m_pDoc->GetPersonList()[0].maDisplayName);

    // Different ID is added.
    ScPersonData aPerson2;
    aPerson2.maId = u"{BBBB}"_ustr;
    aPerson2.maDisplayName = u"Bob"_ustr;
    m_pDoc->AddPerson(aPerson2);
    CPPUNIT_ASSERT_EQUAL(size_t(2), m_pDoc->GetPersonList().size());

    // Lookup by ID.
    const ScPersonData* pFound = m_pDoc->GetPersonById(u"{BBBB}"_ustr);
    CPPUNIT_ASSERT(pFound);
    CPPUNIT_ASSERT_EQUAL(u"Bob"_ustr, pFound->maDisplayName);

    // Unknown ID returns nullptr.
    CPPUNIT_ASSERT(!m_pDoc->GetPersonById(u"{CCCC}"_ustr));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteBasic)
{
    m_pDoc->InsertTab(0, u"PostIts"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    CPPUNIT_ASSERT(!m_pDoc->HasNotes());

    // Check for note's presence in all tables before inserting any notes.
    for (SCTAB i = 0; i <= MAXTAB; ++i)
    {
        bool bHasNotes = m_pDoc->HasTabNotes(i);
        CPPUNIT_ASSERT(!bHasNotes);
    }

    ScAddress aAddr(2, 2, 0); // cell C3
    ScPostIt *pNote = m_pDoc->GetOrCreateNote(aAddr);

    pNote->SetText(aAddr, u"Hello world"_ustr);
    pNote->SetAuthor(u"Jim Bob"_ustr);

    ScPostIt *pGetNote = m_pDoc->GetNote(aAddr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("note should be itself", pNote, pGetNote);

    // Insert one row at row 1.
    bool bInsertRow = m_pDoc->InsertRow(0, 0, m_pDoc->MaxCol(), 0, 1, 1);
    CPPUNIT_ASSERT_MESSAGE("failed to insert row", bInsertRow );

    CPPUNIT_ASSERT_MESSAGE("note hasn't moved", !m_pDoc->GetNote(aAddr));
    aAddr.IncRow(); // cell C4
    CPPUNIT_ASSERT_EQUAL_MESSAGE("note not there", pNote, m_pDoc->GetNote(aAddr));

    // Insert column at column A.
    bool bInsertCol = m_pDoc->InsertCol(0, 0, m_pDoc->MaxRow(), 0, 1, 1);
    CPPUNIT_ASSERT_MESSAGE("failed to insert column", bInsertCol );

    CPPUNIT_ASSERT_MESSAGE("note hasn't moved", !m_pDoc->GetNote(aAddr));
    aAddr.IncCol(); // cell D4
    CPPUNIT_ASSERT_EQUAL_MESSAGE("note not there", pNote, m_pDoc->GetNote(aAddr));

    // Insert a new sheet to shift the current sheet to the right.
    m_pDoc->InsertTab(0, u"Table2"_ustr);
    CPPUNIT_ASSERT_MESSAGE("note hasn't moved", !m_pDoc->GetNote(aAddr));
    aAddr.IncTab(); // Move to the next sheet.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("note not there", pNote, m_pDoc->GetNote(aAddr));

    m_pDoc->DeleteTab(0);
    aAddr.IncTab(-1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("note not there", pNote, m_pDoc->GetNote(aAddr));

    // Insert cell at C4.  This should NOT shift the note position.
    bInsertRow = m_pDoc->InsertRow(2, 0, 2, 0, 3, 1);
    CPPUNIT_ASSERT_MESSAGE("Failed to insert cell at C4.", bInsertRow);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note shouldn't have moved but it has.", pNote, m_pDoc->GetNote(aAddr));

    // Delete cell at C4.  Again, this should NOT shift the note position.
    m_pDoc->DeleteRow(2, 0, 2, 0, 3, 1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note shouldn't have moved but it has.", pNote, m_pDoc->GetNote(aAddr));

    // Now, with the note at D4, delete cell D3. This should shift the note one cell up.
    m_pDoc->DeleteRow(3, 0, 3, 0, 2, 1);
    aAddr.IncRow(-1); // cell D3
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note at D4 should have shifted up to D3.", pNote, m_pDoc->GetNote(aAddr));

    // Delete column C. This should shift the note one cell left.
    m_pDoc->DeleteCol(0, 0, m_pDoc->MaxRow(), 0, 2, 1);
    aAddr.IncCol(-1); // cell C3
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note at D3 should have shifted left to C3.", pNote, m_pDoc->GetNote(aAddr));

    // Insert a text where the note is.
    m_pDoc->SetString(aAddr, u"Note is here."_ustr);

    // Delete row 1. This should shift the note from C3 to C2.
    m_pDoc->DeleteRow(0, 0, m_pDoc->MaxCol(), 0, 0, 1);
    aAddr.IncRow(-1); // C2
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note at C3 should have shifted up to C2.", pNote, m_pDoc->GetNote(aAddr));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteDeleteRow)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress aPos(1, 1, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aPos);
    pNote->SetText(aPos, u"Hello"_ustr);
    pNote->SetAuthor(u"Jim Bob"_ustr);

    CPPUNIT_ASSERT_MESSAGE("there should be a note", m_pDoc->HasNote(1, 1, 0));

    // test with IsBlockEmpty
    CPPUNIT_ASSERT_MESSAGE("The Block should be detected as empty (no Notes)", m_pDoc->IsEmptyData(0, 0, 100, 100, 0));
    CPPUNIT_ASSERT_MESSAGE("The Block should NOT be detected as empty", !m_pDoc->IsBlockEmpty(0, 0, 100, 100, 0));

    m_pDoc->DeleteRow(0, 0, m_pDoc->MaxCol(), 0, 1, 1);

    CPPUNIT_ASSERT_MESSAGE("there should be no more note", !m_pDoc->HasNote(1, 1, 0));

    // Set values and notes into B3:B4.
    aPos = ScAddress(1,2,0); // B3
    m_pDoc->SetString(aPos, u"First"_ustr);
    ScNoteUtil::CreateNoteFromString(*m_pDoc, aPos, u"First Note"_ustr, false, false);

    aPos = ScAddress(1,3,0); // B4
    m_pDoc->SetString(aPos, u"Second"_ustr);
    ScNoteUtil::CreateNoteFromString(*m_pDoc, aPos, u"Second Note"_ustr, false, false);

    // Delete row 2.
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    rDocFunc.DeleteCells(ScRange(0,1,0,m_pDoc->MaxCol(),1,0), &aMark, DelCellCmd::CellsUp, true);

    // Check to make sure the notes have shifted upward.
    pNote = m_pDoc->GetNote(ScAddress(1,1,0));
    CPPUNIT_ASSERT_MESSAGE("B2 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"First Note"_ustr, pNote->GetText());
    pNote = m_pDoc->GetNote(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("B3 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"Second Note"_ustr, pNote->GetText());
    pNote = m_pDoc->GetNote(ScAddress(1,3,0));
    CPPUNIT_ASSERT_MESSAGE("B4 should NOT have a note.", !pNote);

    // Undo.

    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT_MESSAGE("Failed to get undo manager.", pUndoMgr);
    m_pDoc->CreateAllNoteCaptions(); // to make sure that all notes have their corresponding caption objects...

    pUndoMgr->Undo();
    pNote = m_pDoc->GetNote(ScAddress(1,1,0));
    CPPUNIT_ASSERT_MESSAGE("B2 should NOT have a note.", !pNote);
    pNote = m_pDoc->GetNote(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("B3 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"First Note"_ustr, pNote->GetText());
    pNote = m_pDoc->GetNote(ScAddress(1,3,0));
    CPPUNIT_ASSERT_MESSAGE("B4 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"Second Note"_ustr, pNote->GetText());

    // Delete row 3.
    rDocFunc.DeleteCells(ScRange(0,2,0,m_pDoc->MaxCol(),2,0), &aMark, DelCellCmd::CellsUp, true);

    pNote = m_pDoc->GetNote(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("B3 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"Second Note"_ustr, pNote->GetText());
    pNote = m_pDoc->GetNote(ScAddress(1,3,0));
    CPPUNIT_ASSERT_MESSAGE("B4 should NOT have a note.", !pNote);

    // Undo and check the result.
    pUndoMgr->Undo();
    pNote = m_pDoc->GetNote(ScAddress(1,2,0));
    CPPUNIT_ASSERT_MESSAGE("B3 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"First Note"_ustr, pNote->GetText());
    pNote = m_pDoc->GetNote(ScAddress(1,3,0));
    CPPUNIT_ASSERT_MESSAGE("B4 should have a note.", pNote);
    CPPUNIT_ASSERT_EQUAL(u"Second Note"_ustr, pNote->GetText());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteDeleteCol)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress rAddr(1, 1, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(rAddr);
    pNote->SetText(rAddr, u"Hello"_ustr);
    pNote->SetAuthor(u"Jim Bob"_ustr);

    CPPUNIT_ASSERT_MESSAGE("there should be a note", m_pDoc->HasNote(1, 1, 0));

    m_pDoc->DeleteCol(0, 0, m_pDoc->MaxRow(), 0, 1, 1);

    CPPUNIT_ASSERT_MESSAGE("there should be no more note", !m_pDoc->HasNote(1, 1, 0));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteLifeCycle)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress aPos(1,1,0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aPos);
    CPPUNIT_ASSERT_MESSAGE("Failed to insert a new cell comment.", pNote);

    pNote->SetText(aPos, u"New note"_ustr);
    std::unique_ptr<ScPostIt> pNote2 = m_pDoc->ReleaseNote(aPos);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This note instance is expected to be identical to the original.", pNote, pNote2.get());
    CPPUNIT_ASSERT_MESSAGE("The note shouldn't be here after it's been released.", !m_pDoc->HasNote(aPos));

    // Modify the internal state of the note instance to make sure it's really
    // been released.
    pNote->SetText(aPos, u"New content"_ustr);

    // Re-insert the note back to the same place.
    m_pDoc->SetNote(aPos, std::move(pNote2));
    SdrCaptionObj* pCaption = pNote->GetOrCreateCaption(aPos);
    CPPUNIT_ASSERT_MESSAGE("Failed to create a caption object.", pCaption);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This caption should belong to the drawing layer of the document.",
                           m_pDoc->GetDrawLayer(), static_cast<ScDrawLayer*>(&pCaption->getSdrModelFromSdrObject()));

    // Copy B2 with note to a clipboard.

    ScClipParam aClipParam(ScRange(aPos), false);
    ScDocument aClipDoc(SCDOCMODE_CLIP);
    ScMarkData aMarkData(m_pDoc->GetSheetLimits());
    aMarkData.SelectOneTable(0);
    m_pDoc->CopyToClip(aClipParam, &aClipDoc, &aMarkData, false, true);

    ScPostIt* pClipNote = aClipDoc.GetNote(aPos);
    CPPUNIT_ASSERT_MESSAGE("Failed to copy note to the clipboard.", pClipNote);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Note on the clipboard should share the same caption object from the original.",
                           pCaption, pClipNote->GetCaption());


    // Move B2 to B3 with note, which creates an ScUndoDragDrop, and Undo.

    ScAddress aOrigPos(aPos);
    ScAddress aMovePos(1,2,0);
    ScPostIt* pOrigNote = m_pDoc->GetNote(aOrigPos);
    const SdrCaptionObj* pOrigCaption = pOrigNote->GetOrCreateCaption(aOrigPos);
    bool const bCut = true;       // like Drag&Drop
    bool bRecord = true;    // record Undo
    bool const bPaint = false;    // don't care about
    bool bApi = true;       // API to prevent dialogs
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    bool bMoveDone = rDocFunc.MoveBlock(ScRange(aOrigPos, aOrigPos), aMovePos, bCut, bRecord, bPaint, bApi);
    CPPUNIT_ASSERT_MESSAGE("Cells not moved", bMoveDone);

    // Verify the note move.
    ScPostIt* pGoneNote = m_pDoc->GetNote(aOrigPos);
    CPPUNIT_ASSERT_MESSAGE("Failed to move the note from source.", !pGoneNote);
    ScPostIt* pMoveNote = m_pDoc->GetNote(aMovePos);
    CPPUNIT_ASSERT_MESSAGE("Failed to move the note to destination.", pMoveNote);

    // The caption object should not be identical, it was newly created upon
    // Drop from clipboard.
    // pOrigCaption is a dangling pointer.
    const SdrCaptionObj* pMoveCaption = pMoveNote->GetOrCreateCaption(aMovePos);
    CPPUNIT_ASSERT_MESSAGE("Captions identical after move.", pOrigCaption != pMoveCaption);

    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoMgr);
    pUndoMgr->Undo();   // this should not crash ... tdf#92995

    // Verify the note move Undo.
    pMoveNote = m_pDoc->GetNote(aMovePos);
    CPPUNIT_ASSERT_MESSAGE("Failed to undo the note move from destination.", !pMoveNote);
    pOrigNote = m_pDoc->GetNote(aOrigPos);
    CPPUNIT_ASSERT_MESSAGE("Failed to undo the note move to source.", pOrigNote);

    // The caption object still should not be identical.
    // pMoveCaption is a dangling pointer.
    pOrigCaption = pOrigNote->GetOrCreateCaption(aOrigPos);
    CPPUNIT_ASSERT_MESSAGE("Captions identical after move undo.", pOrigCaption != pMoveCaption);


    // Create a note at B4, merge B4 and B5 with ScUndoMerge, and Undo.

    ScAddress aPosB4(1,3,0);
    ScPostIt* pNoteB4 = m_pDoc->GetOrCreateNote(aPosB4);
    CPPUNIT_ASSERT_MESSAGE("Failed to insert cell comment at B4.", pNoteB4);
    const SdrCaptionObj* pCaptionB4 = pNoteB4->GetOrCreateCaption(aPosB4);
    ScCellMergeOption aCellMergeOption(1,3,2,3);
    rDocFunc.MergeCells( aCellMergeOption, true /*bContents*/, bRecord, bApi, false /*bEmptyMergedCells*/ );

    SfxUndoManager* pMergeUndoManager = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pMergeUndoManager);
    pMergeUndoManager->Undo();  // this should not crash ... tdf#105667

    // Undo contained the original caption object pointer which was still alive
    // at B4 after the merge and not cloned nor recreated during Undo.
    ScPostIt* pUndoNoteB4 = m_pDoc->GetNote(aPosB4);
    CPPUNIT_ASSERT_MESSAGE("No cell comment at B4 after Undo.", pUndoNoteB4);
    const SdrCaptionObj* pUndoCaptionB4 = pUndoNoteB4->GetCaption();
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Captions not identical after Merge Undo.", pCaptionB4, pUndoCaptionB4);


    // In a second document copy a note from B5 to clipboard, close the
    // document and then paste the note into this document.
    {
        ScDocShellRef xDocSh2;
        getNewDocShell(xDocSh2);
        ScDocument* pDoc2 = &xDocSh2->GetDocument();
        pDoc2->InsertTab(0, u"OtherSheet1"_ustr);
        pDoc2->InitDrawLayer(xDocSh2.get());

        ScAddress aPosB5(1,4,0);
        ScPostIt* pOtherNoteB5 = pDoc2->GetOrCreateNote(aPosB5);
        CPPUNIT_ASSERT_MESSAGE("Failed to insert cell comment at B5.", pOtherNoteB5);
        const SdrCaptionObj* pOtherCaptionB5 = pOtherNoteB5->GetOrCreateCaption(aPosB5);
        CPPUNIT_ASSERT_MESSAGE("No caption at B5.", pOtherCaptionB5);

        ScDocument aClipDoc2(SCDOCMODE_CLIP);
        copyToClip( pDoc2, ScRange(aPosB5), &aClipDoc2);

        // There's no ScTransferObject involved in the "fake" clipboard copy
        // and ScDocument dtor asking IsClipboardSource() gets no, so emulate
        // the part that normally is responsible for forgetting the caption
        // objects.
        aClipDoc2.ClosingClipboardSource();

        pDoc2->DeleteTab(0);
        xDocSh2->DoClose();
        xDocSh2.clear();

        pasteFromClip( m_pDoc, ScRange(aPosB5), &aClipDoc2); // should not crash... tdf#104967
        ScPostIt* pNoteB5 = m_pDoc->GetNote(aPosB5);
        CPPUNIT_ASSERT_MESSAGE("Failed to paste cell comment at B5.", pNoteB5);
        const SdrCaptionObj* pCaptionB5 = pNoteB5->GetOrCreateCaption(aPosB5);
        CPPUNIT_ASSERT_MESSAGE("No caption at pasted B5.", pCaptionB5);
        // Do not test if  pCaptionB5 != pOtherCaptionB5  because since pDoc2
        // has been closed and the caption been deleted objects *may* be
        // allocated at the very same memory location.
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testNoteCopyPaste)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    // Insert in B2 a text and cell comment.
    ScAddress aPos(1,1,0);
    m_pDoc->SetString(aPos, u"Text"_ustr);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aPos);
    CPPUNIT_ASSERT(pNote);
    pNote->SetText(aPos, u"Note1"_ustr);

    // Insert in B4 a number and cell comment.
    aPos.SetRow(3);
    m_pDoc->SetValue(aPos, 1.1);
    pNote = m_pDoc->GetOrCreateNote(aPos);
    CPPUNIT_ASSERT(pNote);
    pNote->SetText(aPos, u"Note2"_ustr);

    // Copy B2:B4 to clipboard.
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    ScRange aCopyRange(1,1,0,1,3,0);
    ScDocument aClipDoc(SCDOCMODE_CLIP);
    aClipDoc.ResetClip(m_pDoc, &aMark);
    ScClipParam aClipParam(aCopyRange, false);
    m_pDoc->CopyToClip(aClipParam, &aClipDoc, &aMark, false, false);

    // Make sure the notes are in the clipboard.
    pNote = aClipDoc.GetNote(ScAddress(1,1,0));
    CPPUNIT_ASSERT(pNote);
    CPPUNIT_ASSERT_EQUAL(u"Note1"_ustr, pNote->GetText());

    pNote = aClipDoc.GetNote(ScAddress(1,3,0));
    CPPUNIT_ASSERT(pNote);
    CPPUNIT_ASSERT_EQUAL(u"Note2"_ustr, pNote->GetText());

    // Paste to B6:B8 but only cell notes.
    ScRange aDestRange(1,5,0,1,7,0);
    m_pDoc->CopyFromClip(aDestRange, aMark, InsertDeleteFlags::NOTE, nullptr, &aClipDoc);

    // Make sure the notes are there.
    pNote = m_pDoc->GetNote(ScAddress(1,5,0));
    CPPUNIT_ASSERT(pNote);
    CPPUNIT_ASSERT_EQUAL(u"Note1"_ustr, pNote->GetText());

    pNote = m_pDoc->GetNote(ScAddress(1,7,0));
    CPPUNIT_ASSERT(pNote);
    CPPUNIT_ASSERT_EQUAL(u"Note2"_ustr, pNote->GetText());

    // Test that GetNotesInRange includes the end of its range
    // and so can find the note
    std::vector<sc::NoteEntry> aNotes;
    m_pDoc->GetNotesInRange(ScRange(1,7,0), aNotes);
    CPPUNIT_ASSERT_EQUAL(size_t(1), aNotes.size());

    m_pDoc->DeleteTab(0);
}

// tdf#112454
CPPUNIT_TEST_FIXTURE(Test, testNoteContainsNotesInRange)
{
    m_pDoc->InsertTab(0, u"PostIts"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress aAddr(2, 2, 0); // cell C3

    CPPUNIT_ASSERT_MESSAGE("Claiming there's notes in a document that doesn't have any.",
                           !m_pDoc->ContainsNotesInRange((ScRange(ScAddress(0, 0, 0), aAddr))));

    m_pDoc->GetOrCreateNote(aAddr);

    CPPUNIT_ASSERT_MESSAGE("Claiming there's notes in range that doesn't have any.",
                           !m_pDoc->ContainsNotesInRange(ScRange(ScAddress(0, 0, 0), ScAddress(0, 1, 0))));
    CPPUNIT_ASSERT_MESSAGE("Note not detected that lies on border of range.",
                           m_pDoc->ContainsNotesInRange((ScRange(ScAddress(0, 0, 0), aAddr))));
    CPPUNIT_ASSERT_MESSAGE("Note not detected that lies in inner area of range.",
                           m_pDoc->ContainsNotesInRange((ScRange(ScAddress(0, 0, 0), ScAddress(3, 3, 0)))));
}

CPPUNIT_TEST_FIXTURE(Test, testAreasWithNotes)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    ScAddress rAddr(1, 5, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(rAddr);
    pNote->SetText(rAddr, u"Hello"_ustr);
    pNote->SetAuthor(u"Jim Bob"_ustr);
    ScAddress rAddrMin(2, 2, 0);
    ScPostIt* pNoteMin = m_pDoc->GetOrCreateNote(rAddrMin);
    pNoteMin->SetText(rAddrMin, u"Hello"_ustr);

    SCCOL col;
    SCROW row;
    bool dataFound;

    // only cell notes (empty content)

    dataFound = m_pDoc->GetDataStart(0,col,row);

    CPPUNIT_ASSERT_MESSAGE("No DataStart found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("DataStart wrong col for notes", static_cast<SCCOL>(1), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("DataStart wrong row for notes", static_cast<SCROW>(2), row);

    dataFound = m_pDoc->GetCellArea(0,col,row);

    CPPUNIT_ASSERT_MESSAGE("No CellArea found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("CellArea wrong col for notes", static_cast<SCCOL>(2), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("CellArea wrong row for notes", static_cast<SCROW>(5), row);

    bool bNotes = true;
    dataFound = m_pDoc->GetPrintArea(0,col,row, bNotes);

    CPPUNIT_ASSERT_MESSAGE("No PrintArea found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong col for notes", static_cast<SCCOL>(2), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong row for notes", static_cast<SCROW>(5), row);

    bNotes = false;
    dataFound = m_pDoc->GetPrintArea(0,col,row, bNotes);
    CPPUNIT_ASSERT_MESSAGE("No PrintArea should be found", !dataFound);

    bNotes = true;
    dataFound = m_pDoc->GetPrintAreaVer(0,0,1,row, bNotes); // cols 0 & 1
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row for notes", static_cast<SCROW>(5), row);

    dataFound = m_pDoc->GetPrintAreaVer(0,2,3,row, bNotes); // cols 2 & 3
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row for notes", static_cast<SCROW>(2), row);

    dataFound = m_pDoc->GetPrintAreaVer(0,20,21,row, bNotes); // cols 20 & 21
    CPPUNIT_ASSERT_MESSAGE("PrintAreaVer found", !dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row for notes", static_cast<SCROW>(0), row);

    bNotes = false;
    dataFound = m_pDoc->GetPrintAreaVer(0,0,1,row, bNotes); // col 0 & 1
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer should be found", !dataFound);

    // now add cells with value, check that notes are taken into account in good cases

    m_pDoc->SetString(0, 3, 0, u"Some Text"_ustr);
    m_pDoc->SetString(3, 3, 0, u"Some Text"_ustr);

    dataFound = m_pDoc->GetDataStart(0,col,row);

    CPPUNIT_ASSERT_MESSAGE("No DataStart found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("DataStart wrong col", static_cast<SCCOL>(0), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("DataStart wrong row", static_cast<SCROW>(2), row);

    dataFound = m_pDoc->GetCellArea(0,col,row);

    CPPUNIT_ASSERT_MESSAGE("No CellArea found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("CellArea wrong col", static_cast<SCCOL>(3), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("CellArea wrong row", static_cast<SCROW>(5), row);

    bNotes = true;
    dataFound = m_pDoc->GetPrintArea(0,col,row, bNotes);

    CPPUNIT_ASSERT_MESSAGE("No PrintArea found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong col", static_cast<SCCOL>(3), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong row", static_cast<SCROW>(5), row);

    bNotes = false;
    dataFound = m_pDoc->GetPrintArea(0,col,row, bNotes);
    CPPUNIT_ASSERT_MESSAGE("No PrintArea found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong col", static_cast<SCCOL>(3), col);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintArea wrong row", static_cast<SCROW>(3), row);

    bNotes = true;
    dataFound = m_pDoc->GetPrintAreaVer(0,0,1,row, bNotes); // cols 0 & 1
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row", static_cast<SCROW>(5), row);

    dataFound = m_pDoc->GetPrintAreaVer(0,2,3,row, bNotes); // cols 2 & 3
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row", static_cast<SCROW>(3), row);

    bNotes = false;
    dataFound = m_pDoc->GetPrintAreaVer(0,0,1,row, bNotes); // cols 0 & 1
    CPPUNIT_ASSERT_MESSAGE("No PrintAreaVer found", dataFound);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("PrintAreaVer wrong row", static_cast<SCROW>(3), row);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAnchoredRotatedShape)
{
    m_pDoc->InsertTab(0, u"TestTab"_ustr);
    SCROW nRow1, nRow2;
    bool bHidden = m_pDoc->RowHidden(0, 0, &nRow1, &nRow2);
    CPPUNIT_ASSERT_MESSAGE("new sheet should have all rows visible", !bHidden);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", SCROW(0), nRow1);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("new sheet should have all rows visible", m_pDoc->MaxRow(), nRow2);

    m_pDoc->InitDrawLayer();
    ScDrawLayer *pDrawLayer = m_pDoc->GetDrawLayer();
    CPPUNIT_ASSERT_MESSAGE("must have a draw layer", pDrawLayer != nullptr);
    SdrPage* pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("must have a draw page", pPage != nullptr);
    m_pDoc->SetRowHeightRange(0, m_pDoc->MaxRow(), 0, o3tl::toTwips(1000, o3tl::Length::mm100));
    constexpr tools::Long TOLERANCE = 30; //30 hmm
    for ( SCCOL nCol = 0; nCol < m_pDoc->MaxCol(); ++nCol )
        m_pDoc->SetColWidth(nCol, 0, o3tl::toTwips(1000, o3tl::Length::mm100));
    {
        //Add a rect
        tools::Rectangle aRect( 4000, 5000, 10000, 7000 );

        tools::Rectangle aRotRect( 6000, 3000, 8000, 9000 );
        rtl::Reference<SdrRectObj> pObj = new SdrRectObj(*pDrawLayer, aRect);
        pPage->InsertObject(pObj.get());
        Point aRef1(pObj->GetSnapRect().Center());
        Degree100 nAngle = 9000_deg100; //90 deg.
        double nSin = 1.0; // sin(90 deg)
        double nCos = 0.0; // cos(90 deg)
        pObj->Rotate(aRef1,nAngle,nSin,nCos);

        ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, true);

        tools::Rectangle aSnap = pObj->GetSnapRect();
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.GetHeight(), aSnap.GetHeight(), TOLERANCE );
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.GetWidth(), aSnap.GetWidth(), TOLERANCE );
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.Left(), aSnap.Left(), TOLERANCE );
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.Top(), aSnap.Top(), TOLERANCE );

        ScDrawObjData aAnchor;
        ScDrawObjData* pData = ScDrawLayer::GetObjData( pObj.get() );
        CPPUNIT_ASSERT_MESSAGE("Failed to get drawing object meta-data.", pData);

        aAnchor.maStart = pData->maStart;
        aAnchor.maEnd = pData->maEnd;

        m_pDoc->SetDrawPageSize(0);

        // increase row 5 by 2000 hmm
        m_pDoc->SetRowHeight(5, 0, o3tl::toTwips(3000, o3tl::Length::mm100));
        // increase col 6 by 1000 hmm
        m_pDoc->SetColWidth(6, 0, o3tl::toTwips(2000, o3tl::Length::mm100));

        aRotRect.setWidth( aRotRect.GetWidth() + 1000 );
        aRotRect.setHeight( aRotRect.GetHeight() + 2000 );

        m_pDoc->SetDrawPageSize(0);

        aSnap = pObj->GetSnapRect();

        // ensure that width and height have been adjusted accordingly
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.GetHeight(), aSnap.GetHeight(), TOLERANCE );
        CPPUNIT_ASSERT_DOUBLES_EQUAL( aRotRect.GetWidth(), aSnap.GetWidth(), TOLERANCE );

        // ensure that anchor start and end addresses haven't changed
        CPPUNIT_ASSERT_EQUAL( aAnchor.maStart.Row(), pData->maStart.Row() ); // start row 0
        CPPUNIT_ASSERT_EQUAL( aAnchor.maStart.Col(), pData->maStart.Col() ); // start column 5
        CPPUNIT_ASSERT_EQUAL( aAnchor.maEnd.Row(), pData->maEnd.Row() ); // end row 3
        CPPUNIT_ASSERT_EQUAL( aAnchor.maEnd.Col(), pData->maEnd.Col() ); // end col 7
    }
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testCellTextWidth)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    ScAddress aTopCell(0, 0, 0);

    // Sheet is empty.
    std::unique_ptr<ScColumnTextWidthIterator> pIter(new ScColumnTextWidthIterator(*m_pDoc, aTopCell, m_pDoc->MaxRow()));
    CPPUNIT_ASSERT_MESSAGE("Column should have no text widths stored.", !pIter->hasCell());

    // Sheet only has one cell.
    m_pDoc->SetString(0, 0, 0, u"Only one cell"_ustr);
    pIter.reset(new ScColumnTextWidthIterator(*m_pDoc, aTopCell, m_pDoc->MaxRow()));
    CPPUNIT_ASSERT_MESSAGE("Column should have a cell.", pIter->hasCell());
    CPPUNIT_ASSERT_EQUAL(SCROW(0), pIter->getPos());

    // Setting a text width here should commit it to the column.
    sal_uInt16 nTestVal = 432;
    pIter->setValue(nTestVal);
    CPPUNIT_ASSERT_EQUAL(nTestVal, m_pDoc->GetTextWidth(aTopCell));

    // Set values to row 2 through 6.
    for (SCROW i = 2; i <= 6; ++i)
        m_pDoc->SetString(0, i, 0, u"foo"_ustr);

    // Set values to row 10 through 18.
    for (SCROW i = 10; i <= 18; ++i)
        m_pDoc->SetString(0, i, 0, u"foo"_ustr);

    {
        // Full range.
        pIter.reset(new ScColumnTextWidthIterator(*m_pDoc, aTopCell, m_pDoc->MaxRow()));
        SCROW aRows[] = { 0, 2, 3, 4, 5, 6, 10, 11, 12, 13, 14, 15, 16, 17, 18 };
        for (const auto& rRow : aRows)
        {
            CPPUNIT_ASSERT_MESSAGE("Cell expected, but not there.", pIter->hasCell());
            CPPUNIT_ASSERT_EQUAL(rRow, pIter->getPos());
            pIter->next();
        }
        CPPUNIT_ASSERT_MESSAGE("Iterator should have ended.", !pIter->hasCell());
    }

    {
        // Specify start and end rows (6 - 16)
        ScAddress aStart = aTopCell;
        aStart.SetRow(6);
        pIter.reset(new ScColumnTextWidthIterator(*m_pDoc, aStart, 16));
        SCROW aRows[] = { 6, 10, 11, 12, 13, 14, 15, 16 };
        for (const auto& rRow : aRows)
        {
            CPPUNIT_ASSERT_MESSAGE("Cell expected, but not there.", pIter->hasCell());
            CPPUNIT_ASSERT_EQUAL(rRow, pIter->getPos());
            pIter->next();
        }
        CPPUNIT_ASSERT_MESSAGE("Iterator should have ended.", !pIter->hasCell());
    }

    // Clear from row 3 to row 17. After this, we should only have cells at rows 0, 2 and 18.
    clearRange(m_pDoc, ScRange(0, 3, 0, 0, 17, 0));

    {
        // Full range again.
        pIter.reset(new ScColumnTextWidthIterator(*m_pDoc, aTopCell, m_pDoc->MaxRow()));
        SCROW aRows[] = { 0, 2, 18 };
        for (const auto& rRow : aRows)
        {
            CPPUNIT_ASSERT_MESSAGE("Cell expected, but not there.", pIter->hasCell());
            CPPUNIT_ASSERT_EQUAL(rRow, pIter->getPos());
            pIter->next();
        }
        CPPUNIT_ASSERT_MESSAGE("Iterator should have ended.", !pIter->hasCell());
    }

    // Delete row 2 which shifts all cells below row 2 upward. After this, we
    // should only have cells at rows 0 and 17.
    m_pDoc->DeleteRow(0, 0, m_pDoc->MaxCol(), MAXTAB, 2, 1);
    {
        // Full range again.
        pIter.reset(new ScColumnTextWidthIterator(*m_pDoc, aTopCell, m_pDoc->MaxRow()));
        SCROW aRows[] = { 0, 17 };
        for (const auto& rRow : aRows)
        {
            CPPUNIT_ASSERT_MESSAGE("Cell expected, but not there.", pIter->hasCell());
            CPPUNIT_ASSERT_EQUAL(rRow, pIter->getPos());
            pIter->next();
        }
        CPPUNIT_ASSERT_MESSAGE("Iterator should have ended.", !pIter->hasCell());
    }

    m_pDoc->DeleteTab(0);
}

static bool checkEditTextIterator(sc::EditTextIterator& rIter, const char** pChecks)
{
    const EditTextObject* pText = rIter.first();
    const char* p = *pChecks;

    for (int i = 0; i < 100; ++i) // cap it to 100 loops.
    {
        if (!pText)
            // No more edit cells. The check string array should end too.
            return p == nullptr;

        if (!p)
            // More edit cell, but no more check string. Bad.
            return false;

        if (pText->GetParagraphCount() != 1)
            // For this test, we don't handle multi-paragraph text.
            return false;

        if (pText->GetText(0) != OUString::createFromAscii(p))
            // Text differs from what's expected.
            return false;

        pText = rIter.next();
        ++pChecks;
        p = *pChecks;
    }

    return false;
}

CPPUNIT_TEST_FIXTURE(Test, testEditTextIterator)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    {
        // First, try with an empty sheet.
        sc::EditTextIterator aIter(*m_pDoc,0);
        const char* pChecks[] = { nullptr };
        CPPUNIT_ASSERT_MESSAGE("Wrong iterator behavior.", checkEditTextIterator(aIter, pChecks));
    }

    ScFieldEditEngine& rEditEngine = m_pDoc->GetEditEngine();

    {
        // Only set one edit cell.
        rEditEngine.SetTextCurrentDefaults(u"A2"_ustr);
        m_pDoc->SetEditText(ScAddress(0,1,0), rEditEngine.CreateTextObject());
        sc::EditTextIterator aIter(*m_pDoc,0);
        const char* pChecks[] = { "A2", nullptr };
        CPPUNIT_ASSERT_MESSAGE("Wrong iterator behavior.", checkEditTextIterator(aIter, pChecks));
    }

    {
        // Add a series of edit cells.
        rEditEngine.SetTextCurrentDefaults(u"A5"_ustr);
        m_pDoc->SetEditText(ScAddress(0,4,0), rEditEngine.CreateTextObject());
        rEditEngine.SetTextCurrentDefaults(u"A6"_ustr);
        m_pDoc->SetEditText(ScAddress(0,5,0), rEditEngine.CreateTextObject());
        rEditEngine.SetTextCurrentDefaults(u"A7"_ustr);
        m_pDoc->SetEditText(ScAddress(0,6,0), rEditEngine.CreateTextObject());
        sc::EditTextIterator aIter(*m_pDoc,0);
        const char* pChecks[] = { "A2", "A5", "A6", "A7", nullptr };
        CPPUNIT_ASSERT_MESSAGE("Wrong iterator behavior.", checkEditTextIterator(aIter, pChecks));
    }

    {
        // Add more edit cells to column C. Skip column B.
        rEditEngine.SetTextCurrentDefaults(u"C1"_ustr);
        m_pDoc->SetEditText(ScAddress(2,0,0), rEditEngine.CreateTextObject());
        rEditEngine.SetTextCurrentDefaults(u"C3"_ustr);
        m_pDoc->SetEditText(ScAddress(2,2,0), rEditEngine.CreateTextObject());
        rEditEngine.SetTextCurrentDefaults(u"C4"_ustr);
        m_pDoc->SetEditText(ScAddress(2,3,0), rEditEngine.CreateTextObject());
        sc::EditTextIterator aIter(*m_pDoc,0);
        const char* pChecks[] = { "A2", "A5", "A6", "A7", "C1", "C3", "C4", nullptr };
        CPPUNIT_ASSERT_MESSAGE("Wrong iterator behavior.", checkEditTextIterator(aIter, pChecks));
    }

    {
        // Add some numeric, string and formula cells.  This shouldn't affect the outcome.
        m_pDoc->SetString(ScAddress(0,99,0), u"=ROW()"_ustr);
        m_pDoc->SetValue(ScAddress(1,3,0), 1.2);
        m_pDoc->SetString(ScAddress(2,4,0), u"Simple string"_ustr);
        sc::EditTextIterator aIter(*m_pDoc,0);
        const char* pChecks[] = { "A2", "A5", "A6", "A7", "C1", "C3", "C4", nullptr };
        CPPUNIT_ASSERT_MESSAGE("Wrong iterator behavior.", checkEditTextIterator(aIter, pChecks));
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testImportStream)
{
    sc::AutoCalcSwitch aAC(*m_pDoc, true); // turn on auto calc.
    sc::UndoSwitch aUndo(*m_pDoc, true); // enable undo.

    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(0,1,0), u"=SUM(A1:C1)"_ustr); // A2

    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(0,1,0)));

    // CSV import options.
    ScAsciiOptions aOpt;
    aOpt.SetFieldSeps(u","_ustr);

    // Import values to A1:C1.
    ScImportExport aObj(*m_pDoc, ScAddress(0,0,0));
    aObj.SetImportBroadcast(true);
    aObj.SetExtOptions(aOpt);
    aObj.ImportString(u"1,2,3"_ustr, SotClipboardFormatId::STRING);

    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(1,0,0)));
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(2,0,0)));

    // Formula value should have been updated.
    CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(0,1,0)));

    // Undo, and check the result.
    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT_MESSAGE("Failed to get the undo manager.", pUndoMgr);
    pUndoMgr->Undo();

    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(1,0,0)));
    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(2,0,0)));

    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(0,1,0))); // formula

    // Redo, and check the result.
    pUndoMgr->Redo();

    CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(1,0,0)));
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(2,0,0)));

    CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(0,1,0))); // formula

    pUndoMgr->Clear();

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDeleteContents)
{
    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true); // turn on auto calc.
    sc::UndoSwitch aUndoSwitch(*m_pDoc, true); // enable undo.

    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetValue(ScAddress(3,1,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,2,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,3,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,4,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,5,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,6,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,7,0), 1.0);
    m_pDoc->SetValue(ScAddress(3,8,0), 1.0);
    m_pDoc->SetString(ScAddress(3,15,0), u"=SUM(D2:D15)"_ustr);

    CPPUNIT_ASSERT_EQUAL(8.0, m_pDoc->GetValue(ScAddress(3,15,0))); // formula

    // Delete D2:D6.
    ScRange aRange(3,1,0,3,5,0);
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    aMark.SetMarkArea(aRange);

    ScDocumentUniquePtr pUndoDoc(new ScDocument(SCDOCMODE_UNDO));
    pUndoDoc->InitUndo(*m_pDoc, 0, 0);
    m_pDoc->CopyToDocument(aRange, InsertDeleteFlags::CONTENTS, false, *pUndoDoc, &aMark);
    ScUndoDeleteContents aUndo(*m_xDocShell, aMark, aRange, std::move(pUndoDoc), false, InsertDeleteFlags::CONTENTS, true);

    clearRange(m_pDoc, aRange);
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(3,15,0))); // formula

    aUndo.Undo();
    CPPUNIT_ASSERT_EQUAL(8.0, m_pDoc->GetValue(ScAddress(3,15,0))); // formula

    aUndo.Redo();
    CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(3,15,0))); // formula

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testTransliterateText)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Set texts to A1:A3.
    m_pDoc->SetString(ScAddress(0,0,0), u"Mike"_ustr);
    m_pDoc->SetString(ScAddress(0,1,0), u"Noah"_ustr);
    m_pDoc->SetString(ScAddress(0,2,0), u"Oscar"_ustr);

    // Change them to uppercase.
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SetMarkArea(ScRange(0,0,0,0,2,0));
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    rFunc.TransliterateText(
        aMark, TransliterationFlags::LOWERCASE_UPPERCASE, true);

    CPPUNIT_ASSERT_EQUAL(u"MIKE"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"NOAH"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(u"OSCAR"_ustr, m_pDoc->GetString(ScAddress(0,2,0)));

    // Test the undo and redo.
    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT_MESSAGE("Failed to get undo manager.", pUndoMgr);

    pUndoMgr->Undo();
    CPPUNIT_ASSERT_EQUAL(u"Mike"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"Noah"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(u"Oscar"_ustr, m_pDoc->GetString(ScAddress(0,2,0)));

    pUndoMgr->Redo();
    CPPUNIT_ASSERT_EQUAL(u"MIKE"_ustr, m_pDoc->GetString(ScAddress(0,0,0)));
    CPPUNIT_ASSERT_EQUAL(u"NOAH"_ustr, m_pDoc->GetString(ScAddress(0,1,0)));
    CPPUNIT_ASSERT_EQUAL(u"OSCAR"_ustr, m_pDoc->GetString(ScAddress(0,2,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFormulaToValue)
{
    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
    FormulaGrammarSwitch aFGSwitch(m_pDoc, formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1);

    m_pDoc->InsertTab(0, u"Test"_ustr);

    std::vector<std::vector<const char*>> aData = {
        { "=1", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
        { "=2", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
        { "=3", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
        { "=4", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
        { "=5", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
        { "=6", "=RC[-1]*2", "=ISFORMULA(RC[-1])" },
    };

    ScAddress aPos(1,2,0); // B3
    ScRange aDataRange = insertRangeData(m_pDoc, aPos, aData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("failed to insert range data at correct position", aPos, aDataRange.aStart);

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1",  "2", "TRUE" },
            { "2",  "4", "TRUE" },
            { "3",  "6", "TRUE" },
            { "4",  "8", "TRUE" },
            { "5", "10", "TRUE" },
            { "6", "12", "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Initial value");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // Convert B5:C6 to static values, and check the result.
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    ScRange aConvRange(1,4,0,2,5,0); // B5:C6
    rFunc.ConvertFormulaToValue(aConvRange, false);

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1",  "2",  "TRUE" },
            { "2",  "4",  "TRUE" },
            { "3",  "6", "FALSE" },
            { "4",  "8", "FALSE" },
            { "5", "10",  "TRUE" },
            { "6", "12",  "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Converted");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // Make sure that B3:B4 and B7:B8 are formula cells.
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,2,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,3,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,6,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,7,0)));

    // Make sure that B5:C6 are numeric cells.
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(1,4,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(1,5,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(2,4,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(2,5,0)));

    // Make sure that formula cells in C3:C4 and C7:C8 are grouped.
    const ScFormulaCell* pFC = m_pDoc->GetFormulaCell(ScAddress(2,2,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedTopRow());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedLength());
    pFC = m_pDoc->GetFormulaCell(ScAddress(2,6,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(6), pFC->GetSharedTopRow());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedLength());

    // Undo and check.
    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoMgr);
    pUndoMgr->Undo();

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1",  "2", "TRUE" },
            { "2",  "4", "TRUE" },
            { "3",  "6", "TRUE" },
            { "4",  "8", "TRUE" },
            { "5", "10", "TRUE" },
            { "6", "12", "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "After undo");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // B3:B8 should all be (ungrouped) formula cells.
    for (SCROW i = 2; i <= 7; ++i)
    {
        pFC = m_pDoc->GetFormulaCell(ScAddress(1,i,0));
        CPPUNIT_ASSERT(pFC);
        CPPUNIT_ASSERT(!pFC->IsShared());
    }

    // C3:C8 should be shared formula cells.
    pFC = m_pDoc->GetFormulaCell(ScAddress(2,2,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedTopRow());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(6), pFC->GetSharedLength());

    // Redo and check.
    pUndoMgr->Redo();
    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1",  "2",  "TRUE" },
            { "2",  "4",  "TRUE" },
            { "3",  "6", "FALSE" },
            { "4",  "8", "FALSE" },
            { "5", "10",  "TRUE" },
            { "6", "12",  "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Converted");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // Make sure that B3:B4 and B7:B8 are formula cells.
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,2,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,3,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,6,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_FORMULA, m_pDoc->GetCellType(ScAddress(1,7,0)));

    // Make sure that B5:C6 are numeric cells.
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(1,4,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(1,5,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(2,4,0)));
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(2,5,0)));

    // Make sure that formula cells in C3:C4 and C7:C8 are grouped.
    pFC = m_pDoc->GetFormulaCell(ScAddress(2,2,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedTopRow());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedLength());
    pFC = m_pDoc->GetFormulaCell(ScAddress(2,6,0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(6), pFC->GetSharedTopRow());
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(2), pFC->GetSharedLength());

    // Undo again and make sure the recovered formulas in C5:C6 still track B5:B6.
    pUndoMgr->Undo();
    m_pDoc->SetValue(ScAddress(1,4,0), 10);
    m_pDoc->SetValue(ScAddress(1,5,0), 11);
    CPPUNIT_ASSERT_EQUAL(20.0, m_pDoc->GetValue(ScAddress(2,4,0)));
    CPPUNIT_ASSERT_EQUAL(22.0, m_pDoc->GetValue(ScAddress(2,5,0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFormulaToValueUninterpreted)
{
    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);

    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Nothing reads the cell after entry, so its result is not computed yet
    m_pDoc->SetString(ScAddress(0, 0, 0), u"=1+1"_ustr);
    const ScFormulaCell* pFC = m_pDoc->GetFormulaCell(ScAddress(0, 0, 0));
    CPPUNIT_ASSERT(pFC);
    CPPUNIT_ASSERT(pFC->GetDirty());

    m_xDocShell->GetDocFunc().ConvertFormulaToValue(ScRange(ScAddress(0, 0, 0)), false);

    // Without the fix in place, this test would have failed with
    // - Expected: 2
    // - Actual  : 0
    CPPUNIT_ASSERT_EQUAL(CELLTYPE_VALUE, m_pDoc->GetCellType(ScAddress(0, 0, 0)));
    CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 0, 0)));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFormulaToValue2)
{
    sc::AutoCalcSwitch aACSwitch(*m_pDoc, true);
    FormulaGrammarSwitch aFGSwitch(m_pDoc, formula::FormulaGrammar::GRAM_ENGLISH_XL_R1C1);

    m_pDoc->InsertTab(0, u"Test"_ustr);

    std::vector<std::vector<const char*>> aData = {
        { "=1", "=ISFORMULA(RC[-1])" },
        { "=2", "=ISFORMULA(RC[-1])" },
        {  "3", "=ISFORMULA(RC[-1])" },
        { "=4", "=ISFORMULA(RC[-1])" },
        { "=5", "=ISFORMULA(RC[-1])" },
    };

    // Insert data into B2:C6.
    ScAddress aPos(1,1,0); // B2
    ScRange aDataRange = insertRangeData(m_pDoc, aPos, aData);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("failed to insert range data at correct position", aPos, aDataRange.aStart);

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1", "TRUE" },
            { "2", "TRUE" },
            { "3", "FALSE" },
            { "4", "TRUE" },
            { "5", "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Initial value");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // Convert B3:B5 to a value.
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    ScRange aConvRange(1,2,0,1,4,0); // B3:B5
    rFunc.ConvertFormulaToValue(aConvRange, false);

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1", "TRUE" },
            { "2", "FALSE" },
            { "3", "FALSE" },
            { "4", "FALSE" },
            { "5", "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Initial value");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    // Undo and check.
    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoMgr);
    pUndoMgr->Undo();

    {
        // Expected output table content.  0 = empty cell
        std::vector<std::vector<const char*>> aOutputCheck = {
            { "1", "TRUE" },
            { "2", "TRUE" },
            { "3", "FALSE" },
            { "4", "TRUE" },
            { "5", "TRUE" },
        };

        bool bSuccess = checkOutput(m_pDoc, aDataRange, aOutputCheck, "Initial value");
        CPPUNIT_ASSERT_MESSAGE("Table output check failed", bSuccess);
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testColumnFindEditCells)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Test the basics with real edit cells, using Column A.

    SCROW nResRow = m_pDoc->GetFirstEditTextRow(ScRange(0,0,0,0,m_pDoc->MaxRow(),0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be no edit cells.", SCROW(-1), nResRow);
    nResRow = m_pDoc->GetFirstEditTextRow(ScRange(0,0,0,0,0,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be no edit cells.", SCROW(-1), nResRow);
    nResRow = m_pDoc->GetFirstEditTextRow(ScRange(0,0,0,0,10,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be no edit cells.", SCROW(-1), nResRow);

    ScFieldEditEngine& rEE = m_pDoc->GetEditEngine();
    rEE.SetTextCurrentDefaults(u"Test"_ustr);
    m_pDoc->SetEditText(ScAddress(0,0,0), rEE.CreateTextObject());
    const EditTextObject* pObj = m_pDoc->GetEditText(ScAddress(0,0,0));
    CPPUNIT_ASSERT_MESSAGE("There should be an edit cell here.", pObj);

    ScRange aRange(0,0,0,0,0,0);
    nResRow = m_pDoc->GetFirstEditTextRow(aRange);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There is an edit cell here.", SCROW(0), nResRow);

    aRange.aStart.SetRow(1);
    aRange.aEnd.SetRow(1);
    nResRow = m_pDoc->GetFirstEditTextRow(aRange);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There shouldn't be an edit cell in specified range.", SCROW(-1), nResRow);

    aRange.aStart.SetRow(2);
    aRange.aEnd.SetRow(4);
    nResRow = m_pDoc->GetFirstEditTextRow(aRange);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There shouldn't be an edit cell in specified range.", SCROW(-1), nResRow);

    aRange.aStart.SetRow(0);
    aRange.aEnd.SetRow(m_pDoc->MaxRow());
    nResRow = m_pDoc->GetFirstEditTextRow(aRange);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be an edit cell in specified range.", SCROW(0), nResRow);

    m_pDoc->SetString(ScAddress(0,0,0), u"Test"_ustr);
    m_pDoc->SetValue(ScAddress(0,2,0), 1.0);
    ScRefCellValue aCell;
    aCell.assign(*m_pDoc, ScAddress(0,0,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This should be a string cell.", CELLTYPE_STRING, aCell.getType());
    aCell.assign(*m_pDoc, ScAddress(0,1,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This should be an empty cell.", CELLTYPE_NONE, aCell.getType());
    aCell.assign(*m_pDoc, ScAddress(0,2,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This should be a numeric cell.", CELLTYPE_VALUE, aCell.getType());
    aCell.assign(*m_pDoc, ScAddress(0,3,0));
    CPPUNIT_ASSERT_EQUAL_MESSAGE("This should be an empty cell.", CELLTYPE_NONE, aCell.getType());

    aRange.aStart.SetRow(1);
    aRange.aEnd.SetRow(1);
    nResRow = m_pDoc->GetFirstEditTextRow(aRange);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There shouldn't be an edit cell in specified range.", SCROW(-1), nResRow);

    // Test with non-edit cell but with ambiguous script type.

    m_pDoc->SetString(ScAddress(1,11,0), u"Some text"_ustr);
    m_pDoc->SetString(ScAddress(1,12,0), u"Some text"_ustr);
    m_pDoc->SetString(ScAddress(1,13,0), u"Other text"_ustr);

    m_pDoc->SetScriptType(ScAddress(1,11,0), (SvtScriptType::LATIN | SvtScriptType::ASIAN));
    m_pDoc->SetScriptType(ScAddress(1,12,0), (SvtScriptType::LATIN | SvtScriptType::ASIAN));
    m_pDoc->SetScriptType(ScAddress(1,13,0), (SvtScriptType::LATIN | SvtScriptType::ASIAN));

    nResRow = m_pDoc->GetFirstEditTextRow(ScRange(ScAddress(1,11,0)));
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(11), nResRow);
    nResRow = m_pDoc->GetFirstEditTextRow(ScRange(ScAddress(1,12,0)));
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(12), nResRow);

    for (SCROW i = 0; i <= 5; ++i)
        m_pDoc->SetString(ScAddress(2,i,0), u"Text"_ustr);

    m_pDoc->SetScriptType(ScAddress(2,5,0), (SvtScriptType::LATIN | SvtScriptType::ASIAN));

    nResRow = m_pDoc->GetFirstEditTextRow(ScRange(ScAddress(2,1,0)));
    CPPUNIT_ASSERT_EQUAL(static_cast<SCROW>(-1), nResRow);

    m_pDoc->DeleteTab(0);
}


CPPUNIT_TEST_FIXTURE(Test, testSetFormula)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    static struct aInputs
    {
        SCROW nRow;
        SCCOL nCol;
        const char* aFormula1;      // Represents the formula that is input to SetFormula function.
        const char* aFormula2;      // Represents the formula that is actually stored in the cell.
        formula::FormulaGrammar::Grammar const eGram;

    } const aTest[] = {
        { 5 , 4 , "=SUM($D$2:$F$3)"             ,"=SUM($D$2:$F$3)" , formula::FormulaGrammar::Grammar::GRAM_ENGLISH     },
        { 5 , 5 , "=A1-$C2+B$3-$F$4"            ,"=A1-$C2+B$3-$F$4", formula::FormulaGrammar::Grammar::GRAM_NATIVE      },
        { 6 , 6 , "=A1-$C2+B$3-$F$4"            ,"=A1-$C2+B$3-$F$4", formula::FormulaGrammar::Grammar::GRAM_NATIVE_XL_A1},
        { 7 , 8 , "=[.A1]-[.$C2]+[.G$3]-[.$F$4]","=A1-$C2+G$3-$F$4", formula::FormulaGrammar::Grammar::GRAM_ODFF        }
    };

    for(const auto& rTest : aTest)
    {
        m_pDoc->SetFormula(ScAddress(rTest.nCol, rTest.nRow, 0), OUString::createFromAscii(rTest.aFormula1), rTest.eGram);
        OUString aBuffer = m_pDoc->GetFormula(rTest.nCol, rTest.nRow, 0);

        CPPUNIT_ASSERT_EQUAL_MESSAGE("Failed to set formula", OUString::createFromAscii(rTest.aFormula2), aBuffer);
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testMultipleDataCellsInRange)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    ScRange aRange(1,2,0); // B3
    sc::MultiDataCellState aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::Empty, aState.meState);

    // Set a numeric value to B3.
    m_pDoc->SetValue(ScAddress(1,2,0), 1.0);
    aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::HasOneCell, aState.meState);
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aState.mnCol1);
    CPPUNIT_ASSERT_EQUAL(SCROW(2), aState.mnRow1);

    // Set another numeric value to B4.
    m_pDoc->SetValue(ScAddress(1,3,0), 2.0);
    aRange.aEnd.SetRow(3); // B3:B4
    aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::HasMultipleCells, aState.meState);
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aState.mnCol1);
    CPPUNIT_ASSERT_EQUAL(SCROW(2), aState.mnRow1);

    // Set the query range to B4:B5.  Now it should only report one cell, with
    // B4 being the first non-empty cell.
    aRange.aStart.SetRow(3);
    aRange.aEnd.SetRow(4);
    aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::HasOneCell, aState.meState);
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aState.mnCol1);
    CPPUNIT_ASSERT_EQUAL(SCROW(3), aState.mnRow1);

    // Set the query range to A1:C3.  The first non-empty cell should be B3.
    aRange = ScRange(0,0,0,2,2,0);
    aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::HasOneCell, aState.meState);
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aState.mnCol1);
    CPPUNIT_ASSERT_EQUAL(SCROW(2), aState.mnRow1);

    // Set string cells to D4 and F5, and query D3:F5.  D4 should be the first
    // non-empty cell.
    m_pDoc->SetString(ScAddress(3,3,0), u"foo"_ustr);
    m_pDoc->SetString(ScAddress(5,4,0), u"bar"_ustr);
    aRange = ScRange(3,2,0,5,4,0);
    aState = m_pDoc->HasMultipleDataCells(aRange);
    CPPUNIT_ASSERT_EQUAL(sc::MultiDataCellState::HasMultipleCells, aState.meState);
    CPPUNIT_ASSERT_EQUAL(SCCOL(3), aState.mnCol1);
    CPPUNIT_ASSERT_EQUAL(SCROW(3), aState.mnRow1);

    // TODO : add more test cases as needed.

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testFormulaWizardSubformula)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    m_pDoc->SetString(ScAddress(1,0,0), u"=1"_ustr);          // B1
    m_pDoc->SetString(ScAddress(1,1,0), u"=1/0"_ustr);        // B2
    m_pDoc->SetString(ScAddress(1,2,0), u"=gibberish"_ustr);  // B3

    ScSimpleFormulaCalculator aFCell1( *m_pDoc, ScAddress(0,0,0), u"=B1:B3"_ustr, true );
    FormulaError nErrCode = aFCell1.GetErrCode();
    CPPUNIT_ASSERT( nErrCode == FormulaError::NONE || aFCell1.IsMatrix() );
    CPPUNIT_ASSERT_EQUAL( u"{1|#DIV/0!|#NAME?}"_ustr, aFCell1.GetString().getString() );

    m_pDoc->SetString(ScAddress(1,0,0), u"=NA()"_ustr);       // B1
    m_pDoc->SetString(ScAddress(1,1,0), u"2"_ustr);           // B2
    m_pDoc->SetString(ScAddress(1,2,0), u"=1+2"_ustr);        // B3
    ScSimpleFormulaCalculator aFCell2( *m_pDoc, ScAddress(0,0,0), u"=B1:B3"_ustr, true );
    nErrCode = aFCell2.GetErrCode();
    CPPUNIT_ASSERT( nErrCode == FormulaError::NONE || aFCell2.IsMatrix() );
    CPPUNIT_ASSERT_EQUAL( u"{#N/A|2|3}"_ustr, aFCell2.GetString().getString() );

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDiagonalBorders)
{
    m_pDoc->InsertTab(0, u"Diagonal"_ustr);

    ScAddress aPos;
    const editeng::SvxBorderLine* pLine;
    const ScPatternAttr* pPat;

    // diagonal down border
    ::editeng::SvxBorderLine dDownBorderLine(nullptr, 1);
    SvxLineItem dDownLineItem(ATTR_BORDER_TLBR);
    dDownLineItem.SetLine(&dDownBorderLine);

    // set diagonal down border to cell(A1)
    m_pDoc->ApplyAttr(0, 0, 0, dDownLineItem);

    aPos = { 0, 0, 0 };
    pPat = m_pDoc->GetPattern(aPos);
    CPPUNIT_ASSERT(pPat);

    pLine = pPat->GetItem(ATTR_BORDER_TLBR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal down border was expected, but not found!", pLine);

    // diagonal up border
    ::editeng::SvxBorderLine dUpBorderLine(nullptr, 1);
    SvxLineItem dUpLineItem(ATTR_BORDER_BLTR);
    dUpLineItem.SetLine(&dUpBorderLine);

    // set diagonal up border to cell(A2)
    m_pDoc->ApplyAttr(0, 1, 0, dUpLineItem);

    aPos = { 0, 1, 0 };
    pPat = m_pDoc->GetPattern(aPos);
    CPPUNIT_ASSERT(pPat);

    pLine = pPat->GetItem(ATTR_BORDER_BLTR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal up border was expected, but not found!", pLine);

    // diagonal down and up border in the same cell (A5)
    m_pDoc->ApplyAttr(0, 4, 0, dDownLineItem);
    m_pDoc->ApplyAttr(0, 4, 0, dUpLineItem);

    // test if both borders are applied successfully in the same cell (A5)
    aPos = { 0, 4, 0 };
    pPat = m_pDoc->GetPattern(aPos);
    CPPUNIT_ASSERT(pPat);

    pLine = pPat->GetItem(ATTR_BORDER_TLBR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal down border was expected, but not found!", pLine);
    pLine = pPat->GetItem(ATTR_BORDER_BLTR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal up border was expected, but not found!", pLine);

    // test if both borders are removed successfully
    dDownLineItem.SetLine(nullptr);
    dUpLineItem.SetLine(nullptr);

    // SetLine(nullptr) should remove the lines from (A5)
    m_pDoc->ApplyAttr(0, 4, 0, dDownLineItem);
    m_pDoc->ApplyAttr(0, 4, 0, dUpLineItem);

    pPat = m_pDoc->GetPattern(aPos);
    CPPUNIT_ASSERT(pPat);

    pLine = pPat->GetItem(ATTR_BORDER_TLBR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal down border was not expected, but is found!", !pLine);
    pLine = pPat->GetItem(ATTR_BORDER_BLTR).GetLine();
    CPPUNIT_ASSERT_MESSAGE("Diagonal up border was not expected, but is found!", !pLine);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testWholeDocBorders)
{
    m_pDoc->InsertTab(0, u"Borders"_ustr);

    // Set outside border to be on all sides, and inside borders to be only vertical.
    // This should result in edge borders of the spreadsheets being set, but internal
    // borders between cells should be only vertical, not horizontal.
    ::editeng::SvxBorderLine line(nullptr, 50, SvxBorderLineStyle::SOLID);
    SvxBoxItem borderItem(ATTR_BORDER);
    borderItem.SetLine(&line, SvxBoxItemLine::LEFT);
    borderItem.SetLine(&line, SvxBoxItemLine::RIGHT);
    borderItem.SetLine(&line, SvxBoxItemLine::TOP);
    borderItem.SetLine(&line, SvxBoxItemLine::BOTTOM);
    SvxBoxInfoItem boxInfoItem(ATTR_BORDER);
    boxInfoItem.SetLine(&line, SvxBoxInfoItemLine::VERT);

    ScMarkData mark( m_pDoc->GetSheetLimits(), ScRange( 0, 0, 0, m_pDoc->MaxCol(), m_pDoc->MaxRow(), 0 ));
    m_pDoc->ApplySelectionFrame( mark, borderItem, &boxInfoItem );

    const ScPatternAttr* attr;
    attr = m_pDoc->GetPattern( 0, 0, 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( 1, 0, 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( 0, 1, 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( 1, 1, 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( m_pDoc->MaxCol(), 0, 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( 0, m_pDoc->MaxRow(), 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetBottom());

    attr = m_pDoc->GetPattern( m_pDoc->MaxCol(), m_pDoc->MaxRow(), 0 );
    CPPUNIT_ASSERT(attr);
    CPPUNIT_ASSERT(!attr->GetItem(ATTR_BORDER).GetTop());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetLeft());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetRight());
    CPPUNIT_ASSERT(attr->GetItem(ATTR_BORDER).GetBottom());

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testSetStringAndNote)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // We need a drawing layer in order to create caption objects.
    m_pDoc->InitDrawLayer(m_xDocShell.get());

    //note on A1
    ScAddress aAdrA1 (0, 0, 0);
    ScPostIt* pNote = m_pDoc->GetOrCreateNote(aAdrA1);
    pNote->SetText(aAdrA1, u"Hello world in A1"_ustr);

    m_pDoc->SetString(0, 0, 0, u""_ustr);

    pNote = m_pDoc->GetNote(aAdrA1);
    CPPUNIT_ASSERT(pNote);

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testUndoDataAnchor)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);
    CPPUNIT_ASSERT_EQUAL_MESSAGE("There should be only 1 sheets to begin with",
                               static_cast<SCTAB>(1), m_pDoc->GetTableCount());

    m_pDoc->InitDrawLayer();
    ScDrawLayer* pDrawLayer = m_pDoc->GetDrawLayer();
    CPPUNIT_ASSERT_MESSAGE("No drawing layer.", pDrawLayer);
    SdrPage* pPage = pDrawLayer->GetPage(0);
    CPPUNIT_ASSERT_MESSAGE("No page instance for the 1st sheet.", pPage);

    // Insert an object.
    tools::Rectangle aObjRect(2,1000,100,1100);
    rtl::Reference<SdrObject> pObj = new SdrRectObj(*pDrawLayer, aObjRect);
    pPage->InsertObject(pObj.get());
    ScDrawLayer::SetCellAnchoredFromPosition(*pObj, *m_pDoc, 0, false);

    // Get anchor data
    ScDrawObjData* pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve user data for this object.", pData);

    ScAddress aOldStart = pData->maStart;
    ScAddress aOldEnd   = pData->maEnd;

    // Get non rotated anchor data
    ScDrawObjData* pNData = ScDrawLayer::GetNonRotatedObjData( pObj.get() );
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve non rotated user data for this object.", pNData);

    ScAddress aNOldStart = pNData->maStart;
    ScAddress aNOldEnd   = pNData->maEnd;
    CPPUNIT_ASSERT_EQUAL(aOldStart, aNOldStart);
    CPPUNIT_ASSERT_EQUAL(aOldEnd, aNOldEnd);

    //pDrawLayer->BeginCalcUndo(false);
    // Insert a new row at row 3.
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    rFunc.InsertCells(ScRange( 0, aOldStart.Row() - 1, 0, m_pDoc->MaxCol(), aOldStart.Row(), 0 ), &aMark, INS_INSROWS_BEFORE, true, true);

    pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve user data for this object.", pData);

    ScAddress aNewStart = pData->maStart;
    ScAddress aNewEnd   = pData->maEnd;

    // Get non rotated anchor data
    pNData = ScDrawLayer::GetNonRotatedObjData( pObj.get() );
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve non rotated user data for this object.", pNData);

    ScAddress aNNewStart = pNData->maStart;
    ScAddress aNNewEnd   = pNData->maEnd;
    CPPUNIT_ASSERT_EQUAL(aNewStart, aNNewStart);
    CPPUNIT_ASSERT_EQUAL(aNewEnd, aNNewEnd);
    CPPUNIT_ASSERT_MESSAGE("Failed to compare Address.", aNewStart  != aOldStart );
    CPPUNIT_ASSERT_MESSAGE("Failed to compare Address.", aNewEnd  != aOldEnd );
    CPPUNIT_ASSERT_MESSAGE("Failed to compare Address.", aNNewStart != aNOldStart );
    CPPUNIT_ASSERT_MESSAGE("Failed to compare Address.", aNNewEnd != aNOldEnd );

    SfxUndoManager* pUndoMgr = m_pDoc->GetUndoManager();
    CPPUNIT_ASSERT(pUndoMgr);
    pUndoMgr->Undo();

    // Check state
    ScAnchorType oldType = ScDrawLayer::GetAnchorType(*pObj);
    CPPUNIT_ASSERT_EQUAL_MESSAGE( "Failed to check state SCA_CELL.", SCA_CELL, oldType);

    // Get anchor data
    pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve user data for this object.", pData);

    // Get non rotated anchor data
    pNData = ScDrawLayer::GetNonRotatedObjData( pObj.get() );
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve non rotated user data for this object.", pNData);

    // Check if data has moved to new rows
    CPPUNIT_ASSERT_EQUAL(pData->maStart, aOldStart);
    CPPUNIT_ASSERT_EQUAL(pData->maEnd, aOldEnd);

    CPPUNIT_ASSERT_EQUAL(pNData->maStart, aNOldStart);
    CPPUNIT_ASSERT_EQUAL(pNData->maEnd, aNOldEnd);

    pUndoMgr->Redo();

    // Get anchor data
    pData = ScDrawLayer::GetObjData(pObj.get());
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve user data for this object.", pData);

    // Get non rotated anchor data
    pNData = ScDrawLayer::GetNonRotatedObjData( pObj.get() );
    CPPUNIT_ASSERT_MESSAGE("Failed to retrieve non rotated user data for this object.", pNData);

    // Check if data has moved to new rows
    CPPUNIT_ASSERT_EQUAL(pData->maStart, aNewStart);
    CPPUNIT_ASSERT_EQUAL(pData->maEnd, aNewEnd);

    CPPUNIT_ASSERT_EQUAL(pNData->maStart, aNNewStart);
    CPPUNIT_ASSERT_EQUAL(pNData->maEnd, aNNewEnd);

    m_pDoc->DeleteTab(0);
}


CPPUNIT_TEST_FIXTURE(Test, testEmptyCalcDocDefaults)
{
    CPPUNIT_ASSERT_EQUAL( sal_uInt64(0), m_pDoc->GetCellCount() );
    CPPUNIT_ASSERT_EQUAL( sal_uInt64(0), m_pDoc->GetFormulaGroupCount() );
    CPPUNIT_ASSERT_EQUAL( sal_uInt64(0), m_pDoc->GetCodeCount() );
    CPPUNIT_ASSERT_EQUAL( int(CharCompressType::NONE), static_cast<int>(m_pDoc->GetAsianCompression()) );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasPrintRange() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInVBAMode() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasNotes() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsCutMode() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsEmbedFonts() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsEmbedUsedFontsOnly() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsEmbedFontScriptLatin() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsEmbedFontScriptAsian() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsEmbedFontScriptComplex() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsEmbedded() );

    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsDocEditable() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsDocProtected() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsDocVisible() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsUserInteractionEnabled() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasAnyCalcNotification() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsAutoCalcShellDisabled() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsForcedFormulaPending() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsCalculatingFormulaTree() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsClipOrUndo() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsClipboard() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsUndo() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsUndoEnabled() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsCutMode() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsClipboardSource() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInsertingFromOtherDoc() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->PastingDrawFromOtherDoc() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsAdjustHeightLocked() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsExecuteLinkEnabled() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsChangeReadOnlyEnabled() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IdleCalcTextWidth() );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsIdleEnabled() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsDetectiveDirty() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasLinkFormulaNeedingCheck() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsChartListenerCollectionNeedsUpdate() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasRangeOverflow() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsImportingXML() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsCalcingAfterLoad() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->GetNoListening() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsValidAsianCompression() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->GetAsianKerning() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsValidAsianKerning() );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInInterpreter() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInInterpreterTableOp() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInDtorClear() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsExpandRefs() );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsInLinkUpdate() );

    SCTAB tab = m_pDoc->GetVisibleTab();

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsVisible(tab) );
    CPPUNIT_ASSERT_EQUAL( true, m_pDoc->IsDefaultTabBgColor(tab) );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasTable(tab) );

    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->IsActiveScenario(tab) );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasCalcNotification(tab) );
    CPPUNIT_ASSERT_EQUAL( false, m_pDoc->HasManualBreaks(tab) );
}

void Test::checkPrecisionAsShown( OUString& rCode, double fValue, double fExpectedRoundVal )
{
    SvNumberFormatter* pFormatter = m_pDoc->GetFormatTable();
    sal_uInt32 nFormat = pFormatter->GetEntryKey( rCode );
    if ( nFormat == NUMBERFORMAT_ENTRY_NOT_FOUND )
    {
        sal_Int32 nCheckPos = 0;
        SvNumFormatType nType;
        pFormatter->PutEntry( rCode, nCheckPos, nType, nFormat );
        CPPUNIT_ASSERT_EQUAL( sal_Int32(0), nCheckPos );
    }
    double fRoundValue = m_pDoc->RoundValueAsShown( fValue, nFormat );
    OString aMessage = "Format \"" +
        OUStringToOString( rCode, RTL_TEXTENCODING_ASCII_US ) +
        "\" is not correctly rounded";
    CPPUNIT_ASSERT_EQUAL_MESSAGE( aMessage.getStr(), fExpectedRoundVal, fRoundValue );
}

CPPUNIT_TEST_FIXTURE(Test, testPrecisionAsShown)
{
    m_pDoc->InsertTab(0, u"Test"_ustr);

    // Turn on "precision as shown" option.
    setCalcAsShown( m_pDoc, true);

    OUString aCode;
    double fValue, fExpectedRoundVal;
    {   // decimal rounding
        aCode = u"0.00"_ustr;
        fValue = 1.0/3.0;
        fExpectedRoundVal = 0.33;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 10.001;
        fExpectedRoundVal = 10.0;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // thousand rounding tdf#106253
        aCode = u"0,,"_ustr;
        fValue = 4.0e9 / 7.0;
        fExpectedRoundVal = 571e6;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        aCode = u"\"k\"[$$-409]* #,;[RED]-\"k\"[$$-409]* #,"_ustr;
        fValue = 4.0e8 / 7.0;
        fExpectedRoundVal = 57.143e6;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // percent rounding
        aCode = u"0.00%"_ustr;
        fValue = 4.0 / 7.0;
        fExpectedRoundVal = 0.5714;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 40.0 / 7.0;
        fExpectedRoundVal = 5.7143;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // scientific rounding
        aCode = u"0.00E0"_ustr;
        fValue = 400000.0 / 7.0;
        fExpectedRoundVal = 57100.0;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 70000.0;
        fExpectedRoundVal = 5.71e-5;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        // engineering rounding tdf#106252
        aCode = u"##0.000E0"_ustr;
        fValue = 400000.0 / 7.0;
        fExpectedRoundVal = 57.143e3;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4000000.0 / 7.0;
        fExpectedRoundVal = 571.429e3;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 40000000.0 / 7.0;
        fExpectedRoundVal = 5.714e6;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 70000.0;
        fExpectedRoundVal = 57.143e-6;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 7000.0;
        fExpectedRoundVal = 571.429e-6;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 700.0;
        fExpectedRoundVal = 5.714e-3;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        aCode = u"##?0.0#E0"_ustr;
        fValue = 400000.0 / 7.0;
        fExpectedRoundVal = 5.71e4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4000000.0 / 7.0;
        fExpectedRoundVal = 57.14e4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 40000000.0 / 7.0;
        fExpectedRoundVal = 571.43e4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 400000000.0 / 7.0;
        fExpectedRoundVal = 5714.29e4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 70000.0;
        fExpectedRoundVal = 5714.29e-8;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 7000.0;
        fExpectedRoundVal = 5.71e-4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 700.0;
        fExpectedRoundVal = 57.14e-4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
        fValue = 4.0 / 70.0;
        fExpectedRoundVal = 571.43e-4;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // fraction rounding tdf#105657
        aCode = u"# ?/?"_ustr;
        fValue = 0.35;
        fExpectedRoundVal = 1.0/3.0;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // exact fraction
        aCode = u"# ?/??"_ustr;
        fValue = 0.35;
        fExpectedRoundVal = 0.35;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        checkPrecisionAsShown( aCode, -fValue, -fExpectedRoundVal );
    }
    {   // several sub-formats tdf#106052
        aCode = u"0.00;-0.000"_ustr;
        fValue = 1.0/3.0;
        fExpectedRoundVal = 0.33;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
        fValue = -1.0/3.0;
        fExpectedRoundVal = -0.333;
        checkPrecisionAsShown( aCode,  fValue,  fExpectedRoundVal );
    }

    setCalcAsShown( m_pDoc, false);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testProtectedSheetEditByRow)
{
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    m_pDoc->InsertTab(0, u"Protected"_ustr);

    {
        // Remove protected flags from rows 2-5.
        ScPatternAttr aAttr(m_pDoc->getCellAttributeHelper());
        aAttr.ItemSetPut(ScProtectionAttr(false));
        m_pDoc->ApplyPatternAreaTab(0, 1, m_pDoc->MaxCol(), 4, 0, aAttr);

        // Protect the sheet without any options.
        ScTableProtection aProtect;
        aProtect.setProtected(true);
        m_pDoc->SetTabProtection(0, &aProtect);

        // Try to delete row 3.  It should fail.
        ScRange aRow3(0,2,0,m_pDoc->MaxCol(),2,0);
        ScMarkData aMark(m_pDoc->GetSheetLimits());
        aMark.SelectOneTable(0);
        bool bDeleted = rDocFunc.DeleteCells(aRow3, &aMark, DelCellCmd::Rows, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of row 3 should fail.", !bDeleted);

        // Protect the sheet but allow row deletion.
        aProtect.setOption(ScTableProtection::DELETE_ROWS, true);
        m_pDoc->SetTabProtection(0, &aProtect);

        // Now we should be able to delete row 3.
        bDeleted = rDocFunc.DeleteCells(aRow3, &aMark, DelCellCmd::Rows, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of row 3 should succeed.", bDeleted);

        // But, row deletion should still fail on a protected row.
        ScRange aRow10(0,9,0,m_pDoc->MaxCol(),9,0);
        bDeleted = rDocFunc.DeleteCells(aRow10, &aMark, DelCellCmd::Rows, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of row 10 should not be allowed.", !bDeleted);

        // Try inserting a new row.  It should fail.
        bool bInserted = rDocFunc.InsertCells(aRow3, &aMark, INS_INSROWS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("row insertion at row 3 should fail.", !bInserted);

        // Allow row insertions.
        aProtect.setOption(ScTableProtection::INSERT_ROWS, true);
        m_pDoc->SetTabProtection(0, &aProtect);

        bInserted = rDocFunc.InsertCells(aRow3, &aMark, INS_INSROWS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("row insertion at row 3 should succeed.", bInserted);

        // Row insertion is allowed even when the rows above and below have protected flags set.
        bInserted = rDocFunc.InsertCells(aRow10, &aMark, INS_INSROWS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("row insertion at row 10 should succeed.", bInserted);
    }

    m_pDoc->InsertTab(1, u"Matrix"_ustr); // This sheet is unprotected.

    {
        // Insert matrix into B2:C3.
        ScMarkData aMark(m_pDoc->GetSheetLimits());
        aMark.SelectOneTable(1);
        m_pDoc->InsertMatrixFormula(1, 1, 2, 2, aMark, u"={1;2|3;4}"_ustr);

        CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(1,1,1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(2,1,1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(1,2,1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(2,2,1)));

        // Try to insert a row at row 3.  It should fail because of matrix's presence.

        ScRange aRow3(0,2,1,m_pDoc->MaxCol(),2,1);
        bool bInserted = rDocFunc.InsertCells(aRow3, &aMark, INS_INSROWS_BEFORE, true, true);
        CPPUNIT_ASSERT_MESSAGE("row insertion at row 3 should fail.", !bInserted);
    }

    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testProtectedSheetEditByColumn)
{
    ScDocFunc& rDocFunc = m_xDocShell->GetDocFunc();
    m_pDoc->InsertTab(0, u"Protected"_ustr);

    {
        // Remove protected flags from columns B to E.
        ScPatternAttr aAttr(m_pDoc->getCellAttributeHelper());
        aAttr.ItemSetPut(ScProtectionAttr(false));
        m_pDoc->ApplyPatternAreaTab(1, 0, 4, m_pDoc->MaxRow(), 0, aAttr);

        // Protect the sheet without any options.
        ScTableProtection aProtect;
        aProtect.setProtected(true);
        m_pDoc->SetTabProtection(0, &aProtect);

        // Try to delete column C.  It should fail.
        ScRange aCol3(2,0,0,2,m_pDoc->MaxRow(),0);
        ScMarkData aMark(m_pDoc->GetSheetLimits());
        aMark.SelectOneTable(0);
        bool bDeleted = rDocFunc.DeleteCells(aCol3, &aMark, DelCellCmd::Cols, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of column 3 should fail.", !bDeleted);

        // Protect the sheet but allow column deletion.
        aProtect.setOption(ScTableProtection::DELETE_COLUMNS, true);
        m_pDoc->SetTabProtection(0, &aProtect);

        // Now we should be able to delete column C.
        bDeleted = rDocFunc.DeleteCells(aCol3, &aMark, DelCellCmd::Cols, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of column 3 should succeed.", bDeleted);

        // But, column deletion should still fail on a protected column.
        ScRange aCol10(9,0,0,9,m_pDoc->MaxRow(),0);
        bDeleted = rDocFunc.DeleteCells(aCol10, &aMark, DelCellCmd::Cols, true);
        CPPUNIT_ASSERT_MESSAGE("deletion of column 10 should not be allowed.", !bDeleted);

        // Try inserting a new column.  It should fail.
        bool bInserted = rDocFunc.InsertCells(aCol3, &aMark, INS_INSCOLS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("column insertion at column 3 should fail.", !bInserted);

        // Allow column insertions.
        aProtect.setOption(ScTableProtection::INSERT_COLUMNS, true);
        m_pDoc->SetTabProtection(0, &aProtect);

        bInserted = rDocFunc.InsertCells(aCol3, &aMark, INS_INSCOLS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("column insertion at column 3 should succeed.", bInserted);

        // Column insertion is allowed even when the columns above and below have protected flags set.
        bInserted = rDocFunc.InsertCells(aCol10, &aMark, INS_INSCOLS_AFTER, true, true);
        CPPUNIT_ASSERT_MESSAGE("column insertion at column 10 should succeed.", bInserted);
    }

    m_pDoc->InsertTab(1, u"Matrix"_ustr); // This sheet is unprotected.

    {
        // Insert matrix into B2:C3.
        ScMarkData aMark(m_pDoc->GetSheetLimits());
        aMark.SelectOneTable(1);
        m_pDoc->InsertMatrixFormula(1, 1, 2, 2, aMark, u"={1;2|3;4}"_ustr);

        CPPUNIT_ASSERT_EQUAL(1.0, m_pDoc->GetValue(ScAddress(1,1,1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(2,1,1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(1,2,1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(2,2,1)));

        // Try to insert a column at column C.  It should fail because of matrix's presence.

        ScRange aCol3(2,0,1,2,m_pDoc->MaxRow(),1);
        bool bInserted = rDocFunc.InsertCells(aCol3, &aMark, INS_INSCOLS_BEFORE, true, true);
        CPPUNIT_ASSERT_MESSAGE("column insertion at column C should fail.", !bInserted);
    }

    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testInsertColumnsWithFormulaCells)
{
    m_pDoc->InsertTab(0, u"Tab1"_ustr);

    std::set<SCCOL> aCols = m_pDoc->QueryColumnsWithFormulaCells(0);
    CPPUNIT_ASSERT_MESSAGE("empty sheet should contain no formula cells.", aCols.empty());

    auto equals = [](const std::set<SCCOL>& left, const std::set<SCCOL>& right)
    {
        return left == right;
    };

    // insert formula cells in columns 2, 4 and 6.
    m_pDoc->SetFormula(ScAddress(2, 2, 0), u"=1"_ustr, m_pDoc->GetGrammar());
    m_pDoc->SetFormula(ScAddress(4, 2, 0), u"=1"_ustr, m_pDoc->GetGrammar());
    m_pDoc->SetFormula(ScAddress(6, 2, 0), u"=1"_ustr, m_pDoc->GetGrammar());

    aCols = m_pDoc->QueryColumnsWithFormulaCells(0);

    std::set<SCCOL> aExpected = { 2, 4, 6 };
    CPPUNIT_ASSERT_MESSAGE("Columns 2, 4 and 6 should contain formula cells.", equals(aExpected, aCols));

    // Insert 2 columns at column A to shift everything to right by 2.
    m_pDoc->InsertCol(0, 0, m_pDoc->MaxRow(), 0, 0, 2);

    aExpected = { 4, 6, 8 };
    aCols = m_pDoc->QueryColumnsWithFormulaCells(0);
    CPPUNIT_ASSERT_MESSAGE("Columns 4, 6 and 8 should contain formula cells.", equals(aExpected, aCols));

    try
    {
        m_pDoc->CheckIntegrity(0);
    }
    catch (const std::exception& e)
    {
        std::ostringstream os;
        os << "document integrity check failed: " << e.what();
        CPPUNIT_FAIL(os.str());
    }

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testDocumentModelAccessor_getDocumentCurrencies)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // Check document currencies - expect 0
    auto pAccessor = m_xDocShell->GetDocumentModelAccessor();
    CPPUNIT_ASSERT(pAccessor);
    CPPUNIT_ASSERT_EQUAL(size_t(0), pAccessor->getDocumentCurrencies().size());

    // Set a currency to a cell
    {
        m_pDoc->SetValue(ScAddress(0, 0, 0), 2.0);

        OUString aCode = u"#.##0,00[$€-424]"_ustr;

        sal_Int32 nCheckPos;
        SvNumFormatType eType;
        sal_uInt32 nFormat;

        m_pDoc->GetFormatTable()->PutEntry(aCode, nCheckPos, eType, nFormat, LANGUAGE_SLOVENIAN);
        CPPUNIT_ASSERT_EQUAL(SvNumFormatType::CURRENCY, eType);

        ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
        aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
        m_pDoc->ApplyPattern(0, 0, 0, aNewAttrs); // A1.

        CPPUNIT_ASSERT_EQUAL(u"2,00€"_ustr, m_pDoc->GetString(ScAddress(0, 0, 0)));
    }

    // Check document currencies again
    auto aCurrencyIDs = pAccessor->getDocumentCurrencies();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aCurrencyIDs.size());

    CPPUNIT_ASSERT_EQUAL(LANGUAGE_SLOVENIAN, aCurrencyIDs[0].eLanguage);
    CPPUNIT_ASSERT_EQUAL(u"-424"_ustr, aCurrencyIDs[0].aExtension);
    CPPUNIT_ASSERT_EQUAL(u"€"_ustr, aCurrencyIDs[0].aSymbol);

    // Set the same currency to 2 more cells
    {
        m_pDoc->SetValue(ScAddress(1, 1, 0), 5.0);
        m_pDoc->SetValue(ScAddress(2, 2, 0), 7.0);

        OUString aCode = u"#.##0,00[$€-424]"_ustr;

        sal_Int32 nCheckPos;
        SvNumFormatType eType;
        sal_uInt32 nFormat;

        m_pDoc->GetFormatTable()->PutEntry(aCode, nCheckPos, eType, nFormat, LANGUAGE_SLOVENIAN);
        CPPUNIT_ASSERT_EQUAL(SvNumFormatType::CURRENCY, eType);

        ScPatternAttr aNewAttrs(m_pDoc->getCellAttributeHelper());
        aNewAttrs.ItemSetPut(SfxUInt32Item(ATTR_VALUE_FORMAT, nFormat));
        m_pDoc->ApplyPattern(1, 1, 0, aNewAttrs); // B2.
        m_pDoc->ApplyPattern(2, 2, 0, aNewAttrs); // C3.

        CPPUNIT_ASSERT_EQUAL(u"5,00€"_ustr, m_pDoc->GetString(ScAddress(1, 1, 0)));
        CPPUNIT_ASSERT_EQUAL(u"7,00€"_ustr, m_pDoc->GetString(ScAddress(2, 2, 0)));
    }

    // Check document currencies again - should be 1 entry only
    aCurrencyIDs = pAccessor->getDocumentCurrencies();
    CPPUNIT_ASSERT_EQUAL(size_t(1), aCurrencyIDs.size());

    CPPUNIT_ASSERT_EQUAL(LANGUAGE_SLOVENIAN, aCurrencyIDs[0].eLanguage);
    CPPUNIT_ASSERT_EQUAL(u"-424"_ustr, aCurrencyIDs[0].aExtension);
    CPPUNIT_ASSERT_EQUAL(u"€"_ustr, aCurrencyIDs[0].aSymbol);
}

CPPUNIT_TEST_FIXTURE(Test, testOverwriteContent)
{
    m_pDoc->InsertTab(0, u"Tab0"_ustr);
    m_pDoc->InsertTab(1, u"Tab1"_ustr);
    m_pDoc->InsertTab(2, u"Tab2"_ustr);
    m_pDoc->InsertTab(3, u"Tab3"_ustr);

    // For cross table formula checking (forward and backward)
    m_pDoc->SetValue(ScAddress(0, 0, 0), 99.0); // Tab0.A1
    m_pDoc->SetValue(ScAddress(0, 0, 3), 66.0); // Tab3.A1

    std::vector<std::vector<OUString>> aData = {
        { u"Column 1"_ustr, u"Column 2"_ustr, u"Column 3"_ustr,   },
        {  u"7"_ustr,       u"A"_ustr,        u"=A2+5"_ustr },
        {  u"=2+2"_ustr,    u"C"_ustr,        u"=Tab3.A1"_ustr }, // forward cross-tab reference
        {  u"=A3-2"_ustr,   u"D"_ustr,        u"=SUM(A2:A6)"_ustr },
        {  u"3"_ustr,       u"B"_ustr,        u"=C3-60"_ustr },
        {  u"5"_ustr,       u"E"_ustr,        u"=Tab0.A1"_ustr }, // backward cross-tab reference
    };

    for (size_t i = 0; i < aData.size(); ++i)
    {
        for (size_t j = 0; j < aData[0].size(); ++j)
        {
            if (!aData[i][j].isEmpty())
            {
                m_pDoc->SetString(j, i, 1, aData[i][j]);
            }
        }
    }
    m_pDoc->CalcAll();

    // Verify Tab1 values and formulas before overwrite
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1))); // A3-2=2
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 1))); // =A2+5=12
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1))); // =Tab3.A1
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 1))); // =SUM(A2:A6)
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));  // =C3-60
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1))); // =Tab0.A1
    }

    // Tab2 should be empty
    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(1, 2, 2)));
    CPPUNIT_ASSERT_EQUAL(0.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));

    // Overwrite Tab2 with Tab1 content
    m_pDoc->OverwriteContent(1, 2);

    // Tab1 should be unchanged
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1)));
    }

    // Tab2 values should match Tab1 and the formulas adjusted if needed
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 2)));
    }

    // Check Tab2 formulas were copied correctly
    {
        CPPUNIT_ASSERT_EQUAL(u"=2+2"_ustr, m_pDoc->GetFormula(0, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A3-2"_ustr, m_pDoc->GetFormula(0, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A2+5"_ustr, m_pDoc->GetFormula(2, 1, 2));
        CPPUNIT_ASSERT_EQUAL(u"='Tab3'.A1"_ustr, m_pDoc->GetFormula(2, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=SUM(A2:A6)"_ustr, m_pDoc->GetFormula(2, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=C3-60"_ustr, m_pDoc->GetFormula(2, 4, 2));
        CPPUNIT_ASSERT_EQUAL(u"=Tab0.A1"_ustr, m_pDoc->GetFormula(2, 5, 2));
    }

    // Change Tab1 and see if it affects Tab2
    m_pDoc->SetValue(ScAddress(0, 1, 1), 10.0); // 7 -> 10
    m_pDoc->CalcAll();

    // Check Tab 1
    {
        CPPUNIT_ASSERT_EQUAL(10.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(15.0, m_pDoc->GetValue(ScAddress(2, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(24.0, m_pDoc->GetValue(ScAddress(2, 3, 1))); // =SUM(A2:A6)=10+4+2+3+5
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1)));
    }

    // Check Tab 2
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 2)));
    }

    // Overwrite Tab2 again with updated Tab1
    m_pDoc->OverwriteContent(1, 2);

    // Check Tab 2
    {
        CPPUNIT_ASSERT_EQUAL(10.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(15.0, m_pDoc->GetValue(ScAddress(2, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(24.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 2)));
    }

    // Check formulas are still the same
    {
        CPPUNIT_ASSERT_EQUAL(u"=2+2"_ustr, m_pDoc->GetFormula(0, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A3-2"_ustr, m_pDoc->GetFormula(0, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A2+5"_ustr, m_pDoc->GetFormula(2, 1, 2));
        CPPUNIT_ASSERT_EQUAL(u"='Tab3'.A1"_ustr, m_pDoc->GetFormula(2, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=SUM(A2:A6)"_ustr, m_pDoc->GetFormula(2, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=C3-60"_ustr, m_pDoc->GetFormula(2, 4, 2));
        CPPUNIT_ASSERT_EQUAL(u"=Tab0.A1"_ustr, m_pDoc->GetFormula(2, 5, 2));
    }

    m_pDoc->DeleteTab(3);
    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutoFilterFlagsAfterColumnInsertUndoRedo)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // Set up data - A1:C3
    m_pDoc->SetString(0, 0, 0, u"Col1"_ustr);
    m_pDoc->SetString(1, 0, 0, u"Col2"_ustr);
    m_pDoc->SetString(2, 0, 0, u"Col3"_ustr);
    m_pDoc->SetString(0, 1, 0, u"1"_ustr);
    m_pDoc->SetString(1, 1, 0, u"A"_ustr);
    m_pDoc->SetString(2, 1, 0, u"X"_ustr);
    m_pDoc->SetString(0, 2, 0, u"2"_ustr);
    m_pDoc->SetString(1, 2, 0, u"B"_ustr);
    m_pDoc->SetString(2, 2, 0, u"Y"_ustr);

    // Set up auto-filter on A1:C3
    ScDBData* pDBData = new ScDBData(u"NONAME"_ustr, 0, 0, 0, 2, 2);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));
    pDBData->SetAutoFilter(true);
    m_pDoc->ApplyFlagsTab(0, 0, 2, 0, 0, ScMF::Auto);

    // Verify initial auto-filter flags on A1:C1
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(0, 0, 0));
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(1, 0, 0));
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(2, 0, 0));

    // Insert a column before B
    ScRange aColumnRange(1, 0, 0, 1, m_pDoc->MaxRow(), 0);
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    aMark.SetMarkArea(aColumnRange);
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    rFunc.InsertCells(aColumnRange, &aMark, INS_INSCOLS_BEFORE, true, true);

    // After insert: auto-filter range is expanded
    // The DB data range should have expanded via reference tracking
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);

    ScRange aArea;
    pDBData->GetArea(aArea);
    // Auto-filter range should now be A1:D3 (column B was inserted)
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(3), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:D1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(4, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(5, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    // Undo the column insert
    m_pDoc->GetUndoManager()->Undo();

    // Back to A:C
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);
    pDBData->GetArea(aArea);
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(2), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:C1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(4, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    // Redo the column insert
    m_pDoc->GetUndoManager()->Redo();

    // Back to A:D
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);
    pDBData->GetArea(aArea);
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(3), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:D1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(4, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(5, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testAutoFilterFlagsAfterColumnDeleteUndoRedo)
{
    m_pDoc->InsertTab(0, u"Sheet1"_ustr);

    // Set up data: header in row 0, data in rows 1-2, columns A-C
    m_pDoc->SetString(0, 0, 0, u"Col1"_ustr);
    m_pDoc->SetString(1, 0, 0, u"Col2"_ustr);
    m_pDoc->SetString(2, 0, 0, u"Col3"_ustr);
    m_pDoc->SetString(0, 1, 0, u"1"_ustr);
    m_pDoc->SetString(1, 1, 0, u"A"_ustr);
    m_pDoc->SetString(2, 1, 0, u"X"_ustr);
    m_pDoc->SetString(0, 2, 0, u"2"_ustr);
    m_pDoc->SetString(1, 2, 0, u"B"_ustr);
    m_pDoc->SetString(2, 2, 0, u"Y"_ustr);

    // Set up auto-filter on A1:C3
    ScDBData* pDBData = new ScDBData(u"NONAME"_ustr, 0, 0, 0, 2, 2);
    m_pDoc->SetAnonymousDBData(0, std::unique_ptr<ScDBData>(pDBData));
    pDBData->SetAutoFilter(true);
    m_pDoc->ApplyFlagsTab(0, 0, 2, 0, 0, ScMF::Auto);

    // Verify initial auto-filter flags on A1:C1
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(0, 0, 0));
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(1, 0, 0));
    CPPUNIT_ASSERT(m_pDoc->HasAutoFilter(2, 0, 0));

    // Delete column B
    ScRange aColRange(1, 0, 0, 1, m_pDoc->MaxRow(), 0);
    ScMarkData aMark(m_pDoc->GetSheetLimits());
    aMark.SelectOneTable(0);
    aMark.SetMarkArea(aColRange);
    ScDocFunc& rFunc = m_xDocShell->GetDocFunc();
    rFunc.DeleteCells(aColRange, &aMark, DelCellCmd::Cols, true);

    // Auto-filter range shrunk to A:B
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);
    ScRange aArea;
    pDBData->GetArea(aArea);
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:B1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    // Undo the column delete - column B re-inserted
    m_pDoc->GetUndoManager()->Undo();

    // Back to A:C
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);
    pDBData->GetArea(aArea);
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(2), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:C1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(4, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    // Redo the column delete
    m_pDoc->GetUndoManager()->Redo();

    // Back to A:B
    pDBData = m_pDoc->GetAnonymousDBData(0);
    CPPUNIT_ASSERT(pDBData);
    pDBData->GetArea(aArea);
    CPPUNIT_ASSERT_EQUAL(SCCOL(0), aArea.aStart.Col());
    CPPUNIT_ASSERT_EQUAL(SCCOL(1), aArea.aEnd.Col());

    // Check ScMF::Auto flags (A1:B1)
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(0, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(bool(m_pDoc->GetAttr(1, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(2, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(3, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));
    CPPUNIT_ASSERT(!bool(m_pDoc->GetAttr(4, 0, 0, ATTR_MERGE_FLAG).GetValue() & ScMF::Auto));

    m_pDoc->DeleteTab(0);
}

CPPUNIT_TEST_FIXTURE(Test, testCopyTabContent)
{
    m_pDoc->InsertTab(0, u"Tab0"_ustr);
    m_pDoc->InsertTab(1, u"Tab1"_ustr);
    m_pDoc->InsertTab(2, u"Tab2"_ustr);
    m_pDoc->InsertTab(3, u"Tab3"_ustr);

    // For cross-sheet formula checking (forward and backward)
    m_pDoc->SetValue(ScAddress(0, 0, 0), 99.0); // Tab0.A1
    m_pDoc->SetValue(ScAddress(0, 0, 3), 66.0); // Tab3.A1

    std::vector<std::vector<OUString>> aData = {
        { u"Column 1"_ustr, u"Column 2"_ustr, u"Column 3"_ustr },
        {  u"7"_ustr,       u"A"_ustr,        u"=A2+5"_ustr },
        {  u"=2+2"_ustr,    u"C"_ustr,        u"=Tab3.A1"_ustr }, // forward cross-tab reference
        {  u"=A3-2"_ustr,   u"D"_ustr,        u"=SUM(A2:A6)"_ustr },
        {  u"3"_ustr,       u"B"_ustr,        u"=C3-60"_ustr },
        {  u"5"_ustr,       u"E"_ustr,        u"=Tab0.A1"_ustr }, // backward cross-tab reference
    };

    for (size_t i = 0; i < aData.size(); ++i)
    {
        for (size_t j = 0; j < aData[0].size(); ++j)
        {
            if (!aData[i][j].isEmpty())
            {
                m_pDoc->SetString(j, i, 1, aData[i][j]);
            }
        }
    }
    m_pDoc->CalcAll();

    // Verify Tab1 values and formulas before copy
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 1))); // =A2+5=12
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1))); // =Tab3.A1
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 1))); // =SUM(A2:A6)
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));  // =C3-60
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1))); // =Tab0.A1
    }

    // Copy Tab1 to index 2 - Tab1_2
    m_pDoc->CopyTab(1, 2);
    CPPUNIT_ASSERT_EQUAL(static_cast<SCTAB>(5), m_pDoc->GetTableCount());

    // Tab3 value shifted from index 3 to index 4
    CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(0, 0, 4)));
    // Tab0 value stays at index 0
    CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(0, 0, 0)));

    // Tab1 (tab index 1) should be unchanged
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1)));
    }

    // The copy Tab1_2 (tab index 2) should have the same data as Tab1
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 2)));
    }

    // Check the formulas in Tab1_2
    {
        CPPUNIT_ASSERT_EQUAL(u"=2+2"_ustr, m_pDoc->GetFormula(0, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A3-2"_ustr, m_pDoc->GetFormula(0, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=A2+5"_ustr, m_pDoc->GetFormula(2, 1, 2));
        CPPUNIT_ASSERT_EQUAL(u"='Tab3'.A1"_ustr, m_pDoc->GetFormula(2, 2, 2));
        CPPUNIT_ASSERT_EQUAL(u"=SUM(A2:A6)"_ustr, m_pDoc->GetFormula(2, 3, 2));
        CPPUNIT_ASSERT_EQUAL(u"=C3-60"_ustr, m_pDoc->GetFormula(2, 4, 2));
        CPPUNIT_ASSERT_EQUAL(u"=Tab0.A1"_ustr, m_pDoc->GetFormula(2, 5, 2));
    }

    // Change Tab1 and verify the copy is independent
    m_pDoc->SetValue(ScAddress(0, 1, 1), 10.0); // 7 -> 10
    m_pDoc->CalcAll();

    // Tab1 should be changed
    {
        CPPUNIT_ASSERT_EQUAL(10.0, m_pDoc->GetValue(ScAddress(0, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 1)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 1)));

        CPPUNIT_ASSERT_EQUAL(15.0, m_pDoc->GetValue(ScAddress(2, 1, 1)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 1)));
        CPPUNIT_ASSERT_EQUAL(24.0, m_pDoc->GetValue(ScAddress(2, 3, 1))); // =SUM(A2:A6)=10+4+2+3+5
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 1)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 1)));
    }

    // No change in Tab1_2
    {
        CPPUNIT_ASSERT_EQUAL(7.0, m_pDoc->GetValue(ScAddress(0, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(4.0, m_pDoc->GetValue(ScAddress(0, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(2.0, m_pDoc->GetValue(ScAddress(0, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(3.0, m_pDoc->GetValue(ScAddress(0, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(5.0, m_pDoc->GetValue(ScAddress(0, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(u"A"_ustr, m_pDoc->GetString(ScAddress(1, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(u"C"_ustr, m_pDoc->GetString(ScAddress(1, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(u"D"_ustr, m_pDoc->GetString(ScAddress(1, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(u"B"_ustr, m_pDoc->GetString(ScAddress(1, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(u"E"_ustr, m_pDoc->GetString(ScAddress(1, 5, 2)));

        CPPUNIT_ASSERT_EQUAL(12.0, m_pDoc->GetValue(ScAddress(2, 1, 2)));
        CPPUNIT_ASSERT_EQUAL(66.0, m_pDoc->GetValue(ScAddress(2, 2, 2)));
        CPPUNIT_ASSERT_EQUAL(21.0, m_pDoc->GetValue(ScAddress(2, 3, 2)));
        CPPUNIT_ASSERT_EQUAL(6.0, m_pDoc->GetValue(ScAddress(2, 4, 2)));
        CPPUNIT_ASSERT_EQUAL(99.0, m_pDoc->GetValue(ScAddress(2, 5, 2)));
    }

    m_pDoc->DeleteTab(4);
    m_pDoc->DeleteTab(3);
    m_pDoc->DeleteTab(2);
    m_pDoc->DeleteTab(1);
    m_pDoc->DeleteTab(0);
}

CPPUNIT_PLUGIN_IMPLEMENT();

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