# encoding: utf-8

"""Unit test suite for docx.opc.package module"""

from __future__ import absolute_import, division, print_function, unicode_literals

import pytest

from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.coreprops import CoreProperties
from docx.opc.package import OpcPackage, Unmarshaller
from docx.opc.packuri import PACKAGE_URI, PackURI
from docx.opc.part import Part
from docx.opc.parts.coreprops import CorePropertiesPart
from docx.opc.pkgreader import PackageReader
from docx.opc.rel import _Relationship, Relationships

from ..unitutil.mock import (
    call,
    class_mock,
    instance_mock,
    loose_mock,
    method_mock,
    Mock,
    patch,
    PropertyMock,
    property_mock,
)


class DescribeOpcPackage(object):

    def it_can_open_a_pkg_file(self, PackageReader_, PartFactory_,
                               Unmarshaller_):
        # mockery ----------------------
        pkg_file = Mock(name='pkg_file')
        pkg_reader = PackageReader_.from_file.return_value
        # exercise ---------------------
        pkg = OpcPackage.open(pkg_file)
        # verify -----------------------
        PackageReader_.from_file.assert_called_once_with(pkg_file)
        Unmarshaller_.unmarshal.assert_called_once_with(pkg_reader, pkg,
                                                        PartFactory_)
        assert isinstance(pkg, OpcPackage)

    def it_initializes_its_rels_collection_on_first_reference(
            self, Relationships_):
        pkg = OpcPackage()
        rels = pkg.rels
        Relationships_.assert_called_once_with(PACKAGE_URI.baseURI)
        assert rels == Relationships_.return_value

    def it_can_add_a_relationship_to_a_part(self, pkg_with_rels_, rel_attrs_):
        reltype, target, rId = rel_attrs_
        pkg = pkg_with_rels_
        # exercise ---------------------
        pkg.load_rel(reltype, target, rId)
        # verify -----------------------
        pkg._rels.add_relationship.assert_called_once_with(
            reltype, target, rId, False
        )

    def it_can_establish_a_relationship_to_another_part(
            self, relate_to_part_fixture_):
        pkg, part_, reltype, rId = relate_to_part_fixture_
        _rId = pkg.relate_to(part_, reltype)
        pkg.rels.get_or_add.assert_called_once_with(reltype, part_)
        assert _rId == rId

    def it_can_provide_a_list_of_the_parts_it_contains(self):
        # mockery ----------------------
        parts = [Mock(name='part1'), Mock(name='part2')]
        pkg = OpcPackage()
        # verify -----------------------
        with patch.object(OpcPackage, 'iter_parts', return_value=parts):
            assert pkg.parts == [parts[0], parts[1]]

    def it_can_iterate_over_parts_by_walking_rels_graph(self):
        # +----------+       +--------+
        # | pkg_rels |-----> | part_1 |
        # +----------+       +--------+
        #      |               |    ^
        #      v               v    |
        #   external         +--------+
        #                    | part_2 |
        #                    +--------+
        part1, part2 = (Mock(name='part1'), Mock(name='part2'))
        part1.rels = {
            1: Mock(name='rel1', is_external=False, target_part=part2)
        }
        part2.rels = {
            1: Mock(name='rel2', is_external=False, target_part=part1)
        }
        pkg = OpcPackage()
        pkg._rels = {
            1: Mock(name='rel3', is_external=False, target_part=part1),
            2: Mock(name='rel4', is_external=True),
        }
        # verify -----------------------
        assert part1 in pkg.iter_parts()
        assert part2 in pkg.iter_parts()
        assert len([p for p in pkg.iter_parts()]) == 2

    def it_can_find_the_next_available_vector_partname(
        self, next_partname_fixture, iter_parts_, PackURI_, packuri_
    ):
        """A vector partname is one with a numeric suffix, like header42.xml."""
        parts_, expected_value = next_partname_fixture
        iter_parts_.return_value = iter(parts_)
        PackURI_.return_value = packuri_
        package = OpcPackage()

        partname = package.next_partname(template="/foo/bar/baz%d.xml")

        PackURI_.assert_called_once_with(expected_value)
        assert partname is packuri_

    def it_can_find_a_part_related_by_reltype(self, related_part_fixture_):
        pkg, reltype, related_part_ = related_part_fixture_
        related_part = pkg.part_related_by(reltype)
        pkg.rels.part_with_reltype.assert_called_once_with(reltype)
        assert related_part is related_part_

    def it_can_save_to_a_pkg_file(
            self, pkg_file_, PackageWriter_, parts, parts_):
        pkg = OpcPackage()
        pkg.save(pkg_file_)
        for part in parts_:
            part.before_marshal.assert_called_once_with()
        PackageWriter_.write.assert_called_once_with(
            pkg_file_, pkg._rels, parts_
        )

    def it_provides_access_to_the_core_properties(self, core_props_fixture):
        opc_package, core_properties_ = core_props_fixture
        core_properties = opc_package.core_properties
        assert core_properties is core_properties_

    def it_provides_access_to_the_core_properties_part_to_help(
            self, core_props_part_fixture):
        opc_package, core_properties_part_ = core_props_part_fixture
        core_properties_part = opc_package._core_properties_part
        assert core_properties_part is core_properties_part_

    def it_creates_a_default_core_props_part_if_none_present(
        self, part_related_by_, CorePropertiesPart_, relate_to_, core_properties_part_
    ):
        part_related_by_.side_effect = KeyError
        CorePropertiesPart_.default.return_value = core_properties_part_
        opc_package = OpcPackage()

        core_properties_part = opc_package._core_properties_part

        CorePropertiesPart_.default.assert_called_once_with(opc_package)
        relate_to_.assert_called_once_with(
            opc_package, core_properties_part_, RT.CORE_PROPERTIES
        )
        assert core_properties_part is core_properties_part_

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

    @pytest.fixture
    def core_props_fixture(
            self, _core_properties_part_prop_, core_properties_part_,
            core_properties_):
        opc_package = OpcPackage()
        _core_properties_part_prop_.return_value = core_properties_part_
        core_properties_part_.core_properties = core_properties_
        return opc_package, core_properties_

    @pytest.fixture
    def core_props_part_fixture(
            self, part_related_by_, core_properties_part_):
        opc_package = OpcPackage()
        part_related_by_.return_value = core_properties_part_
        return opc_package, core_properties_part_

    @pytest.fixture(
        params=[((), 1), ((1,), 2), ((1, 2), 3), ((2, 3), 1), ((1, 3), 2)]
    )
    def next_partname_fixture(self, request, iter_parts_):
        existing_partname_ns, next_partname_n = request.param
        parts_ = [
            instance_mock(
                request, Part, name="part[%d]" % idx, partname="/foo/bar/baz%d.xml" % n
            )
            for idx, n in enumerate(existing_partname_ns)
        ]
        expected_value = "/foo/bar/baz%d.xml" % next_partname_n
        return parts_, expected_value

    @pytest.fixture
    def relate_to_part_fixture_(self, request, pkg, rels_, reltype):
        rId = 'rId99'
        rel_ = instance_mock(request, _Relationship, name='rel_', rId=rId)
        rels_.get_or_add.return_value = rel_
        pkg._rels = rels_
        part_ = instance_mock(request, Part, name='part_')
        return pkg, part_, reltype, rId

    @pytest.fixture
    def related_part_fixture_(self, request, rels_, reltype):
        related_part_ = instance_mock(request, Part, name='related_part_')
        rels_.part_with_reltype.return_value = related_part_
        pkg = OpcPackage()
        pkg._rels = rels_
        return pkg, reltype, related_part_

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

    @pytest.fixture
    def CorePropertiesPart_(self, request):
        return class_mock(request, 'docx.opc.package.CorePropertiesPart')

    @pytest.fixture
    def core_properties_(self, request):
        return instance_mock(request, CoreProperties)

    @pytest.fixture
    def core_properties_part_(self, request):
        return instance_mock(request, CorePropertiesPart)

    @pytest.fixture
    def _core_properties_part_prop_(self, request):
        return property_mock(request, OpcPackage, '_core_properties_part')

    @pytest.fixture
    def iter_parts_(self, request):
        return method_mock(request, OpcPackage, "iter_parts")

    @pytest.fixture
    def PackageReader_(self, request):
        return class_mock(request, 'docx.opc.package.PackageReader')

    @pytest.fixture
    def PackURI_(self, request):
        return class_mock(request, "docx.opc.package.PackURI")

    @pytest.fixture
    def packuri_(self, request):
        return instance_mock(request, PackURI)

    @pytest.fixture
    def PackageWriter_(self, request):
        return class_mock(request, 'docx.opc.package.PackageWriter')

    @pytest.fixture
    def PartFactory_(self, request):
        return class_mock(request, 'docx.opc.package.PartFactory')

    @pytest.fixture
    def part_related_by_(self, request):
        return method_mock(request, OpcPackage, 'part_related_by')

    @pytest.fixture
    def parts(self, request, parts_):
        """
        Return a mock patching property OpcPackage.parts, reversing the
        patch after each use.
        """
        _patch = patch.object(
            OpcPackage, 'parts', new_callable=PropertyMock,
            return_value=parts_
        )
        request.addfinalizer(_patch.stop)
        return _patch.start()

    @pytest.fixture
    def parts_(self, request):
        part_ = instance_mock(request, Part, name='part_')
        part_2_ = instance_mock(request, Part, name='part_2_')
        return [part_, part_2_]

    @pytest.fixture
    def pkg(self, request):
        return OpcPackage()

    @pytest.fixture
    def pkg_file_(self, request):
        return loose_mock(request)

    @pytest.fixture
    def pkg_with_rels_(self, request, rels_):
        pkg = OpcPackage()
        pkg._rels = rels_
        return pkg

    @pytest.fixture
    def Relationships_(self, request):
        return class_mock(request, 'docx.opc.package.Relationships')

    @pytest.fixture
    def rel_attrs_(self, request):
        reltype = 'http://rel/type'
        target_ = instance_mock(request, Part, name='target_')
        rId = 'rId99'
        return reltype, target_, rId

    @pytest.fixture
    def relate_to_(self, request):
        return method_mock(request, OpcPackage, 'relate_to')

    @pytest.fixture
    def rels_(self, request):
        return instance_mock(request, Relationships)

    @pytest.fixture
    def reltype(self, request):
        return 'http://rel/type'

    @pytest.fixture
    def Unmarshaller_(self, request):
        return class_mock(request, 'docx.opc.package.Unmarshaller')


