/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice 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 <libepubgen/libepubgen.h>

#include <com/sun/star/document/XFilter.hpp>
#include <com/sun/star/frame/Desktop.hpp>
#include <com/sun/star/frame/XStorable.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/packages/zip/ZipFileAccess.hpp>

#include <comphelper/processfactory.hxx>
#include <comphelper/propertysequence.hxx>
#include <comphelper/string.hxx>
#include <test/bootstrapfixture.hxx>
#include <test/xmltesttools.hxx>
#include <unotest/macros_test.hxx>
#include <unotools/docinfohelper.hxx>
#include <unotools/mediadescriptor.hxx>
#include <unotools/tempfile.hxx>
#include <unotools/ucbstreamhelper.hxx>

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

namespace
{
char const DATA_DIRECTORY[] = "/writerperfect/qa/unit/data/writer/epubexport/";

/// Tests the EPUB export filter.
class EPUBExportTest : public test::BootstrapFixture,
                       public unotest::MacrosTest,
                       public XmlTestTools
{
    uno::Reference<uno::XComponentContext> mxComponentContext;
    uno::Reference<lang::XComponent> mxComponent;
    utl::TempFile maTempFile;
    xmlDocPtr mpXmlDoc = nullptr;
    uno::Reference<packages::zip::XZipFileAccess2> mxZipFile;
    OUString maFilterOptions;

public:
    void setUp() override;
    void tearDown() override;
    void registerNamespaces(xmlXPathContextPtr& pXmlXpathCtx) override;
    void createDoc(const OUString& rFile, const uno::Sequence<beans::PropertyValue>& rFilterData);
    /// Returns an XML representation of the stream named rName in the exported package.
    xmlDocPtr parseExport(const OUString& rName);
    /// Parses a CSS representation of the stream named rName and returns it.
    std::map<OUString, std::vector<OUString>> parseCss(const OUString& rName);
    /// Looks up a key of a class in rCss.
    static OUString getCss(std::map<OUString, std::vector<OUString>>& rCss, const OUString& rClass,
                           const OUString& rKey);
    void testOutlineLevel();
    void testMimetype();
    void testEPUB2();
    void testEPUBFixedLayout();
    void testEPUBFixedLayoutOption();
    void testEPUBFixedLayoutImplicitBreak();
    void testPageBreakSplit();
    void testSpanAutostyle();
    void testParaAutostyleCharProps();
    void testMeta();
    void testMetaXMP();
    void testMetaAPI();
    void testCoverImage();
    void testParaNamedstyle();
    void testCharNamedstyle();
    void testNamedStyleInheritance();
    void testNestedSpan();
    void testLineBreak();
    void testEscape();
    void testParaCharProps();
    void testSection();
    void testList();
    void testImage();
    void testImageBorder();
    void testImageNospan();
    void testTable();
    void testTableRowSpan();
    void testTableCellBorder();
    void testTableCellWidth();
    void testTableRowHeight();
    void testLink();
    void testLinkInvalid();
    void testLinkCharFormat();
    void testLinkNamedCharFormat();
    void testTableWidth();
    void testTextBox();
    void testFontEmbedding();
    void testImageLink();
    void testFootnote();
    void testPopup();
    void testPopupMedia();
    void testPopupAPI();
    void testPageSize();
    void testSVG();
    void testTdf115623SingleWritingMode();
    void testTdf115623SplitByChapter();
    void testTdf115623ManyPageSpans();
    void testSimpleRuby();

    CPPUNIT_TEST_SUITE(EPUBExportTest);
    CPPUNIT_TEST(testOutlineLevel);
    CPPUNIT_TEST(testMimetype);
    CPPUNIT_TEST(testEPUB2);
    CPPUNIT_TEST(testEPUBFixedLayout);
    CPPUNIT_TEST(testEPUBFixedLayoutOption);
    CPPUNIT_TEST(testEPUBFixedLayoutImplicitBreak);
    CPPUNIT_TEST(testPageBreakSplit);
    CPPUNIT_TEST(testSpanAutostyle);
    CPPUNIT_TEST(testParaAutostyleCharProps);
    CPPUNIT_TEST(testMeta);
    CPPUNIT_TEST(testMetaXMP);
    CPPUNIT_TEST(testMetaAPI);
    CPPUNIT_TEST(testCoverImage);
    CPPUNIT_TEST(testParaNamedstyle);
    CPPUNIT_TEST(testCharNamedstyle);
    CPPUNIT_TEST(testNamedStyleInheritance);
    CPPUNIT_TEST(testNestedSpan);
    CPPUNIT_TEST(testLineBreak);
    CPPUNIT_TEST(testEscape);
    CPPUNIT_TEST(testParaCharProps);
    CPPUNIT_TEST(testSection);
    CPPUNIT_TEST(testList);
    CPPUNIT_TEST(testImage);
    CPPUNIT_TEST(testImageBorder);
    CPPUNIT_TEST(testImageNospan);
    CPPUNIT_TEST(testTable);
    CPPUNIT_TEST(testTableRowSpan);
    CPPUNIT_TEST(testTableCellBorder);
    CPPUNIT_TEST(testTableCellWidth);
    CPPUNIT_TEST(testTableRowHeight);
    CPPUNIT_TEST(testLink);
    CPPUNIT_TEST(testLinkInvalid);
    CPPUNIT_TEST(testLinkCharFormat);
    CPPUNIT_TEST(testLinkNamedCharFormat);
    CPPUNIT_TEST(testTableWidth);
    CPPUNIT_TEST(testTextBox);
    CPPUNIT_TEST(testFontEmbedding);
    CPPUNIT_TEST(testImageLink);
    CPPUNIT_TEST(testFootnote);
    CPPUNIT_TEST(testPopup);
    CPPUNIT_TEST(testPopupMedia);
    CPPUNIT_TEST(testPopupAPI);
    CPPUNIT_TEST(testPageSize);
    CPPUNIT_TEST(testSVG);
    CPPUNIT_TEST(testTdf115623SingleWritingMode);
    CPPUNIT_TEST(testTdf115623SplitByChapter);
    CPPUNIT_TEST(testTdf115623ManyPageSpans);
    CPPUNIT_TEST(testSimpleRuby);
    CPPUNIT_TEST_SUITE_END();
};

void EPUBExportTest::setUp()
{
    test::BootstrapFixture::setUp();

    mxComponentContext.set(comphelper::getComponentContext(getMultiServiceFactory()));
    mxDesktop.set(frame::Desktop::create(mxComponentContext));
}

void EPUBExportTest::tearDown()
{
    if (mxComponent.is())
        mxComponent->dispose();

    if (mpXmlDoc)
    {
        xmlFreeDoc(mpXmlDoc);
        mpXmlDoc = nullptr;
    }

    test::BootstrapFixture::tearDown();
}

void EPUBExportTest::registerNamespaces(xmlXPathContextPtr& pXmlXpathCtx)
{
    xmlXPathRegisterNs(pXmlXpathCtx, BAD_CAST("dc"), BAD_CAST("http://purl.org/dc/elements/1.1/"));
    xmlXPathRegisterNs(pXmlXpathCtx, BAD_CAST("opf"), BAD_CAST("http://www.idpf.org/2007/opf"));
    xmlXPathRegisterNs(pXmlXpathCtx, BAD_CAST("xhtml"), BAD_CAST("http://www.w3.org/1999/xhtml"));
    xmlXPathRegisterNs(pXmlXpathCtx, BAD_CAST("svg"), BAD_CAST("http://www.w3.org/2000/svg"));
}

void EPUBExportTest::createDoc(const OUString& rFile,
                               const uno::Sequence<beans::PropertyValue>& rFilterData)
{
    // Import the bugdoc and export as EPUB.
    OUString aURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + rFile;
    mxComponent = loadFromDesktop(aURL);
    CPPUNIT_ASSERT(mxComponent.is());

    uno::Reference<frame::XStorable> xStorable(mxComponent, uno::UNO_QUERY);
    maTempFile.EnableKillingFile();
    utl::MediaDescriptor aMediaDescriptor;
    aMediaDescriptor["FilterName"] <<= OUString("EPUB");
    if (maFilterOptions.isEmpty())
        aMediaDescriptor["FilterData"] <<= rFilterData;
    else
        aMediaDescriptor["FilterOptions"] <<= maFilterOptions;
    xStorable->storeToURL(maTempFile.GetURL(), aMediaDescriptor.getAsConstPropertyValueList());
    mxZipFile
        = packages::zip::ZipFileAccess::createWithURL(mxComponentContext, maTempFile.GetURL());
}

xmlDocPtr EPUBExportTest::parseExport(const OUString& rName)
{
    uno::Reference<io::XInputStream> xInputStream(mxZipFile->getByName(rName), uno::UNO_QUERY);
    std::shared_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true));
    return parseXmlStream(pStream.get());
}

