# encoding: utf-8

"""
Test suite for docx.image.tiff module
"""

from __future__ import absolute_import, print_function

import pytest

from docx.compat import BytesIO
from docx.image.constants import MIME_TYPE, TIFF_TAG
from docx.image.helpers import BIG_ENDIAN, LITTLE_ENDIAN, StreamReader
from docx.image.tiff import (
    _AsciiIfdEntry, _IfdEntries, _IfdEntry, _IfdEntryFactory, _IfdParser,
    _LongIfdEntry, _RationalIfdEntry, _ShortIfdEntry, Tiff, _TiffParser
)

from ..unitutil.mock import (
    call, class_mock, function_mock, initializer_mock, instance_mock,
    loose_mock, method_mock
)


class DescribeTiff(object):

    def it_can_construct_from_a_tiff_stream(self, from_stream_fixture):
        (stream_, _TiffParser_, Tiff__init_, px_width, px_height, horz_dpi,
         vert_dpi) = from_stream_fixture
        tiff = Tiff.from_stream(stream_)
        _TiffParser_.parse.assert_called_once_with(stream_)
        Tiff__init_.assert_called_once_with(
            px_width, px_height, horz_dpi, vert_dpi
        )
        assert isinstance(tiff, Tiff)

    def it_knows_its_content_type(self):
        tiff = Tiff(None, None, None, None)
        assert tiff.content_type == MIME_TYPE.TIFF

    def it_knows_its_default_ext(self):
        tiff = Tiff(None, None, None, None)
        assert tiff.default_ext == 'tiff'

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

    @pytest.fixture
    def from_stream_fixture(
            self, stream_, _TiffParser_, tiff_parser_, Tiff__init_):
        px_width, px_height = 111, 222
        horz_dpi, vert_dpi = 333, 444
        tiff_parser_.px_width = px_width
        tiff_parser_.px_height = px_height
        tiff_parser_.horz_dpi = horz_dpi
        tiff_parser_.vert_dpi = vert_dpi
        return (
            stream_, _TiffParser_, Tiff__init_, px_width, px_height,
            horz_dpi, vert_dpi
        )

    @pytest.fixture
    def Tiff__init_(self, request):
        return initializer_mock(request, Tiff)

    @pytest.fixture
    def _TiffParser_(self, request, tiff_parser_):
        _TiffParser_ = class_mock(request, 'docx.image.tiff._TiffParser')
        _TiffParser_.parse.return_value = tiff_parser_
        return _TiffParser_

    @pytest.fixture
    def tiff_parser_(self, request):
        return instance_mock(request, _TiffParser)

    @pytest.fixture
    def stream_(self, request):
        return instance_mock(request, BytesIO)


