/* -*- 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 <swmodeltestbase.hxx>

#include <com/sun/star/style/PageStyleLayout.hpp>

#include <vcl/gdimtf.hxx>
#include <svx/svdpage.hxx>
#include <o3tl/string_view.hxx>
#include <sfx2/linkmgr.hxx>
#include <sfx2/viewfrm.hxx>
#include <sfx2/dispatch.hxx>
#include <editeng/frmdiritem.hxx>

#include <wrtsh.hxx>
#include <docsh.hxx>
#include <unotxdoc.hxx>
#include <drawdoc.hxx>
#include <IDocumentDrawModelAccess.hxx>
#include <IDocumentLinksAdministration.hxx>
#include <IDocumentState.hxx>
#include <IDocumentLayoutAccess.hxx>
#include <rootfrm.hxx>
#include <itabenum.hxx>
#include <fmtanchr.hxx>
#include <fmtsrnd.hxx>
#include <IDocumentSettingAccess.hxx>
#include <fmtfsize.hxx>
#include <fmtfollowtextflow.hxx>
#include <view.hxx>
#include <cmdid.h>
#include <node.hxx>
#include <ndtxt.hxx>
#include <tabfrm.hxx>
#include <cntfrm.hxx>

using namespace css;
using namespace css::uno;

/// Covers sw/source/core/layout/ fixes.
class SwCoreLayoutTest : public SwModelTestBase
{
public:
    SwCoreLayoutTest()
        : SwModelTestBase(u"/sw/qa/core/layout/data/"_ustr)
    {
    }
};

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTableFlyOverlap)
{
    // Load a document that has an image anchored in the header.
    // It also has a table which has the wrap around the image.
    createSwDoc("table-fly-overlap.docx");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    SwTwips nFlyTop = getXPath(pXmlDoc, "//header/txt/anchored/fly/infos/bounds", "top").toInt32();
    SwTwips nFlyHeight
        = getXPath(pXmlDoc, "//header/txt/anchored/fly/infos/bounds", "height").toInt32();
    SwTwips nFlyBottom = nFlyTop + nFlyHeight;
    SwTwips nTableFrameTop = getXPath(pXmlDoc, "//tab/infos/bounds", "top").toInt32();
    SwTwips nTablePrintTop = getXPath(pXmlDoc, "//tab/infos/prtBounds", "top").toInt32();
    SwTwips nTableTop = nTableFrameTop + nTablePrintTop;
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater or equal than: 3579
    // - Actual  : 2210
    // i.e. the table's top border overlapped with the image, even if the image's wrap mode was set
    // to parallel.
    CPPUNIT_ASSERT_GREATEREQUAL(nFlyBottom, nTableTop);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTdf128195)
{
    // Load a document that has two paragraphs in the header.
    // The second paragraph should have its bottom spacing applied.
    createSwDoc("tdf128195.docx");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    sal_Int32 nTxtHeight = getXPath(pXmlDoc, "//header/txt[2]/infos/bounds", "height").toInt32();
    sal_Int32 nTxtBottom = getXPath(pXmlDoc, "//header/txt[2]/infos/bounds", "bottom").toInt32();
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(2269), nTxtHeight);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(3529), nTxtBottom);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testBIRT)
{
    // this looped
    createSwDoc("birt_min.odt");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testBorderCollapseCompat)
{
    // Load a document with a border conflict: top cell has a dotted bottom border, bottom cell has
    // a solid upper border.
    createSwDoc("border-collapse-compat.docx");
    SwDocShell* pShell = getSwDocShell();
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);

    // Make sure the solid border has priority.
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 48
    // i.e. there was no single cell border with width=20, rather there were 48 border parts
    // (forming a dotted border), all with width=40.
    assertXPath(pXmlDoc, "//polyline[@style='solid']", "width", u"20");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testBtlrTableRowSpan)
{
    // Load a document which has a table. The A1 cell has btlr text direction, and the A1..A3 cells
    // are merged.
    createSwDoc("btlr-table-row-span.odt");
    SwDocShell* pShell = getSwDocShell();
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);

    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: USA
    // - Actual  : West
    // i.e. the "USA" text completely disappeared.
    assertXPathContent(pXmlDoc, "//textarray[1]/text", u"USA");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTableFlyOverlapSpacing)
{
    // Load a document that has an image on the right of a table.  The table wraps around the image.
    createSwDoc("table-fly-overlap-spacing.docx");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    SwTwips nFlyTop = getXPath(pXmlDoc, "//body/txt/anchored/fly/infos/bounds", "top").toInt32();
    SwTwips nFlyHeight
        = getXPath(pXmlDoc, "//body/txt/anchored/fly/infos/bounds", "height").toInt32();
    SwTwips nFlyBottom = nFlyTop + nFlyHeight;
    SwTwips nTableFrameTop = getXPath(pXmlDoc, "//tab/infos/bounds", "top").toInt32();
    SwTwips nTablePrintTop = getXPath(pXmlDoc, "//tab/infos/prtBounds", "top").toInt32();
    SwTwips nTableTop = nTableFrameTop + nTablePrintTop;
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater or equal than: 3993
    // - Actual  : 3993
    // i.e. the table was below the image, not on the left of the image.
    CPPUNIT_ASSERT_LESS(nFlyBottom, nTableTop);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTablesMoveBackwards)
{
    // Load a document with 1 pages: empty content on first page, then 21 tables on the second page.
    createSwDoc("tables-move-backwards.odt");
    SwDocShell* pDocShell = getSwDocShell();
    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();

    // Delete the content on the first page.
    pWrtShell->SttEndDoc(/*bStart=*/true);
    pWrtShell->EndPg(/*bSelect=*/true);
    pWrtShell->DelLeft();

    // Calc the layout and check the number of pages.
    calcLayout();
    xmlDocUniquePtr pLayout = parseLayoutDump();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 2
    // i.e. there was an unexpected 2nd page, as only 20 out of 21 tables were moved to the first
    // page.
    assertXPath(pLayout, "//page", 1);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testContinuousEndnotesMoveBackwards)
{
    // Load a document with the ContinuousEndnotes flag turned on.
    createSwDoc("continuous-endnotes-move-backwards.doc");
    xmlDocUniquePtr pLayout = parseLayoutDump();
    // We have 2 pages.
    assertXPath(pLayout, "/root/page", 2);
    // No endnote container on page 1.
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 0
    // - Actual  : 1
    // i.e. there were unexpected endnotes on page 1.
    assertXPath(pLayout, "/root/page[1]//ftncont", 0);
    // All endnotes are in a container on page 2.
    assertXPath(pLayout, "/root/page[2]//ftncont", 1);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testAnchorPositionBasedOnParagraph)
{
    // tdf#134783 check whether position of shape is good if it is anchored to paragraph and
    // the "Don't add space between paragraphs of the same style" option is set
    createSwDoc("tdf134783_testAnchorPositionBasedOnParagraph.fodt");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    CPPUNIT_ASSERT(pXmlDoc);
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[1]/bounds", "top", u"1671");
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[1]/bounds", "bottom", u"1732");
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[2]/bounds", "top", u"1947");
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[2]/bounds", "bottom", u"2008");
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[3]/bounds", "top", u"3783");
    assertXPath(pXmlDoc, "(//anchored/SwAnchoredDrawObject)[3]/bounds", "bottom", u"3844");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTextBoxStaysInsideShape)
{
    // tdf#135198: check whether text box stays inside shape after moving it upwards
    createSwDoc("shape-textbox.odt");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    CPPUNIT_ASSERT(pXmlDoc);

    // Without the fix in place, this test would have failed with
    // - Expected: 1932
    // - Actual  : 7476
    assertXPath(pXmlDoc, "//anchored/fly/infos/bounds", "top", u"1932");
    assertXPath(pXmlDoc, "//anchored/fly/infos/bounds", "bottom", u"7184");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTextBoxNotModifiedOnOpen)
{
    // tdf#138050: a freshly opened document containing a shape with a text box
    // should not appear to be modified
    createSwDoc("textbox-phantom-change.docx");
    SwDoc* pDoc = getSwDoc();

    // Without the fix in place this test would have shown that the document
    // was modified due to a fix to tdf#135198
    CPPUNIT_ASSERT(!pDoc->getIDocumentState().IsModified());
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTextBoxAutoGrowVertical)
{
    createSwDoc("textbox-autogrow-vertical.docx");
    SwDoc* pDoc = getSwDoc();
    SdrPage* pPage = pDoc->getIDocumentDrawModelAccess().GetDrawModel()->GetPage(0);
    SdrObject* pShape = pPage->GetObj(0);
    tools::Rectangle aShapeRect = pShape->GetCurrentBoundRect();

    xmlDocUniquePtr pLayout = parseLayoutDump();
    CPPUNIT_ASSERT(pLayout);
    sal_Int32 nFlyLeft = getXPath(pLayout, "//anchored/fly/infos/bounds", "left").toInt32();
    sal_Int32 nFlyTop = getXPath(pLayout, "//anchored/fly/infos/bounds", "top").toInt32();
    sal_Int32 nFlyRight = getXPath(pLayout, "//anchored/fly/infos/bounds", "right").toInt32();
    sal_Int32 nFlyBottom = getXPath(pLayout, "//anchored/fly/infos/bounds", "bottom").toInt32();
    tools::Rectangle aFlyRect(nFlyLeft, nFlyTop, nFlyRight, nFlyBottom);
    // Without the accompanying fix in place, this test would have failed, as aFlyRect was too wide,
    // so it was not inside aShapeRect anymore.
    CPPUNIT_ASSERT(aShapeRect.Contains(aFlyRect));
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTextboxModification)
{
    // Load a document with a textbox in it: the layout will have to position the shape part.
    createSwDoc("textbox-modification.docx");
    SwDocShell* pDocShell = getSwDocShell();

    // Without the accompanying fix in place, this test would have failed, as the document was
    // marked as modified right after the import.
    CPPUNIT_ASSERT(!pDocShell->IsModified());
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTextBoxInHeaderIsPositioned)
{
    // Load a document with a floating text box in the header
    createSwDoc("header-textbox.docx");
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    CPPUNIT_ASSERT(pXmlDoc);

    // Without the fix in place, this test would have failed with
    // - Expected: 8051
    // - Actual  : 1418
    // Comparison with 7000 chosen due to variability between devices
    CPPUNIT_ASSERT_GREATEREQUAL(
        double(7000), getXPath(pXmlDoc, "//anchored/fly/infos/bounds", "left").toDouble());
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testBtlrNestedCell)
{
    // Load a document with a nested table, the inner A1 cell has a btlr text direction.
    createSwDoc("btlr-nested-cell.odt");
    SwDoc* pDoc = getSwDoc();
    SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout();
    SwFrame* pPage = pLayout->GetLower();
    SwFrame* pBody = pPage->GetLower();
    SwFrame* pOuterTable = pBody->GetLower()->GetNext();
    SwFrame* pInnerTable = pOuterTable->GetLower()->GetLower()->GetLower();

    // Check the paint area of the only text frame in the cell.
    SwFrame* pTextFrame = pInnerTable->GetLower()->GetLower()->GetLower();
    tools::Long nFrameBottom = pTextFrame->getFrameArea().Bottom();
    SwRect aPaintArea = pTextFrame->GetPaintArea();

    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater or equal than: 2829
    // - Actual  : 2080
    // i.e. part of the text frame area was not painted, hiding the actual text.
    CPPUNIT_ASSERT_GREATEREQUAL(nFrameBottom, aPaintArea.Bottom());
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testKeepwithnextFullheight)
{
    // The document has a heading (keep with next) and a full-page image in the next paragraph, i.e.
    // conflicting requirements.
    // Without the accompanying fix in place, this test would have failed with a layout loop in
    // SwEditShell::CalcLayout().
    createSwDoc("keepwithnext-fullheight.fodt");

    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    CPPUNIT_ASSERT(pXmlDoc);
    // Make sure the document has 2 pages.
    assertXPath(pXmlDoc, "//page", 2);
    // Heading stays on page 1 to avoid a layout loop.
    assertXPathContent(pXmlDoc, "//page[1]/body/txt[2]", u"Heading");
    // Image stays on page 2.
    assertXPath(pXmlDoc, "//page[2]/body/txt/anchored/fly", 1);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testGutterMargin)
{
    // Create a document, remember the old left edge of the page print area (the rectangle that is
    // inside margins).
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    uno::Reference<beans::XPropertySet> xStandard(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout();
    SwFrame* pPage = pLayout->GetLower();
    tools::Long nOldLeft = pPage->getFramePrintArea().Left();

    // Set the gutter margin to 2cm.
    sal_Int32 nGutterMm100 = 2000;
    xStandard->setPropertyValue(u"GutterMargin"_ustr, cpo::uno::Any(nGutterMm100));

    // Verify that the new left edge is larger.
    tools::Long nNewLeft = pPage->getFramePrintArea().Left();
    tools::Long nGutterTwips = o3tl::toTwips(nGutterMm100, o3tl::Length::mm100);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1134
    // - Actual  : 0
    // i.e. the gutter was not added to the left margin.
    CPPUNIT_ASSERT_EQUAL(nGutterTwips, nNewLeft - nOldLeft);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testGutterTopMargin)
{
    // Create a document, remember the old top edge of the page print area (the rectangle that is
    // inside margins).
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    uno::Reference<lang::XMultiServiceFactory> xFactory(mxComponent, uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xSettings(
        xFactory->createInstance(u"com.sun.star.document.Settings"_ustr), uno::UNO_QUERY);
    xSettings->setPropertyValue(u"GutterAtTop"_ustr, cpo::uno::Any(true));
    uno::Reference<beans::XPropertySet> xStandard(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout();
    SwFrame* pPage = pLayout->GetLower();
    tools::Long nOldTop = pPage->getFramePrintArea().Top();

    // Set the gutter margin to 2cm.
    sal_Int32 nGutterMm100 = 2000;
    xStandard->setPropertyValue(u"GutterMargin"_ustr, cpo::uno::Any(nGutterMm100));

    // Verify that the new top edge is larger.
    tools::Long nNewTop = pPage->getFramePrintArea().Top();
    tools::Long nGutterTwips = o3tl::toTwips(nGutterMm100, o3tl::Length::mm100);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1134
    // - Actual  : 0
    // i.e. the gutter was not added to the left margin.
    CPPUNIT_ASSERT_EQUAL(nGutterTwips, nNewTop - nOldTop);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testGutterMirrorMargin)
{
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    SwDocShell* pDocShell = getSwDocShell();
    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
    pWrtShell->InsertPageBreak();
    SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout();
    SwFrame* pPage = pLayout->GetLower();
    tools::Long nOldLeft = pPage->getFramePrintArea().Left();
    SwFrame* pPage2 = pPage->GetNext();
    tools::Long nOldRight = pPage2->getFramePrintArea().Right();

    uno::Reference<beans::XPropertySet> xStandard(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    xStandard->setPropertyValue(u"PageStyleLayout"_ustr,
                                cpo::uno::Any(style::PageStyleLayout_MIRRORED));
    sal_Int32 nGutterMm100 = 2000;
    xStandard->setPropertyValue(u"GutterMargin"_ustr, cpo::uno::Any(nGutterMm100));

    tools::Long nNewLeft = pPage->getFramePrintArea().Left();
    tools::Long nGutterTwips = o3tl::toTwips(nGutterMm100, o3tl::Length::mm100);
    CPPUNIT_ASSERT_EQUAL(nGutterTwips, nNewLeft - nOldLeft);
    tools::Long nNewRight = pPage2->getFramePrintArea().Right();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1134
    // - Actual  : 0
    // i.e. the gutter was missing on the second, mirrored page.
    CPPUNIT_ASSERT_EQUAL(nGutterTwips, nOldRight - nNewRight);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testRtlGutterMargin)
{
    // Given a document with a right margin:
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    uno::Reference<beans::XPropertySet> xStandard(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    SwRootFrame* pLayout = pDoc->getIDocumentLayoutAccess().GetCurrentLayout();
    SwFrame* pPage = pLayout->GetLower();
    tools::Long nOldRight = pPage->getFramePrintArea().Right();

    // When setting enable RTL gutter mode and setting a gutter margin:
    xStandard->setPropertyValue(u"RtlGutter"_ustr, cpo::uno::Any(true));
    sal_Int32 nGutterMm100 = 2000;
    xStandard->setPropertyValue(u"GutterMargin"_ustr, cpo::uno::Any(nGutterMm100));

    // Then make sure the new right edge of the print area is decreased:
    tools::Long nNewRight = pPage->getFramePrintArea().Right();
    tools::Long nGutterTwips = o3tl::toTwips(nGutterMm100, o3tl::Length::mm100);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1134
    // - Actual  : 0
    // i.e. the gutter was missing on the right side.
    CPPUNIT_ASSERT_EQUAL(nGutterTwips, nOldRight - nNewRight);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testGutterMarginPageBorder)
{
// FIXME this is 3369 on macOS -- calculate this number dynamically?
// FIXME this is random on Windows at the moment (in two subsequent tests without any scaling,
//       the actual values were 6346, 10066) - something broke metafile generation on Windows?
#if !defined(MACOSX) && !defined(_WIN32)
    // Given a document with a non-0 gutter margin.
    createSwDoc();
    uno::Reference<beans::XPropertySet> xStandard(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    sal_Int32 nGutterMm100 = 2000;
    xStandard->setPropertyValue(u"GutterMargin"_ustr, cpo::uno::Any(nGutterMm100));

    // When setting a left border.
    table::BorderLine2 aBorder;
    aBorder.LineWidth = 2;
    aBorder.OuterLineWidth = 2;
    xStandard->setPropertyValue(u"LeftBorder"_ustr, cpo::uno::Any(aBorder));

    // Then make sure border is at the left edge of the text area.
    SwDocShell* pShell = getSwDocShell();
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2565
    // - Actual  : 1425
    // Where 2565 is close to the left edge of the text area (2553).
    assertXPath(pXmlDoc, "//polyline[@style='solid']/point[1]", "x", u"2565");
#endif
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testTdf45908_invoice)
{
    // without the fix, this was hanging (and slowly consuming memory) on fileopen.
    createSwDoc("tdf45908_invoice.odt");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testVerticallyMergedCellBorder)
{
    // Given a document with a table: 2 columns, 5 rows. B2 -> B5 is merged:
    createSwDoc("vmerge-cell-border.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering the table:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Make sure that B4->B5 has no borders.
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect vertical positions of all border points.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    std::vector<sal_Int32> aBorderPositions;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); ++i)
    {
        xmlNodePtr pXmlNode = pXmlNodes->nodeTab[i];
        xmlChar* pValue = xmlGetProp(pXmlNode, BAD_CAST("y"));
        sal_Int32 nValue = o3tl::toInt32(reinterpret_cast<char const*>(pValue));
        aBorderPositions.push_back(nValue);
    }
    xmlXPathFreeObject(pXmlObj);
    // Collect top and bottom of the B1->B3 rows.
    xmlDocUniquePtr pLayout = parseLayoutDump();
    pXmlObj = getXPathNode(pLayout, "//tab/row/infos/bounds");
    pXmlNodes = pXmlObj->nodesetval;
    std::vector<sal_Int32> aLayoutPositions;
    for (int i = 0; i < 3; ++i)
    {
        xmlNodePtr pXmlNode = pXmlNodes->nodeTab[i];
        if (i == 0)
        {
            xmlChar* pValue = xmlGetProp(pXmlNode, BAD_CAST("top"));
            sal_Int32 nValue = o3tl::toInt32(reinterpret_cast<char const*>(pValue));
            aLayoutPositions.push_back(nValue);
        }
        xmlChar* pValue = xmlGetProp(pXmlNode, BAD_CAST("bottom"));
        sal_Int32 nValue = o3tl::toInt32(reinterpret_cast<char const*>(pValue));
        aLayoutPositions.push_back(nValue);
    }
    xmlXPathFreeObject(pXmlObj);
    // Check if any border is outside the B1->B3 range.
    for (const auto nBorderPosition : aBorderPositions)
    {
        bool bFound = false;
        for (const auto nLayoutPosition : aLayoutPositions)
        {
            if (std::abs(nBorderPosition - nLayoutPosition) <= 30)
            {
                bFound = true;
                break;
            }
        }
        std::stringstream ss;
        ss << "Bad vertical position for border point: " << nBorderPosition;
        ss << " Expected positions: ";
        for (size_t i = 0; i < aLayoutPositions.size(); ++i)
        {
            if (i > 0)
            {
                ss << ", ";
            }
            ss << aLayoutPositions[i];
        }

        // Without the accompanying fix in place, this test would have failed with:
        // - Bad vertical position for border point: 5624 Expected positions: 3022, 3540, 4059, 4578
        // i.e. the middle vertical border end was the bottom of B5, not bottom of B3.
        CPPUNIT_ASSERT_MESSAGE(ss.str(), bFound);
    }
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testCrashRemoveFromLayout)
{
    createSwDoc("tdf122894-4.doc");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testLinkedBullet)
{
    // Given a document with a graphic bullet, where the image is a linked one:
    createSwDoc("linked-bullet.odt");
    SwDocShell* pShell = getSwDocShell();
    // allow link updates and trigger fetch so the linked bullet image
    // is loaded before rendering
    pShell->getEmbeddedObjectContainer().setUserAllowsLinkUpdate(true);
    sfx2::LinkManager& rLinkMgr = getSwDoc()->getIDocumentLinksAdministration().GetLinkManager();
    rLinkMgr.UpdateAllLinks(false, nullptr, u""_ustr);

    // When rendering that document:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure the render result contains exactly one bitmap:
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 0
    // i.e. the bullet's bitmap was lost.
    assertXPath(pXmlDoc, "//bmpexscale", 1);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testInnerCellBorderIntersect)
{
    // Given a table with both outer and inner borders:
    createSwDoc("inner-border.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering table borders:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure that inner and outer borders don't overlap in Word compatibility mode,
    // and inner borders are reduced to prevent an overlap:
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect start/end (vertical) positions of horizontal borders.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    std::vector<std::pair<sal_Int32, sal_Int32>> aBorderStartEnds;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartY = xmlGetProp(pStart, BAD_CAST("y"));
        xmlChar* pEndY = xmlGetProp(pEnd, BAD_CAST("y"));
        sal_Int32 nStartY = o3tl::toInt32(reinterpret_cast<char const*>(pStartY));
        sal_Int32 nEndY = o3tl::toInt32(reinterpret_cast<char const*>(pEndY));
        if (nStartY != nEndY)
        {
            // Vertical border.
            continue;
        }
        xmlChar* pStartX = xmlGetProp(pStart, BAD_CAST("x"));
        xmlChar* pEndX = xmlGetProp(pEnd, BAD_CAST("x"));
        sal_Int32 nStartX = o3tl::toInt32(reinterpret_cast<char const*>(pStartX));
        sal_Int32 nEndX = o3tl::toInt32(reinterpret_cast<char const*>(pEndX));
        aBorderStartEnds.emplace_back(nStartX, nEndX);
    }
    xmlXPathFreeObject(pXmlObj);
    // We have 3 lines: top, middle and bottom. The top and the bottom one is a full line, since
    // it's an outer border. The middle one has increased start and decreased end to avoid an
    // overlap.
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), aBorderStartEnds.size());
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].first, aBorderStartEnds[2].first);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater than: 1724
    // - Actual  : 1724
    // i.e. the middle line's start was the same as the top line start, while what we want is a
    // larger X position, so the start of the middle line doesn't overlap with the thick vertical
    // outer border on the left of the table.
    CPPUNIT_ASSERT_GREATER(aBorderStartEnds[0].first, aBorderStartEnds[1].first);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].second, aBorderStartEnds[2].second);
    CPPUNIT_ASSERT_LESS(aBorderStartEnds[0].second, aBorderStartEnds[1].second);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testInnerCellBorderNocompatIntersect)
{
    // Given a table with both outer and inner borders:
    createSwDoc();
    SwDocShell* pShell = getSwDocShell();
    SwDoc* pDoc = pShell->GetDoc();
    // Set the default page style's writing direction to vertical:
    SwPageDesc aStandard(pDoc->GetPageDesc(0));
    SvxFrameDirectionItem aDirection(SvxFrameDirection::Vertical_RL_TB, RES_FRAMEDIR);
    aStandard.GetMaster().SetFormatAttr(aDirection);
    pDoc->ChgPageDesc(0, aStandard);
    // Insert a 4x4 table:
    SwWrtShell* pWrtShell = pShell->GetWrtShell();
    SwInsertTableOptions aTableOptions(SwInsertTableFlags::DefaultBorder, 0);
    pWrtShell->InsertTable(aTableOptions, 4, 4);

    // When rendering table borders:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure that all horizontal lines have the same length:
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect start/end (vertical) positions of horizontal borders.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    std::vector<std::pair<sal_Int32, sal_Int32>> aBorderStartEnds;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartY = xmlGetProp(pStart, BAD_CAST("y"));
        xmlChar* pEndY = xmlGetProp(pEnd, BAD_CAST("y"));
        sal_Int32 nStartY = o3tl::toInt32(reinterpret_cast<char const*>(pStartY));
        sal_Int32 nEndY = o3tl::toInt32(reinterpret_cast<char const*>(pEndY));
        if (nStartY != nEndY)
        {
            // Vertical border.
            continue;
        }
        xmlChar* pStartX = xmlGetProp(pStart, BAD_CAST("x"));
        xmlChar* pEndX = xmlGetProp(pEnd, BAD_CAST("x"));
        sal_Int32 nStartX = o3tl::toInt32(reinterpret_cast<char const*>(pStartX));
        sal_Int32 nEndX = o3tl::toInt32(reinterpret_cast<char const*>(pEndX));
        aBorderStartEnds.emplace_back(nStartX, nEndX);
    }
    xmlXPathFreeObject(pXmlObj);
    // We have 5 lines: top of 4 cells + bottom.
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(5), aBorderStartEnds.size());
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 9719
    // - Actual  : 10064
    // i.e. the 2nd line started later than the first one, which is incorrect.
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].first, aBorderStartEnds[1].first);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].first, aBorderStartEnds[2].first);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].first, aBorderStartEnds[3].first);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].first, aBorderStartEnds[4].first);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].second, aBorderStartEnds[1].second);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].second, aBorderStartEnds[2].second);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].second, aBorderStartEnds[3].second);
    CPPUNIT_ASSERT_EQUAL(aBorderStartEnds[0].second, aBorderStartEnds[4].second);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testDoubleBorderVertical)
{
    // Given a table with a left and right double border, outer is thick, inner is thin:
    createSwDoc("double-border-vertical.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering that document:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure the left border is thick+thin and the right border is thin+thick (from left to
    // right):
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect widths of vertical lines.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    // Horizontal position -> width.
    std::map<sal_Int32, sal_Int32> aBorderWidths;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartX = xmlGetProp(pStart, BAD_CAST("x"));
        xmlChar* pEndX = xmlGetProp(pEnd, BAD_CAST("x"));
        sal_Int32 nStartX = o3tl::toInt32(reinterpret_cast<char const*>(pStartX));
        sal_Int32 nEndX = o3tl::toInt32(reinterpret_cast<char const*>(pEndX));
        if (nStartX != nEndX)
        {
            // Horizontal border.
            continue;
        }
        xmlChar* pWidth = xmlGetProp(pStart->parent, BAD_CAST("width"));
        sal_Int32 nWidth = o3tl::toInt32(reinterpret_cast<char const*>(pWidth));
        aBorderWidths[nStartX] = nWidth;
    }
    xmlXPathFreeObject(pXmlObj);
    std::vector<sal_Int32> aBorderWidthVec;
    std::transform(aBorderWidths.begin(), aBorderWidths.end(), std::back_inserter(aBorderWidthVec),
                   [](const std::pair<sal_Int32, sal_Int32>& rPair) { return rPair.second; });
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(4), aBorderWidthVec.size());
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater than: 120
    // - Actual  : 60
    // i.e. the left border was thin+thick, not thick+thin.
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[1], aBorderWidthVec[0]);
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[2], aBorderWidthVec[3]);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testDoubleBorderHorizontal)
{
    // Given a table with a top and bottom double border, outer is thin, inner is thick:
    createSwDoc("double-border-horizontal.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering table borders:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure the top border is thin+thick and the bottom border is thick+thin (from top to
    // bottom):
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect widths of horizontal lines.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    // Vertical position -> width.
    std::map<sal_Int32, sal_Int32> aBorderWidths;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartY = xmlGetProp(pStart, BAD_CAST("y"));
        xmlChar* pEndY = xmlGetProp(pEnd, BAD_CAST("y"));
        sal_Int32 nStartY = o3tl::toInt32(reinterpret_cast<char const*>(pStartY));
        sal_Int32 nEndY = o3tl::toInt32(reinterpret_cast<char const*>(pEndY));
        if (nStartY != nEndY)
        {
            // Vertical border.
            continue;
        }
        xmlChar* pWidth = xmlGetProp(pStart->parent, BAD_CAST("width"));
        sal_Int32 nWidth = o3tl::toInt32(reinterpret_cast<char const*>(pWidth));
        aBorderWidths[nStartY] = nWidth;
    }
    xmlXPathFreeObject(pXmlObj);
    std::vector<sal_Int32> aBorderWidthVec;
    std::transform(aBorderWidths.begin(), aBorderWidths.end(), std::back_inserter(aBorderWidthVec),
                   [](const std::pair<sal_Int32, sal_Int32>& rPair) { return rPair.second; });
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(4), aBorderWidthVec.size());
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[0], aBorderWidthVec[1]);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater than: 120
    // - Actual  : 60
    // i.e. the bottom border was thin+thick, not thick+thin.
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[3], aBorderWidthVec[2]);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testParaBorderInCellClip)
{
    // Given a document which has outside-cell borders defined, which should not be visible:
    createSwDoc("para-border-in-cell-clip.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering those borders:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure that we have clipping setup for both paragraphs inside the table cell:
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2
    // - Actual  : 0
    // - XPath '//clipregion/polygon' number of nodes is incorrect
    // i.e. there was no clipping applied, leading to unexpected left/right borders.
    assertXPath(pXmlDoc, "//clipregion/polygon", 2);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testDoublePageBorder)
{
    // Given a page with a top and bottom double border, outer is thick, inner is thin:
    createSwDoc("double-page-border.docx");
    SwDocShell* pShell = getSwDocShell();

    // When rendering that document:
    std::shared_ptr<GDIMetaFile> xMetaFile = pShell->GetPreviewMetaFile();

    // Then make sure the top border is thick+thing and the bottom border is thin+thick (from top to
    // bottom):
    MetafileXmlDump dumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(dumper, *xMetaFile);
    // Collect widths of horizontal lines.
    xmlXPathObjectPtr pXmlObj = getXPathNode(pXmlDoc, "//polyline[@style='solid']/point");
    xmlNodeSetPtr pXmlNodes = pXmlObj->nodesetval;
    // Vertical position -> width.
    std::map<sal_Int32, sal_Int32> aBorderWidths;
    for (int i = 0; i < xmlXPathNodeSetGetLength(pXmlNodes); i += 2)
    {
        xmlNodePtr pStart = pXmlNodes->nodeTab[i];
        xmlNodePtr pEnd = pXmlNodes->nodeTab[i + 1];
        xmlChar* pStartY = xmlGetProp(pStart, BAD_CAST("y"));
        xmlChar* pEndY = xmlGetProp(pEnd, BAD_CAST("y"));
        sal_Int32 nStartY = o3tl::toInt32(reinterpret_cast<char const*>(pStartY));
        sal_Int32 nEndY = o3tl::toInt32(reinterpret_cast<char const*>(pEndY));
        if (nStartY != nEndY)
        {
            // Vertical border.
            continue;
        }
        xmlChar* pWidth = xmlGetProp(pStart->parent, BAD_CAST("width"));
        sal_Int32 nWidth = o3tl::toInt32(reinterpret_cast<char const*>(pWidth));
        aBorderWidths[nStartY] = nWidth;
    }
    xmlXPathFreeObject(pXmlObj);
    std::vector<sal_Int32> aBorderWidthVec;
    std::transform(aBorderWidths.begin(), aBorderWidths.end(), std::back_inserter(aBorderWidthVec),
                   [](const std::pair<sal_Int32, sal_Int32>& rPair) { return rPair.second; });
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(4), aBorderWidthVec.size());
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[1], aBorderWidthVec[0]);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater than: 60
    // - Actual  : 15
    // i.e. the bottom border was thick+thin, not thin+thick.
    CPPUNIT_ASSERT_GREATER(aBorderWidthVec[2], aBorderWidthVec[3]);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testNegativePageBorder)
{
    // FIXME: the DPI check should be removed when either (1) the test is fixed to work with
    // non-default DPI; or (2) unit tests on Windows are made to use svp VCL plugin.
    if (!IsDefaultDPI())
        return;

    // Given a document with a top margin and a negative border distance:
    createSwDoc();
    SwDocShell* pDocShell = getSwDocShell();
    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
    pWrtShell->Insert(u"test"_ustr);
    uno::Reference<beans::XPropertySet> xPageStyle(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    xPageStyle->setPropertyValue(u"TopMargin"_ustr,
                                 cpo::uno::Any(static_cast<sal_Int32>(501))); // 284 twips
    table::BorderLine2 aBorder;
    aBorder.LineWidth = 159; // 90 twips
    aBorder.OuterLineWidth = 159;
    xPageStyle->setPropertyValue(u"TopBorder"_ustr, cpo::uno::Any(aBorder));
    sal_Int32 nTopBorderDistance = -646; // -366 twips
    xPageStyle->setPropertyValue(u"TopBorderDistance"_ustr, cpo::uno::Any(nTopBorderDistance));
    nTopBorderDistance = 0;
    xPageStyle->getPropertyValue(u"TopBorderDistance"_ustr) >>= nTopBorderDistance;
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(-646), nTopBorderDistance);

    // When rendering that border:
    std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile();

    // Then make sure that the negative distance pushes the horizontal borderline down:
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 899
    // - Actual  : 524
    // i.e. the negative border distance was rounded up to 0 in lcl_CalcBorderRect().
    // Ideally this would be 284 (top of first page) + 284 (top margin) + 366 (border distance) =
    // 934.
    assertXPath(pXmlDoc, "//polyline[@style='solid']/point[1]", "y", u"899");
    assertXPath(pXmlDoc, "//polyline[@style='solid']/point[2]", "y", u"899");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testNegativePageBorderNoMargin)
{
    // FIXME: the DPI check should be removed when either (1) the test is fixed to work with
    // non-default DPI; or (2) unit tests on Windows are made to use svp VCL plugin.
    if (!IsDefaultDPI())
        return;

    // Given a document with no top margin and a negative border distance:
    createSwDoc();
    SwDocShell* pDocShell = getSwDocShell();
    SwWrtShell* pWrtShell = pDocShell->GetWrtShell();
    pWrtShell->Insert(u"test"_ustr);
    uno::Reference<beans::XPropertySet> xPageStyle(
        getStyles(u"PageStyles"_ustr)->getByName(u"Standard"_ustr), uno::UNO_QUERY);
    xPageStyle->setPropertyValue(u"TopMargin"_ustr,
                                 cpo::uno::Any(static_cast<sal_Int32>(0))); // 0 twips
    table::BorderLine2 aBorder;
    aBorder.LineWidth = 159; // 90 twips
    aBorder.OuterLineWidth = 159;
    xPageStyle->setPropertyValue(u"TopBorder"_ustr, cpo::uno::Any(aBorder));
    sal_Int32 nTopBorderDistance = -1147; // -650 twips
    xPageStyle->setPropertyValue(u"TopBorderDistance"_ustr, cpo::uno::Any(nTopBorderDistance));

    // When rendering that border:
    std::shared_ptr<GDIMetaFile> xMetaFile = pDocShell->GetPreviewMetaFile();

    // Then make sure that the negative distance pushes the horizontal borderline down:
    MetafileXmlDump aDumper;
    xmlDocUniquePtr pXmlDoc = dumpAndParse(aDumper, *xMetaFile);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 899
    // - Actual  : 329
    // i.e. this failed differently: the lack of top margin caused a second problem.
    // Ideally this would be 284 (top of first page) + 650 (border distance) =
    // 934.
    assertXPath(pXmlDoc, "//polyline[@style='solid']/point[1]", "y", u"899");
    assertXPath(pXmlDoc, "//polyline[@style='solid']/point[2]", "y", u"899");
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testFollowTextFlowWrapInBackground)
{
    // Given a document with a table, and a graphic inside that table -- anchored, wrap set to
    // through and follow-text-flow set to true:
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    pDoc->getIDocumentSettingAccess().set(DocumentSettingId::USE_FORMER_TEXT_WRAPPING, true);
    SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell();
    SwInsertTableOptions aTableOptions(SwInsertTableFlags::DefaultBorder, 0);
    pWrtShell->InsertTable(aTableOptions, 1, 1);
    pWrtShell->MoveTable(GotoPrevTable, fnTableStart);
    SfxItemSet aFrameSet(pDoc->GetAttrPool(), svl::Items<RES_FRMATR_BEGIN, RES_FRMATR_END - 1>);
    SwFormatAnchor aAnchor(RndStdIds::FLY_AT_CHAR);
    aFrameSet.Put(aAnchor);
    SwFormatSurround aSurround(text::WrapTextMode_THROUGH);
    aFrameSet.Put(aSurround);
    SwFormatFrameSize aSize(SwFrameSize::Fixed, 1000, 1000);
    aFrameSet.Put(aSize);
    SwFormatFollowTextFlow aFlow(true);
    aFrameSet.Put(aFlow);
    Graphic aGrf;

    // When inserting that image:
    pWrtShell->SwFEShell::Insert(OUString(), OUString(), &aGrf, &aFrameSet);

    // Then make sure that the cell height grows to have space for the graphic, given that
    // background=true is not specified.
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    sal_Int32 nCellHeight = getXPath(pXmlDoc, "//cell[1]/infos/bounds", "height").toInt32();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected greater than: 1000
    // - Actual  : 396
    // i.e. the image was larger than the containing cell.
    CPPUNIT_ASSERT_GREATER(static_cast<sal_Int32>(1000), nCellHeight);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testPageRemoveFlyTable)
{
    // Given a document with a ToC and several tables, one table marked with a bookmark:
    createSwDoc("page-remove-fly-table.odt");

    // When updating the ToC and incrementally formatting the document:
    SwView* pView = getSwDocShell()->GetView();
    SfxDispatcher& rDispatcher = *pView->GetViewFrame().GetDispatcher();
    rDispatcher.Execute(FN_UPDATE_TOX);
    SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell();
    pWrtShell->Reformat();

    // Then make sure that the 2nd table below the bookmark has no unwanted top margin:
    pWrtShell->GotoMark(SwMarkName(u"test"_ustr));
    pWrtShell->Down(/*bSelect=*/false, /*nCount=*/1, /*bBasicCall=*/false);
    pWrtShell->Down(/*bSelect=*/false, /*nCount=*/1, /*bBasicCall=*/false);
    SwCursor* pCursor = pWrtShell->GetCursor();
    SwTextNode* pTextNode = pCursor->GetPoint()->GetNode().GetTextNode();
    SwFrame* pTextFrame = pTextNode->getLayoutFrame(nullptr);
    SwTabFrame* pInnerTable = pTextFrame->FindTabFrame();
    SwTabFrame* pOuterTable = pInnerTable->GetUpper()->FindTabFrame();
    tools::Long nActual = pOuterTable->getFramePrintArea().Top();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 0
    // - Actual  : 5879
    // i.e. the problematic table had a large, unwanted/leftover top margin.
    CPPUNIT_ASSERT_EQUAL(static_cast<tools::Long>(0), nActual);
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testPageRemoveFlyNoTable)
{
    createSwDoc("page-remove-fly-no-table.fodt");
    // This never returned.
    calcLayout();
}

