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

#include <config_folders.h>

#include <COKit/COKit.hxx>

#include <comphelper/scopeguard.hxx>
#include <osl/file.hxx>
#include <rtl/bootstrap.hxx>

#include <com/sun/star/animations/XAnimationNodeSupplier.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/beans/XPropertyContainer.hpp>
#include <com/sun/star/beans/PropertyAttribute.hpp>
#include <com/sun/star/document/XDocumentPropertiesSupplier.hpp>
#include <com/sun/star/document/XDocumentProperties.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/frame/XStorable.hpp>
#include <com/sun/star/uno/Reference.hxx>
#include <com/sun/star/drawing/LineStyle.hpp>
#include <com/sun/star/drawing/FillStyle.hpp>
#include <com/sun/star/drawing/XDrawView.hpp>
#include <com/sun/star/drawing/XDrawPage.hpp>
#include <com/sun/star/drawing/XMasterPageTarget.hpp>
#include <com/sun/star/style/ParagraphAdjust.hpp>
#include <com/sun/star/table/XMergeableCell.hpp>
#include <com/sun/star/text/WritingMode2.hpp>
#include <com/sun/star/view/XSelectionSupplier.hpp>

#include <comphelper/propertysequence.hxx>
#include <editeng/adjustitem.hxx>
#include <editeng/editobj.hxx>
#include <editeng/editview.hxx>
#include <editeng/eeitem.hxx>
#include <editeng/flditem.hxx>
#include <editeng/outliner.hxx>
#include <sfx2/dispatch.hxx>
#include <sfx2/request.hxx>
#include <sfx2/viewfrm.hxx>
#include <svl/intitem.hxx>
#include <svl/srchitem.hxx>
#include <svx/svxids.hrc>
#include <svx/svdoashp.hxx>
#include <svx/svdpagv.hxx>
#include <svx/svdorect.hxx>
#include <svx/svdhdl.hxx>
#include <svx/svddrgmt.hxx>
#include <svx/svdotable.hxx>
#include <svx/xlineit0.hxx>
#include <svx/xfillit0.hxx>
#include <svx/xflclit.hxx>
#include <svx/xflgrit.hxx>
#include <svx/xlndsit.hxx>
#include <SlideSorterViewShell.hxx>
#include <SlideSorter.hxx>
#include <controller/SlsClipboard.hxx>
#include <controller/SlideSorterController.hxx>
#include <controller/SlsPageSelector.hxx>
#include <svl/stritem.hxx>
#include <undo/undomanager.hxx>
#include <vcl/scheduler.hxx>
#include <docmodel/uno/UnoComplexColor.hxx>
#include <comphelper/propertyvalue.hxx>
#include <comphelper/sequenceashashmap.hxx>
#include <comphelper/kit.hxx>
#include <comphelper/scopeguard.hxx>
#include <docmodel/uno/UnoTheme.hxx>
#include <docmodel/theme/Theme.hxx>
#include <docmodel/color/ComplexColorJSON.hxx>

#include <drawdoc.hxx>
#include <DrawDocShell.hxx>
#include <DesignTemplates.hxx>
#include <ViewShell.hxx>
#include <app.hrc>
#include <sdpage.hxx>
#include <unomodel.hxx>
#include <osl/thread.hxx>
#include <tools/json_writer.hxx>
#include <slideshow.hxx>
#include <sdresid.hxx>
#include <strings.hrc>
#include <unopage.hxx>
#include <editeng/outlobj.hxx>
#include <sdmod.hxx>
#include <svx/svdotext.hxx>
#include <xmloff/autolayout.hxx>

using namespace ::com::sun::star;

/// Impress UI tests.
class SdUiImpressTest : public SdModelTestBase
{
public:
    SdUiImpressTest()
        : SdModelTestBase(u"/sd/qa/unit/data/"_ustr)
    {
    }

    void checkCurrentPageNumber(sal_uInt16 nNum);
    void insertStringToObject(sal_uInt16 nObj, std::u16string_view rStr, bool bUseEscape);
    sd::slidesorter::SlideSorterViewShell*
    getSlideSorterViewShell(const css::uno::Reference<css::lang::XComponent>& xComp = nullptr);
    void lcl_search(const OUString& rKey, bool bFindAll = false, bool bBackwards = false);
};

void SdUiImpressTest::checkCurrentPageNumber(sal_uInt16 nNum)
{
    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawView> xDrawView(xModel->getCurrentController(), uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xPage(xDrawView->getCurrentPage(), uno::UNO_SET_THROW);
    uno::Reference<beans::XPropertySet> xPropertySet(xPage, uno::UNO_QUERY);

    sal_uInt16 nPageNumber;
    xPropertySet->getPropertyValue(u"Number"_ustr) >>= nPageNumber;
    CPPUNIT_ASSERT_EQUAL(nNum, nPageNumber);
}

void SdUiImpressTest::insertStringToObject(sal_uInt16 nObj, std::u16string_view rStr,
                                           bool bUseEscape)
{
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pPage = pViewShell->GetActualPage();
    SdrObject* pShape = pPage->GetObj(nObj);
    CPPUNIT_ASSERT_MESSAGE("No Shape", pShape);
    SdrView* pView = pViewShell->GetView();
    pView->MarkObj(pShape, pView->GetSdrPageView());
    Scheduler::ProcessEventsToIdle();

    CPPUNIT_ASSERT(!pView->IsTextEdit());

    typeString(pImpressDocument, rStr);

    CPPUNIT_ASSERT(pView->IsTextEdit());

    if (bUseEscape)
    {
        typeKey(pImpressDocument, KEY_ESCAPE);
        CPPUNIT_ASSERT(!pView->IsTextEdit());
    }
}

sd::slidesorter::SlideSorterViewShell*
SdUiImpressTest::getSlideSorterViewShell(const css::uno::Reference<css::lang::XComponent>& xComp)
{
    auto pXImpressDocument
        = dynamic_cast<SdXImpressDocument*>(xComp.is() ? xComp.get() : mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    sd::slidesorter::SlideSorterViewShell* pSSVS = nullptr;
    // Same as in sd/qa/unit/misc-tests.cxx
    for (int i = 0; i < 1000; i++)
    {
        // Process all Tasks - slide sorter is created here
        Scheduler::ProcessEventsToIdle();
        if ((pSSVS = sd::slidesorter::SlideSorterViewShell::GetSlideSorter(
                 pViewShell->GetViewShellBase()))
            != nullptr)
            break;
        osl::Thread::wait(std::chrono::milliseconds(100));
    }
    CPPUNIT_ASSERT(pSSVS);
    return pSSVS;
}

void SdUiImpressTest::lcl_search(const OUString& rKey, bool bFindAll, bool bBackwards)
{
    Scheduler::ProcessEventsToIdle();
    SvxSearchCmd eSearch = bFindAll ? SvxSearchCmd::FIND_ALL : SvxSearchCmd::FIND;

    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence({
        { "SearchItem.SearchString", cpo::uno::Any(rKey) },
        { "SearchItem.Backward", cpo::uno::Any(bBackwards) },
        { "SearchItem.Command", cpo::uno::Any(sal_uInt16(eSearch)) },
    }));

    dispatchCommand(mxComponent, u".uno:ExecuteSearch"_ustr, aPropertyValues);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureTransformExtractSlide)
{
    createSdImpressDoc("odp/tdf161430.odp");

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"JumpToSlideByName": "Slide 3"},
            {"MoveSlide": 0},
            {"RenameSlide": "Slide3-Renamed"},
            {"DeleteSlide": 2},
            {"JumpToSlide": 2},
            {"DeleteSlide": ""},
            {"JumpToSlide": 1},
            {"DuplicateSlide": ""},
            {"RenameSlide": "Slide1-Duplicated"},
            {"InsertMasterSlide": 1},
            {"RenameSlide": "SlideInserted-1"},
            {"ChangeLayout": 18},
            {"JumpToSlide": "last"},
            {"InsertMasterSlideByName": "Topic Separator white"},
            {"RenameSlide": "SlideInserted-Name"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_2CONTENT"},
            {"SetText.0": "first"},
            {"SetText.1": "second"},
            {"SetText.2": "third"},
            {"DuplicateSlide": 1},
            {"MoveSlide.2": 6}
        ]
    }
}
)json"_ustr;

    //transform
    cpo::uno::Sequence<css::beans::PropertyValue> aArgs = {
        comphelper::makePropertyValue(u"DataJson"_ustr, aJson),
    };
    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr, aArgs);

    //extract
    tools::JsonWriter aJsonWriter;
    std::string_view aCommand(".uno:ExtractDocumentStructure?filter=slides");
    auto pXPresDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    pXPresDocument->getCommandValues(aJsonWriter, aCommand);

    OString aExpectedStr
        = "{ \"DocStructure\": { \"SlideCount\": 7, \"MasterSlideCount\": 8, \"MasterSlides\": { "
          "\"MasterSlide 0\": { \"Name\": \"Topic_Separator_Purple\", \"Theme\": \"Office Theme\""
          "}, \"MasterSlide 1\": { \"Name\": \"Content_sidebar_White\", \"Theme\": \"Office Theme"
          "\"}, \"MasterSlide 2\": { \"Name\": \"Topic Separator white\", \"Theme\": \"Office The"
          "me\"}, \"MasterSlide 3\": { \"Name\": \"Content_sidebar_White_\", \"Theme\": \"Office"
          "\"}, \"MasterSlide 4\": { \"Name\": \"Topic_Separator_Purple_\", \"Theme\": \"Office\""
          "}, \"MasterSlide 5\": { \"Name\": \"Content_White_Purple_Sidebar\", \"Theme\": \"Offic"
          "e\"}, \"MasterSlide 6\": { \"Name\": \"Default 1\", \"Theme\": \"Office\"}, \"MasterSl"
          "ide 7\": { \"Name\": \"Default 1_\", \"Theme\": \"Office\"}}, \"Slides\": { \"Slide 0"
          "\": { \"SlideName\": \"Slide3-Renamed\", \"MasterSlideName\": \"Content_White_Purple_S"
          "idebar\", \"LayoutId\": 3, \"LayoutName\": \"AUTOLAYOUT_TITLE_2CONTENT\", \"ObjectCoun"
          "t\": 4, \"Objects\": { \"Objects 0\": { \"Kind\": \"Title\", \"TextCount\": 1, \"Texts"
          "\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Friendly Open Source Project"
          "\"]}}}, \"Objects 1\": { \"Kind\": \"Graphic\"}, \"Objects 2\": { \"Kind\": \"Outline"
          "\", \"TextCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 9, \"Paragraphs\": [ "
          "\"Real Open Source\", \"100% open-source code\", \"Built with LibreOffice technology\""
          ", \"Built with Free Software technology stacks: primarily C++\", \"Runs best on Linux"
          "\", \"Open Development\", \"Anyone can contribute & participate\", \"Follow commits an"
          "d tickets\", \"Public community calls - forum has details\"]}}}, \"Objects 3\": { \"Ki"
          "nd\": \"Outline\", \"TextCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 5, \"Pa"
          "ragraphs\": [ \"Focus:\", \"a non-renewable resource.\", \"Office Productivity & Docum"
          "ents\", \"Excited about migrating your\\u0001documents\", \"Grateful to our partners f"
          "or solving\\u0001other problems.\"]}}}}}, \"Slide 1\": { \"SlideName\": \"Slide 2\", "
          "\"MasterSlideName\": \"Topic_Separator_Purple\", \"LayoutId\": 3, \"LayoutName\": \"AU"
          "TOLAYOUT_TITLE_2CONTENT\", \"ObjectCount\": 1, \"Objects\": { \"Objects 0\": { \"TextC"
          "ount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 3, \"Paragraphs\": [ \"Collabora "
          "Online\", \"\", \"Powerful Online Collaboration\"]}}}}}, \"Slide 2\": { \"SlideName\":"
          " \"Slide1-Duplicated\", \"MasterSlideName\": \"Topic_Separator_Purple\", \"LayoutId\":"
          " 3, \"LayoutName\": \"AUTOLAYOUT_TITLE_2CONTENT\", \"ObjectCount\": 1, \"Objects\": { "
          "\"Objects 0\": { \"TextCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 3, \"Para"
          "graphs\": [ \"Collabora Online\", \"\", \"Powerful Online Collaboration\"]}}}}}, \"Sli"
          "de 3\": { \"SlideName\": \"SlideInserted-1\", \"MasterSlideName\": \"Content_sidebar_W"
          "hite\", \"LayoutId\": 18, \"LayoutName\": \"AUTOLAYOUT_TITLE_4CONTENT\", \"ObjectCount"
          "\": 5, \"Objects\": { \"Objects 0\": { \"Kind\": \"Title\", \"Empty\": true, \"TextCou"
          "nt\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Click to add"
          " Title\"]}}}, \"Objects 1\": { \"Kind\": \"Outline\", \"Empty\": true, \"TextCount\": "
          "1, \"Texts\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Click to add Text"
          "\"]}}}, \"Objects 2\": { \"Kind\": \"Outline\", \"Empty\": true, \"TextCount\": 1, \"T"
          "exts\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Click to add Text\"]}}},"
          " \"Objects 3\": { \"Kind\": \"Outline\", \"Empty\": true, \"TextCount\": 1, \"Texts\":"
          " { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Click to add Text\"]}}}, \"Obje"
          "cts 4\": { \"Kind\": \"Outline\", \"Empty\": true, \"TextCount\": 1, \"Texts\": { \"Te"
          "xt 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"Click to add Text\"]}}}}}, \"Slide 4\""
          ": { \"SlideName\": \"Slide 5\", \"MasterSlideName\": \"Topic_Separator_Purple\", \"Lay"
          "outId\": 3, \"LayoutName\": \"AUTOLAYOUT_TITLE_2CONTENT\", \"ObjectCount\": 1, \"Objec"
          "ts\": { \"Objects 0\": { \"TextCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 1"
          ", \"Paragraphs\": [ \"With thanks to our Partners, Customers & Community !\"]}}}}}, \""
          "Slide 5\": { \"SlideName\": \"SlideInserted-Name\", \"MasterSlideName\": \"Topic Separ"
          "ator white\", \"LayoutId\": 3, \"LayoutName\": \"AUTOLAYOUT_TITLE_2CONTENT\", \"Object"
          "Count\": 3, \"Objects\": { \"Objects 0\": { \"Kind\": \"Title\", \"TextCount\": 1, \"T"
          "exts\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"first\"]}}}, \"Objects 1"
          "\": { \"Kind\": \"Outline\", \"TextCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount"
          "\": 1, \"Paragraphs\": [ \"second\"]}}}, \"Objects 2\": { \"Kind\": \"Outline\", \"Tex"
          "tCount\": 1, \"Texts\": { \"Text 0\": { \"ParaCount\": 1, \"Paragraphs\": [ \"third\"]"
          "}}}}}, \"Slide 6\": { \"SlideName\": \"Slide 7\", \"MasterSlideName\": \"Topic_Separat"
          "or_Purple\", \"LayoutId\": 3, \"LayoutName\": \"AUTOLAYOUT_TITLE_2CONTENT\", \"ObjectC"
          "ount\": 1, \"Objects\": { \"Objects 0\": { \"TextCount\": 1, \"Texts\": { \"Text 0\": "
          "{ \"ParaCount\": 3, \"Paragraphs\": [ \"Collabora Online\", \"\", \"Powerful Online Co"
          "llaboration\"]}}}}}}}}"_ostr;

    CPPUNIT_ASSERT_EQUAL(aExpectedStr, aJsonWriter.finishAndGetAsOString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureExtractSlideText)
{
    // The text filter returns the slide text as markdown, read from the live
    // document so that text still being typed is included.
    createSdImpressDoc();

    // Type into the title placeholder and keep the edit session open: the
    // typed text lives only in the editing outliner at this point.
    insertStringToObject(0, u"Typed while editing", /*bUseEscape*/ false);

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    {
        tools::JsonWriter aJsonWriter;
        pImpressDocument->getCommandValues(aJsonWriter,
                                           ".uno:ExtractDocumentStructure?filter=text");
        const OString aJson = aJsonWriter.finishAndGetAsOString();

        CPPUNIT_ASSERT(aJson.indexOf("\"BodyText\"") >= 0);
        CPPUNIT_ASSERT(aJson.indexOf("\"format\": \"markdown\"") >= 0);
        CPPUNIT_ASSERT(aJson.indexOf("\"truncated\": false") >= 0);
        // The body has one markdown section per slide.
        CPPUNIT_ASSERT(aJson.indexOf("## Slide 1: ") >= 0);
        // The open edit session's text is included.
        CPPUNIT_ASSERT(aJson.indexOf("Typed while editing") >= 0);
        // The untouched content placeholder shows only its prompt text, which
        // is not document content.
        CPPUNIT_ASSERT(aJson.indexOf("Click to add") < 0);
    }

    // The committed text is still returned once the edit session is closed.
    typeKey(pImpressDocument, KEY_ESCAPE);
    {
        tools::JsonWriter aJsonWriter;
        pImpressDocument->getCommandValues(aJsonWriter,
                                           ".uno:ExtractDocumentStructure?filter=text");
        const OString aJson = aJsonWriter.finishAndGetAsOString();

        CPPUNIT_ASSERT(aJson.indexOf("Typed while editing") >= 0);
        CPPUNIT_ASSERT(aJson.indexOf("Click to add") < 0);
    }
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureUndo)
{
    // The whole transform must collapse into one undo step: one undo reverts
    // it, one redo restores it.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    // A fresh Impress document starts with a single slide.
    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pImpressDocument->getDrawPages()->getCount());

    auto getSlide0Title = [&]() -> OUString {
        SdPage* pPage = pImpressDocument->GetDoc()->GetSdPage(0, PageKind::Standard);
        SdrObject* pObj = pPage ? pPage->GetObj(0) : nullptr;
        if (pObj && pObj->IsSdrTextObj())
        {
            SdrTextObj* pTextObj = static_cast<SdrTextObj*>(pObj);
            if (pTextObj->GetOutlinerParaObject())
                return pTextObj->GetOutlinerParaObject()->GetTextObject().GetText(0);
        }
        return OUString();
    };

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Generated title"},
            {"InsertMasterSlide": 0},
            {"SetText.0": "Second slide"},
            {"InsertMasterSlide": 0},
            {"SetText.0": "Third slide"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    // The transform added two slides (three total) and set the first title.
    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pImpressDocument->getDrawPages()->getCount());
    CPPUNIT_ASSERT_EQUAL(u"Generated title"_ustr, getSlide0Title());

    // One undo reverts everything: back to one slide, title no longer set.
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pImpressDocument->getDrawPages()->getCount());
    CPPUNIT_ASSERT(getSlide0Title() != u"Generated title"_ustr);

    // A single redo restores all of it.
    dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pImpressDocument->getDrawPages()->getCount());
    CPPUNIT_ASSERT_EQUAL(u"Generated title"_ustr, getSlide0Title());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureInsertSlideNotesMaster)
{
    // An inserted slide's notes page must keep a notes master. If it instead
    // points at the standard master, the notes placeholder text leaks onto the
    // slide and the slide master after a PPTX round-trip.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"InsertMasterSlide": 0},
            {"SetText.0": "Second slide"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    SdDrawDocument* pDoc = pImpressDocument->GetDoc();
    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pImpressDocument->getDrawPages()->getCount());

    SdPage* pNotesMaster
        = static_cast<SdPage*>(&(pDoc->GetSdPage(1, PageKind::Notes)->TRG_GetMasterPage()));
    CPPUNIT_ASSERT_EQUAL(PageKind::Notes, pNotesMaster->GetPageKind());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureApplyTemplate)
{
    // ApplyTemplate gives a generated deck the bundled template's design. Every
    // slide is themed, including slides inserted after the ApplyTemplate command -
    // not just the first one. Each slide also gets the master that fits its
    // layout, so a title slide and a content slide use different template masters.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    SdDrawDocument* pDoc = pImpressDocument->GetDoc();
    const OUString aBeforeMaster
        = static_cast<SdPage*>(&pDoc->GetSdPage(0, PageKind::Standard)->TRG_GetMasterPage())
              ->GetName();

    // A title slide, a content slide, and a section divider - three layouts that
    // the Cobalt template pairs with three different masters.
    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "Cobalt"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_ONLY"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pImpressDocument->getDrawPages()->getCount());

    auto masterName = [&](sal_uInt16 nPage) {
        return static_cast<SdPage*>(
                   &pDoc->GetSdPage(nPage, PageKind::Standard)->TRG_GetMasterPage())
            ->GetName();
    };
    // Every slide is on a Cobalt master, not the plain default one...
    CPPUNIT_ASSERT(masterName(0).startsWith("Cobalt"));
    CPPUNIT_ASSERT(masterName(1).startsWith("Cobalt"));
    CPPUNIT_ASSERT(masterName(2).startsWith("Cobalt"));
    CPPUNIT_ASSERT(aBeforeMaster != masterName(0));
    // ...and the title, content, and divider slides each use a distinct master,
    // matching how the template's own example slides pair layouts with masters.
    CPPUNIT_ASSERT(masterName(0) != masterName(1));
    CPPUNIT_ASSERT(masterName(1) != masterName(2));
    CPPUNIT_ASSERT(masterName(0) != masterName(2));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignMasterRoleFromName)
{
    // A master's part is read from role keywords in its name, so a real
    // template whose masters all share one or two layouts still themes a deck
    // by part. The names here are those a designed template carries.
    using sd::DesignMasterRole;
    using sd::DesignMasterRoleFromName;

    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Title, DesignMasterRoleFromName(u"Title_Slide"));
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Content, DesignMasterRoleFromName(u"Content_Outline_Normal"));
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Divider, DesignMasterRoleFromName(u"Topic_Separator_Purple"));
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Closing, DesignMasterRoleFromName(u"Ending_Slide"));

    // The match ignores letter case.
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Divider, DesignMasterRoleFromName(u"section break"));

    // A name that carries both a divider keyword and "title" reads as a divider,
    // because the divider group is tested before the title group.
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Divider, DesignMasterRoleFromName(u"Section Title"));
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Divider,
                         DesignMasterRoleFromName(u"Title & Content Divider"));

    // "title" is a substring match, so a name whose only keyword is part of a
    // longer word still reads as a title.
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Title, DesignMasterRoleFromName(u"Subtitle"));

    // A name with no part keyword is left Unknown, so the part falls to the
    // example slide's layout instead - this is how the bundled templates, whose
    // masters are named Cobalt, Cobalt1, ..., are still placed by layout.
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Unknown, DesignMasterRoleFromName(u"Cobalt1"));
    CPPUNIT_ASSERT_EQUAL(DesignMasterRole::Unknown, DesignMasterRoleFromName(u"Default"));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureSetSlidePart)
{
    // A SetSlidePart command labels a slide's part; the engine places the slide
    // on the template's master for that part, overriding the slide's own layout.
    // An unknown part word is dropped, leaving the slide to the part-based
    // default. Cobalt's masters are Cobalt (body), Cobalt1 (opening), Cobalt2
    // (divider).
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    // Three content-layout slides, labelled opening, divider, and a word that is
    // not a part.
    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "Cobalt"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "One"},
            {"SetSlidePart": "opening"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Two"},
            {"SetSlidePart": "divider"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Three"},
            {"SetSlidePart": "bogus"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pImpressDocument->getDrawPages()->getCount());

    auto masterName = [&](sal_uInt16 nPage) {
        return static_cast<SdPage*>(
                   &pDoc->GetSdPage(nPage, PageKind::Standard)->TRG_GetMasterPage())
            ->GetName();
    };
    // The opening label lands on the opening master and the divider label on the
    // divider master, each winning over the content layout. The unknown part is
    // dropped, so the third slide takes the body master.
    CPPUNIT_ASSERT_EQUAL(u"Cobalt1"_ustr, masterName(0));
    CPPUNIT_ASSERT_EQUAL(u"Cobalt2"_ustr, masterName(1));
    CPPUNIT_ASSERT_EQUAL(u"Cobalt"_ustr, masterName(2));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureDuplicateSlideKeepsPart)
{
    // A duplicated slide plays the same part as its source, so the copy of a
    // divider slide lands on the divider master too, not on the body master
    // its content layout suggests.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "Cobalt"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Break"},
            {"SetSlidePart": "divider"},
            {"DuplicateSlide": 0}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pImpressDocument->getDrawPages()->getCount());

    auto masterName = [&](sal_uInt16 nPage) {
        return static_cast<SdPage*>(
                   &pDoc->GetSdPage(nPage, PageKind::Standard)->TRG_GetMasterPage())
            ->GetName();
    };
    CPPUNIT_ASSERT_EQUAL(u"Cobalt2"_ustr, masterName(0));
    CPPUNIT_ASSERT_EQUAL(u"Cobalt2"_ustr, masterName(1));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureSetNotes)
{
    // A SetNotes command writes the speaker notes of the current slide, and one
    // undo reverts the whole transform including the notes.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    auto notesText = [&]() -> OUString {
        SdPage* pNotes = pDoc->GetSdPage(0, PageKind::Notes);
        SdrObject* pObj = pNotes ? pNotes->GetPresObj(PresObjKind::Notes) : nullptr;
        if (pObj && pObj->IsSdrTextObj())
        {
            SdrTextObj* pTextObj = static_cast<SdrTextObj*>(pObj);
            if (pTextObj->GetOutlinerParaObject())
                return pTextObj->GetOutlinerParaObject()->GetTextObject().GetText(0);
        }
        return OUString();
    };

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"SetText.0": "Slide title"},
            {"SetNotes": "Speak to the roadmap here."}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(u"Speak to the roadmap here."_ustr, notesText());

    // One undo clears the notes again.
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT(notesText() != u"Speak to the roadmap here."_ustr);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureApplyTemplateKeepsUntouchedSlides)
{
    // Re-applying an already applied template re-themes only the slides the
    // transform touches. A slide the transform leaves alone keeps the
    // geometry and the master it had, so the user's manual edits between two
    // AI turns survive the second turn.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    // A themed two-slide deck: an opening slide and a body slide, on two
    // different Cobalt masters.
    static constexpr OUString aFirstJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "Cobalt"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "One"},
            {"SetSlidePart": "opening"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Two"}
        ]
    }
}
)json"_ustr;
    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aFirstJson) });

    auto masterName = [&](sal_uInt16 nPage) {
        return static_cast<SdPage*>(
                   &pDoc->GetSdPage(nPage, PageKind::Standard)->TRG_GetMasterPage())
            ->GetName();
    };
    CPPUNIT_ASSERT_EQUAL(u"Cobalt1"_ustr, masterName(0));
    CPPUNIT_ASSERT_EQUAL(u"Cobalt"_ustr, masterName(1));

    // The user moves slide 1's title by hand.
    SdPage* pFirstSlide = pDoc->GetSdPage(0, PageKind::Standard);
    SdrObject* pTitle = pFirstSlide->GetObj(0);
    tools::Rectangle aMovedRect = pTitle->GetLogicRect();
    aMovedRect.Move(1000, 1000);
    pTitle->SetLogicRect(aMovedRect);

    // The next AI turn carries the template again but only edits slide 2.
    static constexpr OUString aSecondJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "Cobalt"},
            {"JumpToSlide": 1},
            {"SetText.0": "Two updated"}
        ]
    }
}
)json"_ustr;
    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aSecondJson) });

    // Slide 1 keeps the moved title and the opening master, even though this
    // turn named no part for it; slide 2 stays on a template master.
    CPPUNIT_ASSERT_EQUAL(aMovedRect, pFirstSlide->GetObj(0)->GetLogicRect());
    CPPUNIT_ASSERT_EQUAL(u"Cobalt1"_ustr, masterName(0));
    CPPUNIT_ASSERT(masterName(1).startsWith("Cobalt"));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureApplyTemplateUnknownName)
{
    // A name that matches no template file is rejected: the deck keeps its
    // original master rather than the command reaching for an arbitrary file.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    SdDrawDocument* pDoc = pImpressDocument->GetDoc();
    const OUString aBeforeMaster
        = static_cast<SdPage*>(&pDoc->GetSdPage(0, PageKind::Standard)->TRG_GetMasterPage())
              ->GetName();

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "NoSuchTemplate"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    const OUString aAfterMaster
        = static_cast<SdPage*>(&pDoc->GetSdPage(0, PageKind::Standard)->TRG_GetMasterPage())
              ->GetName();
    CPPUNIT_ASSERT_EQUAL(aBeforeMaster, aAfterMaster);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignTemplateCatalog)
{
    // The catalog the picker is built from lists the bundled design templates,
    // each with a thumbnail the picker can show.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    tools::JsonWriter aJsonWriter;
    pImpressDocument->getCommandValues(aJsonWriter, ".uno:GetDesignTemplates");
    const OString aJson = aJsonWriter.finishAndGetAsOString();

    // The client's commandvalues handler reads commandName and commandValues, so
    // the response must carry both - returning the templates bare would be dropped.
    CPPUNIT_ASSERT(aJson.indexOf(".uno:GetDesignTemplates") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("commandValues") >= 0);
    // Bundled templates appear by name...
    CPPUNIT_ASSERT(aJson.indexOf("Cobalt") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("Ivory") >= 0);
    // ...and carry a base64 PNG thumbnail data URL the picker shows. The JSON
    // writer escapes the slash in the media type, so match a slash-free prefix.
    CPPUNIT_ASSERT(aJson.indexOf("data:image") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("base64") >= 0);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignTemplateCatalogSkipsInvalidNames)
{
    // A template whose file name breaks the naming contract (at most 64 plain
    // letters, digits, spaces, hyphens, or underscores - here a '!') is not
    // offered: the server drops such a pick, so listing it would put a dead
    // entry in the picker.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    OUString aDirUrl = u"$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER "/cool-ai-templates/"_ustr;
    rtl::Bootstrap::expandMacros(aDirUrl);
    const OUString aBadUrl = aDirUrl + "Bad!name.otp";
    CPPUNIT_ASSERT_EQUAL(osl::FileBase::E_None,
                         osl::File::copy(aDirUrl + "Cobalt.otp", aBadUrl));
    comphelper::ScopeGuard aCleanup([&aBadUrl] { osl::File::remove(aBadUrl); });

    tools::JsonWriter aJsonWriter;
    pImpressDocument->getCommandValues(aJsonWriter, ".uno:GetDesignTemplates");
    const OString aJson = aJsonWriter.finishAndGetAsOString();

    CPPUNIT_ASSERT(aJson.indexOf("Cobalt") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("Bad!name") < 0);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignTemplateLayouts)
{
    // The per-name design query reports the slide layouts the template's
    // example slides cover. It is a separate command from the template list, so
    // the reply is told apart from the list reply by its command name. The
    // reply echoes the full command, parameters included.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    tools::JsonWriter aJsonWriter;
    // The lowercase name must match too - names resolve ignoring letter case.
    pImpressDocument->getCommandValues(aJsonWriter, ".uno:GetDesignTemplateDesigns?name=cobalt");
    const OString aJson = aJsonWriter.finishAndGetAsOString();

    CPPUNIT_ASSERT(aJson.indexOf("name=cobalt") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("layouts") >= 0);
    // Cobalt ships example slides, so at least one layout must be reported.
    CPPUNIT_ASSERT(aJson.indexOf("AUTOLAYOUT_") >= 0);

    // The same reply lists the template's design masters with the part each
    // plays, so the model can put each slide on the design that fits its role.
    CPPUNIT_ASSERT(aJson.indexOf("masters") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("Cobalt") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("role") >= 0);
}

