"""
novelWriter – ToOdt Class Tester
================================

This file is a part of novelWriter
Copyright (C) 2021 Veronica Berglyd Olsen and novelWriter contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations

import xml.etree.ElementTree as ET
import zipfile

from shutil import copyfile

import pytest

from PyQt5.QtGui import QColor

from novelwriter.common import xmlIndent
from novelwriter.constants import nwHeadFmt
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt
from novelwriter.formats.toodt import ODTParagraphStyle, ODTTextStyle, ToOdt, XMLParagraph, _mkTag

from tests.tools import ODT_IGNORE, cmpFiles, xmlToText


@pytest.mark.core
def testFmtToOdt_Init(mockGUI):
    """Test initialisation of the ODT document."""
    project = NWProject()

    # Flat Doc
    # ========

    odt = ToOdt(project, isFlat=True)
    odt.initDocument()

    # Document XML
    assert odt._dFlat.tag == _mkTag("office", "document")
    assert odt._dCont.tag == ""
    assert odt._dMeta.tag == ""
    assert odt._dStyl.tag == ""

    # Content XML
    assert odt._xMeta.tag == _mkTag("office", "meta")
    assert odt._xFont.tag == _mkTag("office", "font-face-decls")
    assert odt._xFnt2.tag == ""
    assert odt._xStyl.tag == _mkTag("office", "styles")
    assert odt._xAuto.tag == _mkTag("office", "automatic-styles")
    assert odt._xAut2.tag == ""
    assert odt._xMast.tag == _mkTag("office", "master-styles")
    assert odt._xBody.tag == _mkTag("office", "body")
    assert odt._xText.tag == _mkTag("office", "text")

    # ODT Doc
    # =======

    odt = ToOdt(project, isFlat=False)
    odt.initDocument()

    # Document XML
    assert odt._dFlat.tag == ""
    assert odt._dCont.tag == _mkTag("office", "document-content")
    assert odt._dMeta.tag == _mkTag("office", "document-meta")
    assert odt._dStyl.tag == _mkTag("office", "document-styles")

    # Content XML
    assert odt._xMeta.tag == _mkTag("office", "meta")
    assert odt._xFont.tag == _mkTag("office", "font-face-decls")
    assert odt._xFnt2.tag == _mkTag("office", "font-face-decls")
    assert odt._xStyl.tag == _mkTag("office", "styles")
    assert odt._xAuto.tag == _mkTag("office", "automatic-styles")
    assert odt._xAut2.tag == _mkTag("office", "automatic-styles")
    assert odt._xMast.tag == _mkTag("office", "master-styles")
    assert odt._xBody.tag == _mkTag("office", "body")
    assert odt._xText.tag == _mkTag("office", "text")


@pytest.mark.core
def testFmtToOdt_TextFormatting(mockGUI):
    """Test formatting of paragraphs."""
    project = NWProject()
    odt = ToOdt(project, isFlat=True)

    odt.initDocument()
    assert xmlToText(odt._xText) == "<office:text />"

    # Paragraph Style
    # ===============
    oStyle = ODTParagraphStyle("test")

    assert odt._paraStyle("stuff", oStyle) == "Standard"
    assert odt._paraStyle("Text_20_body", oStyle) == "Text_20_body"

    # Create new para style
    oStyle.setTextAlign("center")
    assert odt._paraStyle("Text_20_body", oStyle) == "P1"

    # Return the same style on second call
    assert odt._paraStyle("Text_20_body", oStyle) == "P1"

    assert list(odt._mainPara.keys()) == [
        "Text_20_body", "First_20_line_20_indent", "Text_20_Meta", "Title", "Separator",
        "Heading_20_1", "Heading_20_2", "Heading_20_3", "Heading_20_4", "Header", "Footnote",
    ]

    key = "55db6c1d22ff5aba93f0f67c8d4a857a26e2d3813dfbcba1ef7c0d424f501be5"
    assert key in odt._autoPara
    assert isinstance(odt._autoPara[key], ODTParagraphStyle)
    assert odt._autoPara[key].name == "P1"

    # Paragraph Formatting
    # ====================
    oStyle = ODTParagraphStyle("test")

    # No Text
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, "")
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard" />'
        '</office:text>'
    )

    # No Format
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, "Hello World")
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">Hello World</text:p>'
        '</office:text>'
    )

    # Heading Level None
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, "Hello World", isHead=True)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:h text:style-name="Standard">Hello World</text:h>'
        '</office:text>'
    )

    # Heading Level 1
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, "Hello World", isHead=True, oLevel="1")
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:h text:style-name="Standard" text:outline-level="1">Hello World</text:h>'
        '</office:text>'
    )

    # Formatted Text
    text = "A bold word"
    fmt = [(2, TextFmt.B_B, ""), (6, TextFmt.B_E, "")]
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">A <text:span text:style-name="T1">bold</text:span> '
        'word</text:p>'
        '</office:text>'
    )

    # Incorrectly Formatted Text
    text = "A few words"
    fmt = [(2, TextFmt.B_B, ""), (5, TextFmt.B_E, ""), (7, 99999, "")]
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">A <text:span text:style-name="T1">few</text:span> '
        'words</text:p>'
        '</office:text>'
    )
    odt._errData = []

    # Unclosed format
    text = "A bold word"
    fmt = [(2, TextFmt.B_B, "")]
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">A '
        '<text:span text:style-name="T1">bold word</text:span></text:p>'
        '</office:text>'
    )

    # Tabs and Breaks
    text = "Hello\n\tWorld"
    fmt = []
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">Hello<text:line-break /><text:tab />World</text:p>'
        '</office:text>'
    )


@pytest.mark.core
def testFmtToOdt_Fields(mockGUI):
    """Test formatting of footnotes."""
    project = NWProject()
    odt = ToOdt(project, True)
    odt.initDocument()

    # Field Builder
    xNode = odt._generateField("a:allWords", 0x00)
    assert isinstance(xNode, ET.Element)
    assert xmlToText(xNode) == (
        '<text:user-field-getstyle:data-style-name="N0" text:name="ManuscriptAllWords">'
        '0</text:user-field-get>'
    )
    # assert odt._usedFields == [(xNode.find(_wTag("t")), "b")]

    assert odt._generateField("a", 0x00) is None

    # Full Processing
    odt._text = (
        "Word Count: [field:allWords]\n"
        "Character Count: [field:allChars]\n"
        "Chicken Count: [field:allChickens]\n"
    )
    odt.tokenizeText()
    odt.doConvert()
    odt.countStats()
    assert xmlToText(odt._xBody) == (
        '<office:body><office:text>'
        '<text:p text:style-name="Text_20_body">'
        'Word Count: <text:user-field-get style:data-style-name="N0" '
        'text:name="ManuscriptAllWords">0</text:user-field-get><text:line-break />'
        'Character Count: <text:user-field-get style:data-style-name="N0" '
        'text:name="ManuscriptAllChars">0</text:user-field-get><text:line-break />'
        'Chicken Count: <text:user-field-get style:data-style-name="N0" '
        'text:name="ManuscriptAllChickens">0</text:user-field-get></text:p>'
        '</office:text></office:body>'
    )


@pytest.mark.core
def testFmtToOdt_DialogueFormatting(mockGUI):
    """Test formatting of dialogue."""
    project = NWProject()
    odt = ToOdt(project, isFlat=True)
    odt.setDialogHighlight(True)
    odt.initDocument()
    oStyle = ODTParagraphStyle("test")

    # Regular dialogue
    text = "Text with 'dialogue in it.'"
    fmt = [(10, TextFmt.COL_B, "dialog"), (27, TextFmt.COL_E, "")]
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">Text with '
        '<text:span text:style-name="T1">\'dialogue in it.\'</text:span></text:p>'
        '</office:text>'
    )

    # Alternative dialogue
    text = "Text with ::dialogue in it.::"
    fmt = [(10, TextFmt.COL_B, "altdialog"), (29, TextFmt.COL_E, "")]
    xTest = ET.Element(_mkTag("office", "text"))
    odt._addTextPar(xTest, "Standard", oStyle, text, tFmt=fmt)
    assert odt.errData == []
    assert xmlToText(xTest) == (
        '<office:text>'
        '<text:p text:style-name="Standard">Text with '
        '<text:span text:style-name="T2">::dialogue in it.::</text:span></text:p>'
        '</office:text>'
    )


@pytest.mark.core
def testFmtToOdt_ConvertHeaders(mockGUI):
    """Test the converter of the ToOdt class."""
    project = NWProject()
    odt = ToOdt(project, isFlat=True)
    odt._isNovel = True

    # Header 1
    odt._text = "# Title\n"
    odt.setPartitionFormat(f"Part{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="P1" text:outline-level="1">Part'
        '<text:line-break />Title</text:h>'
        '</office:text>'
    )

    # Header 2
    odt._text = "## Title\n"
    odt.setChapterFormat(f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="P2" text:outline-level="2">Chapter 1'
        '<text:line-break />Title</text:h>'
        '</office:text>'
    )

    # Header 3
    odt._text = "### Title\n"
    odt.setSceneFormat(f"Scene {nwHeadFmt.SC_ABS}{nwHeadFmt.BR}{nwHeadFmt.TITLE}")
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene 1'
        '<text:line-break />Title</text:h>'
        '</office:text>'
    )

    # Header 4
    odt._text = "#### Title\n"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_4" text:outline-level="4">Title</text:h>'
        '</office:text>'
    )

    # Title
    odt._isFirst = True
    odt._text = "#! Title\n"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Title">Title</text:p>'
        '</office:text>'
    )

    # Unnumbered chapter
    odt._text = "##! Prologue\n"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="P2" text:outline-level="2">Prologue</text:h>'
        '</office:text>'
    )


@pytest.mark.core
def testFmtToOdt_ConvertParagraphs(mockGUI):
    """Test the converter of the ToOdt class."""
    project = NWProject()
    odt = ToOdt(project, isFlat=True)
    odt._isNovel = True

    def getStyle(styleName):
        for style in odt._autoPara.values():
            if style.name == styleName:
                return style
        return None

    # Nested Markdown Text
    odt._text = "Some ~~nested **bold** and _italics_ text~~ text."
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some '
        '<text:span text:style-name="T1">nested </text:span>'
        '<text:span text:style-name="T2">bold</text:span>'
        '<text:span text:style-name="T1"> and </text:span>'
        '<text:span text:style-name="T3">italics</text:span>'
        '<text:span text:style-name="T1"> text</text:span> text.</text:p>'
        '</office:text>'
    )

    # Nested Shortcode Text, Emphasis
    odt._text = "Some [s]nested [b]bold[/b] [u]and[/u] [i]italics[/i] text[/s] text."
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some '
        '<text:span text:style-name="T1">nested </text:span>'
        '<text:span text:style-name="T2">bold</text:span>'
        '<text:span text:style-name="T1"> </text:span>'
        '<text:span text:style-name="T4">and</text:span>'
        '<text:span text:style-name="T1"> </text:span>'
        '<text:span text:style-name="T3">italics</text:span>'
        '<text:span text:style-name="T1"> text</text:span> text.</text:p>'
        '</office:text>'
    )

    # Shortcode Text, Super/Subscript
    odt._text = "Some super[sup]script[/sup] and sub[sub]script[/sub] text."
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some '
        'super<text:span text:style-name="T5">script</text:span> and '
        'sub<text:span text:style-name="T6">script</text:span> text.</text:p>'
        '</office:text>'
    )

    # Shortcode Text, Underline/Highlight
    odt._text = "Some [u]underlined and [m]highlighted[/m][/u] text."
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some '
        '<text:span text:style-name="T7">underlined and </text:span>'
        '<text:span text:style-name="T8">highlighted</text:span> text.</text:p>'
        '</office:text>'
    )

    # Hard Break
    odt._text = "Some text.\nNext line\n"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some text.<text:line-break />Next line</text:p>'
        '</office:text>'
    )

    # Tab
    odt._text = "\tItem 1\tItem 2\n"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body"><text:tab />Item 1<text:tab />Item 2</text:p>'
        '</office:text>'
    )

    # Tab in Format
    odt._text = "Some **bold\ttext**"
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Some <text:span text:style-name="T9">'
        'bold<text:tab />text</text:span></text:p>'
        '</office:text>'
    )

    # Multiple Spaces
    odt._text = (
        "### Scene\n\n"
        "Hello World\n\n"
        "Hello  World\n\n"
        "Hello   World\n\n"
    )
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
        '<text:p text:style-name="Text_20_body">Hello World</text:p>'
        '<text:p text:style-name="Text_20_body">Hello <text:s />World</text:p>'
        '<text:p text:style-name="Text_20_body">Hello <text:s text:c="2" />World</text:p>'
        '</office:text>'
    )

    # Synopsis, Short, Comment, Keywords
    odt._text = (
        "### Scene\n\n"
        "@pov: Jane\n\n"
        "% synopsis: So it begins\n\n"
        "% short: Then what\n\n"
        "% A plain comment\n\n"
    )
    odt.setSynopsis(True)
    odt.setComments(True)
    odt.setKeywords(True)
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
        '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T10">'
        'Point of View:</text:span> <text:span text:style-name="T11">Jane</text:span></text:p>'
        '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T12">'
        'Synopsis:</text:span> '
        '<text:span text:style-name="T13">So it begins</text:span></text:p>'
        '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T12">'
        'Short Description:</text:span> '
        '<text:span text:style-name="T13">Then what</text:span></text:p>'
        '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T14">'
        'Comment:</text:span> '
        '<text:span text:style-name="T15">A plain comment</text:span></text:p>'
        '</office:text>'
    )

    # Scene Separator
    odt._text = "### Scene One\n\nText\n\n### Scene Two\n\nText"
    odt.setSceneFormat("* * *", False)
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Separator">* * *</text:p>'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '<text:p text:style-name="Separator">* * *</text:p>'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '</office:text>'
    )

    # Scene Break
    odt._text = "### Scene One\n\nText\n\n### Scene Two\n\nText"
    odt.setSceneFormat("", False)
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body" />'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '<text:p text:style-name="Text_20_body" />'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '</office:text>'
    )

    # Paragraph Styles
    odt._text = (
        "### Scene\n\n"
        "@pov: Jane\n"
        "@char: John\n"
        "@plot: Main\n\n"
        ">> Right align\n\n"
        "Left Align <<\n\n"
        ">> Centered <<\n\n"
        "> Left indent\n\n"
        "Right indent <\n\n"
    )
    odt.setSceneFormat(nwHeadFmt.TITLE, False)
    odt.setKeywords(True)
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
        '<text:p text:style-name="P1"><text:span text:style-name="T10">'
        'Point of View:</text:span> <text:span text:style-name="T11">Jane</text:span></text:p>'
        '<text:p text:style-name="P2"><text:span text:style-name="T10">'
        'Characters:</text:span> <text:span text:style-name="T11">John</text:span></text:p>'
        '<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T10">'
        'Plot:</text:span> <text:span text:style-name="T11">Main</text:span></text:p>'
        '<text:p text:style-name="P3">Right align</text:p>'
        '<text:p text:style-name="Text_20_body">Left Align</text:p>'
        '<text:p text:style-name="P4">Centered</text:p>'
        '<text:p text:style-name="P5">Left indent</text:p>'
        '<text:p text:style-name="P6">Right indent</text:p>'
        '</office:text>'
    )
    assert getStyle("P1")._pAttr["margin-bottom"] == ["fo", "0.000cm"]  # type: ignore
    assert getStyle("P2")._pAttr["margin-bottom"] == ["fo", "0.000cm"]  # type: ignore
    assert getStyle("P2")._pAttr["margin-top"] == ["fo", "0.000cm"]  # type: ignore
    assert getStyle("P3")._pAttr["text-align"] == ["fo", "right"]  # type: ignore
    assert getStyle("P4")._pAttr["text-align"] == ["fo", "center"]  # type: ignore
    assert getStyle("P5")._pAttr["margin-left"] == ["fo", "1.693cm"]  # type: ignore
    assert getStyle("P6")._pAttr["margin-right"] == ["fo", "1.693cm"]  # type: ignore

    # Justified
    odt._text = (
        "### Scene\n\n"
        "Regular paragraph\n\n"
        "with\nbreak\n\n"
        "Left Align <<\n\n"
    )
    odt.setJustify(True)
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    odt.setJustify(False)
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="Heading_20_3" text:outline-level="3">Scene</text:h>'
        '<text:p text:style-name="P7">Regular paragraph</text:p>'
        '<text:p text:style-name="Text_20_body">with<text:line-break />break</text:p>'
        '<text:p text:style-name="Text_20_body">Left Align</text:p>'
        '</office:text>'
    )
    assert getStyle("P7")._pAttr["text-align"] == ["fo", "justify"]  # type: ignore

    # Page Breaks
    odt._text = (
        "## Chapter One\n\n"
        "Text\n\n"
        "## Chapter Two\n\n"
        "Text\n\n"
    )
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:h text:style-name="P8" text:outline-level="2">Chapter One</text:h>'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '<text:h text:style-name="P8" text:outline-level="2">Chapter Two</text:h>'
        '<text:p text:style-name="Text_20_body">Text</text:p>'
        '</office:text>'
    )

    # Footnotes
    odt._text = (
        "Text with one[footnote:fa], **two**[footnote:fd], "
        "or three[footnote:fb] footnotes.[footnote:fe]\n\n"
        "%footnote.fa: Footnote text A.[footnote:fc]\n\n"
        "%footnote.fc: This footnote is skipped.\n\n"
        "%footnote.fd: Another footnote.\n\n"
        "%footnote.fe: Again?\n\n"
    )
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Text with one'
        '<text:note text:id="ftn1" text:note-class="footnote">'
        '<text:note-citation>1</text:note-citation>'
        '<text:note-body>'
        '<text:p text:style-name="Footnote">Footnote text A.</text:p>'
        '</text:note-body>'
        '</text:note>, <text:span text:style-name="T9">two</text:span>'
        '<text:note text:id="ftn2" text:note-class="footnote">'
        '<text:note-citation>2</text:note-citation>'
        '<text:note-body>'
        '<text:p text:style-name="Footnote">Another footnote.</text:p>'
        '</text:note-body>'
        '</text:note>, or three footnotes.'
        '<text:note text:id="ftn3" text:note-class="footnote">'
        '<text:note-citation>3</text:note-citation>'
        '<text:note-body>'
        '<text:p text:style-name="Footnote">Again?</text:p>'
        '</text:note-body>'
        '</text:note></text:p>'
        '</office:text>'
    )

    # Test for issue #1412
    # ====================
    # See: https://github.com/vkbo/novelWriter/issues/1412

    odt._text = "Test text \\**_bold_** and more."
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.closeDocument()
    assert odt.errData == []
    assert xmlToText(odt._xText) == (
        '<office:text>'
        '<text:p text:style-name="Text_20_body">Test text **<text:span text:style-name="T16">'
        'bold</text:span>** and more.</text:p>'
        '</office:text>'
    )


@pytest.mark.core
def testFmtToOdt_ConvertDirect(mockGUI):
    """Test the converter directly using the ToOdt class to reach some
    otherwise hard to reach conditions.
    """
    project = NWProject()
    doc = ToOdt(project, isFlat=True)

    doc._isNovel = True

    # Justified
    doc = ToOdt(project, isFlat=True)
    doc._blocks = [
        (BlockTyp.TEXT, "", "This is a paragraph", [], BlockFmt.JUSTIFY),
    ]
    doc.initDocument()
    doc.doConvert()
    doc.closeDocument()
    assert (
        '<style:style style:name="P1" style:family="paragraph" '
        'style:parent-style-name="Text_20_body">'
        '<style:paragraph-properties fo:text-align="justify" />'
        '</style:style>'
    ) in xmlToText(doc._xAuto)
    assert xmlToText(doc._xText) == (
        '<office:text>'
        '<text:p text:style-name="P1">This is a paragraph</text:p>'
        '</office:text>'
    )

    # Page Break After
    doc = ToOdt(project, isFlat=True)
    doc._blocks = [
        (BlockTyp.TEXT, "", "This is a paragraph", [], BlockFmt.PBA),
    ]
    doc.initDocument()
    doc.doConvert()
    doc.closeDocument()
    assert (
        '<style:style style:name="P1" style:family="paragraph" '
        'style:parent-style-name="Text_20_body">'
        '<style:paragraph-properties fo:break-after="page" />'
        '</style:style>'
    ) in xmlToText(doc._xAuto)
    assert xmlToText(doc._xText) == (
        '<office:text>'
        '<text:p text:style-name="P1">This is a paragraph</text:p>'
        '</office:text>'
    )


@pytest.mark.core
def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths, ipsumText):
    """Test the document save functions."""
    project = NWProject()
    project.data.setAuthor("Jane Smith")
    project.data.setName("Test Project")
    project.data.setSaveCount(1234)
    project.data.setEditTime(3674096)

    odt = ToOdt(project, isFlat=True)
    odt._isNovel = True
    odt.setHeaderFormat(nwHeadFmt.DOC_AUTO, 1)
    assert odt._headerFormat == nwHeadFmt.DOC_AUTO

    odt.setPageLayout(148, 210, 20, 18, 17, 15)
    assert odt._mDocWidth  == "14.800cm"
    assert odt._mDocHeight == "21.000cm"
    assert odt._mDocTop    == "2.000cm"
    assert odt._mDocBtm    == "1.800cm"
    assert odt._mDocLeft   == "1.700cm"
    assert odt._mDocRight  == "1.500cm"

    odt._text = (
        "#! My Novel\n\n"
        "**Word Count: [field:allWords]**\n"
        "[field:paragraphCount] paragrphs\n"
        "Web: http://example.com\n\n"
        "## Chapter One\n\n"
        f"{ipsumText[0]}\n\n"
        "## Chapter Two\n\n"
        f"{ipsumText[1]}[footnote:abc]\n\n"
        "%Footnote.abc: Lorem ipsum\n\n"
    )
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.countStats()
    odt.closeDocument()

    flatFile = fncPath / "document.fodt"
    testFile = tstPaths.outDir / "fmtToOdt_SaveFlat_document.fodt"
    compFile = tstPaths.refDir / "fmtToOdt_SaveFlat_document.fodt"

    odt.saveDocument(flatFile)
    assert flatFile.exists()

    copyfile(flatFile, testFile)
    assert cmpFiles(testFile, compFile, ignoreStart=ODT_IGNORE)


@pytest.mark.core
def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths, ipsumText):
    """Test the document save functions."""
    project = NWProject()
    project.data.setAuthor("Jane Smith")
    project.data.setName("Test Project")
    project.data.setSaveCount(1234)
    project.data.setEditTime(3674096)

    odt = ToOdt(project, isFlat=False)
    odt._isNovel = True

    # Set a format without page number
    odt.setHeaderFormat(f"{nwHeadFmt.DOC_PROJECT} - {nwHeadFmt.DOC_AUTHOR}", 0)

    odt._text = (
        "#! My Novel\n\n"
        "**Word Count: [field:allWords]**\n"
        "[field:paragraphCount] paragrphs\n"
        "Web: http://example.com\n\n"
        "## Chapter One\n\n"
        f"{ipsumText[0]}\n\n"
        "## Chapter Two\n\n"
        f"{ipsumText[1]}[footnote:abc]\n\n"
        "%Footnote.abc: Lorem ipsum\n\n"
    )
    odt.tokenizeText()
    odt.initDocument()
    odt.doConvert()
    odt.countStats()
    odt.closeDocument()

    fullFile = fncPath / "document.odt"

    odt.saveDocument(fullFile)
    assert fullFile.exists()
    assert zipfile.is_zipfile(fullFile)

    maniFile = tstPaths.outDir / "fmtToOdt_SaveFull_manifest.xml"
    settFile = tstPaths.outDir / "fmtToOdt_SaveFull_settings.xml"
    contFile = tstPaths.outDir / "fmtToOdt_SaveFull_content.xml"
    metaFile = tstPaths.outDir / "fmtToOdt_SaveFull_meta.xml"
    stylFile = tstPaths.outDir / "fmtToOdt_SaveFull_styles.xml"

    maniComp = tstPaths.refDir / "fmtToOdt_SaveFull_manifest.xml"
    settComp = tstPaths.refDir / "fmtToOdt_SaveFull_settings.xml"
    contComp = tstPaths.refDir / "fmtToOdt_SaveFull_content.xml"
    metaComp = tstPaths.refDir / "fmtToOdt_SaveFull_meta.xml"
    stylComp = tstPaths.refDir / "fmtToOdt_SaveFull_styles.xml"

    extaxtTo = tstPaths.outDir / "fmtToOdt_SaveFull"

    with zipfile.ZipFile(fullFile, mode="r") as zipObj:
        zipObj.extract("META-INF/manifest.xml", extaxtTo)
        zipObj.extract("settings.xml", extaxtTo)
        zipObj.extract("content.xml", extaxtTo)
        zipObj.extract("meta.xml", extaxtTo)
        zipObj.extract("styles.xml", extaxtTo)

    maniOut = tstPaths.outDir / "fmtToOdt_SaveFull" / "META-INF" / "manifest.xml"
    settOut = tstPaths.outDir / "fmtToOdt_SaveFull" / "settings.xml"
    contOut = tstPaths.outDir / "fmtToOdt_SaveFull" / "content.xml"
    metaOut = tstPaths.outDir / "fmtToOdt_SaveFull" / "meta.xml"
    stylOut = tstPaths.outDir / "fmtToOdt_SaveFull" / "styles.xml"

    def prettifyXml(inFile, outFile):
        with open(outFile, mode="wb") as fStream:
            xml = ET.parse(inFile)
            xmlIndent(xml)
            xml.write(fStream, encoding="utf-8", xml_declaration=True)

    prettifyXml(maniOut, maniFile)
    prettifyXml(settOut, settFile)
    prettifyXml(contOut, contFile)
    prettifyXml(metaOut, metaFile)
    prettifyXml(stylOut, stylFile)

    assert cmpFiles(maniFile, maniComp)
    assert cmpFiles(settFile, settComp)
    assert cmpFiles(contFile, contComp)
    assert cmpFiles(metaFile, metaComp, ignoreStart=ODT_IGNORE)
    assert cmpFiles(stylFile, stylComp)


@pytest.mark.core
def testFmtToOdt_ODTParagraphStyle():
    """Test the ODTParagraphStyle class."""
    parStyle = ODTParagraphStyle("test")

    # Set Attributes
    # ==============

    # Display, Parent, Next Style
    assert parStyle._mAttr["display-name"]      == ["style", None]
    assert parStyle._mAttr["parent-style-name"] == ["style", None]
    assert parStyle._mAttr["next-style-name"]   == ["style", None]

    parStyle.setDisplayName("Name")
    parStyle.setParentStyleName("Name")
    parStyle.setNextStyleName("Name")

    assert parStyle._mAttr["display-name"]      == ["style", "Name"]
    assert parStyle._mAttr["parent-style-name"] == ["style", "Name"]
    assert parStyle._mAttr["next-style-name"]   == ["style", "Name"]

    # Outline Level
    assert parStyle._mAttr["default-outline-level"] == ["style", None]
    parStyle.setOutlineLevel("0")
    assert parStyle._mAttr["default-outline-level"] == ["style", None]
    parStyle.setOutlineLevel("1")
    assert parStyle._mAttr["default-outline-level"] == ["style", "1"]
    parStyle.setOutlineLevel("2")
    assert parStyle._mAttr["default-outline-level"] == ["style", "2"]
    parStyle.setOutlineLevel("3")
    assert parStyle._mAttr["default-outline-level"] == ["style", "3"]
    parStyle.setOutlineLevel("4")
    assert parStyle._mAttr["default-outline-level"] == ["style", "4"]
    parStyle.setOutlineLevel("5")
    assert parStyle._mAttr["default-outline-level"] == ["style", None]

    # Class
    assert parStyle._mAttr["class"] == ["style", None]
    parStyle.setClass("stuff")
    assert parStyle._mAttr["class"] == ["style", None]
    parStyle.setClass("text")
    assert parStyle._mAttr["class"] == ["style", "text"]
    parStyle.setClass("chapter")
    assert parStyle._mAttr["class"] == ["style", "chapter"]
    parStyle.setClass("stuff")
    assert parStyle._mAttr["class"] == ["style", None]

    # Set Paragraph Style
    # ===================

    # Margins & Line Height
    assert parStyle._pAttr["margin-top"]    == ["fo", None]
    assert parStyle._pAttr["margin-bottom"] == ["fo", None]
    assert parStyle._pAttr["margin-left"]   == ["fo", None]
    assert parStyle._pAttr["margin-right"]  == ["fo", None]
    assert parStyle._pAttr["text-indent"]   == ["fo", None]
    assert parStyle._pAttr["line-height"]   == ["fo", None]

    parStyle.setMarginTop("0.000cm")
    parStyle.setMarginBottom("0.000cm")
    parStyle.setMarginLeft("0.000cm")
    parStyle.setMarginRight("0.000cm")
    parStyle.setTextIndent("0.000cm")
    parStyle.setLineHeight("1.15")

    assert parStyle._pAttr["margin-top"]    == ["fo", "0.000cm"]
    assert parStyle._pAttr["margin-bottom"] == ["fo", "0.000cm"]
    assert parStyle._pAttr["margin-left"]   == ["fo", "0.000cm"]
    assert parStyle._pAttr["margin-right"]  == ["fo", "0.000cm"]
    assert parStyle._pAttr["text-indent"]   == ["fo", "0.000cm"]
    assert parStyle._pAttr["line-height"]   == ["fo", "1.15"]

    # Text Alignment
    assert parStyle._pAttr["text-align"] == ["fo", None]
    parStyle.setTextAlign("stuff")
    assert parStyle._pAttr["text-align"] == ["fo", None]
    parStyle.setTextAlign("start")
    assert parStyle._pAttr["text-align"] == ["fo", "start"]
    parStyle.setTextAlign("center")
    assert parStyle._pAttr["text-align"] == ["fo", "center"]
    parStyle.setTextAlign("end")
    assert parStyle._pAttr["text-align"] == ["fo", "end"]
    parStyle.setTextAlign("justify")
    assert parStyle._pAttr["text-align"] == ["fo", "justify"]
    parStyle.setTextAlign("inside")
    assert parStyle._pAttr["text-align"] == ["fo", "inside"]
    parStyle.setTextAlign("outside")
    assert parStyle._pAttr["text-align"] == ["fo", "outside"]
    parStyle.setTextAlign("left")
    assert parStyle._pAttr["text-align"] == ["fo", "left"]
    parStyle.setTextAlign("right")
    assert parStyle._pAttr["text-align"] == ["fo", "right"]
    parStyle.setTextAlign("stuff")
    assert parStyle._pAttr["text-align"] == ["fo", None]

    # Break Before
    assert parStyle._pAttr["break-before"] == ["fo", None]
    parStyle.setBreakBefore("stuff")
    assert parStyle._pAttr["break-before"] == ["fo", None]
    parStyle.setBreakBefore("auto")
    assert parStyle._pAttr["break-before"] == ["fo", "auto"]
    parStyle.setBreakBefore("column")
    assert parStyle._pAttr["break-before"] == ["fo", "column"]
    parStyle.setBreakBefore("page")
    assert parStyle._pAttr["break-before"] == ["fo", "page"]
    parStyle.setBreakBefore("even-page")
    assert parStyle._pAttr["break-before"] == ["fo", "even-page"]
    parStyle.setBreakBefore("odd-page")
    assert parStyle._pAttr["break-before"] == ["fo", "odd-page"]
    parStyle.setBreakBefore("inherit")
    assert parStyle._pAttr["break-before"] == ["fo", "inherit"]
    parStyle.setBreakBefore("stuff")
    assert parStyle._pAttr["break-before"] == ["fo", None]

    # Break After
    assert parStyle._pAttr["break-after"]  == ["fo", None]
    parStyle.setBreakAfter("stuff")
    assert parStyle._pAttr["break-after"]  == ["fo", None]
    parStyle.setBreakAfter("auto")
    assert parStyle._pAttr["break-after"] == ["fo", "auto"]
    parStyle.setBreakAfter("column")
    assert parStyle._pAttr["break-after"] == ["fo", "column"]
    parStyle.setBreakAfter("page")
    assert parStyle._pAttr["break-after"] == ["fo", "page"]
    parStyle.setBreakAfter("even-page")
    assert parStyle._pAttr["break-after"] == ["fo", "even-page"]
    parStyle.setBreakAfter("odd-page")
    assert parStyle._pAttr["break-after"] == ["fo", "odd-page"]
    parStyle.setBreakAfter("inherit")
    assert parStyle._pAttr["break-after"] == ["fo", "inherit"]
    parStyle.setBreakAfter("stuff")
    assert parStyle._pAttr["break-after"] == ["fo", None]

    # Text Attributes
    # ===============

    # Font Name, Family and Size
    assert parStyle._tAttr["font-name"]   == ["style", None]
    assert parStyle._tAttr["font-family"] == ["fo", None]
    assert parStyle._tAttr["font-size"]   == ["fo", None]

    parStyle.setFontName("Verdana")
    parStyle.setFontFamily("Verdana")
    parStyle.setFontSize("12pt")

    assert parStyle._tAttr["font-name"]   == ["style", "Verdana"]
    assert parStyle._tAttr["font-family"] == ["fo", "Verdana"]
    assert parStyle._tAttr["font-size"]   == ["fo", "12pt"]

    # Font Weight
    assert parStyle._tAttr["font-weight"] == ["fo", None]
    parStyle.setFontWeight("stuff")
    assert parStyle._tAttr["font-weight"] == ["fo", None]
    parStyle.setFontWeight("normal")
    assert parStyle._tAttr["font-weight"] == ["fo", "normal"]
    parStyle.setFontWeight("bold")
    assert parStyle._tAttr["font-weight"] == ["fo", "bold"]
    parStyle.setFontWeight("100")
    assert parStyle._tAttr["font-weight"] == ["fo", "100"]
    parStyle.setFontWeight("200")
    assert parStyle._tAttr["font-weight"] == ["fo", "200"]
    parStyle.setFontWeight("300")
    assert parStyle._tAttr["font-weight"] == ["fo", "300"]
    parStyle.setFontWeight("400")
    assert parStyle._tAttr["font-weight"] == ["fo", "400"]
    parStyle.setFontWeight("500")
    assert parStyle._tAttr["font-weight"] == ["fo", "500"]
    parStyle.setFontWeight("600")
    assert parStyle._tAttr["font-weight"] == ["fo", "600"]
    parStyle.setFontWeight("700")
    assert parStyle._tAttr["font-weight"] == ["fo", "700"]
    parStyle.setFontWeight("800")
    assert parStyle._tAttr["font-weight"] == ["fo", "800"]
    parStyle.setFontWeight("900")
    assert parStyle._tAttr["font-weight"] == ["fo", "900"]
    parStyle.setFontWeight("stuff")
    assert parStyle._tAttr["font-weight"] == ["fo", None]

    # Colour & Opacity
    assert parStyle._tAttr["color"]   == ["fo", None]
    assert parStyle._tAttr["opacity"] == ["loext", None]

    parStyle.setColor(QColor(0, 0, 0, 128))

    assert parStyle._tAttr["color"]   == ["fo", "#000000"]
    assert parStyle._tAttr["opacity"] == ["loext", "50%"]

    parStyle.setColor(None)

    assert parStyle._tAttr["color"]   == ["fo", None]
    assert parStyle._tAttr["opacity"] == ["loext", None]

    # Pack XML
    # ========
    xStyle = ET.Element("test")
    parStyle.packXML(xStyle)
    assert xmlToText(xStyle) == (
        '<test>'
        '<style:style style:name="test" style:family="paragraph" style:display-name="Name" '
        'style:parent-style-name="Name" style:next-style-name="Name">'
        '<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.000cm" '
        'fo:margin-left="0.000cm" fo:margin-right="0.000cm" fo:text-indent="0.000cm" '
        'fo:line-height="1.15" />'
        '<style:text-properties style:font-name="Verdana" fo:font-family="Verdana" '
        'fo:font-size="12pt" />'
        '</style:style>'
        '</test>'
    )

    # Changes
    # =======

    aStyle = ODTParagraphStyle("test")
    oStyle = ODTParagraphStyle("test")
    assert aStyle.checkNew(oStyle) is False
    assert aStyle.getID() == oStyle.getID()

    aStyle.setDisplayName("Name1")
    oStyle.setDisplayName("Name2")
    assert aStyle.checkNew(oStyle) is True
    assert aStyle.getID() != oStyle.getID()

    aStyle = ODTParagraphStyle("test")
    oStyle = ODTParagraphStyle("test")
    aStyle.setMarginTop("0.000cm")
    oStyle.setMarginTop("1.000cm")
    assert aStyle.checkNew(oStyle) is True
    assert aStyle.getID() != oStyle.getID()

    aStyle = ODTParagraphStyle("test")
    oStyle = ODTParagraphStyle("test")
    aStyle.setColor(QColor(0, 0, 0))
    oStyle.setColor(QColor(42, 42, 42))
    assert aStyle.checkNew(oStyle) is True
    assert aStyle.getID() != oStyle.getID()


@pytest.mark.core
def testFmtToOdt_ODTTextStyle():
    """Test the ODTTextStyle class."""
    txtStyle = ODTTextStyle("test")

    # Font Weight
    assert txtStyle._tAttr["font-weight"] == ["fo", None]
    txtStyle.setFontWeight("stuff")
    assert txtStyle._tAttr["font-weight"] == ["fo", None]
    txtStyle.setFontWeight("normal")
    assert txtStyle._tAttr["font-weight"] == ["fo", "normal"]
    txtStyle.setFontWeight("bold")
    assert txtStyle._tAttr["font-weight"] == ["fo", "bold"]
    txtStyle.setFontWeight("100")
    assert txtStyle._tAttr["font-weight"] == ["fo", "100"]
    txtStyle.setFontWeight("200")
    assert txtStyle._tAttr["font-weight"] == ["fo", "200"]
    txtStyle.setFontWeight("300")
    assert txtStyle._tAttr["font-weight"] == ["fo", "300"]
    txtStyle.setFontWeight("400")
    assert txtStyle._tAttr["font-weight"] == ["fo", "400"]
    txtStyle.setFontWeight("500")
    assert txtStyle._tAttr["font-weight"] == ["fo", "500"]
    txtStyle.setFontWeight("600")
    assert txtStyle._tAttr["font-weight"] == ["fo", "600"]
    txtStyle.setFontWeight("700")
    assert txtStyle._tAttr["font-weight"] == ["fo", "700"]
    txtStyle.setFontWeight("800")
    assert txtStyle._tAttr["font-weight"] == ["fo", "800"]
    txtStyle.setFontWeight("900")
    assert txtStyle._tAttr["font-weight"] == ["fo", "900"]
    txtStyle.setFontWeight("stuff")
    assert txtStyle._tAttr["font-weight"] == ["fo", None]

    # Font Style
    assert txtStyle._tAttr["font-style"] == ["fo", None]
    txtStyle.setFontStyle("stuff")
    assert txtStyle._tAttr["font-style"] == ["fo", None]
    txtStyle.setFontStyle("normal")
    assert txtStyle._tAttr["font-style"] == ["fo", "normal"]
    txtStyle.setFontStyle("italic")
    assert txtStyle._tAttr["font-style"] == ["fo", "italic"]
    txtStyle.setFontStyle("oblique")
    assert txtStyle._tAttr["font-style"] == ["fo", "oblique"]
    txtStyle.setFontStyle("stuff")
    assert txtStyle._tAttr["font-style"] == ["fo", None]

    # Text Color
    assert txtStyle._tAttr["color"] == ["fo", None]
    txtStyle.setColor("#012345")  # type: ignore
    assert txtStyle._tAttr["color"] == ["fo", None]
    txtStyle.setColor(QColor(255, 128, 0))
    assert txtStyle._tAttr["color"] == ["fo", "#ff8000"]
    txtStyle.setColor(None)
    assert txtStyle._tAttr["color"] == ["fo", None]

    # Background Color
    assert txtStyle._tAttr["background-color"] == ["fo", None]
    txtStyle.setBackgroundColor("#012345")  # type: ignore
    assert txtStyle._tAttr["background-color"] == ["fo", None]
    txtStyle.setBackgroundColor(QColor(255, 128, 0))
    assert txtStyle._tAttr["background-color"] == ["fo", "#ff8000"]
    txtStyle.setBackgroundColor(None)
    assert txtStyle._tAttr["background-color"] == ["fo", None]

    # Text Position
    assert txtStyle._tAttr["text-position"] == ["style", None]
    txtStyle.setTextPosition("stuff")
    assert txtStyle._tAttr["text-position"] == ["style", None]
    txtStyle.setTextPosition("super")
    assert txtStyle._tAttr["text-position"] == ["style", "super 58%"]
    txtStyle.setTextPosition("sub")
    assert txtStyle._tAttr["text-position"] == ["style", "sub 58%"]
    txtStyle.setTextPosition("stuff")
    assert txtStyle._tAttr["text-position"] == ["style", None]

    # Line Through Style
    assert txtStyle._tAttr["text-line-through-style"] == ["style", None]
    txtStyle.setStrikeStyle("stuff")
    assert txtStyle._tAttr["text-line-through-style"] == ["style", None]
    txtStyle.setStrikeStyle("none")
    assert txtStyle._tAttr["text-line-through-style"] == ["style", "none"]
    txtStyle.setStrikeStyle("solid")
    assert txtStyle._tAttr["text-line-through-style"] == ["style", "solid"]
    txtStyle.setStrikeStyle("stuff")
    assert txtStyle._tAttr["text-line-through-style"] == ["style", None]

    # Line Through Type
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", None]
    txtStyle.setStrikeType("stuff")
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", None]
    txtStyle.setStrikeType("none")  # Deprecated in ODF 1.3
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", None]
    txtStyle.setStrikeType("single")
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", "single"]
    txtStyle.setStrikeType("double")
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", "double"]
    txtStyle.setStrikeType("stuff")
    assert txtStyle._tAttr["text-line-through-type"]  == ["style", None]

    # Underline Style
    assert txtStyle._tAttr["text-underline-style"] == ["style", None]
    txtStyle.setUnderlineStyle("stuff")
    assert txtStyle._tAttr["text-underline-style"] == ["style", None]
    txtStyle.setUnderlineStyle("none")
    assert txtStyle._tAttr["text-underline-style"] == ["style", "none"]
    txtStyle.setUnderlineStyle("solid")
    assert txtStyle._tAttr["text-underline-style"] == ["style", "solid"]
    txtStyle.setUnderlineStyle("stuff")
    assert txtStyle._tAttr["text-underline-style"] == ["style", None]

    # Underline Width
    assert txtStyle._tAttr["text-underline-width"] == ["style", None]
    txtStyle.setUnderlineWidth("stuff")
    assert txtStyle._tAttr["text-underline-width"] == ["style", None]
    txtStyle.setUnderlineWidth("auto")
    assert txtStyle._tAttr["text-underline-width"] == ["style", "auto"]
    txtStyle.setUnderlineWidth("stuff")
    assert txtStyle._tAttr["text-underline-width"] == ["style", None]

    # Underline Colour
    assert txtStyle._tAttr["text-underline-color"] == ["style", None]
    txtStyle.setUnderlineColor("stuff")
    assert txtStyle._tAttr["text-underline-color"] == ["style", None]
    txtStyle.setUnderlineColor("font-color")
    assert txtStyle._tAttr["text-underline-color"] == ["style", "font-color"]
    txtStyle.setUnderlineColor("stuff")
    assert txtStyle._tAttr["text-underline-color"] == ["style", None]

    # Pack XML
    # ========
    txtStyle.setFontWeight("bold")
    txtStyle.setFontStyle("italic")
    txtStyle.setTextPosition("super")
    txtStyle.setStrikeStyle("solid")
    txtStyle.setStrikeType("single")
    xStyle = ET.Element("test")
    txtStyle.packXML(xStyle)
    assert xmlToText(xStyle) == (
        '<test><style:style style:name="test" style:family="text"><style:text-properties '
        'fo:font-weight="bold" fo:font-style="italic" style:text-position="super 58%" '
        'style:text-line-through-style="solid" style:text-line-through-type="single" />'
        '</style:style></test>'
    )


@pytest.mark.core
def testFmtToOdt_XMLParagraph():
    """Test XML encoding of paragraph."""
    # Stage 1 : Text
    # ==============

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    # Plain Text
    xmlPar.appendText("Hello World")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello World</text:p>'
        '</root>'
    )

    # Text Span
    xmlPar.appendSpan("spanned text", "T1", "")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello World'
        '<text:span text:style-name="T1">spanned text</text:span>'
        '</text:p>'
        '</root>'
    )

    # Tail Text
    xmlPar.appendText("more text")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello World'
        '<text:span text:style-name="T1">spanned text</text:span>'
        'more text</text:p>'
        '</root>'
    )

    assert xmlPar.checkError() == (0, "")

    # Stage 2 : Line Breaks
    # =====================

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    # Plain Text w/Line Break
    xmlPar.appendText("Hello\nWorld\n!!")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:line-break />World<text:line-break />!!</text:p>'
        '</root>'
    )

    # Text Span w/Line Break
    xmlPar.appendSpan("spanned\ntext", "T1", "")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:line-break />World<text:line-break />!!'
        '<text:span text:style-name="T1">spanned<text:line-break />text</text:span></text:p>'
        '</root>'
    )

    # Tail Text w/Line Break
    xmlPar.appendText("more\ntext")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:line-break />World<text:line-break />!!'
        '<text:span text:style-name="T1">spanned<text:line-break />text</text:span>'
        'more<text:line-break />text</text:p>'
        '</root>'
    )

    assert xmlPar.checkError() == (0, "")

    # Stage 3 : Tabs
    # ==============

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    # Plain Text w/Line Break
    xmlPar.appendText("Hello\tWorld\t!!")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:tab />World<text:tab />!!</text:p>'
        '</root>'
    )

    # Text Span w/Line Break
    xmlPar.appendSpan("spanned\ttext", "T1", "")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:tab />World<text:tab />!!'
        '<text:span text:style-name="T1">spanned<text:tab />text</text:span></text:p>'
        '</root>'
    )

    # Tail Text w/Line Break
    xmlPar.appendText("more\ttext")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:tab />World<text:tab />!!'
        '<text:span text:style-name="T1">spanned<text:tab />text</text:span>'
        'more<text:tab />text</text:p>'
        '</root>'
    )

    # Tail Text w/Link
    xmlPar.appendSpan("Example", "T1", "http://www.example.com")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello<text:tab />World<text:tab />!!'
        '<text:span text:style-name="T1">spanned<text:tab />text</text:span>'
        'more<text:tab />text'
        '<text:a xlink:type="simple" xlink:href="http://www.example.com" text:style-name="T1">'
        'Example</text:a></text:p>'
        '</root>'
    )

    assert xmlPar.checkError() == (0, "")

    # Stage 4 : Spaces
    # ================

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    # Plain Text w/Spaces
    xmlPar.appendText("Hello  World   !!")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello <text:s />World <text:s text:c="2" />!!</text:p>'
        '</root>'
    )

    # Text Span w/Spaces
    xmlPar.appendSpan("spanned    text", "T1", "")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello <text:s />World <text:s text:c="2" />!!'
        '<text:span text:style-name="T1">spanned <text:s text:c="3" />text</text:span></text:p>'
        '</root>'
    )

    # Tail Text w/Spaces
    xmlPar.appendText("more     text")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p>Hello <text:s />World <text:s text:c="2" />!!'
        '<text:span text:style-name="T1">spanned <text:s text:c="3" />text</text:span>'
        'more <text:s text:c="4" />text</text:p>'
        '</root>'
    )

    assert xmlPar.checkError() == (0, "")

    # Stage 5 : Lots of Spaces
    # ========================

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    # Plain Text w/Many Spaces
    xmlPar.appendText("  \t A \n  B ")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p><text:s text:c="2" /><text:tab /> A <text:line-break /> <text:s />B </text:p>'
        '</root>'
    )

    # Text Span w/Many Spaces
    xmlPar.appendSpan("  C  \t  D \n E ", "T1", "")
    assert xmlToText(xRoot) == (
        '<root>'
        '<text:p><text:s text:c="2" /><text:tab /> A <text:line-break /> <text:s />B '
        '<text:span text:style-name="T1"> <text:s />C <text:s /><text:tab /> <text:s />D '
        '<text:line-break /> E </text:span></text:p>'
        '</root>'
    )

    assert xmlPar.checkError() == (0, "")

    # Check Error
    # ===========

    xRoot = ET.Element("root")
    xElem = ET.SubElement(xRoot, _mkTag("text", "p"))
    xmlPar = XMLParagraph(xElem)

    xmlPar.appendText("A")
    xmlPar._nState = 5
    xmlPar.appendText("B")

    assert xmlPar.checkError() == (1, "1 char(s) were not written: 'AB'")


@pytest.mark.core
def testFmtToOdt_MkTag():
    """Test the tag maker function."""
    assert _mkTag("office", "text") == "{urn:oasis:names:tc:opendocument:xmlns:office:1.0}text"
    assert _mkTag("style", "text") == "{urn:oasis:names:tc:opendocument:xmlns:style:1.0}text"
    assert _mkTag("blabla", "text") == "text"