class Describe_TiffParser(object):

    def it_can_parse_the_properties_from_a_tiff_stream(
            self, from_stream_fixture):
        (stream_, _make_stream_reader_, _IfdEntries_, ifd0_offset_,
         stream_rdr_, _TiffParser__init_, ifd_entries_) = from_stream_fixture
        tiff_parser = _TiffParser.parse(stream_)
        _make_stream_reader_.assert_called_once_with(stream_)
        _IfdEntries_.from_stream.assert_called_once_with(
            stream_rdr_, ifd0_offset_
        )
        _TiffParser__init_.assert_called_once_with(ifd_entries_)
        assert isinstance(tiff_parser, _TiffParser)

    def it_makes_a_stream_reader_to_help_parse(self, mk_stream_rdr_fixture):
        stream, StreamReader_, endian, stream_rdr_ = mk_stream_rdr_fixture
        stream_rdr = _TiffParser._make_stream_reader(stream)
        StreamReader_.assert_called_once_with(stream, endian)
        assert stream_rdr is stream_rdr_

    def it_knows_image_width_and_height_after_parsing(self):
        px_width, px_height = 42, 24
        entries = {
            TIFF_TAG.IMAGE_WIDTH:  px_width,
            TIFF_TAG.IMAGE_LENGTH: px_height,
        }
        ifd_entries = _IfdEntries(entries)
        tiff_parser = _TiffParser(ifd_entries)
        assert tiff_parser.px_width == px_width
        assert tiff_parser.px_height == px_height

    def it_knows_the_horz_and_vert_dpi_after_parsing(self, dpi_fixture):
        tiff_parser, expected_horz_dpi, expected_vert_dpi = dpi_fixture
        assert tiff_parser.horz_dpi == expected_horz_dpi
        assert tiff_parser.vert_dpi == expected_vert_dpi

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

    @pytest.fixture(params=[
        (1,     150,  240,  72,  72),
        (2,      42,   24,  42,  24),
        (3,     100,  200, 254, 508),
        (2,    None, None,  72,  72),
        (None,   96,  100,  96, 100),
    ])
    def dpi_fixture(self, request):
        resolution_unit, x_resolution, y_resolution = request.param[:3]
        expected_horz_dpi, expected_vert_dpi = request.param[3:]

        entries = {}
        if resolution_unit is not None:
            entries[TIFF_TAG.RESOLUTION_UNIT] = resolution_unit
        if x_resolution is not None:
            entries[TIFF_TAG.X_RESOLUTION] = x_resolution
        if y_resolution is not None:
            entries[TIFF_TAG.Y_RESOLUTION] = y_resolution

        tiff_parser = _TiffParser(entries)
        return tiff_parser, expected_horz_dpi, expected_vert_dpi

    @pytest.fixture
    def from_stream_fixture(
            self, stream_, _make_stream_reader_, _IfdEntries_, ifd0_offset_,
            stream_rdr_, _TiffParser__init_, ifd_entries_):
        return (
            stream_, _make_stream_reader_, _IfdEntries_, ifd0_offset_,
            stream_rdr_, _TiffParser__init_, ifd_entries_
        )

    @pytest.fixture
    def _IfdEntries_(self, request, ifd_entries_):
        _IfdEntries_ = class_mock(request, 'docx.image.tiff._IfdEntries')
        _IfdEntries_.from_stream.return_value = ifd_entries_
        return _IfdEntries_

    @pytest.fixture
    def ifd_entries_(self, request):
        return instance_mock(request, _IfdEntries)

    @pytest.fixture
    def ifd0_offset_(self, request):
        return instance_mock(request, int)

    @pytest.fixture
    def _make_stream_reader_(self, request, stream_rdr_):
        return method_mock(
            request, _TiffParser, '_make_stream_reader',
            return_value=stream_rdr_
        )

    @pytest.fixture(params=[
        (b'MM\x00*', BIG_ENDIAN),
        (b'II*\x00', LITTLE_ENDIAN),
    ])
    def mk_stream_rdr_fixture(self, request, StreamReader_, stream_rdr_):
        bytes_, endian = request.param
        stream = BytesIO(bytes_)
        return stream, StreamReader_, endian, stream_rdr_

    @pytest.fixture
    def stream_(self, request):
        return instance_mock(request, BytesIO)

    @pytest.fixture
    def StreamReader_(self, request, stream_rdr_):
        return class_mock(
            request, 'docx.image.tiff.StreamReader', return_value=stream_rdr_
        )

    @pytest.fixture
    def stream_rdr_(self, request, ifd0_offset_):
        stream_rdr_ = instance_mock(request, StreamReader)
        stream_rdr_.read_long.return_value = ifd0_offset_
        return stream_rdr_

    @pytest.fixture
    def _TiffParser__init_(self, request):
        return initializer_mock(request, _TiffParser)


class Describe_IfdEntries(object):

    def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture):
        stream_, offset_, _IfdParser_, _IfdEntries__init_, entries_ = (
            from_stream_fixture
        )
        ifd_entries = _IfdEntries.from_stream(stream_, offset_)
        _IfdParser_.assert_called_once_with(stream_, offset_)
        _IfdEntries__init_.assert_called_once_with(entries_)
        assert isinstance(ifd_entries, _IfdEntries)

    def it_has_basic_mapping_semantics(self):
        key, value = 1, 'foobar'
        entries = {key: value}
        ifd_entries = _IfdEntries(entries)
        assert key in ifd_entries
        assert ifd_entries[key] == value

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

    @pytest.fixture
    def from_stream_fixture(
            self, stream_, offset_, _IfdParser_, ifd_parser_,
            _IfdEntries__init_, ifd_entry_, ifd_entry_2_):
        ifd_parser_.iter_entries.return_value = [ifd_entry_, ifd_entry_2_]
        entries_ = {1: 42, 2: 24}
        return stream_, offset_, _IfdParser_, _IfdEntries__init_, entries_

    @pytest.fixture
    def ifd_entry_(self, request):
        return instance_mock(request, _IfdEntry, tag=1, value=42)

    @pytest.fixture
    def ifd_entry_2_(self, request):
        return instance_mock(request, _IfdEntry, tag=2, value=24)

    @pytest.fixture
    def _IfdEntries__init_(self, request):
        return initializer_mock(request, _IfdEntries)

    @pytest.fixture
    def _IfdParser_(self, request, ifd_parser_):
        return class_mock(
            request, 'docx.image.tiff._IfdParser', return_value=ifd_parser_
        )

    @pytest.fixture
    def ifd_parser_(self, request):
        return instance_mock(request, _IfdParser)

    @pytest.fixture
    def offset_(self, request):
        return instance_mock(request, int)

    @pytest.fixture
    def stream_(self, request):
        return instance_mock(request, BytesIO)


