"""Test suite for the docx.styles.style module."""

import pytest

from docx.enum.style import WD_STYLE_TYPE
from docx.styles.style import (
    BaseStyle,
    CharacterStyle,
    ParagraphStyle,
    StyleFactory,
    _NumberingStyle,
    _TableStyle,
)
from docx.text.font import Font
from docx.text.parfmt import ParagraphFormat

from ..unitutil.cxml import element, xml
from ..unitutil.mock import call, class_mock, function_mock, instance_mock


class DescribeStyleFactory:
    def it_constructs_the_right_type_of_style(self, factory_fixture):
        style_elm, StyleCls_, style_ = factory_fixture
        style = StyleFactory(style_elm)
        StyleCls_.assert_called_once_with(style_elm)
        assert style is style_

    # fixtures -------------------------------------------------------

    @pytest.fixture(params=["paragraph", "character", "table", "numbering"])
    def factory_fixture(
        self,
        request,
        paragraph_style_,
        ParagraphStyle_,
        character_style_,
        CharacterStyle_,
        table_style_,
        _TableStyle_,
        numbering_style_,
        _NumberingStyle_,
    ):
        type_attr_val = request.param
        StyleCls_, style_mock = {
            "paragraph": (ParagraphStyle_, paragraph_style_),
            "character": (CharacterStyle_, character_style_),
            "table": (_TableStyle_, table_style_),
            "numbering": (_NumberingStyle_, numbering_style_),
        }[request.param]
        style_cxml = "w:style{w:type=%s}" % type_attr_val
        style_elm = element(style_cxml)
        return style_elm, StyleCls_, style_mock

    # fixture components -----------------------------------

    @pytest.fixture
    def ParagraphStyle_(self, request, paragraph_style_):
        return class_mock(
            request, "docx.styles.style.ParagraphStyle", return_value=paragraph_style_
        )

    @pytest.fixture
    def paragraph_style_(self, request):
        return instance_mock(request, ParagraphStyle)

    @pytest.fixture
    def CharacterStyle_(self, request, character_style_):
        return class_mock(
            request, "docx.styles.style.CharacterStyle", return_value=character_style_
        )

    @pytest.fixture
    def character_style_(self, request):
        return instance_mock(request, CharacterStyle)

    @pytest.fixture
    def _TableStyle_(self, request, table_style_):
        return class_mock(
            request, "docx.styles.style._TableStyle", return_value=table_style_
        )

    @pytest.fixture
    def table_style_(self, request):
        return instance_mock(request, _TableStyle)

    @pytest.fixture
    def _NumberingStyle_(self, request, numbering_style_):
        return class_mock(
            request, "docx.styles.style._NumberingStyle", return_value=numbering_style_
        )

    @pytest.fixture
    def numbering_style_(self, request):
        return instance_mock(request, _NumberingStyle)