namespace
{
// Stores a design manifest JSON string in the document's AIDesignManifest
// user-defined property, the standard ODF carrier a template declares its
// manifest in.
void lcl_SetDesignManifest(const uno::Reference<lang::XComponent>& xComponent,
                           const OUString& rJson)
{
    uno::Reference<document::XDocumentPropertiesSupplier> xSupplier(xComponent, uno::UNO_QUERY);
    CPPUNIT_ASSERT(xSupplier.is());
    uno::Reference<beans::XPropertyContainer> xUserProps
        = xSupplier->getDocumentProperties()->getUserDefinedProperties();
    CPPUNIT_ASSERT(xUserProps.is());
    try
    {
        xUserProps->removeProperty(u"AIDesignManifest"_ustr);
    }
    catch (const uno::Exception&)
    {
    }
    xUserProps->addProperty(u"AIDesignManifest"_ustr, beans::PropertyAttribute::REMOVABLE,
                            cpo::uno::Any(rJson));
}
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignManifestDeclaredRoles)
{
    // A master a manifest declares takes the declared part and skips the
    // name-and-example heuristic, so the manifest wins even when the example
    // layout would suggest a different part.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    const OUString aMasterName
        = pDoc->GetMasterSdPage(0, PageKind::Standard)->GetName();

    auto roleOf = [&](const OUString& rName) -> sd::DesignTemplateMaster {
        for (const auto& rMaster : sd::CollectDesignTemplateMasters(*pDoc))
            if (rMaster.maName == rName)
                return rMaster;
        return {};
    };

    // Without a manifest the part is inferred, not declared. A closing part can
    // only come from a name keyword, never from an example layout, so the fresh
    // document does not infer one.
    CPPUNIT_ASSERT(!roleOf(aMasterName).mbDeclared);
    CPPUNIT_ASSERT(sd::DesignMasterRole::Closing != roleOf(aMasterName).meRole);

    // Declaring the master a closing overrides whatever the heuristic inferred.
    lcl_SetDesignManifest(mxComponent, "{\"schemaVersion\":1,\"masters\":{\"" + aMasterName
                                           + "\":\"closing\"}}");
    CPPUNIT_ASSERT_EQUAL(sd::DesignMasterRole::Closing, roleOf(aMasterName).meRole);
    CPPUNIT_ASSERT(roleOf(aMasterName).mbDeclared);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignManifestUnreadable)
{
    // A manifest that cannot be parsed, or whose schema version is not
    // recognised, reads as no manifest at all, so the heuristic runs as before.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    lcl_SetDesignManifest(mxComponent, u"this is not json {{{"_ustr);
    CPPUNIT_ASSERT(!sd::ReadAIDesignManifest(*pDoc).has_value());

    // A version below 1 is not a schema the reader knows.
    lcl_SetDesignManifest(mxComponent, u"{\"schemaVersion\":0,\"masters\":{}}"_ustr);
    CPPUNIT_ASSERT(!sd::ReadAIDesignManifest(*pDoc).has_value());

    // A well-formed version-1 manifest reads back as a manifest.
    lcl_SetDesignManifest(mxComponent, u"{\"schemaVersion\":1}"_ustr);
    CPPUNIT_ASSERT(sd::ReadAIDesignManifest(*pDoc).has_value());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureSetSlideIntent)
{
    // A manifest can map a slide intent word straight to a master. A slide the
    // model gives that intent lands on the mapped master, winning over the
    // part-and-layout choice. An intent the manifest maps nothing for falls back
    // to the ordinary choice.
    OUString aDirUrl = u"$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER "/cool-ai-templates/"_ustr;
    rtl::Bootstrap::expandMacros(aDirUrl);

    // Build a template that carries an intent manifest: start from the bundled
    // Cobalt template, add a manifest mapping the intent "quote" to Cobalt's
    // opening master, and save it under a fresh name in the template directory.
    const OUString aIntentTemplateUrl = aDirUrl + "IntentDeck.otp";
    comphelper::ScopeGuard aCleanup(
        [&aIntentTemplateUrl] { osl::File::remove(aIntentTemplateUrl); });
    {
        loadFromURL(aDirUrl + "Cobalt.otp");
        lcl_SetDesignManifest(
            mxComponent, u"{\"schemaVersion\":1,\"intents\":{\"quote\":\"Cobalt1\"}}"_ustr);
        uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY);
        CPPUNIT_ASSERT(xStorable.is());
        xStorable->storeToURL(aIntentTemplateUrl,
                              { comphelper::makePropertyValue(u"FilterName"_ustr,
                                                              u"impress8_template"_ustr) });
    }

    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    // Both slides use the body content layout. The first carries the mapped
    // intent, the second an intent the manifest maps nothing for.
    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ApplyTemplate": "IntentDeck"},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Quote"},
            {"SetSlideIntent": "quote"},
            {"InsertMasterSlide": 0},
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Body"},
            {"SetSlideIntent": "mystery"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pImpressDocument->getDrawPages()->getCount());

    auto masterName = [&](sal_uInt16 nPage) {
        return static_cast<SdPage*>(
                   &pDoc->GetSdPage(nPage, PageKind::Standard)->TRG_GetMasterPage())
            ->GetName();
    };
    // The mapped intent lands on the opening master; the unknown intent falls
    // back to the body master its content layout suggests.
    CPPUNIT_ASSERT_EQUAL(u"Cobalt1"_ustr, masterName(0));
    CPPUNIT_ASSERT_EQUAL(u"Cobalt"_ustr, masterName(1));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDesignTemplateDesignsDeclared)
{
    // When a template declares a manifest, the per-name design query reports
    // each master's part as declared, and adds the manifest's image art
    // direction and content budgets. A template with no manifest leaves those
    // out.
    OUString aDirUrl = u"$BRAND_BASE_DIR/" LIBO_SHARE_FOLDER "/cool-ai-templates/"_ustr;
    rtl::Bootstrap::expandMacros(aDirUrl);

    const OUString aDesignedTemplateUrl = aDirUrl + "DesignedDeck.otp";
    comphelper::ScopeGuard aCleanup(
        [&aDesignedTemplateUrl] { osl::File::remove(aDesignedTemplateUrl); });
    {
        loadFromURL(aDirUrl + "Cobalt.otp");
        lcl_SetDesignManifest(
            mxComponent,
            u"{\"schemaVersion\":1,\"masters\":{\"Cobalt1\":\"opening\"},"
            "\"artDirection\":\"Flat vector deep cobalt blue.\","
            "\"budgets\":{\"maxSlides\":12,\"maxTitleLength\":80}}"_ustr);
        uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY);
        CPPUNIT_ASSERT(xStorable.is());
        xStorable->storeToURL(aDesignedTemplateUrl,
                              { comphelper::makePropertyValue(u"FilterName"_ustr,
                                                              u"impress8_template"_ustr) });
    }

    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    tools::JsonWriter aJsonWriter;
    pImpressDocument->getCommandValues(aJsonWriter,
                                       ".uno:GetDesignTemplateDesigns?name=DesignedDeck");
    const OString aJson = aJsonWriter.finishAndGetAsOString();

    // The declared master reports its part as declared rather than inferred.
    CPPUNIT_ASSERT(aJson.indexOf("declared") >= 0);
    // The art-direction sentence rides the reply.
    CPPUNIT_ASSERT(aJson.indexOf("artDirection") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("Flat vector deep cobalt blue") >= 0);
    // The declared budgets ride it too, under the exact key names.
    CPPUNIT_ASSERT(aJson.indexOf("budgets") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("maxSlides") >= 0);
    CPPUNIT_ASSERT(aJson.indexOf("maxTitleLength") >= 0);
    // A budget the manifest did not state is left out.
    CPPUNIT_ASSERT(aJson.indexOf("maxItemLength") < 0);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureUndoImage)
{
    // Inserting an image replaces a placeholder object; undoing the transform
    // must revert that replace without crashing.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    auto isObj1Graphic = [&]() {
        SdPage* pPage = pImpressDocument->GetDoc()->GetSdPage(0, PageKind::Standard);
        SdrObject* pObj = pPage ? pPage->GetObj(1) : nullptr;
        return pObj && pObj->GetObjIdentifier() == SdrObjKind::Graphic;
    };

    OUString aImageURL = createFileURL(u"TestImage1.png");
    OUString aJson = "{ \"Transforms\": { \"SlideCommands\": ["
                     "{ \"ChangeLayoutByName\": \"AUTOLAYOUT_TITLE_CONTENT\" },"
                     "{ \"InsertImage.1\": \""
                     + aImageURL + "\" } ] } }";

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });
    CPPUNIT_ASSERT(isObj1Graphic());

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT(!isObj1Graphic());

    dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
    CPPUNIT_ASSERT(isObj1Graphic());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureInsertImageAtAlt)
{
    // InsertImageAt takes either a plain URL string or an object carrying the
    // URL and a text alternative. The object form sets the graphic and stores
    // the alt text as the object title, which is the accessibility text
    // alternative for an image. The string form still sets the graphic.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    SdDrawDocument* pDoc = pImpressDocument->GetDoc();

    auto obj1 = [&]() {
        SdPage* pPage = pDoc->GetSdPage(0, PageKind::Standard);
        return pPage ? pPage->GetObj(1) : nullptr;
    };

    OUString aImageURL = createFileURL(u"TestImage1.png");

    OUString aObjectJson = "{ \"Transforms\": { \"SlideCommands\": ["
                           "{ \"ChangeLayoutByName\": \"AUTOLAYOUT_TITLE_CONTENT\" },"
                           "{ \"InsertImageAt.0.1\": { \"url\": \""
                           + aImageURL + "\", \"alt\": \"A blue mountain at dawn\" } } ] } }";
    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aObjectJson) });

    CPPUNIT_ASSERT(obj1());
    CPPUNIT_ASSERT_EQUAL(SdrObjKind::Graphic, obj1()->GetObjIdentifier());
    CPPUNIT_ASSERT_EQUAL(u"A blue mountain at dawn"_ustr, obj1()->GetTitle());

    // The plain string form still replaces the placeholder with the graphic; it
    // carries no alt text.
    createSdImpressDoc();
    pDoc = dynamic_cast<SdXImpressDocument*>(mxComponent.get())->GetDoc();
    OUString aStringJson = "{ \"Transforms\": { \"SlideCommands\": ["
                           "{ \"ChangeLayoutByName\": \"AUTOLAYOUT_TITLE_CONTENT\" },"
                           "{ \"InsertImageAt.0.1\": \""
                           + aImageURL + "\" } ] } }";
    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aStringJson) });

    CPPUNIT_ASSERT(obj1());
    CPPUNIT_ASSERT_EQUAL(SdrObjKind::Graphic, obj1()->GetObjIdentifier());
    CPPUNIT_ASSERT(obj1()->GetTitle().isEmpty());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentStructureUnoCommand)
{
    // 1. Create a document;
    // 2. Check that its first slide has two objects (the default title + content placeholders);
    // 3. Perform a "TransformDocumentStructure" with a "UnoCommand" calling ".uno:SelectAll" and
    //    ".uno:Cut";
    // 4. Check that the first slide has no objects now.

    createSdImpressDoc();

    // Let comphelper::dispatchCommand (in KitHelper::dispatchUnoCommand) find the frame
    auto xDesktop = frame::Desktop::create(comphelper::getProcessComponentContext());
    auto pImpressDocument = static_cast<SdXImpressDocument*>(mxComponent.get());
    auto pFrame = pImpressDocument->GetDocShell()->GetFrame();
    CPPUNIT_ASSERT(pFrame);
    xDesktop->setActiveFrame(pFrame->GetFrame().GetFrameInterface());

    CPPUNIT_ASSERT_EQUAL(sal_Int32(2),
                         pImpressDocument->getSdDrawPages()->getDrawPageByIndex(0)->getCount());

    static constexpr OUString aJson = uR"json(
{
    "UnoCommand": {
        "name": ".uno:SelectAll"
    },
    "UnoCommand": {
        "name": ".uno:Cut"
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(0),
                         pImpressDocument->getSdDrawPages()->getDrawPageByIndex(0)->getCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlideCommandJumpToSlideClamps)
{
    // JumpToSlide accepts "last" for the final slide, and an index past the
    // end is clamped to it, so the commands that follow still land on a real
    // slide.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    auto getSlideTitle = [&](sal_uInt16 nPage) -> OUString {
        SdPage* pPage = pImpressDocument->GetDoc()->GetSdPage(nPage, PageKind::Standard);
        SdrObject* pObj = pPage ? pPage->GetObj(0) : nullptr;
        if (pObj && pObj->IsSdrTextObj())
        {
            SdrTextObj* pTextObj = static_cast<SdrTextObj*>(pObj);
            if (pTextObj->GetOutlinerParaObject())
                return pTextObj->GetOutlinerParaObject()->GetTextObject().GetText(0);
        }
        return OUString();
    };

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"InsertMasterSlide": 0},
            {"InsertMasterSlide": 0},
            {"JumpToSlide": 0},
            {"SetText.0": "first"},
            {"JumpToSlide": "last"},
            {"SetText.0": "third"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), pImpressDocument->getDrawPages()->getCount());
    CPPUNIT_ASSERT_EQUAL(u"first"_ustr, getSlideTitle(0));
    CPPUNIT_ASSERT_EQUAL(u"third"_ustr, getSlideTitle(2));

    // An index past the end addresses the last slide instead of being dropped.
    static constexpr OUString aJsonClamp = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"JumpToSlide": 99},
            {"SetText.0": "clamped"}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJsonClamp) });

    CPPUNIT_ASSERT_EQUAL(u"clamped"_ustr, getSlideTitle(2));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlideCommandDeleteSlideKeepsOnlySlide)
{
    // Deleting is refused when the deck has a single slide, so a transform
    // cannot leave an empty document behind.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pImpressDocument->getDrawPages()->getCount());

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"DeleteSlide": 0}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), pImpressDocument->getDrawPages()->getCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlideCommandMarkObject)
{
    // MarkObject selects a shape on the current slide and UnMarkObject
    // releases it again.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();

    static constexpr OUString aJsonMark = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"MarkObject": 0}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJsonMark) });

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1),
                         pView->GetMarkedObjectList().GetMarkCount());

    static constexpr OUString aJsonUnMark = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"UnMarkObject": 0}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJsonUnMark) });

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0),
                         pView->GetMarkedObjectList().GetMarkCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlideCommandEditTextObject)
{
    // EditTextObject edits inside a shape's existing text: SelectText picks a
    // slice and an insert overwrites just that slice, leaving the rest alone.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    static constexpr OUString aJson = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"ChangeLayoutByName": "AUTOLAYOUT_TITLE_CONTENT"},
            {"SetText.0": "Hello World"},
            {"EditTextObject.0": [
                {"SelectText": [0, 0, 0, 5]},
                {"InsertText": "Howdy"}
            ]}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    SdPage* pPage = pImpressDocument->GetDoc()->GetSdPage(0, PageKind::Standard);
    SdrObject* pObj = pPage->GetObj(0);
    CPPUNIT_ASSERT(pObj);
    CPPUNIT_ASSERT(pObj->IsSdrTextObj());
    SdrTextObj* pTextObj = static_cast<SdrTextObj*>(pObj);
    CPPUNIT_ASSERT(pTextObj->GetOutlinerParaObject());
    // The insert replaces the selected "Hello", not the whole text.
    CPPUNIT_ASSERT_EQUAL(u"Howdy World"_ustr,
                         pTextObj->GetOutlinerParaObject()->GetTextObject().GetText(0));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlideCommandInsertImageAtKeepsView)
{
    // InsertImageAt addresses slide and object in the command key itself: the
    // image lands on that slide while the view keeps showing the slide it was
    // on.
    createSdImpressDoc();

    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    // A second slide whose content placeholder the image will replace. The
    // view stays on the first slide because no command after the insert moves
    // it.
    static constexpr OUString aJsonInsert = uR"json(
{
    "Transforms": {
        "SlideCommands": [
            {"InsertMasterSlide": 0}
        ]
    }
}
)json"_ustr;

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJsonInsert) });
    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), pImpressDocument->getDrawPages()->getCount());

    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    const SdrPage* pShownBefore = pView->GetSdrPageView()->GetPage();

    OUString aImageURL = createFileURL(u"TestImage1.png");
    OUString aJson = "{ \"Transforms\": { \"SlideCommands\": ["
                     "{ \"InsertImageAt.1.1\": \""
                     + aImageURL + "\" } ] } }";

    dispatchCommand(mxComponent, u".uno:TransformDocumentStructure"_ustr,
                    { comphelper::makePropertyValue(u"DataJson"_ustr, aJson) });

    SdPage* pPage1 = pImpressDocument->GetDoc()->GetSdPage(1, PageKind::Standard);
    SdrObject* pObj = pPage1->GetObj(1);
    CPPUNIT_ASSERT(pObj);
    CPPUNIT_ASSERT_EQUAL(SdrObjKind::Graphic, pObj->GetObjIdentifier());
    // The view did not navigate to the slide the image was placed on.
    CPPUNIT_ASSERT_EQUAL(pShownBefore, static_cast<const SdrPage*>(pView->GetSdrPageView()->GetPage()));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf111522)
{
    // Load the document and create two new windows.
    createSdImpressDoc("tdf111522.odp");
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_NEWWINDOW, SfxCallMode::SYNCHRON);
    sd::ViewShell* pViewShell1 = pImpressDocument->GetDocShell()->GetViewShell();
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_NEWWINDOW, SfxCallMode::SYNCHRON);
    sd::ViewShell* pViewShell2 = pImpressDocument->GetDocShell()->GetViewShell();
    CPPUNIT_ASSERT(pViewShell1 != pViewShell2);

    // Have slide 1 in window 1, slide 2 in window 2.
    SfxRequest aRequest(*pViewShell2->GetViewFrame(), SID_SWITCHPAGE);
    aRequest.AppendItem(SfxUInt32Item(ID_VAL_WHATPAGE, 1));
    aRequest.AppendItem(
        SfxUInt32Item(ID_VAL_WHATKIND, static_cast<sal_uInt32>(PageKind::Standard)));
    pViewShell2->ExecuteSlot(aRequest, /*bAsync=*/false);

    // Start text edit in window 1.
    SdPage* pPage1 = pViewShell1->GetActualPage();
    SdrObject* pShape1 = pPage1->GetObj(0);
    SdrView* pView1 = pViewShell1->GetView();
    pView1->MarkObj(pShape1, pView1->GetSdrPageView());
    pView1->SdrBeginTextEdit(pShape1);
    CPPUNIT_ASSERT(pView1->IsTextEdit());

    // Move the shape in window 2 & undo.
    SdPage* pPage2 = pViewShell2->GetActualPage();
    CPPUNIT_ASSERT(pPage1 != pPage2);
    SdrObject* pShape2 = pPage2->GetObj(0);
    SdrView* pView2 = pViewShell2->GetView();
    pView2->MarkObj(pShape2, pView2->GetSdrPageView());
    pView2->MoveMarkedObj(Size(1, 1), /*bCopy=*/false);
    // Without the accompanying fix in place, this test would have failed with an assertion failure
    // in SdrObjEditView::SdrEndTextEdit() as mpOldTextEditUndoManager was not nullptr.
    pViewShell2->GetViewFrame()->GetDispatcher()->Execute(SID_UNDO, SfxCallMode::SYNCHRON);

    // Rotate the shape in window 2 & undo.
    pView2->MarkObj(pShape2, pView2->GetSdrPageView());
    pView2->RotateMarkedObj(pShape2->GetLastBoundRect().Center(), /*nAngle=*/45_deg100);
    // Without the accompanying fix in place, this test would have failed with an assertion failure
    // in SdrObjEditView::SdrEndTextEdit() as mpOldTextEditUndoManager was not nullptr.
    pViewShell2->GetViewFrame()->GetDispatcher()->Execute(SID_UNDO, SfxCallMode::SYNCHRON);

    // Start text edit in window 2.
    // tdf#125824
    pView2->MarkObj(pShape2, pView2->GetSdrPageView());
    pView2->SdrBeginTextEdit(pShape2);
    CPPUNIT_ASSERT(pView2->IsTextEdit());
    // Write 'test' inside the shape
    SfxStringItem aInputString(SID_ATTR_CHAR, u"test"_ustr);
    pViewShell2->GetViewFrame()->GetDispatcher()->ExecuteList(SID_ATTR_CHAR, SfxCallMode::SYNCHRON,
                                                              { &aInputString });
    CPPUNIT_ASSERT(pView2->GetTextEditObject());
    EditView& rEditView = pView2->GetTextEditOutlinerView()->GetEditView();
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(4), rEditView.GetSelection().start.nIndex);
    pView2->SdrEndTextEdit();
    // Without the accompanying fix in place, this test would have failed with an assertion failure
    // in SdrObjEditView::SdrEndTextEdit() as mpOldTextEditUndoManager was not nullptr.
    pViewShell2->GetViewFrame()->GetDispatcher()->Execute(SID_UNDO, SfxCallMode::SYNCHRON);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf126197)
{
    // Load the document and create two new windows.
    createSdImpressDoc("tdf126197.odp");
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_NEWWINDOW, SfxCallMode::SYNCHRON);
    sd::ViewShell* pViewShell1 = pImpressDocument->GetDocShell()->GetViewShell();
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_NEWWINDOW, SfxCallMode::SYNCHRON);
    sd::ViewShell* pViewShell2 = pImpressDocument->GetDocShell()->GetViewShell();
    CPPUNIT_ASSERT(pViewShell1 != pViewShell2);

    // Start text edit in window 1.
    SdPage* pPage1 = pViewShell1->GetActualPage();
    SdrObject* pShape1 = pPage1->GetObj(0);
    SdrView* pView1 = pViewShell1->GetView();
    pView1->MarkObj(pShape1, pView1->GetSdrPageView());
    pView1->SdrBeginTextEdit(pShape1);
    CPPUNIT_ASSERT(pView1->IsTextEdit());

    SdPage* pPage2 = pViewShell2->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(pPage1, pPage2);
    SdrObject* pShape2 = pPage2->GetObj(0);
    CPPUNIT_ASSERT_EQUAL(pShape1, pShape2);
    SdrView* pView2 = pViewShell2->GetView();
    pView2->MarkObj(pShape2, pView2->GetSdrPageView());

    // Without the accompanying fix in place, this test would have failed with an assertion failure
    // in SdrObjEditView::SdrEndTextEdit()
    pViewShell2->GetViewFrame()->GetDispatcher()->Execute(SID_DELETE, SfxCallMode::SYNCHRON);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf124708)
{
    createSdImpressDoc("tdf124708.ppt");

    dispatchCommand(mxComponent, u".uno:NextPage"_ustr, {});

    checkCurrentPageNumber(2);

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(16), pActualPage->GetObjCount());

    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:Delete"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pActualPage->GetObjCount());

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(16), pActualPage->GetObjCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf159666)
{
    createSdDrawDoc("tdf159666.odg");

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(12), pActualPage->GetObjCount());

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});

    dispatchCommand(mxComponent, u".uno:Delete"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pActualPage->GetObjCount());

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(12), pActualPage->GetObjCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf143412)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();

    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pActualPage->GetObjCount());

    OUString aImageURL = createFileURL(u"tdf143412.svg");
    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence({
        { "FileName", cpo::uno::Any(aImageURL) },
    }));
    dispatchCommand(mxComponent, u".uno:InsertGraphic"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    // Without the fix in place, this test would have crashed
    // Check that converting an image to the different options doesn't crash

    dispatchCommand(mxComponent, u".uno:ChangeBezier"_ustr, {});

    dispatchCommand(mxComponent, u".uno:ChangePolygon"_ustr, {});

    dispatchCommand(mxComponent, u".uno:convert_to_contour"_ustr, {});

    dispatchCommand(mxComponent, u".uno:ConvertInto3D"_ustr, {});

    dispatchCommand(mxComponent, u".uno:ConvertInto3DLatheFast"_ustr, {});

    dispatchCommand(mxComponent, u".uno:ConvertIntoBitmap"_ustr, {});

    dispatchCommand(mxComponent, u".uno:ConvertIntoMetaFile"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf155211_dashed_line)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();

    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pActualPage->GetObjCount());

    OUString aImageURL = createFileURL(u"tdf155211_dashed_line.svg");
    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence({
        { "FileName", cpo::uno::Any(aImageURL) },
    }));
    dispatchCommand(mxComponent, u".uno:InsertGraphic"_ustr, aArgs);
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    // split the (auto-selected) svg
    dispatchCommand(mxComponent, u".uno:Break"_ustr, {});

    SdrObject* pObject = pActualPage->GetObj(2);
    const XLineStyleItem& rStyleItem = pObject->GetMergedItem(XATTR_LINESTYLE);
    // tdf#115162: Without the fix in place, this test would have failed with
    // - Expected: 2 (LineStyle_DASH)
    // - Actual  : 1 (LineStyle_SOLID)
    CPPUNIT_ASSERT_EQUAL(drawing::LineStyle_DASH, rStyleItem.GetValue());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf169813_prevent_unintended_dashed_line)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();

    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pActualPage->GetObjCount());

    OUString aImageURL = createFileURL(u"tdf169813_prevent_unintended_dashed_line.svg");
    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence({
        { "FileName", cpo::uno::Any(aImageURL) },
    }));
    dispatchCommand(mxComponent, u".uno:InsertGraphic"_ustr, aArgs);
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    // split the (auto-selected) svg
    dispatchCommand(mxComponent, u".uno:Break"_ustr, {});

    SdrObject* pObject = pActualPage->GetObj(3);
    const XLineStyleItem& rStyleItem = pObject->GetMergedItem(XATTR_LINESTYLE);
    // tdf#169813: Without the fix in place, this test would have failed with
    // - Expected: 0 (LineStyle_NONE)
    // - Actual  : 2 (LineStyle_DASH)
    CPPUNIT_ASSERT_EQUAL(drawing::LineStyle_NONE, rStyleItem.GetValue());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf162455)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();

    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pActualPage->GetObjCount());

    OUString aImageURL = createFileURL(u"tdf162455.svg");
    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence({
        { "FileName", cpo::uno::Any(aImageURL) },
    }));
    dispatchCommand(mxComponent, u".uno:InsertGraphic"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    // split the (auto-selected) svg up

    dispatchCommand(mxComponent, u".uno:Break"_ustr, {});

    // Get the newly created shape that has the text '100' in it
    uno::Reference<beans::XPropertySet> xShape(getShapeFromPage(4, 0));
    uno::Reference<text::XTextRange> xParagraph1(getParagraphFromShape(0, xShape));
    uno::Reference<text::XTextRange> xRun(getRunFromParagraph(0, xParagraph1));
    CPPUNIT_ASSERT_EQUAL(u"100"_ustr, xRun->getString());

    uno::Reference<beans::XPropertySet> xPropSet1(xRun, uno::UNO_QUERY);
    double fFontSize1 = xPropSet1->getPropertyValue(u"CharHeight"_ustr).get<double>();

    /* before this fix the font sizes were way too small
      - Expected: 7.5
      - Actual  : 0.300000011920929 */
    CPPUNIT_ASSERT_DOUBLES_EQUAL(7.5, fFontSize1, 0.01);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSetHyperlinkKeepSelection)
{
    // Given an Impress doc with a rectangle shape, in text edit mode, with "f" typed and
    // selected:
    createSdImpressDoc();
    auto* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    SdPage* pPage = pViewShell->GetActualPage();

    rtl::Reference<SdrRectObj> pRect
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(1000, 1000, 5000, 5000));
    pPage->NbcInsertObject(pRect.get());

    pView->MarkObj(pRect.get(), pView->GetSdrPageView());
    pView->SdrBeginTextEdit(pRect.get());
    CPPUNIT_ASSERT(pView->IsTextEdit());

    SfxStringItem aInputString(SID_ATTR_CHAR, u"f"_ustr);
    pViewShell->GetViewFrame()->GetDispatcher()->ExecuteList(SID_ATTR_CHAR, SfxCallMode::SYNCHRON,
                                                             { &aInputString });

    OutlinerView* pOlView = pView->GetTextEditOutlinerView();
    CPPUNIT_ASSERT(pOlView);
    pOlView->SetSelection(ESelection(0, 0, 0, 1));
    CPPUNIT_ASSERT(pOlView->HasSelection());

    // When dispatching .uno:SetHyperlink with Hyperlink.Text differing from the selection,
    // but marked as a hint:
    cpo::uno::Sequence<beans::PropertyValue> aArgs = {
        comphelper::makePropertyValue(u"Hyperlink.Text"_ustr, cpo::uno::Any(u"mytext"_ustr)),
        comphelper::makePropertyValue(u"Hyperlink.TextIsHint"_ustr, cpo::uno::Any(true)),
        comphelper::makePropertyValue(u"Hyperlink.URL"_ustr,
                                      cpo::uno::Any(u"http://www.example.com"_ustr)),
    };
    dispatchCommand(mxComponent, u".uno:SetHyperlink"_ustr, aArgs);

    // Then make sure the previously-selected character is preserved as the URL field's
    // displayed text, not replaced by Hyperlink.Text:
    pView->SdrEndTextEdit();
    const OutlinerParaObject* pOPO = pRect->GetOutlinerParaObject();
    CPPUNIT_ASSERT(pOPO);
    const SvxFieldData* pField
        = pOPO->GetTextObject().GetFieldData(0, 0, css::text::textfield::Type::URL);
    auto* pURLField = dynamic_cast<const SvxURLField*>(pField);
    CPPUNIT_ASSERT(pURLField);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: f
    // - Actual  : mytext
    // i.e. the selected character was lost, the provided text hint was used.
    CPPUNIT_ASSERT_EQUAL(u"f"_ustr, pURLField->GetRepresentation());
}