class Describe_IfdParser(object):

    def it_can_iterate_through_the_directory_entries_in_an_IFD(
            self, iter_fixture):
        (ifd_parser, _IfdEntryFactory_, stream_rdr, offsets,
         expected_entries) = iter_fixture
        entries = [e for e in ifd_parser.iter_entries()]
        assert _IfdEntryFactory_.call_args_list == [
            call(stream_rdr, offsets[0]),
            call(stream_rdr, offsets[1]),
        ]
        assert entries == expected_entries

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

    @pytest.fixture
    def ifd_entry_(self, request):
        return instance_mock(request, _IfdEntry, tag=1, value=42)

    @pytest.fixture
    def ifd_entry_2_(self, request):
        return instance_mock(request, _IfdEntry, tag=2, value=24)

    @pytest.fixture
    def _IfdEntryFactory_(self, request, ifd_entry_, ifd_entry_2_):
        return function_mock(
            request, 'docx.image.tiff._IfdEntryFactory',
            side_effect=[ifd_entry_, ifd_entry_2_]
        )

    @pytest.fixture
    def iter_fixture(self, _IfdEntryFactory_, ifd_entry_, ifd_entry_2_):
        stream_rdr = StreamReader(BytesIO(b'\x00\x02'), BIG_ENDIAN)
        offsets = [2, 14]
        ifd_parser = _IfdParser(stream_rdr, offset=0)
        expected_entries = [ifd_entry_, ifd_entry_2_]
        return (
            ifd_parser, _IfdEntryFactory_, stream_rdr, offsets,
            expected_entries
        )


class Describe_IfdEntryFactory(object):

    def it_constructs_the_right_class_for_a_given_ifd_entry(self, fixture):
        stream_rdr, offset, entry_cls_, ifd_entry_ = fixture
        ifd_entry = _IfdEntryFactory(stream_rdr, offset)
        entry_cls_.from_stream.assert_called_once_with(stream_rdr, offset)
        assert ifd_entry is ifd_entry_

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

    @pytest.fixture(params=[
        (b'\x66\x66\x00\x01', 'BYTE'),
        (b'\x66\x66\x00\x02', 'ASCII'),
        (b'\x66\x66\x00\x03', 'SHORT'),
        (b'\x66\x66\x00\x04', 'LONG'),
        (b'\x66\x66\x00\x05', 'RATIONAL'),
        (b'\x66\x66\x00\x06', 'CUSTOM'),
    ])
    def fixture(
            self, request, ifd_entry_, _IfdEntry_, _AsciiIfdEntry_,
            _ShortIfdEntry_, _LongIfdEntry_, _RationalIfdEntry_):
        bytes_, entry_type = request.param
        entry_cls_ = {
            'BYTE':     _IfdEntry_,
            'ASCII':    _AsciiIfdEntry_,
            'SHORT':    _ShortIfdEntry_,
            'LONG':     _LongIfdEntry_,
            'RATIONAL': _RationalIfdEntry_,
            'CUSTOM':   _IfdEntry_,
        }[entry_type]
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        offset = 0
        return stream_rdr, offset, entry_cls_, ifd_entry_

    @pytest.fixture
    def ifd_entry_(self, request):
        return instance_mock(request, _IfdEntry)

    @pytest.fixture
    def _IfdEntry_(self, request, ifd_entry_):
        _IfdEntry_ = class_mock(request, 'docx.image.tiff._IfdEntry')
        _IfdEntry_.from_stream.return_value = ifd_entry_
        return _IfdEntry_

    @pytest.fixture
    def _AsciiIfdEntry_(self, request, ifd_entry_):
        _AsciiIfdEntry_ = class_mock(
            request, 'docx.image.tiff._AsciiIfdEntry')
        _AsciiIfdEntry_.from_stream.return_value = ifd_entry_
        return _AsciiIfdEntry_

    @pytest.fixture
    def _ShortIfdEntry_(self, request, ifd_entry_):
        _ShortIfdEntry_ = class_mock(
            request, 'docx.image.tiff._ShortIfdEntry')
        _ShortIfdEntry_.from_stream.return_value = ifd_entry_
        return _ShortIfdEntry_

    @pytest.fixture
    def _LongIfdEntry_(self, request, ifd_entry_):
        _LongIfdEntry_ = class_mock(
            request, 'docx.image.tiff._LongIfdEntry')
        _LongIfdEntry_.from_stream.return_value = ifd_entry_
        return _LongIfdEntry_

    @pytest.fixture
    def _RationalIfdEntry_(self, request, ifd_entry_):
        _RationalIfdEntry_ = class_mock(
            request, 'docx.image.tiff._RationalIfdEntry')
        _RationalIfdEntry_.from_stream.return_value = ifd_entry_
        return _RationalIfdEntry_

    @pytest.fixture
    def offset_(self, request):
        return instance_mock(request, int)