class DescribeBaseStyle:
    def it_knows_its_style_id(self, id_get_fixture):
        style, expected_value = id_get_fixture
        assert style.style_id == expected_value

    def it_can_change_its_style_id(self, id_set_fixture):
        style, new_value, expected_xml = id_set_fixture
        style.style_id = new_value
        assert style._element.xml == expected_xml

    def it_knows_its_type(self, type_get_fixture):
        style, expected_value = type_get_fixture
        assert style.type == expected_value

    def it_knows_its_name(self, name_get_fixture):
        style, expected_value = name_get_fixture
        assert style.name == expected_value

    def it_can_change_its_name(self, name_set_fixture):
        style, new_value, expected_xml = name_set_fixture
        style.name = new_value
        assert style._element.xml == expected_xml

    def it_knows_whether_its_a_builtin_style(self, builtin_get_fixture):
        style, expected_value = builtin_get_fixture
        assert style.builtin is expected_value

    def it_knows_whether_its_hidden(self, hidden_get_fixture):
        style, expected_value = hidden_get_fixture
        assert style.hidden == expected_value

    def it_can_change_whether_its_hidden(self, hidden_set_fixture):
        style, value, expected_xml = hidden_set_fixture
        style.hidden = value
        assert style._element.xml == expected_xml

    def it_knows_its_sort_order(self, priority_get_fixture):
        style, expected_value = priority_get_fixture
        assert style.priority == expected_value

    def it_can_change_its_sort_order(self, priority_set_fixture):
        style, value, expected_xml = priority_set_fixture
        style.priority = value
        assert style._element.xml == expected_xml

    def it_knows_whether_its_unhide_when_used(self, unhide_get_fixture):
        style, expected_value = unhide_get_fixture
        assert style.unhide_when_used == expected_value

    def it_can_change_its_unhide_when_used_value(self, unhide_set_fixture):
        style, value, expected_xml = unhide_set_fixture
        style.unhide_when_used = value
        assert style._element.xml == expected_xml

    def it_knows_its_quick_style_setting(self, quick_get_fixture):
        style, expected_value = quick_get_fixture
        assert style.quick_style == expected_value

    def it_can_change_its_quick_style_setting(self, quick_set_fixture):
        style, new_value, expected_xml = quick_set_fixture
        style.quick_style = new_value
        assert style._element.xml == expected_xml

    def it_knows_whether_its_locked(self, locked_get_fixture):
        style, expected_value = locked_get_fixture
        assert style.locked == expected_value

    def it_can_change_whether_its_locked(self, locked_set_fixture):
        style, value, expected_xml = locked_set_fixture
        style.locked = value
        assert style._element.xml == expected_xml

    def it_can_delete_itself_from_the_document(self, delete_fixture):
        style, styles, expected_xml = delete_fixture
        style.delete()
        assert styles.xml == expected_xml
        assert style._element is None

    # fixture --------------------------------------------------------

    @pytest.fixture(
        params=[
            ("w:style", True),
            ("w:style{w:customStyle=0}", True),
            ("w:style{w:customStyle=1}", False),
        ]
    )
    def builtin_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture
    def delete_fixture(self):
        styles = element("w:styles/w:style")
        style = BaseStyle(styles[0])
        expected_xml = xml("w:styles")
        return style, styles, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", False),
            ("w:style/w:semiHidden", True),
            ("w:style/w:semiHidden{w:val=0}", False),
            ("w:style/w:semiHidden{w:val=1}", True),
        ]
    )
    def hidden_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", True, "w:style/w:semiHidden"),
            ("w:style/w:semiHidden{w:val=0}", True, "w:style/w:semiHidden"),
            ("w:style/w:semiHidden{w:val=1}", True, "w:style/w:semiHidden"),
            ("w:style", False, "w:style"),
            ("w:style/w:semiHidden", False, "w:style"),
            ("w:style/w:semiHidden{w:val=1}", False, "w:style"),
        ]
    )
    def hidden_set_fixture(self, request):
        style_cxml, value, expected_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_cxml)
        return style, value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", None),
            ("w:style{w:styleId=Foobar}", "Foobar"),
        ]
    )
    def id_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", "Foo", "w:style{w:styleId=Foo}"),
            ("w:style{w:styleId=Foo}", "Bar", "w:style{w:styleId=Bar}"),
            ("w:style{w:styleId=Bar}", None, "w:style"),
            ("w:style", None, "w:style"),
        ]
    )
    def id_set_fixture(self, request):
        style_cxml, new_value, expected_style_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_style_cxml)
        return style, new_value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", False),
            ("w:style/w:locked", True),
            ("w:style/w:locked{w:val=0}", False),
            ("w:style/w:locked{w:val=1}", True),
        ]
    )
    def locked_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", True, "w:style/w:locked"),
            ("w:style/w:locked{w:val=0}", True, "w:style/w:locked"),
            ("w:style/w:locked{w:val=1}", True, "w:style/w:locked"),
            ("w:style", False, "w:style"),
            ("w:style/w:locked", False, "w:style"),
            ("w:style/w:locked{w:val=1}", False, "w:style"),
        ]
    )
    def locked_set_fixture(self, request):
        style_cxml, value, expected_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_cxml)
        return style, value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style{w:type=table}", None),
            ("w:style{w:type=table}/w:name{w:val=Boofar}", "Boofar"),
            ("w:style{w:type=table}/w:name{w:val=heading 1}", "Heading 1"),
        ]
    )
    def name_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", "Foo", "w:style/w:name{w:val=Foo}"),
            ("w:style/w:name{w:val=Foo}", "Bar", "w:style/w:name{w:val=Bar}"),
            ("w:style/w:name{w:val=Bar}", None, "w:style"),
        ]
    )
    def name_set_fixture(self, request):
        style_cxml, new_value, expected_style_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_style_cxml)
        return style, new_value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", None),
            ("w:style/w:uiPriority{w:val=42}", 42),
        ]
    )
    def priority_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", 42, "w:style/w:uiPriority{w:val=42}"),
            ("w:style/w:uiPriority{w:val=42}", 24, "w:style/w:uiPriority{w:val=24}"),
            ("w:style/w:uiPriority{w:val=24}", None, "w:style"),
        ]
    )
    def priority_set_fixture(self, request):
        style_cxml, value, expected_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_cxml)
        return style, value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", False),
            ("w:style/w:qFormat", True),
            ("w:style/w:qFormat{w:val=0}", False),
            ("w:style/w:qFormat{w:val=on}", True),
        ]
    )
    def quick_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", True, "w:style/w:qFormat"),
            ("w:style/w:qFormat", False, "w:style"),
            ("w:style/w:qFormat", True, "w:style/w:qFormat"),
            ("w:style/w:qFormat{w:val=0}", False, "w:style"),
            ("w:style/w:qFormat{w:val=on}", True, "w:style/w:qFormat"),
        ]
    )
    def quick_set_fixture(self, request):
        style_cxml, new_value, expected_style_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_style_cxml)
        return style, new_value, expected_xml

    @pytest.fixture(
        params=[
            ("w:style", WD_STYLE_TYPE.PARAGRAPH),
            ("w:style{w:type=paragraph}", WD_STYLE_TYPE.PARAGRAPH),
            ("w:style{w:type=character}", WD_STYLE_TYPE.CHARACTER),
            ("w:style{w:type=numbering}", WD_STYLE_TYPE.LIST),
        ]
    )
    def type_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", False),
            ("w:style/w:unhideWhenUsed", True),
            ("w:style/w:unhideWhenUsed{w:val=0}", False),
            ("w:style/w:unhideWhenUsed{w:val=1}", True),
        ]
    )
    def unhide_get_fixture(self, request):
        style_cxml, expected_value = request.param
        style = BaseStyle(element(style_cxml))
        return style, expected_value

    @pytest.fixture(
        params=[
            ("w:style", True, "w:style/w:unhideWhenUsed"),
            ("w:style/w:unhideWhenUsed", False, "w:style"),
            ("w:style/w:unhideWhenUsed{w:val=0}", True, "w:style/w:unhideWhenUsed"),
            ("w:style/w:unhideWhenUsed{w:val=1}", True, "w:style/w:unhideWhenUsed"),
            ("w:style/w:unhideWhenUsed{w:val=1}", False, "w:style"),
            ("w:style", False, "w:style"),
        ]
    )
    def unhide_set_fixture(self, request):
        style_cxml, value, expected_cxml = request.param
        style = BaseStyle(element(style_cxml))
        expected_xml = xml(expected_cxml)
        return style, value, expected_xml