static SdrObject* lcl_findTextBoxByText(const SdPage* pPage, std::u16string_view rText)
{
    for (size_t i = 0; i < pPage->GetObjCount(); ++i)
    {
        SdrObject* pObj = pPage->GetObj(i);
        const SdrTextObj* pTextObj = DynCastSdrTextObj(pObj);
        if (!pTextObj || !pTextObj->HasText())
            continue;
        const OutlinerParaObject* pOPO = pTextObj->GetOutlinerParaObject();
        if (pOPO && pOPO->GetTextObject().GetText(0) == rText)
            return pObj;
    }
    return nullptr;
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPasteTextboxBackground)
{
    // The textbox 'xxx' in the source has draw:fill="none" inherited from a
    // layout-specific named style ("Content_Outline_Normal-outline1") on its
    // master page, not from the auto-style on the frame itself. Copying it
    // into a different document used to drop the inherited fill: the
    // destination's style pool had no "Content_Outline_Normal-outline1", so
    // AttributeProperties' by-name lookup returned null and the clone fell
    // through to applyDefaultStyleSheetFromSdrModel(), giving the pasted
    // shape a solid (white) background.
    //
    // Hold the source via mxComponent2 so it stays alive while we open the
    // destination via mxComponent.
    mxComponent2 = loadFromDesktop(createFileURL(u"odp/paste-textbox-background.odp"),
                                   u"com.sun.star.presentation.PresentationDocument"_ustr);
    CPPUNIT_ASSERT(mxComponent2.is());

    auto* pSrcImpress = dynamic_cast<SdXImpressDocument*>(mxComponent2.get());
    CPPUNIT_ASSERT(pSrcImpress);
    sd::ViewShell* pSrcViewShell = pSrcImpress->GetDocShell()->GetViewShell();
    SdPage* pSrcPage = pSrcViewShell->GetActualPage();

    SdrObject* pSrcTextBox = lcl_findTextBoxByText(pSrcPage, u"xxx");
    CPPUNIT_ASSERT_MESSAGE("source textbox 'xxx' not found", pSrcTextBox);

    // The source's effective fill is none, courtesy of the layout parent.
    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_NONE,
                         pSrcTextBox->GetMergedItem(XATTR_FILLSTYLE).GetValue());

    // Mark the textbox and copy via .uno:Copy. This goes through the
    // standard same-process clipboard path: View::DoCopy ->
    // CreateClipboardDataObject sets SdModule::pTransferClip and puts the
    // data on the system clipboard.
    SdrView* pSrcView = pSrcViewShell->GetView();
    pSrcView->UnmarkAllObj();
    pSrcView->MarkObj(pSrcTextBox, pSrcView->GetSdrPageView());
    dispatchCommand(mxComponent2, u".uno:Copy"_ustr, {});

    // Open a fresh destination - mxComponent2 stays untouched.
    createSdImpressDoc();
    auto* pDstImpress = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pDstImpress);
    sd::ViewShell* pDstViewShell = pDstImpress->GetDocShell()->GetViewShell();
    SdPage* pDstPage = pDstViewShell->GetActualPage();
    const size_t nDstObjsBefore = pDstPage->GetObjCount();

    // Paste in the destination. The destination's InsertData recognises
    // SdModule::pTransferClip as own data and goes through the same-process
    // branch, calling our View::Paste(SdrModel) override on the source's
    // intermediate model.
    dispatchCommand(mxComponent, u".uno:Paste"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(nDstObjsBefore + 1, pDstPage->GetObjCount());
    SdrObject* pDstShape = pDstPage->GetObj(nDstObjsBefore);

    // Without the fix in sd::View::Paste(const SdrModel&, ...) this would
    // be FillStyle_SOLID and the pasted shape would have a different
    // background.
    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_NONE,
                         pDstShape->GetMergedItem(XATTR_FILLSTYLE).GetValue());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPasteTextboxBackgroundCrossProcess)
{
    // Same scenario as testPasteTextboxBackground, but exercising the
    // cross-process path that online (coolwsd) uses. There the source kit
    // and the destination kit are different processes, so the destination
    // sees the clipboard data as a generic non-SdTransferable and goes
    // through InsertData's DRAWING import branch: SvxDrawingLayerImport
    // into a fresh SdDrawDocument, then Paste(rMod).
    //
    // Simulate that here by clearing SdModule::pTransferClip after the
    // copy, so the destination's InsertData no longer recognises the
    // transferable as own data.
    mxComponent2 = loadFromDesktop(createFileURL(u"odp/paste-textbox-background.odp"),
                                   u"com.sun.star.presentation.PresentationDocument"_ustr);
    CPPUNIT_ASSERT(mxComponent2.is());

    auto* pSrcImpress = dynamic_cast<SdXImpressDocument*>(mxComponent2.get());
    CPPUNIT_ASSERT(pSrcImpress);
    sd::ViewShell* pSrcViewShell = pSrcImpress->GetDocShell()->GetViewShell();
    SdPage* pSrcPage = pSrcViewShell->GetActualPage();

    SdrObject* pSrcTextBox = lcl_findTextBoxByText(pSrcPage, u"xxx");
    CPPUNIT_ASSERT_MESSAGE("source textbox 'xxx' not found", pSrcTextBox);
    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_NONE,
                         pSrcTextBox->GetMergedItem(XATTR_FILLSTYLE).GetValue());

    SdrView* pSrcView = pSrcViewShell->GetView();
    pSrcView->UnmarkAllObj();
    pSrcView->MarkObj(pSrcTextBox, pSrcView->GetSdrPageView());
    dispatchCommand(mxComponent2, u".uno:Copy"_ustr, {});

    // Make the destination see the transferable as foreign: this drops it
    // into the DRAWING-import branch of InsertData, the same one online
    // takes when the source kit is a different process.
    SdModule::get()->pTransferClip = nullptr;

    createSdImpressDoc();
    auto* pDstImpress = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pDstImpress);
    sd::ViewShell* pDstViewShell = pDstImpress->GetDocShell()->GetViewShell();
    SdPage* pDstPage = pDstViewShell->GetActualPage();
    const size_t nDstObjsBefore = pDstPage->GetObjCount();

    dispatchCommand(mxComponent, u".uno:Paste"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(nDstObjsBefore + 1, pDstPage->GetObjCount());
    SdrObject* pDstShape = pDstPage->GetObj(nDstObjsBefore);

    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_NONE,
                         pDstShape->GetMergedItem(XATTR_FILLSTYLE).GetValue());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPasteKeepsPositionInKit)
{
    // When kit is active the destination's visible area is not a reliable
    // paste position. A cross-document paste arrives with the selection
    // normalised to the origin, so place the group's top-left at the page
    // origin instead of recentering it on that visible area. Recentering
    // shifted the pasted shape away from the corner.
    mxComponent2 = loadFromDesktop(createFileURL(u"odp/paste-textbox-background.odp"),
                                   u"com.sun.star.presentation.PresentationDocument"_ustr);
    CPPUNIT_ASSERT(mxComponent2.is());

    auto* pSrcImpress = dynamic_cast<SdXImpressDocument*>(mxComponent2.get());
    CPPUNIT_ASSERT(pSrcImpress);
    sd::ViewShell* pSrcViewShell = pSrcImpress->GetDocShell()->GetViewShell();
    SdPage* pSrcPage = pSrcViewShell->GetActualPage();

    SdrObject* pSrcTextBox = lcl_findTextBoxByText(pSrcPage, u"xxx");
    CPPUNIT_ASSERT_MESSAGE("source textbox 'xxx' not found", pSrcTextBox);
    const Size aSrcSize = pSrcTextBox->GetSnapRect().GetSize();

    SdrView* pSrcView = pSrcViewShell->GetView();
    pSrcView->UnmarkAllObj();
    pSrcView->MarkObj(pSrcTextBox, pSrcView->GetSdrPageView());
    dispatchCommand(mxComponent2, u".uno:Copy"_ustr, {});

    // Make the destination see the clipboard as foreign, the cross-process
    // path online takes, so the paste goes through View::Paste(SdrModel).
    SdModule::get()->pTransferClip = nullptr;

    createSdImpressDoc();
    auto* pDstImpress = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pDstImpress);
    sd::ViewShell* pDstViewShell = pDstImpress->GetDocShell()->GetViewShell();
    SdPage* pDstPage = pDstViewShell->GetActualPage();
    const size_t nDstObjsBefore = pDstPage->GetObjCount();

    {
        comphelper::COKit::setActive(true);
        comphelper::ScopeGuard aKitGuard([] { comphelper::COKit::setActive(false); });
        dispatchCommand(mxComponent, u".uno:Paste"_ustr, {});
    }

    CPPUNIT_ASSERT_EQUAL(nDstObjsBefore + 1, pDstPage->GetObjCount());
    SdrObject* pDstShape = pDstPage->GetObj(nDstObjsBefore);

    // The pasted shape sits at the page origin with its size preserved,
    // rather than being shifted by the recentering offset.
    CPPUNIT_ASSERT_EQUAL(tools::Rectangle(Point(0, 0), aSrcSize),
                         pDstShape->GetSnapRect());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSlidePasteSizeMismatch)
{
    // Given a 4:3 presentation, with the first slide selected:
    createSdImpressDoc("odp/4-3.odp");
    sd::slidesorter::controller::SlideSorterController& rSrcCtl
        = getSlideSorterViewShell()->GetSlideSorter().GetController();
    auto pSrcImpress = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdPage* pSrcFirst = pSrcImpress->GetDoc()->GetSdPage(0, PageKind::Standard);
    rSrcCtl.GetPageSelector().DeselectAllPages();
    rSrcCtl.GetPageSelector().SelectPage(pSrcFirst);

    // When copying that slide and pasting it into a 16:9 presentation:
    rSrcCtl.GetClipboard().DoCopy();
    // Disable internal paste to hit the clipboard save + load code-path.
    SdModule::get()->pTransferClip = nullptr;
    mxComponent2 = loadFromDesktop(createFileURL(u"odp/16-9.odp"),
                                   u"com.sun.star.presentation.PresentationDocument"_ustr);
    auto* pDstImpress = dynamic_cast<SdXImpressDocument*>(mxComponent2.get());
    SdDrawDocument* pDstDoc = pDstImpress->GetDoc();
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(1), pDstDoc->GetSdPageCount(PageKind::Standard));
    sd::slidesorter::SlideSorterViewShell* pDstSSVS = getSlideSorterViewShell(mxComponent2);
    sd::slidesorter::controller::SlideSorterController& rDstCtl
        = pDstSSVS->GetSlideSorter().GetController();
    SdPage* pDstLast = pDstDoc->GetSdPage(0, PageKind::Standard);
    rDstCtl.GetPageSelector().DeselectAllPages();
    rDstCtl.GetPageSelector().SelectPage(pDstLast);
    rDstCtl.GetClipboard().PasteSlidesFromSystemClipboard();

    // Then make sure paste results in 2 slides:
    sal_uInt16 nActualPageCount = pDstDoc->GetSdPageCount(PageKind::Standard);
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 2
    // - Actual  : 1
    // i.e. an unexpected non-async dialog popped up, which gets cancelled in silent mode, so the
    // paste fails.
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2), nActualPageCount);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf96206)
{
    // Copying/pasting slide referring to a non-default master with a text duplicated the master

    createSdImpressDoc("odp/tdf96206.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), nMasterPageCnt1);
    rSSController.GetClipboard().DoCopy();
    rSSController.GetClipboard().DoPaste();
    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(nMasterPageCnt1, nMasterPageCnt2);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testDocumentCut)
{
    // Test cutting slides and verifying the document state afterwards
    createSdImpressDoc("odp/tdf96206.odp");

    // Get the slide sorter and controller
    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    // Get document and initial page count
    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    // Get initial page count
    const sal_uInt16 nInitialPageCount = pDoc->GetSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT(nInitialPageCount > 0);

    // Select all slides
    rSSController.GetPageSelector().SelectAllPages();

    // Check that slides are selected
    CPPUNIT_ASSERT(rSSController.GetPageSelector().GetSelectedPageCount() > 0);

    // Cut the selected slides
    rSSController.GetClipboard().DoCut();

    // Paste the cut slides
    rSSController.GetClipboard().DoPaste();

    // After pasting, we should have at least the initial number of pages
    const sal_uInt16 nPageCountAfterPaste = pDoc->GetSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT(nPageCountAfterPaste >= nInitialPageCount);

    // Verify that master page count remains unchanged throughout the operation
    const sal_uInt16 nMasterPageCount = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), nMasterPageCount);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf96708)
{
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    auto& rPageSelector = rSSController.GetPageSelector();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(4), nMasterPageCnt1);
    rSSController.GetClipboard().DoCopy();
    rPageSelector.SelectAllPages();

    // Now wait for timers to trigger creation of auto-layout
    osl::Thread::wait(std::chrono::milliseconds(100));
    Scheduler::ProcessEventsToIdle();

    rSSController.GetClipboard().DoPaste();

    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    //FIXME: tdf#151802: Number of master pages should be 4, it's 5 instead
    // CPPUNIT_ASSERT_EQUAL(nMasterPageCnt1, nMasterPageCnt2);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(5), nMasterPageCnt2);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_default_master)
{
    // Copying/pasting slide referring to a master page.
    createSdImpressDoc("odp/tdf96206.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), nMasterPageCnt1);
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(nMasterPageCnt1 + 1), nMasterPageCnt2);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617)
{
    // Copying/pasting slide referring to a master page.
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(4), nMasterPageCnt1);
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(nMasterPageCnt1 + 1), nMasterPageCnt2);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_test_master_name)
{
    // Copying/pasting slide referring to a master page.
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    // Get the master page name
    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master0"_ustr, aMasterPageName);
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master0"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_test_default_master_name)
{
    // Copying/pasting slide referring to a master page.
    createSdImpressDoc();

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    // Get the master page name
    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Default"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Default"_ustr, aNewMasterPageName);

    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage2 = pDoc->GetMasterSdPage(1, PageKind::Standard);
    OUString aNewMasterPageName2 = pNewMasterPage2->GetName();
    CPPUNIT_ASSERT_EQUAL(u"2_Default"_ustr, aNewMasterPageName2);
}
CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_basic_master_name)
{
    // Basic test for copying/pasting slide referring to a master page
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master0"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master0"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_leading_underscore_master_name)
{
    // Test master page name with underscore at first position
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with leading underscore
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"_Master0"_ustr);

    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"_Master0"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1__Master0"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_middle_underscore_master_name)
{
    // Test master page name with underscore in the middle
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with middle underscore
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"Master_0"_ustr);

    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master_0"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master_0"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_trailing_underscore_master_name)
{
    // Test master page name with underscore at the end
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with trailing underscore
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"Master0_"_ustr);

    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master0_"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master0_"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_space_in_master_name)
{
    // Test master page name with space in the middle
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with space in the middle
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"Master 0"_ustr);

    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master 0"_ustr, aMasterPageName);

    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master 0"_ustr, aNewMasterPageName);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_Double_Copy)
{
    // Test master page name with double copy
    createSdImpressDoc();

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    // Set edit master page mode
    rSSController.ChangeEditMode(EditMode::MasterPage);

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    // Get the master page name
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), nMasterPageCnt1);
    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with master name
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"master"_ustr);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"master"_ustr, aMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);

    // Copy and paste the master page
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(2), nMasterPageCnt2);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(1, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_master"_ustr, aNewMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);

    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    const sal_uInt16 nMasterPageCnt3 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(3), nMasterPageCnt3);
    SdPage* pNewMasterPage2 = pDoc->GetMasterSdPage(2, PageKind::Standard);
    OUString aNewMasterPageName2 = pNewMasterPage2->GetName();
    CPPUNIT_ASSERT_EQUAL(u"2_master"_ustr, aNewMasterPageName2);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);

    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt4 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4), nMasterPageCnt4);
    SdPage* pNewMasterPage3 = pDoc->GetMasterSdPage(3, PageKind::Standard);
    OUString aNewMasterPageName3 = pNewMasterPage3->GetName();
    CPPUNIT_ASSERT_EQUAL(u"3_master"_ustr, aNewMasterPageName3);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_Double_Copy_Default)
{
    // Test master page name with double copy
    createSdImpressDoc();

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    // Set edit master page mode
    rSSController.ChangeEditMode(EditMode::MasterPage);

    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();

    // Get the master page name
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(1), nMasterPageCnt1);
    SdPage* pMasterPage = pDoc->GetMasterSdPage(0, PageKind::Standard);
    // Rename the master page with default name
    pDoc->RenameLayoutTemplate(pMasterPage->GetLayoutName(), u"Default"_ustr);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Default"_ustr, aMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);

    // Copy and paste the master page
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(2), nMasterPageCnt2);

    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(1, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Default"_ustr, aNewMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);
    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    const sal_uInt16 nMasterPageCnt3 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(3), nMasterPageCnt3);
    SdPage* pNewMasterPage2 = pDoc->GetMasterSdPage(2, PageKind::Standard);
    OUString aNewMasterPageName2 = pNewMasterPage2->GetName();
    CPPUNIT_ASSERT_EQUAL(u"2_Default"_ustr, aNewMasterPageName2);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);
    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);
    const sal_uInt16 nMasterPageCnt4 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(4), nMasterPageCnt4);
    SdPage* pNewMasterPage3 = pDoc->GetMasterSdPage(3, PageKind::Standard);
    OUString aNewMasterPageName3 = pNewMasterPage3->GetName();
    CPPUNIT_ASSERT_EQUAL(u"3_Default"_ustr, aNewMasterPageName3);
}
CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf45617_Double_Copy_CopiedPage)
{
    // Test master page name with double copy for copied page which doesn't have a normal page created
    createSdImpressDoc("odp/tdf96708.odp");
    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    // Set edit master page mode
    rSSController.ChangeEditMode(EditMode::MasterPage);
    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    // Get the master page name
    const sal_uInt16 nMasterPageCnt1 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(4), nMasterPageCnt1);
    SdPage* pMasterPage = pDoc->GetMasterSdPage(3, PageKind::Standard);
    OUString aMasterPageName = pMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"Master3"_ustr, aMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pMasterPage);
    // Copy and paste the master page
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt2 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(5), nMasterPageCnt2);
    SdPage* pNewMasterPage = pDoc->GetMasterSdPage(4, PageKind::Standard);
    OUString aNewMasterPageName = pNewMasterPage->GetName();
    CPPUNIT_ASSERT_EQUAL(u"1_Master3"_ustr, aNewMasterPageName);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pNewMasterPage);
    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt3 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(6), nMasterPageCnt3);
    SdPage* pNewMasterPage2 = pDoc->GetMasterSdPage(5, PageKind::Standard);
    OUString aNewMasterPageName2 = pNewMasterPage2->GetName();
    CPPUNIT_ASSERT_EQUAL(u"2_Master3"_ustr, aNewMasterPageName2);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pNewMasterPage2);
    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt4 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(7), nMasterPageCnt4);
    SdPage* pNewMasterPage3 = pDoc->GetMasterSdPage(6, PageKind::Standard);
    OUString aNewMasterPageName3 = pNewMasterPage3->GetName();
    CPPUNIT_ASSERT_EQUAL(u"3_Master3"_ustr, aNewMasterPageName3);

    // Reselect the master page
    rSSController.GetPageSelector().DeselectAllPages();
    rSSController.GetPageSelector().SelectPage(pNewMasterPage);
    // Copy again
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    const sal_uInt16 nMasterPageCnt5 = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(8), nMasterPageCnt5);
    SdPage* pNewMasterPage4 = pDoc->GetMasterSdPage(7, PageKind::Standard);
    OUString aNewMasterPageName4 = pNewMasterPage4->GetName();
    CPPUNIT_ASSERT_EQUAL(u"4_Master3"_ustr, aNewMasterPageName4);
}
CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testUndoMergeMasterPage)
{
    // Test undo for merging master pages only
    createSdImpressDoc("odp/tdf96708.odp");

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();

    // Get document and check initial master page count
    SdXImpressDocument* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDoc = pXImpressDocument->GetDoc();
    const sal_uInt16 nInitialMasterPageCount = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(sal_uInt16(4), nInitialMasterPageCount);

    // Copy and paste master page only
    rSSController.GetClipboard().DoCopy(true);
    rSSController.GetClipboard().DoPaste(true);

    // Check that a master page was added
    const sal_uInt16 nAfterPasteMasterPageCount = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_uInt16>(nInitialMasterPageCount + 1),
                         nAfterPasteMasterPageCount);

    // Undo the master page merge
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});

    // Check that the master page count is back to the initial value
    const sal_uInt16 nAfterUndoMasterPageCount = pDoc->GetMasterSdPageCount(PageKind::Standard);
    CPPUNIT_ASSERT_EQUAL(nInitialMasterPageCount, nAfterUndoMasterPageCount);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf139996)
{
    createSdImpressDoc();

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    auto& rPageSelector = rSSController.GetPageSelector();

    CPPUNIT_ASSERT_EQUAL(1, rPageSelector.GetSelectedPageCount());

    rPageSelector.DeselectAllPages();

    CPPUNIT_ASSERT_EQUAL(0, rPageSelector.GetSelectedPageCount());

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:MovePageUp"_ustr, {});

    dispatchCommand(mxComponent, u".uno:MovePageDown"_ustr, {});

    dispatchCommand(mxComponent, u".uno:MovePageTop"_ustr, {});

    dispatchCommand(mxComponent, u".uno:MovePageBottom"_ustr, {});

    CPPUNIT_ASSERT_EQUAL(0, rPageSelector.GetSelectedPageCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf128651)
{
    // Error was, that undo and redo changes size of the shape. Affected actions were e.g.
    // extrusion on/off, shadow on/off, changes on line or fill attributes.
    // All these actions do not change the snap rectangle.
    createSdImpressDoc("tdf128651_CustomShapeUndo.odp");
    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    SdrObject* pObject = pActualPage->GetObj(0);
    auto pCustomShape = dynamic_cast<SdrObjCustomShape*>(pObject);
    CPPUNIT_ASSERT_MESSAGE("No Shape", pCustomShape);
    const sal_Int32 nOrigWidth(pCustomShape->GetSnapRect().GetWidth());

    SdDrawDocument* pDocument = pXImpressDocument->GetDoc();
    sd::UndoManager* pUndoManager = pDocument->GetUndoManager();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pUndoManager->GetRedoActionCount());

    SdrView* pView = pViewShell->GetView();
    pView->MarkObj(pCustomShape, pView->GetSdrPageView());
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_EXTRUSION_TOGGLE,
                                                         SfxCallMode::SYNCHRON);
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetUndoActionCount());

    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_UNDO, SfxCallMode::SYNCHRON);
    const sal_Int32 nUndoWidth(pCustomShape->GetSnapRect().GetWidth());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Undo changes width", nOrigWidth, nUndoWidth);

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetRedoActionCount());
    pViewShell->GetViewFrame()->GetDispatcher()->Execute(SID_REDO, SfxCallMode::SYNCHRON);
    const sal_Int32 nRedoWidth(pCustomShape->GetSnapRect().GetWidth());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Redo changes width", nUndoWidth, nRedoWidth);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf126605)
{
    createSdImpressDoc();

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});

    insertStringToObject(0, u"Test", /*bUseEscape*/ false);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(1),
                                                 uno::UNO_QUERY);

    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);

    uno::Reference<text::XText> xText
        = uno::Reference<text::XTextRange>(xShape, uno::UNO_QUERY_THROW)->getText();
    CPPUNIT_ASSERT_MESSAGE("Not a text shape", xText.is());

    uno::Reference<container::XEnumerationAccess> paraEnumAccess(xText, uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> paraEnum(paraEnumAccess->createEnumeration());

    // Get first paragraph
    uno::Reference<text::XTextRange> xParagraph(paraEnum->nextElement(), uno::UNO_QUERY_THROW);
    uno::Reference<beans::XPropertySet> xPropSet(xParagraph, uno::UNO_QUERY_THROW);

    sal_Int16 nWritingMode = 0;
    xPropSet->getPropertyValue(u"WritingMode"_ustr) >>= nWritingMode;
    CPPUNIT_ASSERT_EQUAL(text::WritingMode2::LR_TB, nWritingMode);

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:ParaRightToLeft"_ustr, {});

    xPropSet->getPropertyValue(u"WritingMode"_ustr) >>= nWritingMode;
    CPPUNIT_ASSERT_EQUAL(text::WritingMode2::RL_TB, nWritingMode);

    dispatchCommand(mxComponent, u".uno:ParaLeftToRight"_ustr, {});

    xPropSet->getPropertyValue(u"WritingMode"_ustr) >>= nWritingMode;
    CPPUNIT_ASSERT_EQUAL(text::WritingMode2::LR_TB, nWritingMode);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf167029PasteKeepsOutlineLevel)
{
    // Copy a level-1 (depth 0) list paragraph and paste it into a level-2
    // (depth 1) paragraph in various ways. Pasting into a list paragraph must
    // keep that paragraph's own outline level, whatever the paste position and
    // whether the destination is empty. bRtf forces the RTF clipboard flavour
    // instead of the default flat-XML one, since both go through the same
    // paragraph-merge code.
    enum class PastePos
    {
        AfterBullet, // caret right after the bullet (start of the text)
        InMiddle,    // caret in the middle of the text
        AtEnd,       // caret at the end of the line
        IntoEmpty,   // caret in an empty destination paragraph
    };
    auto posName = [](PastePos ePos) -> std::string {
        switch (ePos)
        {
            case PastePos::AfterBullet: return "AfterBullet";
            case PastePos::InMiddle: return "InMiddle";
            case PastePos::AtEnd: return "AtEnd";
            case PastePos::IntoEmpty: return "IntoEmpty";
        }
        return "?";
    };
    auto checkCase = [this, &posName](PastePos ePos, bool bRtf) {
        createSdImpressDoc();
        auto pDoc = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
        sd::ViewShell* pViewShell = pDoc->GetDocShell()->GetViewShell();
        pViewShell->GetActualPage()->SetAutoLayout(AUTOLAYOUT_TITLE_CONTENT, true);
        SdrView* pView = pViewShell->GetView();

        insertStringToObject(1, u"AAA", /*bUseEscape*/ false); // para 0, depth 0
        typeKey(pDoc, KEY_RETURN);
        typeKey(pDoc, KEY_TAB);                                // demote -> depth 1
        if (ePos != PastePos::IntoEmpty)
            typeString(pDoc, u"BBB");                          // para 1, depth 1

        OutlinerView* pOlView = pView->GetTextEditOutlinerView();
        CPPUNIT_ASSERT(pOlView);
        Outliner& rOut = pOlView->GetOutliner();
        CPPUNIT_ASSERT_EQUAL(sal_Int16(0), rOut.GetDepth(0));
        CPPUNIT_ASSERT_EQUAL(sal_Int16(1), rOut.GetDepth(1));

        pOlView->SetSelection(ESelection(0, 0, 0, 3));         // select "AAA"
        pOlView->Copy();

        const sal_Int32 nLen = rOut.GetText(rOut.GetParagraph(1)).getLength();
        ESelection aDest(1, 0, 1, 0);
        switch (ePos)
        {
            case PastePos::AfterBullet: aDest = ESelection(1, 0, 1, 0); break;
            case PastePos::InMiddle: aDest = ESelection(1, nLen / 2, 1, nLen / 2); break;
            case PastePos::AtEnd: aDest = ESelection(1, nLen, 1, nLen); break;
            case PastePos::IntoEmpty: aDest = ESelection(1, 0, 1, 0); break;
        }
        pOlView->SetSelection(aDest);
        // Force the clipboard flavour: a plain OutlinerView::Paste() would paste
        // unformatted text and never exercise the paragraph-merge under test.
        pOlView->PasteSpecial(bRtf ? SotClipboardFormatId::RTF
                                   : SotClipboardFormatId::EDITENGINE_ODF_TEXT_FLAT);

        const std::string aCase = posName(ePos) + (bRtf ? " (RTF)" : " (flat-XML)");

        // Make sure the copied text was actually pasted into the paragraph, so
        // the depth check below is meaningful (and not a no-op paste).
        CPPUNIT_ASSERT_MESSAGE("copied text was not pasted; " + aCase,
                               rOut.GetText(rOut.GetParagraph(1)).indexOf(u"AAA") >= 0);

        // The destination paragraph keeps its own level 2 (depth 1). Without the
        // fix, pasting at the end of the line dropped it to depth 0 (the copied
        // paragraph's level).
        CPPUNIT_ASSERT_EQUAL_MESSAGE(aCase, sal_Int16(1), rOut.GetDepth(1));
    };
    for (bool bRtf : { false, true }) // flat-XML and RTF clipboard flavours
    {
        checkCase(PastePos::AfterBullet, bRtf);
        checkCase(PastePos::InMiddle, bRtf);
        checkCase(PastePos::AtEnd, bRtf);
        checkCase(PastePos::IntoEmpty, bRtf);
    }
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf167029PasteIntoNonOutline)
{
    // Complement to testTdf167029PasteKeepsOutlineLevel: when the destination
    // paragraph is not part of an outline (it has no outline level of its own,
    // depth -1), pasting a leveled paragraph applies the pasted (source) level
    // instead of forcing the destination's "no level" onto the merged text.
    createSdImpressDoc();
    auto pDoc = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pDoc->GetDocShell()->GetViewShell();
    SdPage* pPage = pViewShell->GetActualPage();
    pPage->SetAutoLayout(AUTOLAYOUT_TITLE_CONTENT, true);
    SdrView* pView = pViewShell->GetView();

    // Copy a level-1 (depth 0) paragraph from the outline placeholder.
    insertStringToObject(1, u"AAA", /*bUseEscape*/ false);
    OutlinerView* pOlView = pView->GetTextEditOutlinerView();
    CPPUNIT_ASSERT_EQUAL(sal_Int16(0), pOlView->GetOutliner().GetDepth(0));
    pOlView->SetSelection(ESelection(0, 0, 0, 3));
    pOlView->Copy();
    pView->SdrEndTextEdit();

    // A plain text box: its paragraph is not part of an outline (depth -1).
    rtl::Reference<SdrRectObj> pRect
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(1000, 1000, 9000, 5000));
    pPage->NbcInsertObject(pRect.get());
    pView->MarkObj(pRect.get(), pView->GetSdrPageView());
    pView->SdrBeginTextEdit(pRect.get());
    typeString(pDoc, u"BBB");
    OutlinerView* pBoxView = pView->GetTextEditOutlinerView();
    Outliner& rBoxOut = pBoxView->GetOutliner();
    CPPUNIT_ASSERT_EQUAL(sal_Int16(-1), rBoxOut.GetDepth(0));

    // Paste the copied outline paragraph at the end of the text box paragraph.
    const sal_Int32 nLen = rBoxOut.GetText(rBoxOut.GetParagraph(0)).getLength();
    pBoxView->SetSelection(ESelection(0, nLen, 0, nLen));
    pBoxView->PasteSpecial(SotClipboardFormatId::EDITENGINE_ODF_TEXT_FLAT);
    CPPUNIT_ASSERT_EQUAL(u"BBBAAA"_ustr, rBoxOut.GetText(rBoxOut.GetParagraph(0)));

    // Without an outline level of its own, the destination takes the pasted
    // (source) level 0 rather than keeping its "no level" (-1).
    CPPUNIT_ASSERT_EQUAL(sal_Int16(0), rBoxOut.GetDepth(0));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, tdf139269_prevent_pasting_into_readonly_master_objects)
{
    createSdImpressDoc();

    insertStringToObject(0, u"Test", /*bUseEscape*/ false);

    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});
    dispatchCommand(mxComponent, u".uno:Copy"_ustr, {});

    dispatchCommand(mxComponent, u".uno:SlideMasterPage"_ustr, {});

    // Use tab key to select the first object and try to type something to go
    // into edit mode although it's not possible to add text to it
    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    typeKey(pXImpressDocument, KEY_TAB);
    typeString(pXImpressDocument, u"test");

    dispatchCommand(mxComponent, u".uno:Paste"_ustr, {});

    uno::Reference<drawing::XMasterPagesSupplier> xMasterPagesSupplier(mxComponent,
                                                                       uno::UNO_QUERY_THROW);
    uno::Reference<drawing::XDrawPages> xMasterPages(xMasterPagesSupplier->getMasterPages());
    CPPUNIT_ASSERT(xMasterPages.is());
    uno::Reference<drawing::XDrawPage> xDrawPage(xMasterPages->getByIndex(0), uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);

    // Without the fix in place, this test would have failed with
    // - Expected: Click to edit the title text format
    // - Actual  : Click to edit the title text formatTest
    CPPUNIT_ASSERT_EQUAL(u"Click to edit the title text format"_ustr, xShape->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf100950)
{
    createSdImpressDoc();

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});

    insertStringToObject(0, u"Test", /*bUseEscape*/ true);

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});

    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    auto& rPageSelector = rSSController.GetPageSelector();

    // Without the fix in place, this test would have failed here
    CPPUNIT_ASSERT(rPageSelector.IsPageSelected(2));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf130581_undo_hide_show_slide)
{
    createSdImpressDoc();

    // Hide slide and check the number of available undo actions
    dispatchCommand(mxComponent, u".uno:ShowSlide"_ustr, {});
    dispatchCommand(mxComponent, u".uno:HideSlide"_ustr, {});

    // There should be a single undo action, i.e., hide slide
    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    SdDrawDocument* pDocument = pXImpressDocument->GetDoc();
    sd::UndoManager* pUndoManager = pDocument->GetUndoManager();
    // Check if there is the correct undo action, i.e., hide slide
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(SdResId(STR_UNDO_HIDE_SLIDE), pUndoManager->GetUndoActionComment());
    sd::slidesorter::SlideSorterViewShell* pSSVS = getSlideSorterViewShell();

    // Check if the page is actually hidden
    auto& rSSController = pSSVS->GetSlideSorter().GetController();
    auto& rPageSelector = rSSController.GetPageSelector();
    CPPUNIT_ASSERT_EQUAL(true, rPageSelector.IsPageExcluded(0));

    // Undo hide slide action and check the number of available redo actions
    // including the correct undo action, i.e., hide slide
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetRedoActionCount());
    CPPUNIT_ASSERT_EQUAL(SdResId(STR_UNDO_HIDE_SLIDE), pUndoManager->GetRedoActionComment());
    CPPUNIT_ASSERT_EQUAL(false, rPageSelector.IsPageExcluded(0));

    // Show slide and check the number of available undo actions
    dispatchCommand(mxComponent, u".uno:Redo"_ustr, {});
    CPPUNIT_ASSERT_EQUAL(true, rPageSelector.IsPageExcluded(0));
    dispatchCommand(mxComponent, u".uno:ShowSlide"_ustr, {});
    // There should be two undo actions, i.e., show and hide slide
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(SdResId(STR_UNDO_SHOW_SLIDE), pUndoManager->GetUndoActionComment());
    CPPUNIT_ASSERT_EQUAL(false, rPageSelector.IsPageExcluded(0));

    // Undo show slide and check the number of available undo/redo actions
    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    // There should be one undo action, i.e., hide slide, and one redo action, i.e., show slide
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(SdResId(STR_UNDO_HIDE_SLIDE), pUndoManager->GetUndoActionComment());
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), pUndoManager->GetRedoActionCount());
    CPPUNIT_ASSERT_EQUAL(SdResId(STR_UNDO_SHOW_SLIDE), pUndoManager->GetRedoActionComment());
    CPPUNIT_ASSERT_EQUAL(true, rPageSelector.IsPageExcluded(0));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf129346)
{
    createSdImpressDoc();

    dispatchCommand(mxComponent, u".uno:DiaMode"_ustr, {});
    checkCurrentPageNumber(1);

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});
    checkCurrentPageNumber(2);

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});
    checkCurrentPageNumber(1);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testmoveSlides)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();

    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({ { "PageName", cpo::uno::Any(u"Test 1"_ustr) },
                                           { "WhatLayout", cpo::uno::Any(sal_Int32(1)) },
                                           { "IsPageBack", cpo::uno::Any(false) },
                                           { "IsPageObj", cpo::uno::Any(false) } }));

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, aArgs);
    checkCurrentPageNumber(2);

    CPPUNIT_ASSERT_EQUAL(u"Test 1"_ustr, pViewShell->GetActualPage()->GetName());

    aArgs = comphelper::InitPropertySequence({ { "PageName", cpo::uno::Any(u"Test 2"_ustr) },
                                               { "WhatLayout", cpo::uno::Any(sal_Int32(1)) },
                                               { "IsPageBack", cpo::uno::Any(false) },
                                               { "IsPageObj", cpo::uno::Any(false) } });

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, aArgs);
    checkCurrentPageNumber(3);

    CPPUNIT_ASSERT_EQUAL(u"Test 2"_ustr, pViewShell->GetActualPage()->GetName());

    // Move slide 'Test 2' up
    for (size_t i = 2; i > 0; --i)
    {
        dispatchCommand(mxComponent, u".uno:MovePageUp"_ustr, {});
        checkCurrentPageNumber(i);
        CPPUNIT_ASSERT_EQUAL(u"Test 2"_ustr, pViewShell->GetActualPage()->GetName());
    }

    // Move slide 'Test 2' down
    for (size_t i = 2; i < 4; ++i)
    {
        dispatchCommand(mxComponent, u".uno:MovePageDown"_ustr, {});
        checkCurrentPageNumber(i);
        CPPUNIT_ASSERT_EQUAL(u"Test 2"_ustr, pViewShell->GetActualPage()->GetName());
    }

    // Move slide 'Test 2' to the top
    dispatchCommand(mxComponent, u".uno:MovePageFirst"_ustr, {});
    checkCurrentPageNumber(1);
    CPPUNIT_ASSERT_EQUAL(u"Test 2"_ustr, pViewShell->GetActualPage()->GetName());

    // Move slide 'Test 2' to the bottom
    dispatchCommand(mxComponent, u".uno:MovePageLast"_ustr, {});
    checkCurrentPageNumber(3);
    CPPUNIT_ASSERT_EQUAL(u"Test 2"_ustr, pViewShell->GetActualPage()->GetName());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf68320_hidden_pages_total_slide_count)
{
    createSdImpressDoc();

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});
    dispatchCommand(mxComponent, u".uno:HideSlide"_ustr, {});
    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});
    checkCurrentPageNumber(3);

    insertStringToObject(0, u"Pages: ", /*bUseEscape*/ false);

    dispatchCommand(mxComponent, u".uno:InsertPagesField"_ustr, {});

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    typeString(pXImpressDocument, u"/");

    dispatchCommand(mxComponent, u".uno:InsertPageField"_ustr, {});

    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});
    dispatchCommand(mxComponent, u".uno:Cut"_ustr, {});

    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues = comphelper::InitPropertySequence(
        { { "SelectedFormat",
            cpo::uno::Any(static_cast<sal_uInt32>(SotClipboardFormatId::STRING)) } });

    // Paste as Unformatted text in order to get the field's values
    dispatchCommand(mxComponent, u".uno:ClipboardFormatItems"_ustr, aPropertyValues);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(2),
                                                 uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);

    // Without the fix in place, this test would have failed with
    // - Expected: Pages: 3/3
    // - Actual  : Pages: 2/3
    CPPUNIT_ASSERT_EQUAL(u"Pages: 3/3"_ustr, xShape->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf148620)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xShape(xDrawPage->getByIndex(1), uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(u""_ustr, xShape->getString());

    insertStringToObject(1, u"one", /*bUseEscape*/ false);
    typeKey(pXImpressDocument, KEY_RETURN);
    typeString(pXImpressDocument, u"two");
    typeKey(pXImpressDocument, KEY_RETURN);
    typeString(pXImpressDocument, u"three");
    typeKey(pXImpressDocument, KEY_RETURN);
    typeString(pXImpressDocument, u"four");
    typeKey(pXImpressDocument, KEY_RETURN);
    typeString(pXImpressDocument, u"five");
    typeKey(pXImpressDocument, KEY_RETURN);
    typeString(pXImpressDocument, u"six");

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nFour\nFive\nsix"_ustr, xShape->getString());

    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({ { "KeyModifier", cpo::uno::Any(sal_Int32(0)) } }));
    dispatchCommand(mxComponent, u".uno:OutlineUp"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nFour\nsix\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineUp"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nsix\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineUp"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nsix\nThree\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineUp"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nsix\nTwo\nThree\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineUp"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"six\nOne\nTwo\nThree\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineDown"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nsix\nTwo\nThree\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineDown"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nsix\nThree\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineDown"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nsix\nFour\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineDown"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nFour\nsix\nFive"_ustr, xShape->getString());

    dispatchCommand(mxComponent, u".uno:OutlineDown"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(u"One\nTwo\nThree\nFour\nFive\nsix"_ustr, xShape->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf141703)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());

    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        { { "Rows", cpo::uno::Any(sal_Int32(2)) }, { "Columns", cpo::uno::Any(sal_Int32(2)) } }));

    dispatchCommand(mxComponent, u".uno:InsertTable"_ustr, aArgs);

    // Move to A1 using Alt + Tab and write 'A'
    for (int i = 0; i < 3; i++)
    {
        pXImpressDocument->postKeyEvent(COKitKeyEventType::DOWN, 0, KEY_SHIFT | KEY_TAB);
        pXImpressDocument->postKeyEvent(COKitKeyEventType::UP, 0, KEY_SHIFT | KEY_TAB);
        Scheduler::ProcessEventsToIdle();
    }

    typeString(pXImpressDocument, u"A");

    // Move to A2 with Tab and write 'B'
    typeKey(pXImpressDocument, KEY_TAB);

    typeString(pXImpressDocument, u"B");

    typeKey(pXImpressDocument, KEY_ESCAPE);

    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    auto pTableObject = dynamic_cast<sdr::table::SdrTableObj*>(pActualPage->GetObj(2));
    CPPUNIT_ASSERT(pTableObject);

    uno::Reference<css::table::XTable> xTable(pTableObject->getTable(), uno::UNO_SET_THROW);
    uno::Reference<css::table::XMergeableCell> xCellA1(xTable->getCellByPosition(0, 0),
                                                       uno::UNO_QUERY_THROW);
    uno::Reference<css::table::XMergeableCell> xCellA2(xTable->getCellByPosition(1, 0),
                                                       uno::UNO_QUERY_THROW);

    uno::Reference<text::XText> xTextA1
        = uno::Reference<text::XTextRange>(xCellA1, uno::UNO_QUERY_THROW)->getText();

    // Without the fix in place, this test would have failed with
    // - Expected: A
    // - Actual  :
    CPPUNIT_ASSERT_EQUAL(u"A"_ustr, xTextA1->getString());

    uno::Reference<text::XText> xTextA2
        = uno::Reference<text::XTextRange>(xCellA2, uno::UNO_QUERY_THROW)->getText();
    CPPUNIT_ASSERT_EQUAL(u"B"_ustr, xTextA2->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf164855)
{
    createSdImpressDoc();

    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        { { "Rows", cpo::uno::Any(sal_Int32(2)) }, { "Columns", cpo::uno::Any(sal_Int32(2)) } }));

    dispatchCommand(mxComponent, u".uno:InsertTable"_ustr, aArgs);

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    auto pTableObject = dynamic_cast<sdr::table::SdrTableObj*>(pActualPage->GetObj(2));
    CPPUNIT_ASSERT(pTableObject);

    const EditTextObject& rEdit
        = pTableObject->getText(0)->GetOutlinerParaObject()->GetTextObject();
    const SfxItemSet& rParaAttribs = rEdit.GetParaAttribs(0);
    auto pAdjust = rParaAttribs.GetItem(EE_PARA_JUST);
    CPPUNIT_ASSERT_EQUAL(SvxAdjust::Left, pAdjust->GetAdjust());

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:RightPara"_ustr, {});

    const EditTextObject& rEdit2
        = pTableObject->getText(0)->GetOutlinerParaObject()->GetTextObject();
    const SfxItemSet& rParaAttribs2 = rEdit2.GetParaAttribs(0);
    pAdjust = rParaAttribs2.GetItem(EE_PARA_JUST);
    CPPUNIT_ASSERT_EQUAL(SvxAdjust::Right, pAdjust->GetAdjust());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf127481)
{
    createSdImpressDoc();

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), pActualPage->GetObjCount());

    cpo::uno::Sequence<beans::PropertyValue> aArgs(comphelper::InitPropertySequence(
        { { "Rows", cpo::uno::Any(sal_Int32(1)) }, { "Columns", cpo::uno::Any(sal_Int32(1)) } }));

    dispatchCommand(mxComponent, u".uno:InsertTable"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    dispatchCommand(mxComponent, u".uno:DuplicatePage"_ustr, aArgs);

    checkCurrentPageNumber(2);

    pActualPage = pViewShell->GetActualPage();
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());

    auto pTableObject = dynamic_cast<sdr::table::SdrTableObj*>(pActualPage->GetObj(2));
    CPPUNIT_ASSERT(pTableObject);

    //without the fix, it would crash here
    pViewShell->GetView()->SdrBeginTextEdit(pTableObject);

    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(3), pActualPage->GetObjCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPageFillColor)
{
    // Load the document and create two new windows.
    createSdImpressDoc("tdf126197.odp");
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();

    // Set FillPageColor

    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues = {
        comphelper::makePropertyValue(u"FillColor"_ustr, static_cast<sal_Int32>(0xff0000)),
    };
    dispatchCommand(mxComponent, u".uno:FillPageColor"_ustr, aPropertyValues);

    SdPage* pPage = pViewShell->getCurrentPage();
    const SfxItemSet& rPageAttr = pPage->getSdrPageProperties().GetItemSet();

    const XFillStyleItem* pFillStyle = rPageAttr.GetItem(XATTR_FILLSTYLE);
    drawing::FillStyle eXFS = pFillStyle->GetValue();
    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_SOLID, eXFS);

    Color aColor = rPageAttr.GetItem(XATTR_FILLCOLOR)->GetColorValue();
    CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, aColor);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPageFillGradient)
{
    // Load the document and create two new windows.
    createSdImpressDoc("tdf126197.odp");
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();

    // Set FillPageColor

    cpo::uno::Sequence<beans::PropertyValue> aPropertyValues(comphelper::InitPropertySequence({
        { "FillPageGradientJSON",
          cpo::uno::Any(
              u"{\"style\":\"LINEAR\",\"startcolor\":\"ff0000\",\"endcolor\":\"0000ff\","
              "\"angle\":\"300\",\"border\":\"0\",\"x\":\"0\",\"y\":\"0\",\"intensstart\":"
              "\"100\",\"intensend\":\"100\",\"stepcount\":\"0\"}"_ustr) },
    }));

    dispatchCommand(mxComponent, u".uno:FillPageGradient"_ustr, aPropertyValues);

    SdPage* pPage = pViewShell->getCurrentPage();
    const SfxItemSet& rPageAttr = pPage->getSdrPageProperties().GetItemSet();

    const XFillStyleItem* pFillStyle = rPageAttr.GetItem(XATTR_FILLSTYLE);
    drawing::FillStyle eXFS = pFillStyle->GetValue();
    CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_GRADIENT, eXFS);

    basegfx::BGradient aGradient(rPageAttr.GetItem(XATTR_FILLGRADIENT)->GetGradientValue());
    const basegfx::BColorStops& rColorStops(aGradient.GetColorStops());

    CPPUNIT_ASSERT_EQUAL(size_t(2), rColorStops.size());
    CPPUNIT_ASSERT_EQUAL(0.0, rColorStops.getStopOffset(0));
    CPPUNIT_ASSERT_EQUAL(COL_LIGHTRED, Color(rColorStops.getStopColor(0)));
    CPPUNIT_ASSERT(basegfx::fTools::equal(rColorStops.getStopOffset(1), 1.0));
    CPPUNIT_ASSERT_EQUAL(COL_LIGHTBLUE, Color(rColorStops.getStopColor(1)));
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf134053)
{
    // Error was, that dashes and dots were longer than in MS Office.
    createSdImpressDoc("pptx/tdf134053_dashdot.pptx");
    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdPage* pActualPage = pViewShell->GetActualPage();
    SdrObject* pShape = pActualPage->GetObj(0);
    CPPUNIT_ASSERT_MESSAGE("No Shape", pShape);

    SdDrawDocument* pDocument = pXImpressDocument->GetDoc();
    sd::UndoManager* pUndoManager = pDocument->GetUndoManager();

    // tdf#114613: Without the fix in place, this test would have failed with
    // - Expected: 0
    // - Actual  : 8
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pUndoManager->GetUndoActionCount());
    CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(0), pUndoManager->GetRedoActionCount());

    XDash dash = pShape->GetMergedItem(XATTR_LINEDASH).GetDashValue();

    // Because 0% is not possible as dash length (as of June 2020) 1% is used in the fix.
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Distance", 399.0, dash.GetDistance());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Dot length", 301.0, dash.GetDotLen());
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Dash length", 1.0, dash.GetDashLen());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSpellOnlineParameter)
{
    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    bool bSet = pImpressDocument->GetDoc()->GetOnlineSpell();

    cpo::uno::Sequence<beans::PropertyValue> params(
        comphelper::InitPropertySequence({ { "Enable", cpo::uno::Any(!bSet) } }));
    dispatchCommand(mxComponent, u".uno:SpellOnline"_ustr, params);
    CPPUNIT_ASSERT_EQUAL(!bSet, pImpressDocument->GetDoc()->GetOnlineSpell());

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

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf38669)
{
    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    // Insert shape with ctrl key
    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({ { "KeyModifier", cpo::uno::Any(KEY_MOD1) } }));
    dispatchCommand(mxComponent, u".uno:BasicShapes.rectangle"_ustr, aArgs);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), xDrawPage->getCount());

    typeString(pImpressDocument, u"°");

    uno::Reference<text::XTextRange> xShape(xDrawPage->getByIndex(2), uno::UNO_QUERY);
    // Without the fix in place, this test would have failed with:
    // - Expected: °
    // - Actual  : ㅀ
    CPPUNIT_ASSERT_EQUAL(u"°"_ustr, xShape->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf151417)
{
    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<container::XIndexAccess> xDraws = xDrawPagesSupplier->getDrawPages();
    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), xDraws->getCount());

    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(sal_Int32(2), xDrawPage->getCount());

    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({ { "KeyModifier", cpo::uno::Any(KEY_MOD1) } }));

    // Without the fix in place, this test would have crashed here
    dispatchCommand(mxComponent, u".uno:Edit"_ustr, aArgs);

    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), xDrawPage->getCount());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf123841)
{
    // To check if selecting unfilled rectangle produces unfilled rectangle
    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    CPPUNIT_ASSERT(pImpressDocument);

    cpo::uno::Sequence<beans::PropertyValue> aArgs(
        comphelper::InitPropertySequence({ { "KeyModifier", cpo::uno::Any(KEY_MOD1) } }));
    dispatchCommand(mxComponent, u".uno:Rect_Unfilled"_ustr, aArgs);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);

    CPPUNIT_ASSERT_EQUAL(sal_Int32(3), xDrawPage->getCount());

    for (int i = 0; i < 3; i++)
    {
        uno::Reference<beans::XPropertySet> XPropSet(xDrawPage->getByIndex(i), uno::UNO_QUERY);
        drawing::FillStyle eFillStyle = drawing::FillStyle_NONE;
        XPropSet->getPropertyValue(u"FillStyle"_ustr) >>= eFillStyle;

        // Without the fix in place, this test would have failed with
        // with drawing::FillStyle_NONE != drawing::FillStyle_SOLID
        CPPUNIT_ASSERT_EQUAL(drawing::FillStyle_NONE, eFillStyle);
    }
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testSearchAllInDocumentAndNotes)
{
    // tdf#142478
    // "find all" produces a crash when the search string exists in notes
    // and the document

    createSdImpressDoc("odp/search-all-notes.odp");

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    CPPUNIT_ASSERT(pViewShell);

    lcl_search(u"Crash"_ustr, /*bFindAll=*/true, /*bBackwards=*/true);
}