class Describe_IfdEntry(object):

    def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture):
        (stream_rdr, offset, _parse_value_, value_count, value_offset,
         _IfdEntry__init_, tag_code, value_) = from_stream_fixture
        ifd_entry = _IfdEntry.from_stream(stream_rdr, offset)
        _parse_value_.assert_called_once_with(
            stream_rdr, offset, value_count, value_offset
        )
        _IfdEntry__init_.assert_called_once_with(tag_code, value_)
        assert isinstance(ifd_entry, _IfdEntry)

    def it_provides_read_only_access_to_the_directory_entry(self):
        tag_code, value = 1, 2
        ifd_entry = _IfdEntry(tag_code, value)
        assert (ifd_entry.tag, ifd_entry.value) == (tag_code, value)

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

    @pytest.fixture
    def from_stream_fixture(
            self, _parse_value_, _IfdEntry__init_, value_):
        bytes_ = b'\x00\x01\x66\x66\x00\x00\x00\x02\x00\x00\x00\x03'
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        offset, tag_code, value_count, value_offset = 0, 1, 2, 3
        return (
            stream_rdr, offset, _parse_value_, value_count, value_offset,
            _IfdEntry__init_, tag_code, value_
        )

    @pytest.fixture
    def _IfdEntry__init_(self, request):
        return initializer_mock(request, _IfdEntry)

    @pytest.fixture
    def _parse_value_(self, request, value_):
        return method_mock(
            request, _IfdEntry, '_parse_value', return_value=value_
        )

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


class Describe_AsciiIfdEntry(object):

    def it_can_parse_an_ascii_string_IFD_entry(self):
        bytes_ = b'foobar\x00'
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        val = _AsciiIfdEntry._parse_value(stream_rdr, None, 7, 0)
        assert val == 'foobar'


class Describe_ShortIfdEntry(object):

    def it_can_parse_a_short_int_IFD_entry(self):
        bytes_ = b'foobaroo\x00\x2A'
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        val = _ShortIfdEntry._parse_value(stream_rdr, 0, 1, None)
        assert val == 42


class Describe_LongIfdEntry(object):

    def it_can_parse_a_long_int_IFD_entry(self):
        bytes_ = b'foobaroo\x00\x00\x00\x2A'
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        val = _LongIfdEntry._parse_value(stream_rdr, 0, 1, None)
        assert val == 42


class Describe_RationalIfdEntry(object):

    def it_can_parse_a_rational_IFD_entry(self):
        bytes_ = b'\x00\x00\x00\x2A\x00\x00\x00\x54'
        stream_rdr = StreamReader(BytesIO(bytes_), BIG_ENDIAN)
        val = _RationalIfdEntry._parse_value(stream_rdr, None, 1, 0)
        assert val == 0.5
