from datetime import datetime
from datetime import timezone

import docx
import pytest
from docx import Document
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.oxml import parse_xml
from lxml.etree import tostring
from utils import assert_complex_field_value
from utils import assert_simple_field_value
from utils import cached_complex_field_values
from utils import docx_path
from utils import simple_field_expression

from docxcompose.properties import ComplexField
from docxcompose.properties import CUSTOM_PROPERTY_TYPES
from docxcompose.properties import CustomProperties
from docxcompose.properties import SimpleField
from docxcompose.properties import value2vt
from docxcompose.properties import vt2value
from docxcompose.utils import xpath


class TestIdentifyDocpropertiesInDocument(object):
    def test_identifies_simple_fields_correctly(self):
        document = Document(docx_path("outdated_docproperty_with_umlauts.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert 1 == len(properties), "input should contain 1 simple field docproperty"

        prop = properties[0]
        assert prop.name == "F\xfc\xfc"
        assert isinstance(prop, SimpleField)

    def test_identifies_complex_fields_correctly(self):
        document = Document(docx_path("three_props_in_same_paragraph.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert len(document.paragraphs) == 1, "input file should contains one paragraph"
        assert len(properties) == 3, (
            "input should contain three complex field docproperties"
        )

        # check that all fields were identified as complex fields
        for prop in properties:
            assert isinstance(prop, ComplexField)

        # check that all field names were parsed correctly
        expected_names = ("Text Property", "Number Property", "Text Property")
        for name, prop in zip(expected_names, properties):
            assert name == prop.name

        # check that begin, separate and end were identified correctly
        attrib_key = (
            "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}fldCharType"
        )
        expected_indexes = ((1, 3, 11), (13, 15, 17), (25, 27, 32))
        for prop, indexes in zip(properties, expected_indexes):
            assert prop.begin_run.getchildren()[1].attrib[attrib_key] == "begin"
            assert (
                prop.get_separate_run().getchildren()[1].attrib[attrib_key]
                == "separate"
            )
            assert prop.end_run.getchildren()[1].attrib[attrib_key] == "end"
            assert prop.w_p.index(prop.begin_run) == indexes[0]
            assert prop.w_p.index(prop.get_separate_run()) == indexes[1]
            assert prop.w_p.index(prop.end_run) == indexes[2]

    def test_finds_run_nodes_in_complex_fields_correctly(self):
        document = Document(docx_path("three_props_in_same_paragraph.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert len(properties) == 3, (
            "input should contain three complex field docproperties"
        )

        # In the first field, there are the following runs: begin, docprop,
        # a separate, 3 runs for the value (because of spellcheck) and end
        prop = properties[0]
        assert len(prop.get_runs_for_update()) == 3

        # when dissolving a property the runs that have to be removed
        # are the begin, docprop, and from separate to end (here just
        # separate and end)
        runs = prop.get_runs_to_replace_field_with_value()
        assert len(runs) == 4
        assert runs[0] == prop.begin_run
        assert runs[1] == prop.w_r
        assert runs[2] == prop.get_separate_run()
        assert runs[-1] == prop.end_run

        # In the second field, there are the following runs: begin, docprop,
        # a separate, 1 run for the value and end
        prop = properties[1]
        assert len(prop.get_runs_for_update()) == 1

        # when dissolving a property the runs that have to be removed
        # are from begin to separate (here begin, docprop, separate)
        # and the end
        runs = prop.get_runs_to_replace_field_with_value()
        assert len(runs) == 4
        assert runs[0] == prop.begin_run
        assert runs[1] == prop.w_r
        assert runs[2] == prop.get_separate_run()
        assert runs[-1] == prop.end_run

        # In the first field, there are the following runs: begin, docprop,
        # a separate, 2 runs for the value (because of spellcheck) and end
        prop = properties[2]
        assert len(prop.get_runs_for_update()) == 2

        # when dissolving a property the runs that have to be removed
        # are the begin, docprop, and from separate to end (here just
        # separate and end)
        runs = prop.get_runs_to_replace_field_with_value()
        assert len(runs) == 4
        assert runs[0] == prop.begin_run
        assert runs[1] == prop.w_r
        assert runs[2] == prop.get_separate_run()
        assert runs[-1] == prop.end_run

    def test_finds_run_nodes_in_complex_field_without_separate_correctly(self):
        document = Document(docx_path("complex_field_without_separate.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert len(properties) == 2, (
            "input should contain two complex field docproperties"
        )

        # The "User.FullName" docproperty should be the one without a separate run
        # In this field, there are the following runs: begin, docprop and end
        matches = [prop for prop in properties if prop.name == "User.FullName"]
        assert len(matches) == 1, "There should be only one User.FullName docproperty"
        prop = matches[0]
        assert prop.get_separate_run() is None, (
            "This complex field should not have a separate run."
        )
        assert prop.get_runs_for_update() == [], (
            "As there is no separate run, there should be no run to update"
        )

        # As there are no separate, all runs should be removed when dissolving
        # the property.
        runs = prop.get_runs_to_replace_field_with_value()
        assert len(runs) == 3
        assert runs[0] == prop.begin_run
        assert runs[1] == prop.w_r
        assert runs[2] == prop.end_run

    def test_finds_field_name_and_format_when_split_in_several_runs(self):
        document = Document(docx_path("complex_field_with_split_fieldname.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert len(properties) == 1, (
            "input should contain one complex field docproperties"
        )

        prop = properties[0]

        # make sure that the field name and format are indeed split over several runs
        assert [
            [each.text for each in xpath(run, "w:instrText")] for run in prop._runs[:9]
        ] == [
            [' DOCPROPERTY "ogg.document.document_date"  \\@ "ddd'],
            ["d"],
            [" dd"],
            [" "],
            ["MM"],
            ["M"],
            ["M"],
            [" yy"],
            ['yy hh:mm:s" \\* MERGEFORMAT '],
        ]

        assert (
            prop._get_fieldname_string()
            == ' DOCPROPERTY "ogg.document.document_date"  \\@ "ddd'
            + 'd dd MMMM yyyy hh:mm:s" \\* MERGEFORMAT '
        )

        assert prop.name == "ogg.document.document_date"
        assert prop.date_format == "EEEE dd MMMM yyyy hh:mm:s"

    def test_finds_header_footer_of_different_sections(self):
        document = Document(docx_path("docproperties_header_footer_3_sections.docx"))
        properties = CustomProperties(document).find_docprops_in_document()

        assert len(properties) == 5, (
            "input should contain 5 properties in header/footer and body"
        )

        expected_properties = [
            "Text Property",
            "Number Property",
            "Date Property",
            "Float Property",
            "Boolean Property",
        ]

        assert [each.name for each in properties] == expected_properties


class TestUpdateAllDocproperties(object):
    def test_updates_doc_properties_in_header(self):
        document = Document(docx_path("docproperties_header.docx"))

        assert_simple_field_value(
            "xxx", document.sections[0].header.part.element, "my.text-prop"
        )

        CustomProperties(document).update_all()

        assert_simple_field_value(
            "i'm some text", document.sections[0].header.part.element, "my.text-prop"
        )

    def test_updates_doc_properties_in_footer(self):
        document = Document(docx_path("docproperties_footer.docx"))

        assert_simple_field_value(
            "xxx", document.sections[0].footer.part.element, "my.text-prop"
        )

        CustomProperties(document).update_all()

        assert_simple_field_value(
            "b\xe4hh", document.sections[0].footer.part.element, "my.text-prop"
        )

    def test_updates_doc_properties_different_first_page(self):
        document = Document(
            docx_path("docproperties_different_first_page_1_section.docx")
        )

        assert_simple_field_value(
            "xxx", document.sections[0].first_page_header.part.element, "page1.header"
        )
        assert_simple_field_value(
            "xxx", document.sections[0].first_page_footer.part.element, "page1.footer"
        )
        assert_simple_field_value(
            "0", document.sections[0].header.part.element, "page2.header"
        )
        assert_simple_field_value(
            "01.01.1970", document.sections[0].footer.part.element, "page2.footer"
        )

        CustomProperties(document).update_all()

        assert_simple_field_value(
            "p1h", document.sections[0].first_page_header.part.element, "page1.header"
        )
        assert_simple_field_value(
            "p1f", document.sections[0].first_page_footer.part.element, "page1.footer"
        )
        assert_simple_field_value(
            "42", document.sections[0].header.part.element, "page2.header"
        )
        assert_simple_field_value(
            "18.10.1984", document.sections[0].footer.part.element, "page2.footer"
        )

    def test_updates_doc_properties_different_odd_even_pages(self):
        document = Document(
            docx_path("docproperties_different_odd_even_pages_1_section.docx")
        )

        assert_simple_field_value(
            "xxx", document.sections[0].header.part.element, "odd.header"
        )
        assert_simple_field_value(
            "xxx", document.sections[0].footer.part.element, "odd.footer"
        )
        assert_simple_field_value(
            "0", document.sections[0].even_page_header.part.element, "even.header"
        )
        assert_simple_field_value(
            "Y", document.sections[0].even_page_footer.part.element, "even.footer"
        )

        CustomProperties(document).update_all()

        assert_simple_field_value(
            "odd-header", document.sections[0].header.part.element, "odd.header"
        )
        assert_simple_field_value(
            "odd-footer", document.sections[0].footer.part.element, "odd.footer"
        )
        assert_simple_field_value(
            "1337", document.sections[0].even_page_header.part.element, "even.header"
        )
        assert_simple_field_value(
            "N", document.sections[0].even_page_footer.part.element, "even.footer"
        )

    def test_updates_docproperties_shared_footer(self):
        document = Document(docx_path("docproperties_shared_footer_2_sections.docx"))

        header_1 = document.sections[0].header
        header_2 = document.sections[1].header
        assert not header_1.is_linked_to_previous
        assert not header_2.is_linked_to_previous
        assert_complex_field_value("xxx", header_1.part.element, "section1.header")
        assert_complex_field_value("yyy", header_2.part.element, "section2.header")

        # the same footer should be referenced by both sections
        # the sections should be considered as linked
        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert not footer_1.is_linked_to_previous
        assert footer_2.is_linked_to_previous
        assert_complex_field_value("99", footer_1.part.element, "shared.footer")
        assert_complex_field_value("99", footer_2.part.element, "shared.footer")

        CustomProperties(document).update_all()

        assert_complex_field_value(
            "h\xe4der 1", header_1.part.element, "section1.header"
        )
        assert_complex_field_value(
            "h\xf6der 2", header_2.part.element, "section2.header"
        )

        # the same footer should be referenced by both sections
        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert_complex_field_value("123123123", footer_1.part.element, "shared.footer")
        assert_complex_field_value("123123123", footer_2.part.element, "shared.footer")

    def test_updates_docproperties_shared_header(self):
        document = Document(docx_path("docproperties_shared_header_2_sections.docx"))

        # the same header should be referenced by both sections
        # the sections should be considered as linked
        header_1 = document.sections[0].header
        header_2 = document.sections[1].header
        assert not header_1.is_linked_to_previous
        assert header_2.is_linked_to_previous
        assert_complex_field_value("xxx", header_1.part.element, "shared.header")
        assert_complex_field_value("xxx", header_2.part.element, "shared.header")

        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert not footer_1.is_linked_to_previous
        assert not footer_2.is_linked_to_previous
        assert_complex_field_value("123", footer_1.part.element, "section1.footer")
        assert_complex_field_value("yyy", footer_2.part.element, "section2.footer")

        CustomProperties(document).update_all()

        # the same header should be referenced by both sections
        assert_complex_field_value("sh\xe4red", header_1.part.element, "shared.header")
        assert_complex_field_value("sh\xe4red", header_2.part.element, "shared.header")

        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert_complex_field_value("-1.0", footer_1.part.element, "section1.footer")
        assert_complex_field_value(
            "f\xfc\xfcter", footer_2.part.element, "section2.footer"
        )

    def test_updates_docproperties_shared_header_footer(self):
        document = Document(
            docx_path("docproperties_shared_header_footer_2_sections.docx")
        )

        # the same header should be referenced by both sections
        # the sections should be considered as linked
        header_1 = document.sections[0].header
        header_2 = document.sections[1].header
        assert not header_1.is_linked_to_previous
        assert header_2.is_linked_to_previous
        assert_complex_field_value("blub", header_1.part.element, "shared.header")
        assert_complex_field_value("blub", header_2.part.element, "shared.header")

        # the same footer should be referenced by both sections
        # the sections should be considered as linked
        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert not footer_1.is_linked_to_previous
        assert footer_2.is_linked_to_previous
        assert_complex_field_value("N", footer_1.part.element, "shared.footer")
        assert_complex_field_value("N", footer_2.part.element, "shared.footer")

        CustomProperties(document).update_all()

        # the same header should be referenced by both sections
        assert_complex_field_value("ig bi obe", header_1.part.element, "shared.header")
        assert_complex_field_value("ig bi obe", header_2.part.element, "shared.header")

        # the same footer should be referenced by both sections
        footer_1 = document.sections[0].footer
        footer_2 = document.sections[1].footer
        assert_complex_field_value("Y", footer_1.part.element, "shared.footer")
        assert_complex_field_value("Y", footer_2.part.element, "shared.footer")

    def test_updates_doc_properties_with_umlauts(self):
        document = Document(docx_path("outdated_docproperty_with_umlauts.docx"))

        assert_simple_field_value("xxx", document.element.body, "F\xfc\xfc")

        CustomProperties(document).update_all()

        assert_simple_field_value("j\xe4ja.", document.element.body, "F\xfc\xfc")

    def test_complex_docprop_fields_with_multiple_textnodes_are_updated(self):
        document = Document(docx_path("spellchecked_docproperty.docx"))
        paragraphs = xpath(document.element.body, "//w:p")
        assert len(paragraphs) == 1, "input file contains one paragraph"
        assert len(xpath(document.element.body, "//w:instrText")) == 1, (
            "input contains one complex field docproperty"
        )
        w_p = paragraphs[0]

        cached_values = cached_complex_field_values(w_p)
        assert len(cached_values) == 4, "doc property value is scattered over 4 parts"
        assert "".join(cached_values) == "i will be spllchecked!"

        CustomProperties(document).update_all()

        w_p = xpath(document.element.body, "//w:p")[0]
        cached_values = cached_complex_field_values(w_p)
        assert len(cached_values) == 1, (
            "doc property value has been reset to one cached value"
        )
        assert cached_values[0] == "i will be spllchecked!"

    def test_complex_docprop_with_multiple_textnode_in_same_run_are_updated(self):
        document = Document(docx_path("two_textnodes_in_run_docproperty.docx"))
        paragraphs = xpath(document.element.body, "//w:p")
        assert len(paragraphs) == 1, "input file contains one paragraph"
        assert len(xpath(document.element.body, "//w:instrText")) == 1, (
            "input contains one complex field docproperty"
        )

        w_p = paragraphs[0]
        cached_values = cached_complex_field_values(w_p)
        assert len(cached_values) == 2, "doc property value is scattered over 2 parts"
        assert "".join(cached_values) == "Hello there"

        CustomProperties(document).update_all()

        w_p = xpath(document.element.body, "//w:p")[0]
        cached_values = cached_complex_field_values(w_p)
        assert len(cached_values) == 1, (
            "doc property value has been reset to one cached value"
        )
        assert cached_values[0] == "i will be spllchecked!"

    def test_three_complex_docprop_in_same_paragraph(self):
        document = Document(docx_path("three_props_in_same_paragraph.docx"))
        properties = CustomProperties(document)

        assert len(document.paragraphs) == 1, "input file should contains one paragraph"
        paragraph = document.paragraphs[0]
        assert len(properties.find_docprops_in_document()) == 3, (
            "input should contain three complex field docproperties"
        )

        text = "{text} / {num} mor between the fields {text} and some afte the three fields"
        assert paragraph.text == text.format(text="I was spellcecked", num=0)

        properties.update_all()

        assert paragraph.text == text.format(text="Foo", num=2)

    def test_multiple_identical_docprops_get_updated(self):
        document = Document(docx_path("multiple_identical_properties.docx"))
        assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs"
        for paragraph in document.paragraphs:
            assert len(xpath(paragraph._p, ".//w:instrText")) == 1, (
                "paragraph should contain one complex field docproperties"
            )

            assert paragraph.text == "Foo"

        CustomProperties(document).update_all()

        for i, paragraph in enumerate(document.paragraphs):
            assert paragraph.text == "Bar", "docprop {} was not updated".format(i + 1)

    def test_docproperty_without_separate_does_get_updated(self):
        document = Document(docx_path("complex_field_without_separate.docx"))
        custom_properties = CustomProperties(document)
        properties = custom_properties.find_docprops_in_document()
        paragraphs = document.paragraphs

        # Make sure that a value is set for 'User.FullName'
        assert ("User.FullName", "Test User") in custom_properties.items()
        assert ("Dossier.Title", " Some Title") in custom_properties.items()

        # Make sure that 'User.FullName' field has no separate node
        matches = [prop for prop in properties if prop.name == "User.FullName"]
        assert len(matches) == 1, "There should be only one User.FullName docproperty"
        fullname = matches[0]
        assert fullname.get_separate_run() is None, (
            "This complex field should not have a separate run."
        )

        # Make sure that 'Dossier.Title' field has a separate node
        matches = [prop for prop in properties if prop.name == "Dossier.Title"]
        assert len(matches) == 1, "There should be only one Dossier.Title docproperty"
        title = matches[0]
        assert title.get_separate_run() is not None, (
            "This complex field should have a separate run."
        )

        # Check the content of the paragraphs before update
        assert len(paragraphs) == 2
        assert paragraphs[0].text == "Sachbearbeiter: "
        assert "Sachbearbeiter: " in fullname.w_p.xml
        assert paragraphs[1].text == "Dossier Titel:  "

        custom_properties.update_all()

        # Field with missing separate was not updated
        assert paragraphs[0].text == "Sachbearbeiter: Test User"
        # Next field was updated correctly
        assert paragraphs[1].text == "Dossier Titel:  Some Title"

    def test_date_docprops_with_format_get_updated(self):
        document = Document(docx_path("date_docproperties_with_format.docx"))
        assert len(document.paragraphs) == 3, "input file should contain 3 paragraph"

        expected_values = ["11.06.19", "mardi 11 juin 2019", "11-6-19 0:0:0"]
        for expected, paragraph in zip(expected_values, document.paragraphs):
            assert paragraph.text == expected

        CustomProperties(document).update_all()

        expected_values = ["23.01.20", "jeudi 23 janvier 2020", "23-1-20 10:0:0"]
        for i, (expected, paragraph) in enumerate(
            zip(expected_values, document.paragraphs)
        ):
            assert paragraph.text == expected, (
                "docprop {} was not updated correctly".format(i + 1)
            )

    def test_date_docprops_respect_language(self):
        document = Document(docx_path("date_docproperties_with_format.docx"))
        assert len(document.paragraphs) == 3, "input file should contain 3 paragraph"

        CustomProperties(document).update_all()
        assert document.paragraphs[1].text == "jeudi 23 janvier 2020"

        document.element.xpath(".//w:lang")[0].set(
            docx.oxml.shared.qn("w:val"), "de-CH"
        )
        CustomProperties(document).update_all()
        assert document.paragraphs[1].text == "Donnerstag 23 Januar 2020"

        document.element.xpath(".//w:lang")[0].set(
            docx.oxml.shared.qn("w:val"), "en-US"
        )
        CustomProperties(document).update_all()
        assert document.paragraphs[1].text == "Thursday 23 January 2020"

    def test_docprops_with_split_fieldname_get_updated(self):
        document = Document(docx_path("complex_field_with_split_fieldname.docx"))
        assert len(document.paragraphs) == 1, "input file should contain 1 paragraph"

        paragraph = document.paragraphs[0]
        assert paragraph.text == "Datum: Tuesday 09 February 2021 00:00:00"

        CustomProperties(document).update_all()
        assert paragraph.text == "Datum: Friday 11 March 2022 10:00:0"


class TestUpdateSpecificDocproperty(object):
    def test_simple_field_gets_updated(self):
        document = Document(docx_path("outdated_docproperty_with_umlauts.docx"))
        assert_simple_field_value("xxx", document.element.body, "F\xfc\xfc")

        CustomProperties(document).update("F\xfc\xfc", "new v\xe4lue")

        assert_simple_field_value("new v\xe4lue", document.element.body, "F\xfc\xfc")

    def test_complex_field_gets_updated(self):
        document = Document(docx_path("docproperties.docx"))
        assert len(document.paragraphs) == 6, "input file should contain 6 paragraphs"

        properties = xpath(document.element.body, ".//w:instrText")
        assert len(properties) == 5, (
            "input should contain five complex field docproperties"
        )

        expected_paragraphs = [
            "Custom Doc Properties",
            "Text: Foo Bar",
            "Number: 123",
            "Boolean: Y",
            "Date: 11.06.2019",
            "Float: 1.1",
        ]
        actual_paragraphs = [paragraph.text for paragraph in document.paragraphs]
        assert actual_paragraphs == expected_paragraphs

        CustomProperties(document).update("Number Property", 423)

        expected_paragraphs[2] = "Number: 423"
        actual_paragraphs = [paragraph.text for paragraph in document.paragraphs]
        assert actual_paragraphs == expected_paragraphs

    def test_multiple_identical_docprops_get_updated(self):
        document = Document(docx_path("multiple_identical_properties.docx"))
        assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs"
        for paragraph in document.paragraphs:
            assert len(xpath(paragraph._p, ".//w:instrText")) == 1, (
                "paragraph should contain one complex field docproperties"
            )

            assert paragraph.text == "Foo"

        CustomProperties(document).update("Text Property", "New value")

        for i, paragraph in enumerate(document.paragraphs):
            assert paragraph.text == "New value", "docprop {} was not updated".format(
                i + 1
            )


class TestDissolveField(object):
    def test_removes_simple_field_but_keeps_value(self):
        document = Document(docx_path("outdated_docproperty_with_umlauts.docx"))
        assert len(document.paragraphs) == 1, "input file should contain 1 paragraph"
        fields = xpath(document.element.body, simple_field_expression("F\xfc\xfc"))
        assert len(fields) == 1, "should contain one simple field docproperty"

        assert document.paragraphs[0].text == "Hie chund ds property: "
        assert fields[0].text == "xxx"

        CustomProperties(document).dissolve_fields("F\xfc\xfc")
        fields = xpath(document.element.body, simple_field_expression("F\xfc\xfc"))
        assert len(fields) == 0, "should not contain any docproperties anymore"
        # when simple field is removed, the value is moved one up in the hierarchy
        assert document.paragraphs[0].text == "Hie chund ds property: xxx"

    def test_removes_complex_field_but_keeps_value(self):
        # test fails because field has 2 spaces before docprop name
        document = Document(docx_path("docproperties.docx"))
        assert len(document.paragraphs) == 6, "input file should contain 6 paragraphs"

        properties = xpath(document.element.body, ".//w:instrText")
        assert len(properties) == 5, (
            "input should contain five complex field docproperties"
        )

        expected_paragraphs = [
            "Custom Doc Properties",
            "Text: Foo Bar",
            "Number: 123",
            "Boolean: Y",
            "Date: 11.06.2019",
            "Float: 1.1",
        ]
        actual_paragraphs = [paragraph.text for paragraph in document.paragraphs]
        assert actual_paragraphs == expected_paragraphs

        CustomProperties(document).dissolve_fields("Number Property")

        actual_paragraphs = [paragraph.text for paragraph in document.paragraphs]
        assert actual_paragraphs == expected_paragraphs

        properties = xpath(document.element.body, ".//w:instrText")
        assert len(properties) == 4, "only 4 fields should remain after removal of one"

    def test_dissolves_all_instances_of_given_field(self):
        document = Document(docx_path("multiple_identical_properties.docx"))
        assert len(document.paragraphs) == 3, "input file should contain 3 paragraphs"
        assert len(xpath(document.element.body, ".//w:instrText")) == 3, (
            "document should contain three complex field docproperties"
        )

        for paragraph in document.paragraphs:
            assert paragraph.text == "Foo"

        CustomProperties(document).dissolve_fields("Text Property")

        assert len(document.paragraphs) == 3
        assert len(xpath(document.element.body, ".//w:instrText")) == 0, (
            "document should not contain any complex field anymore"
        )
        for paragraph in document.paragraphs:
            assert paragraph.text == "Foo", "value should have been kept in document"

    def test_dissolving_field_when_three_complex_docprop_in_same_paragraph(self):
        document = Document(docx_path("three_props_in_same_paragraph.docx"))
        assert len(document.paragraphs) == 1, "input file should contains one paragraph"
        paragraph = document.paragraphs[0]
        properties = CustomProperties(document)
        assert len(properties.find_docprops_in_document()) == 3, (
            "input should contain three complex field docproperties"
        )

        text = "{text} / {num} mor between the fields {text} and some afte the three fields"
        assert paragraph.text == text.format(text="I was spellcecked", num=0)

        properties.dissolve_fields("Text Property")

        assert len(document.paragraphs) == 1
        assert len(properties.find_docprops_in_document()) == 1, (
            "document should contain one complex field after removal"
        )
        assert paragraph.text == text.format(text="I was spellcecked", num=0)


def test_get_doc_properties():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert props["Text Property"] == "Foo Bar"
    assert props["Number Property"] == 123
    assert props["Boolean Property"] is True
    assert props["Date Property"] == datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc)

    assert props.get("Text Property") == "Foo Bar"
    assert props.get("Number Property") == 123
    assert props.get("Boolean Property") is True
    assert props.get("Date Property") == datetime(
        2019, 6, 11, 10, 0, tzinfo=timezone.utc
    )


def test_get_doc_property_is_case_insensitive():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert props["text property"] == "Foo Bar"
    assert props.get("text property") == "Foo Bar"


def test_add_doc_properties():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props.add("My Text Property", "foo bar")
    assert props.get("My Text Property") == "foo bar"

    props.add("My Boolean Property", True)
    assert props.get("My Boolean Property") is True

    props.add("My Number Property", 123)
    assert props.get("My Number Property") == 123

    props.add(
        "My Date Property", datetime(2019, 10, 23, 15, 44, 50, tzinfo=timezone.utc)
    )
    assert props.get("My Date Property") == datetime(
        2019, 10, 23, 15, 44, 50, tzinfo=timezone.utc
    )


def test_add_utf8_property():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props.add("My Text Property", "f\xfc\xfc".encode("utf-8"))
    assert props.get("My Text Property") == "f\xfc\xfc"


def test_set_doc_properties():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props["Text Property"] = "baz"
    assert props["Text Property"] == "baz"

    props["Boolean Property"] = False
    assert props["Boolean Property"] is False

    props["Number Property"] = 456
    assert props["Number Property"] == 456

    props["Date Property"] = datetime(2019, 10, 20, 12, 0, tzinfo=timezone.utc)
    assert props["Date Property"] == datetime(2019, 10, 20, 12, 0, tzinfo=timezone.utc)


def test_set_doc_property_is_case_insensitive():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props["text property"] = "baz"
    assert props["Text Property"] == "baz"


def test_delete_doc_properties():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    del props["Text Property"]
    del props["Number Property"]

    assert "Text Property" not in props
    assert "Number Property" not in props

    assert xpath(props._element, ".//cp:property/@pid") == ["2", "3", "4"]


def test_delete_doc_property_is_case_insensitive():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    del props["text property"]

    assert "Text Property" not in props


def test_contains_is_case_insensitive():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert "text property" in props


def test_nullify_doc_properties():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props.nullify("Text Property")
    props.nullify("Number Property")
    props.nullify("Boolean Property")
    props.nullify("Date Property")
    props.nullify("Float Property")

    assert props["Text Property"] == ""
    assert "Number Property" not in props
    assert "Boolean Property" not in props
    assert "Date Property" not in props
    assert "Float Property" not in props


def test_nullify_doc_property_is_case_insensitive():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    props.nullify("text property")
    assert props["Text Property"] == ""


def test_set_doc_property_on_document_without_properties_creates_new_part():
    document = Document(docx_path("master.docx"))
    props = CustomProperties(document)
    props["Text Property"] = "Foo"

    assert props.part is not None
    assert props["Text Property"] == "Foo"

    part = document.part.package.part_related_by(RT.CUSTOM_PROPERTIES)
    assert part is not None


def test_doc_properties_keys():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert props.keys() == [
        "Text Property",
        "Number Property",
        "Boolean Property",
        "Date Property",
        "Float Property",
    ]


def test_doc_properties_values():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert props.values() == [
        "Foo Bar",
        123,
        True,
        datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc),
        1.1,
    ]


def test_doc_properties_items():
    document = Document(docx_path("docproperties.docx"))
    props = CustomProperties(document)

    assert props.items() == [
        ("Text Property", "Foo Bar"),
        ("Number Property", 123),
        ("Boolean Property", True),
        ("Date Property", datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc)),
        ("Float Property", 1.1),
    ]


def test_vt2value_value2vt_roundtrip():
    assert vt2value(value2vt(42)) == 42
    assert vt2value(value2vt(True)) is True
    assert vt2value(value2vt(1.1)) == pytest.approx(1.1)
    dt = datetime(2019, 6, 11, 10, 0, tzinfo=timezone.utc)
    assert vt2value(value2vt(dt)) == dt
    assert vt2value(value2vt("foo")) == "foo"
    assert vt2value(value2vt("")) == ""

    node = parse_xml(CUSTOM_PROPERTY_TYPES["int"])
    node.text = "42"
    assert tostring(value2vt(vt2value(node))) == tostring(node)

    node = parse_xml(CUSTOM_PROPERTY_TYPES["bool"])
    node.text = "true"
    assert tostring(value2vt(vt2value(node))) == tostring(node)

    node = parse_xml(CUSTOM_PROPERTY_TYPES["float"])
    node.text = "1.1"
    assert tostring(value2vt(vt2value(node))) == tostring(node)

    node = parse_xml(CUSTOM_PROPERTY_TYPES["datetime"])
    node.text = "2003-12-31T10:14:55Z"
    assert tostring(value2vt(vt2value(node))) == tostring(node)

    node = parse_xml(CUSTOM_PROPERTY_TYPES["text"])
    node.text = "foo"
    assert tostring(value2vt(vt2value(node))) == tostring(node)

    node = parse_xml(CUSTOM_PROPERTY_TYPES["text"])
    node.text = ""
    assert tostring(value2vt(vt2value(node))) == tostring(node)


def test_vt2value_returns_empty_string_for_missing_text_node():
    node = parse_xml(CUSTOM_PROPERTY_TYPES["text"])
    node.text = None
    assert vt2value(node) == ""