#if !defined(_WIN32) && !defined(MACOSX)
// for some reason, the search for "second" (or "Second") didn't return page 2 in WIN and MACOS
CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf123658_SearchAfterSlideChange)
{
    createSdImpressDoc("odp/tdf123658_SearchAfterSlideChange.odp");

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());

    lcl_search(u"second"_ustr);
    checkCurrentPageNumber(2);

    pXImpressDocument->setPart(0); // Switch to 1st page

    lcl_search(u"of"_ustr);
    // Instead of finding this on the 1st page (or on the 2nd page would be acceptable too)
    // it was going to the third page.
    checkCurrentPageNumber(1);
}
#endif

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testCharColorTheme)
{
    // Given an Impress document with a shape, with its text selected:
    createSdImpressDoc();
    uno::Reference<drawing::XDrawPagesSupplier> xPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xPage(xPagesSupplier->getDrawPages()->getByIndex(0),
                                             uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xShape(xPage->getByIndex(0), uno::UNO_QUERY);
    {
        uno::Reference<text::XSimpleText> xText = xShape->getText();
        xText->insertString(xText->getStart(), u"test"_ustr, false);
    }
    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    uno::Reference<view::XSelectionSupplier> xController(xModel->getCurrentController(),
                                                         uno::UNO_QUERY);
    xController->select(cpo::uno::Any(xShape));
    Scheduler::ProcessEventsToIdle();
    dispatchCommand(mxComponent, u".uno:Text"_ustr, {});
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    CPPUNIT_ASSERT(pView->IsTextEdit());
    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});

    // When picking a theme color on the sidebar:
    {
        model::ComplexColor aComplexColor;
        aComplexColor.setThemeColor(model::ThemeColorType::Accent1);
        aComplexColor.addTransformation({ model::TransformationType::LumMod, 2000 });
        aComplexColor.addTransformation({ model::TransformationType::LumOff, 8000 });

        OUString aJSON
            = OStringToOUString(model::color::convertToJSON(aComplexColor), RTL_TEXTENCODING_UTF8);

        // When setting the fill color of that shape, with theme metadata & effects:
        cpo::uno::Sequence<beans::PropertyValue> aColorArgs = {
            comphelper::makePropertyValue(u"Color.Color"_ustr,
                                          sal_Int32(0xdae3f3)), // 80% light blue
            comphelper::makePropertyValue(u"Color.ComplexColorJSON"_ustr, cpo::uno::Any(aJSON)),
        };
        dispatchCommand(mxComponent, u".uno:Color"_ustr, aColorArgs);
    }

    // Then make sure the theme "metadata" is set in the document model:
    pView->EndTextEditCurrentView();
    CPPUNIT_ASSERT(!pView->IsTextEdit());
    uno::Reference<container::XEnumerationAccess> xShapeParaAccess(xShape, uno::UNO_QUERY);
    uno::Reference<container::XEnumerationAccess> xPara(
        xShapeParaAccess->createEnumeration()->nextElement(), uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xPortion(xPara->createEnumeration()->nextElement(),
                                                 uno::UNO_QUERY);
    {
        uno::Reference<util::XComplexColor> xComplexColor;
        CPPUNIT_ASSERT(xPortion->getPropertyValue(u"CharComplexColor"_ustr) >>= xComplexColor);
        CPPUNIT_ASSERT(xComplexColor.is());
        auto aComplexColor = model::color::getFromXComplexColor(xComplexColor);
        CPPUNIT_ASSERT_EQUAL(model::ThemeColorType::Accent1, aComplexColor.getThemeColorType());
        CPPUNIT_ASSERT_EQUAL(model::TransformationType::LumMod,
                             aComplexColor.getTransformations()[0].meType);
        CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(2000),
                             aComplexColor.getTransformations()[0].mnValue);
        CPPUNIT_ASSERT_EQUAL(model::TransformationType::LumOff,
                             aComplexColor.getTransformations()[1].meType);
        CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(8000),
                             aComplexColor.getTransformations()[1].mnValue);
    }
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testFillColorTheme)
{
    // Given an Impress document with a selected shape:
    createSdImpressDoc();
    uno::Reference<drawing::XDrawPagesSupplier> xPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xPage(xPagesSupplier->getDrawPages()->getByIndex(0),
                                             uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xShape(xPage->getByIndex(0), uno::UNO_QUERY);
    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    uno::Reference<view::XSelectionSupplier> xController(xModel->getCurrentController(),
                                                         uno::UNO_QUERY);
    xController->select(cpo::uno::Any(xShape));

    // Change fill color
    {
        model::ComplexColor aComplexColor;
        aComplexColor.setThemeColor(model::ThemeColorType::Accent1);
        aComplexColor.addTransformation({ model::TransformationType::LumMod, 4000 });
        aComplexColor.addTransformation({ model::TransformationType::LumOff, 6000 });

        OUString aJSON
            = OStringToOUString(model::color::convertToJSON(aComplexColor), RTL_TEXTENCODING_UTF8);

        // When setting the fill color of that shape, with theme metadata & effects:
        cpo::uno::Sequence<beans::PropertyValue> aColorArgs = {
            comphelper::makePropertyValue(u"FillColor.Color"_ustr, sal_Int32(0xed7d31)), // orange
            comphelper::makePropertyValue(u"FillColor.ComplexColorJSON"_ustr,
                                          cpo::uno::Any(aJSON)), // accent 1
        };
        dispatchCommand(mxComponent, u".uno:FillColor"_ustr, aColorArgs);
    }

    // Then make sure the theme index is not lost when the sidebar sets it:
    {
        uno::Reference<util::XComplexColor> xComplexColor;
        CPPUNIT_ASSERT(xShape->getPropertyValue(u"FillComplexColor"_ustr) >>= xComplexColor);
        CPPUNIT_ASSERT(xComplexColor.is());
        auto aComplexColor = model::color::getFromXComplexColor(xComplexColor);
        CPPUNIT_ASSERT_EQUAL(model::ThemeColorType::Accent1, aComplexColor.getThemeColorType());
        CPPUNIT_ASSERT_EQUAL(model::TransformationType::LumMod,
                             aComplexColor.getTransformations()[0].meType);
        CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(4000),
                             aComplexColor.getTransformations()[0].mnValue);
        CPPUNIT_ASSERT_EQUAL(model::TransformationType::LumOff,
                             aComplexColor.getTransformations()[1].meType);
        CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(6000),
                             aComplexColor.getTransformations()[1].mnValue);
    }
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf163805)
{
    createSdImpressDoc();

    uno::Reference<beans::XPropertySet> xShape(getShapeFromPage(0, 0));

    drawing::FillStyle eFillStyle;
    Color aColor;
    CPPUNIT_ASSERT(xShape->getPropertyValue(u"FillStyle"_ustr) >>= eFillStyle);
    CPPUNIT_ASSERT_EQUAL(int(drawing::FillStyle_NONE), static_cast<int>(eFillStyle));
    CPPUNIT_ASSERT(xShape->getPropertyValue(u"FillColor"_ustr) >>= aColor);
    CPPUNIT_ASSERT_EQUAL(Color(0x729fcf), aColor);

    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    uno::Reference<view::XSelectionSupplier> xController(xModel->getCurrentController(),
                                                         uno::UNO_QUERY);
    xController->select(cpo::uno::Any(xShape));

    cpo::uno::Sequence<beans::PropertyValue> aColorArgs
        = { comphelper::makePropertyValue(u"FillColor.Color"_ustr, sal_Int32(0x800000)) };
    dispatchCommand(mxComponent, u".uno:FillColor"_ustr, aColorArgs);

    CPPUNIT_ASSERT(xShape->getPropertyValue(u"FillStyle"_ustr) >>= eFillStyle);

    // Without the fix in place, this test would have failed with
    // - Expected: 1
    // - Actual  : 0
    CPPUNIT_ASSERT_EQUAL(int(drawing::FillStyle_SOLID), static_cast<int>(eFillStyle));
    CPPUNIT_ASSERT(xShape->getPropertyValue(u"FillColor"_ustr) >>= aColor);
    CPPUNIT_ASSERT_EQUAL(COL_RED, aColor);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testFillColorNoColor)
{
    // Given an empty Impress document:
    createSdImpressDoc();
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SfxDispatcher* pDispatcher = pViewShell->GetViewFrame()->GetDispatcher();

    // When dispatching a fill color that only has a fill style (no color), then make sure we don't
    // crash:
    XFillStyleItem aXFillStyleItem(drawing::FillStyle_NONE);
    pDispatcher->ExecuteList(SID_ATTR_FILL_COLOR, SfxCallMode::RECORD, { &aXFillStyleItem });
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf153161)
{
    createSdImpressDoc("odp/tdf153161_FlushToSave.odp");

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xShape(xDrawPage->getByIndex(1), uno::UNO_QUERY);
    OUString sOldText(xShape->getString());
    CPPUNIT_ASSERT(sOldText.startsWith(u"在没有版本控制系统的时期"));

    // Type something, getting into text editing mode (appending) automatically
    insertStringToObject(1, u"Foo Bar", /*bUseEscape*/ false);

    saveAndReload(TestFilter::ODP);

    xDrawPagesSupplier.set(mxComponent, uno::UNO_QUERY);
    xDrawPage.set(xDrawPagesSupplier->getDrawPages()->getByIndex(0), uno::UNO_QUERY);
    xShape.set(xDrawPage->getByIndex(1), uno::UNO_QUERY);
    OUString sExpectedText = sOldText + "Foo Bar";
    CPPUNIT_ASSERT_EQUAL(sExpectedText, xShape->getString());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf148810)
{
    createSdImpressDoc("pptx/tdf148810_PARA_OUTLLEVEL.pptx");

    // type something to get into text editing mode (instead of shape selection).
    insertStringToObject(1, u"x", /*bUseEscape*/ false);

    auto pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    typeKey(pXImpressDocument, KEY_HOME);

    typeKey(pXImpressDocument, KEY_BACKSPACE);

    uno::Reference<beans::XPropertySet> xShape(getShapeFromPage(1, 0));
    uno::Reference<text::XTextRange> xParagraph(getParagraphFromShape(0, xShape));
    uno::Reference<beans::XPropertySet> xPropSet(xParagraph, uno::UNO_QUERY_THROW);
    sal_Int16 nNumberingLevel = -1;
    xPropSet->getPropertyValue(u"NumberingLevel"_ustr) >>= nNumberingLevel;
    CPPUNIT_ASSERT_EQUAL(sal_Int16(0), nNumberingLevel);

    dispatchCommand(mxComponent, u".uno:Undo"_ustr, {});

    nNumberingLevel = -1;
    xPropSet->getPropertyValue(u"NumberingLevel"_ustr) >>= nNumberingLevel;

    // Without the fix in place, this test would have failed with
    // - Expected: 0
    // - Actual  : -1
    CPPUNIT_ASSERT_EQUAL(sal_Int16(0), nNumberingLevel);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf127696)
{
    createSdImpressDoc();

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});

    insertStringToObject(0, u"Test", /*bUseEscape*/ false);
    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});
    dispatchCommand(mxComponent, u".uno:OutlineFont"_ustr, {});

    // Save it as PPTX and load it again.
    saveAndReload(TestFilter::PPTX);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(1),
                                                 uno::UNO_QUERY);

    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);

    uno::Reference<text::XText> xText
        = uno::Reference<text::XTextRange>(xShape, uno::UNO_QUERY_THROW)->getText();
    CPPUNIT_ASSERT_MESSAGE("Not a text shape", xText.is());

    uno::Reference<container::XEnumerationAccess> paraEnumAccess(xText, uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> paraEnum(paraEnumAccess->createEnumeration());

    uno::Reference<text::XTextRange> xParagraph(paraEnum->nextElement(), uno::UNO_QUERY_THROW);

    uno::Reference<container::XEnumerationAccess> runEnumAccess(xParagraph, uno::UNO_QUERY);
    uno::Reference<container::XEnumeration> runEnum = runEnumAccess->createEnumeration();

    uno::Reference<text::XTextRange> xRun(runEnum->nextElement(), uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xPropSet(xRun, uno::UNO_QUERY_THROW);

    bool bContoured = false;
    xPropSet->getPropertyValue(u"CharContoured"_ustr) >>= bContoured;
    CPPUNIT_ASSERT(bContoured);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testThemeShapeInsert)
{
    // Given a document with a theme, accent1 color is set to 0x000004:
    createSdImpressDoc();
    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    uno::Reference<drawing::XMasterPageTarget> xMasterPageTarget(xDrawPage, uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xMasterPage(xMasterPageTarget->getMasterPage(),
                                                    uno::UNO_QUERY);

    auto pTheme = std::make_shared<model::Theme>("mytheme");
    auto pColorSet = std::make_shared<model::ColorSet>("mycolorscheme");
    pColorSet->add(model::ThemeColorType::Dark1, 0x0);
    pColorSet->add(model::ThemeColorType::Light1, 0x1);
    pColorSet->add(model::ThemeColorType::Dark2, 0x2);
    pColorSet->add(model::ThemeColorType::Light2, 0x3);
    pColorSet->add(model::ThemeColorType::Accent1, 0x4);
    pColorSet->add(model::ThemeColorType::Accent2, 0x5);
    pColorSet->add(model::ThemeColorType::Accent3, 0x6);
    pColorSet->add(model::ThemeColorType::Accent4, 0x7);
    pColorSet->add(model::ThemeColorType::Accent5, 0x8);
    pColorSet->add(model::ThemeColorType::Accent6, 0x9);
    pColorSet->add(model::ThemeColorType::Hyperlink, 0xa);
    pColorSet->add(model::ThemeColorType::FollowedHyperlink, 0xb);
    pTheme->setColorSet(pColorSet);

    xMasterPage->setPropertyValue(u"Theme"_ustr, cpo::uno::Any(model::theme::createXTheme(pTheme)));

    // When inserting a shape:
    cpo::uno::Sequence<beans::PropertyValue> aArgs = {
        comphelper::makePropertyValue(u"CreateDirectly"_ustr, true),
    };

    dispatchCommand(mxComponent, u".uno:BasicShapes.round-rectangle"_ustr, aArgs);

    // Then make sure the that fill color of the last shape is the accent1 color:
    sal_Int32 nShapeIndex = xDrawPage->getCount() - 1;
    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(nShapeIndex), uno::UNO_QUERY);
    sal_Int32 nFillColor{};
    xShape->getPropertyValue(u"FillColor"_ustr) >>= nFillColor;

    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(0x4), nFillColor);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testTdf168835)
{
    createSdImpressDoc("pptx/tdf168835.pptx");
    checkCurrentPageNumber(1);

    dispatchCommand(mxComponent, u".uno:InsertPage"_ustr, {});
    checkCurrentPageNumber(2);

    insertStringToObject(0, u"Title of the second slide", /*bUseEscape*/ true);

    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(1),
                                                 uno::UNO_QUERY);

    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);
    uno::Reference<text::XTextRange> xParagraph(getParagraphFromShape(0, xShape));

    CPPUNIT_ASSERT_EQUAL(u"Title of the second slide"_ustr, xParagraph->getString());
    CPPUNIT_ASSERT_EQUAL(44.0f, xShape->getPropertyValue(u"CharHeight"_ustr).get<float>());
    CPPUNIT_ASSERT_EQUAL(sal_Int16(style::ParagraphAdjust_LEFT),
                         xShape->getPropertyValue(u"ParaAdjust"_ustr).get<sal_Int16>());
    CPPUNIT_ASSERT_EQUAL(u"Arial"_ustr,
                         xShape->getPropertyValue(u"CharFontName"_ustr).get<OUString>());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testNumToBullet)
{
    // Given a document with a shape, 2nd paragraph is a numbering:
    createSdImpressDoc("odp/num-to-bullet.odp");
    sd::ViewShell* pViewShell = getSdDocShell()->GetViewShell();
    SdPage* pPage = pViewShell->GetActualPage();
    SdrObject* pShape = pPage->GetObj(0);
    CPPUNIT_ASSERT(pShape);
    SdrView* pView = pViewShell->GetView();
    pView->MarkObj(pShape, pView->GetSdrPageView());
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!pView->IsTextEdit());

    // When turning the numbering to a bullet:
    // Start text edit:
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    typeString(pImpressDocument, u"x");
    CPPUNIT_ASSERT(pView->IsTextEdit());
    // Do the switch:
    dispatchCommand(mxComponent, u".uno:DefaultBullet"_ustr, {});
    // End text edit:
    typeKey(pImpressDocument, KEY_ESCAPE);

    // Then make sure we switch to a bullet and not toggle off the numbering:
    CPPUNIT_ASSERT(!pView->IsTextEdit());
    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xParagraph(getParagraphFromShape(1, xShape),
                                                   uno::UNO_QUERY);
    // Check that list level is 1 & level 1 numbering type is a bullet:
    sal_Int16 nNumberingLevel{};
    xParagraph->getPropertyValue(u"NumberingLevel"_ustr) >>= nNumberingLevel;
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 1
    // - Actual  : 0
    // i.e. list level was -1 (toggle off) instead of switch to bullet.
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int16>(1), nNumberingLevel);
    uno::Reference<container::XIndexAccess> xNumberingRules;
    xParagraph->getPropertyValue(u"NumberingRules"_ustr) >>= xNumberingRules;
    comphelper::SequenceAsHashMap aNumberingRule(xNumberingRules->getByIndex(1));
    sal_Int16 nNumberingType = 0;
    aNumberingRule[u"NumberingType"_ustr] >>= nNumberingType;
    // Bullet and not numbering (ARABIC):
    CPPUNIT_ASSERT_EQUAL(style::NumberingType::CHAR_SPECIAL, nNumberingType);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testBulletOffOn)
{
    // Given a document with a shape, last paragraph is has a bullet:
    createSdImpressDoc("odp/bullet-off-on.odp");
    sd::ViewShell* pViewShell = getSdDocShell()->GetViewShell();
    SdPage* pPage = pViewShell->GetActualPage();
    SdrObject* pShape = pPage->GetObj(0);
    CPPUNIT_ASSERT(pShape);
    SdrView* pView = pViewShell->GetView();
    pView->MarkObj(pShape, pView->GetSdrPageView());
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!pView->IsTextEdit());

    // When turning the bullet off & then back on again:
    // Start text edit:
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    typeString(pImpressDocument, u"x");
    CPPUNIT_ASSERT(pView->IsTextEdit());
    // Toggle off:
    dispatchCommand(mxComponent, u".uno:DefaultBullet"_ustr, {});
    // Toggle on:
    dispatchCommand(mxComponent, u".uno:DefaultBullet"_ustr, {});
    // End text edit:
    typeKey(pImpressDocument, KEY_ESCAPE);

    // Then make sure the result is Level 1 from the master page:
    CPPUNIT_ASSERT(!pView->IsTextEdit());
    uno::Reference<drawing::XDrawPagesSupplier> xDrawPagesSupplier(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawPagesSupplier->getDrawPages()->getByIndex(0),
                                                 uno::UNO_QUERY);
    uno::Reference<beans::XPropertySet> xShape(xDrawPage->getByIndex(0), uno::UNO_QUERY);
    // 5th, last paragraph.
    uno::Reference<beans::XPropertySet> xParagraph(getParagraphFromShape(4, xShape),
                                                   uno::UNO_QUERY);
    uno::Reference<container::XIndexAccess> xNumberingRules;
    xParagraph->getPropertyValue(u"NumberingRules"_ustr) >>= xNumberingRules;
    // Level 1.
    comphelper::SequenceAsHashMap aNumberingRule(xNumberingRules->getByIndex(0));
    sal_Int32 nLeftMargin = 0;
    aNumberingRule[u"LeftMargin"_ustr] >>= nLeftMargin;
    sal_Int32 nFirstLineOffset = 0;
    aNumberingRule[u"FirstLineOffset"_ustr] >>= nFirstLineOffset;
    // Without the accompanying fix in place, this test would have failed with:
    // - Expected: 300
    // - Actual  : 0
    // i.e. the indent was 0cm, even if Level1 and Level2 had other values.
    CPPUNIT_ASSERT_EQUAL(static_cast<sal_Int32>(300), nLeftMargin + nFirstLineOffset);
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testPasteInTextEditWithAnimationNode)
{
    createSdImpressDoc();

    // Give the slide an (empty) animation node, so the paragraph removal that the
    // paste below performs is reported to the animation sequence.
    uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY);
    uno::Reference<drawing::XDrawView> xDrawView(xModel->getCurrentController(), uno::UNO_QUERY);
    uno::Reference<drawing::XDrawPage> xDrawPage(xDrawView->getCurrentPage(), uno::UNO_SET_THROW);
    uno::Reference<animations::XAnimationNodeSupplier> xAnimNodeSupplier(xDrawPage,
                                                                         uno::UNO_QUERY_THROW);
    CPPUNIT_ASSERT(xAnimNodeSupplier->getAnimationNode().is());

    // Type a word into the title placeholder, staying in text edit.
    insertStringToObject(0, u"blah", /*bUseEscape*/ false);

    // Select and copy it, then put the caret at the end so the paste appends.
    auto pImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});
    dispatchCommand(mxComponent, u".uno:Copy"_ustr, {});
    typeKey(pImpressDocument, KEY_END);

    // Without the accompanying fix, this paste asserts in a dbgutil build: the
    // paragraph removal probes the shape text through the UNO forwarder, forcing
    // a text frame layout recompute while the EditEngine layout is suspended.
    dispatchCommand(mxComponent, u".uno:Paste"_ustr, {});

    // The word got appended to itself.
    sd::ViewShell* pViewShell = pImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    dispatchCommand(mxComponent, u".uno:SelectAll"_ustr, {});
    EditView& rEditView = pView->GetTextEditOutlinerView()->GetEditView();
    CPPUNIT_ASSERT_EQUAL(u"blahblah"_ustr, rEditView.GetSelected());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testMultiSelectionResize)
{
    // Two separate rectangles, wireframe (non-solid) dragging so the per-object
    // drag origins are exercised.
    createSdImpressDoc();
    auto* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    SdPage* pPage = pViewShell->GetActualPage();

    pView->SetSolidDragging(false);
    pView->SetSnapEnabled(false);

    rtl::Reference<SdrRectObj> pRect1
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(0, 0, 1000, 1000));
    rtl::Reference<SdrRectObj> pRect2
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(5000, 5000, 6000, 6000));
    pPage->NbcInsertObject(pRect1.get());
    pPage->NbcInsertObject(pRect2.get());

    // A single selection keeps the standard group frame handles, a second object
    // switches to individual handles automatically.
    pView->MarkObj(pRect1.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());
    pView->MarkObj(pRect2.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(!pView->IsFrameDragSingles());

    // Find the lower-right resize handle that belongs to the first rectangle.
    const SdrHdlList& rHdlList = pView->GetHdlList();
    SdrHdl* pDragHdl = nullptr;
    for (size_t i = 0; i < rHdlList.GetHdlCount(); ++i)
    {
        SdrHdl* pHdl = rHdlList.GetHdl(i);
        if (pHdl->GetKind() == SdrHdlKind::LowerRight && pHdl->GetObj() == pRect1.get())
        {
            pDragHdl = pHdl;
            break;
        }
    }
    CPPUNIT_ASSERT(pDragHdl);

    const tools::Rectangle aOrig1(pRect1->GetSnapRect());
    const tools::Rectangle aOrig2(pRect2->GetSnapRect());

    // Dragging that single handle must drive an all-objects resize, not the
    // single-object special drag, with each object anchored at its own origin
    // (top-left for a lower-right drag).
    CPPUNIT_ASSERT(pView->BegDragObj(pDragHdl->GetPos(), nullptr, pDragHdl, 0));
    SdrDragMethod* pDragMethod = pView->GetDragMethod();
    CPPUNIT_ASSERT(dynamic_cast<SdrDragResize*>(pDragMethod));
    std::optional<Point> oOrigin1 = pDragMethod->getIndividualDragOrigin(*pRect1);
    std::optional<Point> oOrigin2 = pDragMethod->getIndividualDragOrigin(*pRect2);
    CPPUNIT_ASSERT(oOrigin1.has_value());
    CPPUNIT_ASSERT(oOrigin2.has_value());
    CPPUNIT_ASSERT_EQUAL(aOrig1.TopLeft(), *oOrigin1);
    CPPUNIT_ASSERT_EQUAL(aOrig2.TopLeft(), *oOrigin2);

    // Double the first rectangle by moving its handle, then commit.
    pView->MovDragObj(Point(2000, 2000));
    pView->EndDragObj();

    const tools::Rectangle aNew1(pRect1->GetSnapRect());
    const tools::Rectangle aNew2(pRect2->GetSnapRect());

    // Both rectangles kept their own top-left origin and doubled in size. The
    // second rectangle did not move, unlike a shared-pivot group resize.
    CPPUNIT_ASSERT_EQUAL(aOrig1.TopLeft(), aNew1.TopLeft());
    CPPUNIT_ASSERT_EQUAL(aOrig1.Left() + 2 * (aOrig1.Right() - aOrig1.Left()), aNew1.Right());
    CPPUNIT_ASSERT_EQUAL(aOrig1.Top() + 2 * (aOrig1.Bottom() - aOrig1.Top()), aNew1.Bottom());
    CPPUNIT_ASSERT_EQUAL(aOrig2.TopLeft(), aNew2.TopLeft());
    CPPUNIT_ASSERT_EQUAL(aOrig2.Left() + 2 * (aOrig2.Right() - aOrig2.Left()), aNew2.Right());
    CPPUNIT_ASSERT_EQUAL(aOrig2.Top() + 2 * (aOrig2.Bottom() - aOrig2.Top()), aNew2.Bottom());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testMultiSelectionRotate)
{
    // Two separate, non-square rectangles, wireframe (non-solid) dragging.
    createSdImpressDoc();
    auto* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());
    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    SdPage* pPage = pViewShell->GetActualPage();

    pView->SetSolidDragging(false);
    pView->SetSnapEnabled(false);

    rtl::Reference<SdrRectObj> pRect1
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(0, 0, 2000, 1000));
    rtl::Reference<SdrRectObj> pRect2
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(5000, 5000, 7000, 6000));
    pPage->NbcInsertObject(pRect1.get());
    pPage->NbcInsertObject(pRect2.get());

    pView->MarkObj(pRect1.get(), pView->GetSdrPageView());
    pView->MarkObj(pRect2.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(!pView->IsFrameDragSingles());

    // Each object gets exactly one rotate handle in the individual handle set.
    const SdrHdlList& rHdlList = pView->GetHdlList();
    int nRotateHandles = 0;
    SdrHdl* pRotateHdl = nullptr;
    for (size_t i = 0; i < rHdlList.GetHdlCount(); ++i)
    {
        SdrHdl* pHdl = rHdlList.GetHdl(i);
        if (pHdl->GetKind() == SdrHdlKind::Rotate)
        {
            ++nRotateHandles;
            if (pHdl->GetObj() == pRect1.get())
                pRotateHdl = pHdl;
        }
    }
    CPPUNIT_ASSERT_EQUAL(2, nRotateHandles);
    CPPUNIT_ASSERT(pRotateHdl);

    const Point aCenter1(pRect1->GetSnapRect().Center());
    const Point aCenter2(pRect2->GetSnapRect().Center());

    // Dragging rect1's rotate handle must drive an all-objects rotate, not the
    // single-object special drag, with each object turning around its own centre.
    CPPUNIT_ASSERT(pView->BegDragObj(pRotateHdl->GetPos(), nullptr, pRotateHdl, 0));
    SdrDragMethod* pDragMethod = pView->GetDragMethod();
    CPPUNIT_ASSERT(pDragMethod);
    CPPUNIT_ASSERT_EQUAL(PointerStyle::Rotate, pDragMethod->GetSdrDragPointer());
    std::optional<Point> oOrigin1 = pDragMethod->getIndividualDragOrigin(*pRect1);
    std::optional<Point> oOrigin2 = pDragMethod->getIndividualDragOrigin(*pRect2);
    CPPUNIT_ASSERT(oOrigin1.has_value());
    CPPUNIT_ASSERT(oOrigin2.has_value());
    CPPUNIT_ASSERT_EQUAL(aCenter1, *oOrigin1);
    CPPUNIT_ASSERT_EQUAL(aCenter2, *oOrigin2);

    // Drag the handle a quarter turn around rect1's centre, then commit.
    pView->MovDragObj(Point(aCenter1.X() + 2000, aCenter1.Y()));
    pView->EndDragObj();

    // Both rectangles rotated by the same angle and each kept its own centre. A
    // shared-pivot group rotate would instead have swung rect2's centre around
    // rect1.
    CPPUNIT_ASSERT_EQUAL(aCenter1, pRect1->GetSnapRect().Center());
    CPPUNIT_ASSERT_EQUAL(aCenter2, pRect2->GetSnapRect().Center());
    CPPUNIT_ASSERT(pRect1->GetRotateAngle() != 0_deg100);
    CPPUNIT_ASSERT_EQUAL(pRect1->GetRotateAngle().get(), pRect2->GetRotateAngle().get());
}