CPPUNIT_TEST_FIXTURE(SwCoreLayoutTest, testNewFollowTextFlowWrapInBackground)
{
    // Given a document with a table, and a graphic inside that table -- anchored, wrap set to
    // through and follow-text-flow set to true, legacy USE_FORMER_TEXT_WRAPPING is not set:
    createSwDoc();
    SwDoc* pDoc = getSwDoc();
    SwWrtShell* pWrtShell = getSwDocShell()->GetWrtShell();
    SwInsertTableOptions aTableOptions(SwInsertTableFlags::DefaultBorder, 0);
    pWrtShell->InsertTable(aTableOptions, 1, 1);
    pWrtShell->MoveTable(GotoPrevTable, fnTableStart);
    SfxItemSet aFrameSet(pDoc->GetAttrPool(), svl::Items<RES_FRMATR_BEGIN, RES_FRMATR_END - 1>);
    SwFormatAnchor aAnchor(RndStdIds::FLY_AT_CHAR);
    aFrameSet.Put(aAnchor);
    SwFormatSurround aSurround(text::WrapTextMode_THROUGH);
    aFrameSet.Put(aSurround);
    SwFormatFrameSize aSize(SwFrameSize::Fixed, 1000, 1000);
    aFrameSet.Put(aSize);
    SwFormatFollowTextFlow aFlow(true);
    aFrameSet.Put(aFlow);
    Graphic aGrf;

    // When inserting that image:
    pWrtShell->SwFEShell::Insert(OUString(), OUString(), &aGrf, &aFrameSet);

    // Then make sure that the cell height grows to have space for the graphic, given that
    // background=true is not specified.
    xmlDocUniquePtr pXmlDoc = parseLayoutDump();
    sal_Int32 nCellHeight = getXPath(pXmlDoc, "//cell[1]/infos/bounds", "height").toInt32();
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected less than: 1000
    // - Actual  : 1120
    // i.e. the cell height was too large, the image influenced it, which is not expected.
    CPPUNIT_ASSERT_LESS(static_cast<sal_Int32>(1000), nCellHeight);
}

CPPUNIT_PLUGIN_IMPLEMENT();

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