std::map<OUString, std::vector<OUString>> EPUBExportTest::parseCss(const OUString& rName)
{
    std::map<OUString, std::vector<OUString>> aRet;

    uno::Reference<io::XInputStream> xInputStream(mxZipFile->getByName(rName), uno::UNO_QUERY);
    std::shared_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true));

    // Minimal CSS handler till orcus is up to our needs.
    OString aLine;
    OUString aRuleName;
    while (!pStream->eof())
    {
        pStream->ReadLine(aLine);
        if (aLine.endsWith("{"))
            // '.name {' -> 'name'
            aRuleName = OUString::fromUtf8(aLine.copy(1, aLine.getLength() - 3));
        else if (aLine.endsWith(";"))
            aRet[aRuleName].push_back(OUString::fromUtf8(aLine));
    }

    return aRet;
}

OUString EPUBExportTest::getCss(std::map<OUString, std::vector<OUString>>& rCss,
                                const OUString& rClass, const OUString& rKey)
{
    OUString aRet;

    auto it = rCss.find(rClass);
    CPPUNIT_ASSERT(it != rCss.end());

    for (const auto& rKeyValue : it->second)
    {
        OUString aKeyValue = rKeyValue.trim();
        std::vector<OUString> aTokens = comphelper::string::split(aKeyValue, ':');
        CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(2), aTokens.size());
        if (aTokens[0].trim() == rKey)
        {
            aRet = aTokens[1].trim();
            if (aRet.endsWith(";"))
                // Ignore trailing semicolon.
                aRet = aRet.copy(0, aRet.getLength() - 1);
            break;
        }
    }

    return aRet;
}