class DescribeCharacterStyle:
    def it_knows_which_style_it_is_based_on(self, base_get_fixture):
        style, StyleFactory_, StyleFactory_calls, base_style_ = base_get_fixture
        base_style = style.base_style

        assert StyleFactory_.call_args_list == StyleFactory_calls
        assert base_style == base_style_

    def it_can_change_its_base_style(self, base_set_fixture):
        style, value, expected_xml = base_set_fixture
        style.base_style = value
        assert style._element.xml == expected_xml

    def it_provides_access_to_its_font(self, font_fixture):
        style, Font_, font_ = font_fixture
        font = style.font
        Font_.assert_called_once_with(style._element)
        assert font is font_

    # fixture --------------------------------------------------------

    @pytest.fixture(
        params=[
            ("w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Foo})", 1, 0),
            ("w:styles/(w:style{w:styleId=Foo},w:style/w:basedOn{w:val=Bar})", 1, -1),
            ("w:styles/w:style", 0, -1),
        ]
    )
    def base_get_fixture(self, request, StyleFactory_):
        styles_cxml, style_idx, base_style_idx = request.param
        styles = element(styles_cxml)
        style = CharacterStyle(styles[style_idx])
        if base_style_idx >= 0:
            base_style = styles[base_style_idx]
            StyleFactory_calls = [call(base_style)]
            expected_value = StyleFactory_.return_value
        else:
            StyleFactory_calls = []
            expected_value = None
        return style, StyleFactory_, StyleFactory_calls, expected_value

    @pytest.fixture(
        params=[
            ("w:style", "Foo", "w:style/w:basedOn{w:val=Foo}"),
            ("w:style/w:basedOn{w:val=Foo}", "Bar", "w:style/w:basedOn{w:val=Bar}"),
            ("w:style/w:basedOn{w:val=Bar}", None, "w:style"),
        ]
    )
    def base_set_fixture(self, request, style_):
        style_cxml, base_style_id, expected_style_cxml = request.param
        style = CharacterStyle(element(style_cxml))
        style_.style_id = base_style_id
        base_style = style_ if base_style_id is not None else None
        expected_xml = xml(expected_style_cxml)
        return style, base_style, expected_xml

    @pytest.fixture
    def font_fixture(self, Font_, font_):
        style = CharacterStyle(element("w:style"))
        return style, Font_, font_

    # fixture components ---------------------------------------------

    @pytest.fixture
    def Font_(self, request, font_):
        return class_mock(request, "docx.styles.style.Font", return_value=font_)

    @pytest.fixture
    def font_(self, request):
        return instance_mock(request, Font)

    @pytest.fixture
    def style_(self, request):
        return instance_mock(request, BaseStyle)

    @pytest.fixture
    def StyleFactory_(self, request):
        return function_mock(request, "docx.styles.style.StyleFactory")