class DescribeUnmarshaller(object):

    def it_can_unmarshal_from_a_pkg_reader(
        self,
        pkg_reader_,
        pkg_,
        part_factory_,
        _unmarshal_parts_,
        _unmarshal_relationships_,
        parts_dict_,
    ):
        _unmarshal_parts_.return_value = parts_dict_
        Unmarshaller.unmarshal(pkg_reader_, pkg_, part_factory_)

        _unmarshal_parts_.assert_called_once_with(pkg_reader_, pkg_, part_factory_)
        _unmarshal_relationships_.assert_called_once_with(
            pkg_reader_, pkg_, parts_dict_
        )
        for part in parts_dict_.values():
            part.after_unmarshal.assert_called_once_with()
        pkg_.after_unmarshal.assert_called_once_with()

    def it_can_unmarshal_parts(
            self, pkg_reader_, pkg_, part_factory_, parts_dict_, partnames_,
            content_types_, reltypes_, blobs_):
        # fixture ----------------------
        partname_, partname_2_ = partnames_
        content_type_, content_type_2_ = content_types_
        reltype_, reltype_2_ = reltypes_
        blob_, blob_2_ = blobs_
        # exercise ---------------------
        parts = Unmarshaller._unmarshal_parts(
            pkg_reader_, pkg_, part_factory_
        )
        # verify -----------------------
        assert (
            part_factory_.call_args_list == [
                call(partname_, content_type_, reltype_, blob_, pkg_),
                call(partname_2_, content_type_2_, reltype_2_, blob_2_, pkg_)
            ]
        )
        assert parts == parts_dict_

    def it_can_unmarshal_relationships(self):
        # test data --------------------
        reltype = 'http://reltype'
        # mockery ----------------------
        pkg_reader = Mock(name='pkg_reader')
        pkg_reader.iter_srels.return_value = (
            ('/',         Mock(name='srel1', rId='rId1', reltype=reltype,
             target_partname='partname1', is_external=False)),
            ('/',         Mock(name='srel2', rId='rId2', reltype=reltype,
             target_ref='target_ref_1',   is_external=True)),
            ('partname1', Mock(name='srel3', rId='rId3', reltype=reltype,
             target_partname='partname2', is_external=False)),
            ('partname2', Mock(name='srel4', rId='rId4', reltype=reltype,
             target_ref='target_ref_2',   is_external=True)),
        )
        pkg = Mock(name='pkg')
        parts = {}
        for num in range(1, 3):
            name = 'part%d' % num
            part = Mock(name=name)
            parts['partname%d' % num] = part
            pkg.attach_mock(part, name)
        # exercise ---------------------
        Unmarshaller._unmarshal_relationships(pkg_reader, pkg, parts)
        # verify -----------------------
        expected_pkg_calls = [
            call.load_rel(reltype, parts['partname1'], 'rId1', False),
            call.load_rel(reltype, 'target_ref_1', 'rId2', True),
            call.part1.load_rel(reltype, parts['partname2'], 'rId3', False),
            call.part2.load_rel(reltype, 'target_ref_2', 'rId4', True),
        ]
        assert pkg.mock_calls == expected_pkg_calls

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

    @pytest.fixture
    def blobs_(self, request):
        blob_ = loose_mock(request, spec=str, name='blob_')
        blob_2_ = loose_mock(request, spec=str, name='blob_2_')
        return blob_, blob_2_

    @pytest.fixture
    def content_types_(self, request):
        content_type_ = loose_mock(request, spec=str, name='content_type_')
        content_type_2_ = loose_mock(request, spec=str, name='content_type_2_')
        return content_type_, content_type_2_

    @pytest.fixture
    def part_factory_(self, request, parts_):
        part_factory_ = loose_mock(request, spec=Part)
        part_factory_.side_effect = parts_
        return part_factory_

    @pytest.fixture
    def partnames_(self, request):
        partname_ = loose_mock(request, spec=str, name='partname_')
        partname_2_ = loose_mock(request, spec=str, name='partname_2_')
        return partname_, partname_2_

    @pytest.fixture
    def parts_(self, request):
        part_ = instance_mock(request, Part, name='part_')
        part_2_ = instance_mock(request, Part, name='part_2')
        return part_, part_2_

    @pytest.fixture
    def parts_dict_(self, request, partnames_, parts_):
        partname_, partname_2_ = partnames_
        part_, part_2_ = parts_
        return {partname_: part_, partname_2_: part_2_}

    @pytest.fixture
    def pkg_(self, request):
        return instance_mock(request, OpcPackage)

    @pytest.fixture
    def pkg_reader_(
            self, request, partnames_, content_types_, reltypes_, blobs_):
        partname_, partname_2_ = partnames_
        content_type_, content_type_2_ = content_types_
        reltype_, reltype_2_ = reltypes_
        blob_, blob_2_ = blobs_
        iter_spart_items = (
            (partname_, content_type_, reltype_, blob_),
            (partname_2_, content_type_2_, reltype_2_, blob_2_),
        )
        pkg_reader_ = instance_mock(request, PackageReader)
        pkg_reader_.iter_sparts.return_value = iter_spart_items
        return pkg_reader_

    @pytest.fixture
    def reltypes_(self, request):
        reltype_ = instance_mock(request, str, name='reltype_')
        reltype_2_ = instance_mock(request, str, name='reltype_2')
        return reltype_, reltype_2_

    @pytest.fixture
    def _unmarshal_parts_(self, request):
        return method_mock(request, Unmarshaller, '_unmarshal_parts', autospec=False)

    @pytest.fixture
    def _unmarshal_relationships_(self, request):
        return method_mock(
            request, Unmarshaller, '_unmarshal_relationships', autospec=False
        )