void EPUBExportTest::testOutlineLevel()
{
    createDoc("outline-level.fodt", {});

    // Make sure that the output is split into two.
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0001.xhtml"));
    // This failed, output was a single section.
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0002.xhtml"));
    CPPUNIT_ASSERT(!mxZipFile->hasByName("OEBPS/sections/section0003.xhtml"));
}

void EPUBExportTest::testMimetype()
{
    createDoc("hello.fodt", {});

    // Check that the mime type is written uncompressed at the expected location.
    SvFileStream aFileStream(maTempFile.GetURL(), StreamMode::READ);
    SvMemoryStream aMemoryStream;
    aMemoryStream.WriteStream(aFileStream);
    OString aExpected("application/epub+zip");
    CPPUNIT_ASSERT(aMemoryStream.GetSize() > static_cast<sal_uInt64>(aExpected.getLength()) + 38);

    OString aActual(static_cast<const char*>(aMemoryStream.GetData()) + 38, aExpected.getLength());
    // This failed: actual data was some garbage, not the uncompressed mime type.
    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);

    mpXmlDoc = parseExport("OEBPS/content.opf");
    // Default is EPUB3.
    assertXPath(mpXmlDoc, "/opf:package", "version", "3.0");

    // This was just "libepubgen/x.y.z", i.e. the LO version was missing.
    OUString aGenerator
        = getXPath(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@name='generator']", "content");
    CPPUNIT_ASSERT(aGenerator.startsWith(utl::DocInfoHelper::GetGeneratorString()));

    uno::Reference<lang::XMultiServiceFactory> xMSF(mxComponentContext->getServiceManager(),
                                                    uno::UNO_QUERY);
    const OUString aServiceName("com.sun.star.comp.Writer.EPUBExportFilter");
    uno::Reference<document::XFilter> xFilter(xMSF->createInstance(aServiceName), uno::UNO_QUERY);
    // Should result in no errors.
    xFilter->cancel();
    // We got back what we expected.
    uno::Reference<lang::XServiceInfo> xServiceInfo(xFilter, uno::UNO_QUERY);
    CPPUNIT_ASSERT_EQUAL(aServiceName, xServiceInfo->getImplementationName());
    CPPUNIT_ASSERT(xServiceInfo->supportsService("com.sun.star.document.ExportFilter"));
}

void EPUBExportTest::testEPUB2()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { // Explicitly request EPUB2.
          { "EPUBVersion", uno::makeAny(static_cast<sal_Int32>(20)) } }));
    createDoc("hello.fodt", aFilterData);

    mpXmlDoc = parseExport("OEBPS/content.opf");
    // This was 3.0, EPUBVersion filter option was ignored and we always emitted EPUB3.
    assertXPath(mpXmlDoc, "/opf:package", "version", "2.0");
}

void EPUBExportTest::testEPUBFixedLayout()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { // Explicitly request fixed layout.
          { "EPUBLayoutMethod",
            uno::makeAny(static_cast<sal_Int32>(libepubgen::EPUB_LAYOUT_METHOD_FIXED)) } }));
    createDoc("hello.fodt", aFilterData);

    mpXmlDoc = parseExport("OEBPS/content.opf");
    // This was missing, EPUBLayoutMethod filter option was ignored and we always emitted reflowable layout.
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@property='rendition:layout']",
                       "pre-paginated");
}

void EPUBExportTest::testEPUBFixedLayoutOption()
{
    // Explicitly request fixed layout, this time via FilterOptions.
    maFilterOptions = "layout=fixed";
    createDoc("hello.fodt", {});

    // This failed, fixed layout was only working via the FilterData map.
    mpXmlDoc = parseExport("OEBPS/content.opf");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@property='rendition:layout']",
                       "pre-paginated");
}