class DescribeParagraphStyle:
    def it_knows_its_next_paragraph_style(self, next_get_fixture):
        style, expected_value = next_get_fixture
        assert style.next_paragraph_style == expected_value

    def it_can_change_its_next_paragraph_style(self, next_set_fixture):
        style, next_style, expected_xml = next_set_fixture
        style.next_paragraph_style = next_style
        assert style.element.xml == expected_xml

    def it_provides_access_to_its_paragraph_format(self, parfmt_fixture):
        style, ParagraphFormat_, paragraph_format_ = parfmt_fixture
        paragraph_format = style.paragraph_format
        ParagraphFormat_.assert_called_once_with(style._element)
        assert paragraph_format is paragraph_format_

    # fixtures -------------------------------------------------------

    @pytest.fixture(
        params=[
            ("H1", "Body"),
            ("H2", "H2"),
            ("Body", "Body"),
            ("Foo", "Foo"),
        ]
    )
    def next_get_fixture(self, request):
        style_name, next_style_name = request.param
        styles = element(
            "w:styles/("
            "w:style{w:type=paragraph,w:styleId=H1}/w:next{w:val=Body},"
            "w:style{w:type=paragraph,w:styleId=H2}/w:next{w:val=Char},"
            "w:style{w:type=paragraph,w:styleId=Body},"
            "w:style{w:type=paragraph,w:styleId=Foo}/w:next{w:val=Bar},"
            "w:style{w:type=character,w:styleId=Char})"
        )
        style_names = ["H1", "H2", "Body", "Foo", "Char"]
        style_elm = styles[style_names.index(style_name)]
        next_style_elm = styles[style_names.index(next_style_name)]
        style = ParagraphStyle(style_elm)
        next_style = ParagraphStyle(next_style_elm) if style_name == "H1" else style
        return style, next_style

    @pytest.fixture(
        params=[
            ("H", "B", "w:style{w:type=paragraph,w:styleId=H}/w:next{w:val=B}"),
            ("H", None, "w:style{w:type=paragraph,w:styleId=H}"),
            ("H", "H", "w:style{w:type=paragraph,w:styleId=H}"),
        ]
    )
    def next_set_fixture(self, request):
        style_name, next_style_name, style_cxml = request.param
        styles = element(
            "w:styles/("
            "w:style{w:type=paragraph,w:styleId=H},"
            "w:style{w:type=paragraph,w:styleId=B})"
        )
        style_elms = {"H": styles[0], "B": styles[1]}
        style = ParagraphStyle(style_elms[style_name])
        next_style = (
            None
            if next_style_name is None
            else ParagraphStyle(style_elms[next_style_name])
        )
        expected_xml = xml(style_cxml)
        return style, next_style, expected_xml

    @pytest.fixture
    def parfmt_fixture(self, ParagraphFormat_, paragraph_format_):
        style = ParagraphStyle(element("w:style"))
        return style, ParagraphFormat_, paragraph_format_

    # fixture components ---------------------------------------------

    @pytest.fixture
    def ParagraphFormat_(self, request, paragraph_format_):
        return class_mock(
            request, "docx.styles.style.ParagraphFormat", return_value=paragraph_format_
        )

    @pytest.fixture
    def paragraph_format_(self, request):
        return instance_mock(request, ParagraphFormat)