CPPUNIT_TEST_FIXTURE(SdUiImpressTest, testMultiSelectionPointEditToggle)
{
    // Selecting multiple objects auto-enables Point Edit Mode (individual
    // handles). Pressing F8 / the toolbar button (.uno:ToggleObjectBezierMode)
    // must still toggle it as before, and the manual choice must persist while
    // the multi-selection is held.
    createSdImpressDoc();
    auto* pXImpressDocument = dynamic_cast<SdXImpressDocument*>(mxComponent.get());

    // dispatchCommand needs an active frame to find the view.
    auto xDesktop = frame::Desktop::create(comphelper::getProcessComponentContext());
    auto pFrame = pXImpressDocument->GetDocShell()->GetFrame();
    CPPUNIT_ASSERT(pFrame);
    xDesktop->setActiveFrame(pFrame->GetFrame().GetFrameInterface());

    sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
    SdrView* pView = pViewShell->GetView();
    SdPage* pPage = pViewShell->GetActualPage();

    rtl::Reference<SdrRectObj> pRect1
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(0, 0, 1000, 1000));
    rtl::Reference<SdrRectObj> pRect2
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(2000, 0, 3000, 1000));
    rtl::Reference<SdrRectObj> pRect3
        = new SdrRectObj(pView->getSdrModelFromSdrView(), tools::Rectangle(4000, 0, 5000, 1000));
    pPage->NbcInsertObject(pRect1.get());
    pPage->NbcInsertObject(pRect2.get());
    pPage->NbcInsertObject(pRect3.get());

    // A single selection keeps the standard frame handles, a second object
    // auto-enables Point Edit Mode.
    pView->MarkObj(pRect1.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());
    pView->MarkObj(pRect2.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(!pView->IsFrameDragSingles());

    // Pressing F8 / the button turns Point Edit Mode off, as it did before.
    dispatchCommand(mxComponent, u".uno:ToggleObjectBezierMode"_ustr, {});
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());

    // Pressing it again turns it back on, so the old toggle is not broken.
    dispatchCommand(mxComponent, u".uno:ToggleObjectBezierMode"_ustr, {});
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(!pView->IsFrameDragSingles());

    // Turn it off, then extend the selection: the manual choice must persist and
    // not be re-forced back on while the selection stays multi.
    dispatchCommand(mxComponent, u".uno:ToggleObjectBezierMode"_ustr, {});
    Scheduler::ProcessEventsToIdle();
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());
    pView->MarkObj(pRect3.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());

    // Dropping back to a single selection restores the standard frame handles and
    // re-arms the auto default, so a fresh multi-selection enables it again.
    pView->UnmarkAllObj(pView->GetSdrPageView());
    pView->MarkObj(pRect1.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(pView->IsFrameDragSingles());
    pView->MarkObj(pRect2.get(), pView->GetSdrPageView());
    CPPUNIT_ASSERT(!pView->IsFrameDragSingles());
}

CPPUNIT_PLUGIN_IMPLEMENT();

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