void EPUBExportTest::testEPUBFixedLayoutImplicitBreak()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { // Explicitly request fixed layout.
          { "EPUBLayoutMethod",
            uno::makeAny(static_cast<sal_Int32>(libepubgen::EPUB_LAYOUT_METHOD_FIXED)) } }));
    createDoc("fxl-2page.fodt", aFilterData);

    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0001.xhtml"));
    // This was missing, implicit page break (as calculated by the layout) was lost on export.
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0002.xhtml"));
    CPPUNIT_ASSERT(!mxZipFile->hasByName("OEBPS/sections/section0003.xhtml"));

    // Make sure that fixed layout has chapter names in the navigation
    // document.
    mpXmlDoc = parseExport("OEBPS/toc.xhtml");
    // This was 'Page 1' instead.
    assertXPathContent(mpXmlDoc, "//xhtml:li[1]/xhtml:a", "First chapter");
    assertXPathContent(mpXmlDoc, "//xhtml:li[2]/xhtml:a", "Second chapter");
}

void EPUBExportTest::testPageBreakSplit()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { // Explicitly request split on page break (instead of on heading).
          { "EPUBSplitMethod",
            uno::makeAny(static_cast<sal_Int32>(libepubgen::EPUB_SPLIT_METHOD_PAGE_BREAK)) } }));
    createDoc("2pages.fodt", aFilterData);

    // Make sure that the output is split into two.
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0001.xhtml"));
    // This failed, output was a single section.
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/sections/section0002.xhtml"));
    CPPUNIT_ASSERT(!mxZipFile->hasByName("OEBPS/sections/section0003.xhtml"));
}

void EPUBExportTest::testSpanAutostyle()
{
    createDoc("span-autostyle.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:span[1]", "class", "span0");
    // This failed, it was still span1, i.e. the bold and the italic formatting
    // did not differ.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:span[2]", "class", "span1");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:span[3]", "class", "span2");
}

void EPUBExportTest::testParaAutostyleCharProps()
{
    createDoc("para-autostyle-char-props.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This failed, para-level char props were not exported.
    assertXPath(mpXmlDoc, "//xhtml:p[1]/xhtml:span", "class", "span0");
    assertXPath(mpXmlDoc, "//xhtml:p[2]/xhtml:span", "class", "span1");
}

void EPUBExportTest::testMeta()
{
    createDoc("meta.fodt", {});

    mpXmlDoc = parseExport("OEBPS/content.opf");
    // This was "Unknown Author", <meta:initial-creator> was not handled.
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:creator", "A U Thor");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:title", "Title");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:language", "hu");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@property='dcterms:modified']",
                       "2017-09-27T09:51:19Z");

    // Make sure that cover image next to the source document is picked up.
    assertXPath(mpXmlDoc, "/opf:package/opf:manifest/opf:item[@href='images/image0001.png']",
                "properties", "cover-image");
    assertXPath(mpXmlDoc, "/opf:package/opf:manifest/opf:item[@href='images/image0001.png']",
                "media-type", "image/png");
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/images/image0001.png"));
}

void EPUBExportTest::testMetaXMP()
{
    createDoc("meta-xmp.fodt", {});
    mpXmlDoc = parseExport("OEBPS/content.opf");

    // These were the libepubgen default values, metadata from a matching .xmp file was not picked up.
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:identifier",
                       "deadbeef-e394-4cd6-9b83-7172794612e5");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:title", "unknown title from xmp");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:creator", "unknown author from xmp");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:language", "nl");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@property='dcterms:modified']",
                       "2016-11-20T17:16:07Z");
}

void EPUBExportTest::testMetaAPI()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { { "RVNGIdentifier", uno::makeAny(OUString("deadc0de-e394-4cd6-9b83-7172794612e5")) },
          { "RVNGTitle", uno::makeAny(OUString("unknown title from api")) },
          { "RVNGInitialCreator", uno::makeAny(OUString("unknown author from api")) },
          { "RVNGLanguage", uno::makeAny(OUString("hu")) },
          { "RVNGDate", uno::makeAny(OUString("2015-11-20T17:16:07Z")) } }));
    createDoc("meta-xmp.fodt", aFilterData);
    mpXmlDoc = parseExport("OEBPS/content.opf");

    // These were values from XMP (deadbeef, etc.), not from API.
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:identifier",
                       "deadc0de-e394-4cd6-9b83-7172794612e5");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:title", "unknown title from api");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:creator", "unknown author from api");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/dc:language", "hu");
    assertXPathContent(mpXmlDoc, "/opf:package/opf:metadata/opf:meta[@property='dcterms:modified']",
                       "2015-11-20T17:16:07Z");
}

void EPUBExportTest::testCoverImage()
{
    OUString aCoverURL = m_directories.getURLFromSrc(DATA_DIRECTORY) + "meta.cover-image.png";
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "RVNGCoverImage", uno::makeAny(aCoverURL) } }));
    createDoc("hello.fodt", aFilterData);
    mpXmlDoc = parseExport("OEBPS/content.opf");

    // Make sure that the explicitly set cover image is used.
    // This failed, as the image was not part of the package.
    assertXPath(mpXmlDoc, "/opf:package/opf:manifest/opf:item[@href='images/image0001.png']",
                "properties", "cover-image");
    assertXPath(mpXmlDoc, "/opf:package/opf:manifest/opf:item[@href='images/image0001.png']",
                "media-type", "image/png");
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/images/image0001.png"));
}

