# encoding: utf-8

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

from __future__ import (
    absolute_import, division, print_function, unicode_literals
)

import pytest

from docx.enum.style import WD_STYLE_TYPE
from docx.styles.style import (
    BaseStyle, _CharacterStyle, _ParagraphStyle, _NumberingStyle,
    StyleFactory, _TableStyle
)
from docx.text.paragraph import ParagraphFormat
from docx.text.run import Font

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


class DescribeStyleFactory(object):

    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(object):

    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 Describe_CharacterStyle(object):

    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 Describe_ParagraphStyle(object):

    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)
        if style_name == 'H1':
            next_style = _ParagraphStyle(next_style_elm)
        else:
            next_style = 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)