void EPUBExportTest::testParaNamedstyle()
{
    createDoc("para-namedstyle.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPath(mpXmlDoc, "//xhtml:p[1]", "class", "para0");
    // This failed, paragraph properties from style were not exported.
    assertXPath(mpXmlDoc, "//xhtml:p[2]", "class", "para1");

    // Test character properties from named paragraph style.
    assertXPath(mpXmlDoc, "//xhtml:p[1]/xhtml:span", "class", "span0");
    // This failed, character properties from paragraph style were not exported.
    assertXPath(mpXmlDoc, "//xhtml:p[2]/xhtml:span", "class", "span1");
}

void EPUBExportTest::testCharNamedstyle()
{
    createDoc("char-namedstyle.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");

    // Test character properties from named text style.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:span[1]", "class", "span0");
    // This failed, character properties from text style were not exported.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:span[2]", "class", "span1");
}

void EPUBExportTest::testNamedStyleInheritance()
{
    createDoc("named-style-inheritance.fodt", {});

    // Find the CSS rule for the blue text.
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    OUString aBlue = getXPath(mpXmlDoc, "//xhtml:p[2]/xhtml:span[2]", "class");

    CPPUNIT_ASSERT_EQUAL(OUString("#0000ff"), EPUBExportTest::getCss(aCssDoc, aBlue, "color"));
    // This failed, the span only had the properties from its style, but not
    // from the style's parent(s).
    CPPUNIT_ASSERT_EQUAL(OUString("'Liberation Mono'"),
                         EPUBExportTest::getCss(aCssDoc, aBlue, "font-family"));
}

void EPUBExportTest::testNestedSpan()
{
    createDoc("nested-span.fodt", {});

    // Check textural content of nested span.
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    // This crashed, span had no content.
    assertXPathContent(mpXmlDoc, "//xhtml:p/xhtml:span[2]", "red");

    // Check formatting of nested span.
    OUString aRed = getXPath(mpXmlDoc, "//xhtml:p/xhtml:span[2]", "class");
    // This failed, direct formatting on top of named style was lost.
    CPPUNIT_ASSERT_EQUAL(OUString("#ff0000"), EPUBExportTest::getCss(aCssDoc, aRed, "color"));
    CPPUNIT_ASSERT_EQUAL(OUString("'Liberation Mono'"),
                         EPUBExportTest::getCss(aCssDoc, aRed, "font-family"));
}

void EPUBExportTest::testLineBreak()
{
    createDoc("line-break.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This was 0, line break was not handled.
    assertXPath(mpXmlDoc, "//xhtml:p[1]/xhtml:span/xhtml:br", 1);
    // This was 0, line break inside span was not handled.
    assertXPath(mpXmlDoc, "//xhtml:p[2]/xhtml:span/xhtml:br", 1);
}

void EPUBExportTest::testEscape()
{
    createDoc("escape.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This was lost.
    assertXPathContent(mpXmlDoc, "//xhtml:p[1]/xhtml:span[1]", OUString::fromUtf8("\xc2\xa0"));
    // Make sure escaping happens only once.
    assertXPathContent(mpXmlDoc, "//xhtml:p[1]/xhtml:span[2]", "a&b");
    // This was also lost.
    assertXPathContent(
        mpXmlDoc, "//xhtml:p[1]/xhtml:span[3]",
        OUString::fromUtf8("\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2"
                           "\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0\xc2\xa0 "));
}

void EPUBExportTest::testParaCharProps()
{
    createDoc("para-char-props.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    // Check formatting of the middle span.
    OUString aMiddle = getXPath(mpXmlDoc, "//xhtml:p/xhtml:span[2]", "class");
    CPPUNIT_ASSERT_EQUAL(OUString("italic"),
                         EPUBExportTest::getCss(aCssDoc, aMiddle, "font-style"));
    // Direct para formatting was lost, only direct char formatting was
    // written, so this failed.
    CPPUNIT_ASSERT_EQUAL(OUString("bold"), EPUBExportTest::getCss(aCssDoc, aMiddle, "font-weight"));
}

void EPUBExportTest::testSection()
{
    createDoc("section.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This was "After.", i.e. in-section content was ignored.
    assertXPathContent(mpXmlDoc, "//xhtml:p[2]/xhtml:span", "In section.");
}

void EPUBExportTest::testList()
{
    createDoc("list.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This was "C", i.e. in-list content was ignored.
    assertXPathContent(mpXmlDoc, "//xhtml:p[2]/xhtml:span", "B");
    // Test nested list content.
    assertXPathContent(mpXmlDoc, "//xhtml:p[6]/xhtml:span", "F");
}

void EPUBExportTest::testImage()
{
    createDoc("image.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:img", 1);
}

void EPUBExportTest::testImageBorder()
{
    createDoc("image-border.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");

    OUString aClass = getXPath(mpXmlDoc, "//xhtml:img", "class");
    // This failed, image had no border.
    CPPUNIT_ASSERT_EQUAL(OUString("0.99pt dashed #ed1c24"),
                         EPUBExportTest::getCss(aCssDoc, aClass, "border"));
}

void EPUBExportTest::testImageNospan()
{
    createDoc("image-nospan.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // Image outside a span was lost.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:img", 1);
}

void EPUBExportTest::testTable()
{
    createDoc("table.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr/xhtml:td", 4);
}

void EPUBExportTest::testTableRowSpan()
{
    createDoc("table-row-span.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This failed, row span wasn't exported.
    assertXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]/xhtml:td[1]", "rowspan", "2");
}

void EPUBExportTest::testTableCellBorder()
{
    createDoc("table-cell-border.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");

    OUString aClass
        = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]/xhtml:td[1]", "class");
    // This failed, cell border wasn't exported.
    CPPUNIT_ASSERT_EQUAL(OUString("0.05pt solid #000000"),
                         EPUBExportTest::getCss(aCssDoc, aClass, "border-left"));
}

void EPUBExportTest::testTableCellWidth()
{
    createDoc("table-cell-width.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    OUString aClass1
        = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]/xhtml:td[1]", "class");
    OUString aClass2
        = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]/xhtml:td[2]", "class");
    OUString aClass3
        = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]/xhtml:td[3]", "class");
    // These failed, all widths were 0.
    CPPUNIT_ASSERT_GREATER(EPUBExportTest::getCss(aCssDoc, aClass2, "width").toDouble(),
                           EPUBExportTest::getCss(aCssDoc, aClass1, "width").toDouble());
    CPPUNIT_ASSERT_GREATER(EPUBExportTest::getCss(aCssDoc, aClass3, "width").toDouble(),
                           EPUBExportTest::getCss(aCssDoc, aClass1, "width").toDouble());
}

void EPUBExportTest::testTableRowHeight()
{
    createDoc("table-row-height.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    OUString aClass1 = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[1]", "class");
    OUString aClass2 = getXPath(mpXmlDoc, "//xhtml:table/xhtml:tbody/xhtml:tr[2]", "class");
    // These failed, both heights were 0.
    CPPUNIT_ASSERT_GREATER(EPUBExportTest::getCss(aCssDoc, aClass2, "height").toDouble(),
                           EPUBExportTest::getCss(aCssDoc, aClass1, "height").toDouble());
}

void EPUBExportTest::testLink()
{
    createDoc("link.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPathContent(mpXmlDoc, "//xhtml:p/xhtml:a/xhtml:span", "https://libreoffice.org/");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:a", "href", "https://libreoffice.org/");
}

void EPUBExportTest::testLinkInvalid()
{
    createDoc("link-invalid.odt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This was 1, invalid relative link was not filtered out.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:a", 0);
}

void EPUBExportTest::testLinkCharFormat()
{
    createDoc("link-charformat.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // <span> was lost, link text having a char format was missing.
    assertXPathContent(mpXmlDoc, "//xhtml:p/xhtml:a/xhtml:span", "https://libreoffice.org/");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:a", "href", "https://libreoffice.org/");
}

void EPUBExportTest::testLinkNamedCharFormat()
{
    // Character properties from named character style on hyperlink was lost.
    createDoc("link-namedcharformat.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    // This failed, there was no span inside the hyperlink.
    assertXPathContent(mpXmlDoc, "//xhtml:p/xhtml:a/xhtml:span", "http://libreoffice.org");
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:a", "href", "http://libreoffice.org/");

    OUString aClass = getXPath(mpXmlDoc, "//xhtml:p/xhtml:a/xhtml:span", "class");
    CPPUNIT_ASSERT_EQUAL(OUString("#ff0000"), EPUBExportTest::getCss(aCssDoc, aClass, "color"));
}

void EPUBExportTest::testTableWidth()
{
    createDoc("table-width.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");

    OUString aClass = getXPath(mpXmlDoc, "//xhtml:table", "class");
    // This failed, relative total width of table was lost.
    CPPUNIT_ASSERT_EQUAL(OUString("50%"), EPUBExportTest::getCss(aCssDoc, aClass, "width"));
}

void EPUBExportTest::testTextBox()
{
    createDoc("text-box.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");

    // This failed, image with caption was lost.
    assertXPath(mpXmlDoc, "//xhtml:img", "class", "frame1");
    // Expected spans:
    // 1) break after the image
    // 2) "Illustration "
    // 3) The sequence field, this was missing (was ": foo" instead).
    assertXPathContent(mpXmlDoc, "//xhtml:div/xhtml:p/xhtml:span[3]", "1");

    OUString aClass = getXPath(mpXmlDoc, "//xhtml:div/xhtml:p/xhtml:span[3]", "class");
    // This failed, the 3rd span was not italic.
    CPPUNIT_ASSERT_EQUAL(OUString("italic"), EPUBExportTest::getCss(aCssDoc, aClass, "font-style"));
}

void EPUBExportTest::testFontEmbedding()
{
#if !defined(MACOSX)
    createDoc("font-embedding.fodt", {});

    // Make sure that the params of defineEmbeddedFont() are all handled.
    // librevenge:name
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    // 'SketchFlow Print' or ''SketchFlow Print1'
    CPPUNIT_ASSERT(EPUBExportTest::getCss(aCssDoc, "font-face", "font-family")
                       .startsWith("'SketchFlow Print"));
    // librevenge:mime-type
    mpXmlDoc = parseExport("OEBPS/content.opf");
    assertXPath(mpXmlDoc, "/opf:package/opf:manifest/opf:item[@href='fonts/font0001.otf']",
                "media-type", "application/vnd.ms-opentype");
    // office:binary-data
    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/fonts/font0001.otf"));
    // librevenge:font-style
    CPPUNIT_ASSERT_EQUAL(OUString("normal"),
                         EPUBExportTest::getCss(aCssDoc, "font-face", "font-style"));
    // librevenge:font-weight
    CPPUNIT_ASSERT_EQUAL(OUString("normal"),
                         EPUBExportTest::getCss(aCssDoc, "font-face", "font-weight"));
#endif
}

void EPUBExportTest::testImageLink()
{
    createDoc("image-link.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // This failed, image was missing.
    assertXPath(mpXmlDoc, "//xhtml:p/xhtml:a/xhtml:img", 1);
}

void EPUBExportTest::testFootnote()
{
    createDoc("footnote.fodt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // These were missing, footnote was lost.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p/xhtml:sup/xhtml:a", "type", "noteref");
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:aside", "type", "footnote");
}

void EPUBExportTest::testPopup()
{
    createDoc("popup.odt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // Test image popup anchor.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p[1]/xhtml:a", "type", "noteref");
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p[1]/xhtml:a/xhtml:img", 1);
    // Test image popup content.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:aside[1]", "type", "footnote");
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:aside[1]/xhtml:img", 1);

    // Test text popup anchor.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p[2]/xhtml:span/xhtml:a", "type", "noteref");
    assertXPathContent(mpXmlDoc, "//xhtml:body/xhtml:p[2]/xhtml:span/xhtml:a", "link");
    // Test text popup content.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:aside[2]", "type", "footnote");
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:aside[2]/xhtml:img", 1);
}

void EPUBExportTest::testPopupMedia()
{
    // This is the same as testPopup(), but the links point to images in the
    // default media directory, not in the document directory.
    createDoc("popup-media.odt", {});

    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // Test image popup anchor. This failed, number of XPath nodes was 0.
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p[1]/xhtml:a", "type", "noteref");
    assertXPath(mpXmlDoc, "//xhtml:body/xhtml:p[1]/xhtml:a/xhtml:img", 1);
}

void EPUBExportTest::testPopupAPI()
{
    // Make sure that the popup works with data from a media directory.
    OUString aMediaDir = m_directories.getURLFromSrc(DATA_DIRECTORY) + "popup";
    uno::Sequence<beans::PropertyValue> aFilterData(
        comphelper::InitPropertySequence({ { "RVNGMediaDir", uno::makeAny(aMediaDir) } }));
    createDoc("popup-api.odt", aFilterData);

    // We have a non-empty anchor image.
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    OUString aAnchor = getXPath(mpXmlDoc, "//xhtml:body/xhtml:p[1]/xhtml:a/xhtml:img", "src");
    CPPUNIT_ASSERT(!aAnchor.isEmpty());
    // We have a non-empty popup image.
    OUString aData = getXPath(mpXmlDoc, "//xhtml:body/xhtml:aside[1]/xhtml:img", "src");
    CPPUNIT_ASSERT(!aData.isEmpty());
    // The anchor is different from the popup image.
    CPPUNIT_ASSERT(aAnchor != aData);
}

void EPUBExportTest::testPageSize()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { { "EPUBLayoutMethod",
            uno::makeAny(static_cast<sal_Int32>(libepubgen::EPUB_LAYOUT_METHOD_FIXED)) } }));
    createDoc("hello.fodt", aFilterData);

    // This failed, viewport was empty, so page size was lost.
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    // 21,59cm x 27.94cm (letter).
    assertXPath(mpXmlDoc, "/xhtml:html/xhtml:head/xhtml:meta[@name='viewport']", "content",
                "width=816, height=1056");

    xmlFreeDoc(mpXmlDoc);
    mpXmlDoc = parseExport("OEBPS/images/image0001.svg");
    // This was 288mm, logic->logic conversion input was a pixel value.
    assertXPath(mpXmlDoc, "/svg:svg", "width", "216mm");
    assertXPath(mpXmlDoc, "/svg:svg", "height", "279mm");
}

void EPUBExportTest::testSVG()
{
    uno::Sequence<beans::PropertyValue> aFilterData(comphelper::InitPropertySequence(
        { { "EPUBLayoutMethod",
            uno::makeAny(static_cast<sal_Int32>(libepubgen::EPUB_LAYOUT_METHOD_FIXED)) } }));
    createDoc("hello.fodt", aFilterData);

    CPPUNIT_ASSERT(mxZipFile->hasByName("OEBPS/images/image0001.svg"));
    uno::Reference<io::XInputStream> xInputStream(
        mxZipFile->getByName("OEBPS/images/image0001.svg"), uno::UNO_QUERY);
    std::shared_ptr<SvStream> pStream(utl::UcbStreamHelper::CreateStream(xInputStream, true));

    SvMemoryStream aMemoryStream;
    aMemoryStream.WriteStream(*pStream);
    OString aExpected("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n<svg");
    CPPUNIT_ASSERT(aMemoryStream.GetSize() > static_cast<sal_uInt64>(aExpected.getLength()));

    // This failed, there was a '<!DOCTYPE' line between the xml and the svg
    // one, causing a validation error.
    OString aActual(static_cast<const char*>(aMemoryStream.GetData()), aExpected.getLength());
    CPPUNIT_ASSERT_EQUAL(aExpected, aActual);

    // This failed, we used the xlink attribute namespace, but we did not
    // define its URL.
    mpXmlDoc = parseExport("OEBPS/images/image0001.svg");
    assertXPathNSDef(mpXmlDoc, "/svg:svg", "xlink", "http://www.w3.org/1999/xlink");
}

void EPUBExportTest::testTdf115623SingleWritingMode()
{
    // Simple page that has single writing mode should work.
    createDoc("tdf115623-single-writing-mode.odt", {});
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    OUString aClass = getXPath(mpXmlDoc, "//xhtml:body", "class");
    CPPUNIT_ASSERT_EQUAL(OUString("vertical-rl"),
                         EPUBExportTest::getCss(aCssDoc, aClass, "writing-mode"));
}

void EPUBExportTest::testTdf115623SplitByChapter()
{
    createDoc("tdf115623-split-by-chapter.odt", {});
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    {
        mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
        OUString aClass = getXPath(mpXmlDoc, "//xhtml:body", "class");
        CPPUNIT_ASSERT_EQUAL(OUString("vertical-rl"),
                             EPUBExportTest::getCss(aCssDoc, aClass, "writing-mode"));
        xmlFreeDoc(mpXmlDoc);
        mpXmlDoc = nullptr;
    }
    // Splitted HTML should keep the same writing-mode.
    {
        mpXmlDoc = parseExport("OEBPS/sections/section0002.xhtml");
        OUString aClass = getXPath(mpXmlDoc, "//xhtml:body", "class");
        CPPUNIT_ASSERT_EQUAL(OUString("vertical-rl"),
                             EPUBExportTest::getCss(aCssDoc, aClass, "writing-mode"));
    }
}

void EPUBExportTest::testTdf115623ManyPageSpans()
{
    createDoc("tdf115623-many-pagespans.odt", {});
    std::map<OUString, std::vector<OUString>> aCssDoc = parseCss("OEBPS/styles/stylesheet.css");
    // Two pages should have different writing modes.
    {
        mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
        OUString aClass = getXPath(mpXmlDoc, "//xhtml:body", "class");
        CPPUNIT_ASSERT_EQUAL(OUString("vertical-rl"),
                             EPUBExportTest::getCss(aCssDoc, aClass, "writing-mode"));
        xmlFreeDoc(mpXmlDoc);
        mpXmlDoc = nullptr;
    }
    {
        mpXmlDoc = parseExport("OEBPS/sections/section0002.xhtml");
        OUString aClass = getXPath(mpXmlDoc, "//xhtml:body", "class");
        CPPUNIT_ASSERT_EQUAL(OUString("horizontal-tb"),
                             EPUBExportTest::getCss(aCssDoc, aClass, "writing-mode"));
    }
}

void EPUBExportTest::testSimpleRuby()
{
    createDoc("simple-ruby.odt", {});
    mpXmlDoc = parseExport("OEBPS/sections/section0001.xhtml");
    assertXPathContent(mpXmlDoc, "//xhtml:body/xhtml:p/xhtml:ruby/xhtml:span", "base text");
    assertXPathContent(mpXmlDoc, "//xhtml:body/xhtml:p/xhtml:ruby/xhtml:rt", "ruby text");
}

CPPUNIT_TEST_SUITE_REGISTRATION(EPUBExportTest);
}

CPPUNIT_PLUGIN_IMPLEMENT();

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