"""Unit test suite for docx.image.jpeg module"""

import io

import pytest

from docx.image.constants import JPEG_MARKER_CODE, MIME_TYPE
from docx.image.helpers import BIG_ENDIAN, StreamReader
from docx.image.jpeg import (
    Exif,
    Jfif,
    Jpeg,
    _App0Marker,
    _App1Marker,
    _JfifMarkers,
    _Marker,
    _MarkerFactory,
    _MarkerFinder,
    _MarkerParser,
    _SofMarker,
)
from docx.image.tiff import Tiff

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


class DescribeJpeg:
    def it_knows_its_content_type(self):
        jpeg = Jpeg(None, None, None, None)
        assert jpeg.content_type == MIME_TYPE.JPEG

    def it_knows_its_default_ext(self):
        jpeg = Jpeg(None, None, None, None)
        assert jpeg.default_ext == "jpg"

    class DescribeExif:
        def it_can_construct_from_an_exif_stream(self, from_exif_fixture):
            # fixture ----------------------
            stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = from_exif_fixture
            # exercise ---------------------
            exif = Exif.from_stream(stream_)
            # verify -----------------------
            _JfifMarkers_.from_stream.assert_called_once_with(stream_)
            assert isinstance(exif, Exif)
            assert exif.px_width == cx
            assert exif.px_height == cy
            assert exif.horz_dpi == horz_dpi
            assert exif.vert_dpi == vert_dpi

    class DescribeJfif:
        def it_can_construct_from_a_jfif_stream(self, from_jfif_fixture):
            stream_, _JfifMarkers_, cx, cy, horz_dpi, vert_dpi = from_jfif_fixture
            jfif = Jfif.from_stream(stream_)
            _JfifMarkers_.from_stream.assert_called_once_with(stream_)
            assert isinstance(jfif, Jfif)
            assert jfif.px_width == cx
            assert jfif.px_height == cy
            assert jfif.horz_dpi == horz_dpi
            assert jfif.vert_dpi == vert_dpi

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

    @pytest.fixture
    def from_exif_fixture(self, stream_, _JfifMarkers_, jfif_markers_):
        px_width, px_height = 111, 222
        horz_dpi, vert_dpi = 333, 444
        jfif_markers_.sof.px_width = px_width
        jfif_markers_.sof.px_height = px_height
        jfif_markers_.app1.horz_dpi = horz_dpi
        jfif_markers_.app1.vert_dpi = vert_dpi
        return (stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi)

    @pytest.fixture
    def from_jfif_fixture(self, stream_, _JfifMarkers_, jfif_markers_):
        px_width, px_height = 111, 222
        horz_dpi, vert_dpi = 333, 444
        jfif_markers_.sof.px_width = px_width
        jfif_markers_.sof.px_height = px_height
        jfif_markers_.app0.horz_dpi = horz_dpi
        jfif_markers_.app0.vert_dpi = vert_dpi
        return (stream_, _JfifMarkers_, px_width, px_height, horz_dpi, vert_dpi)

    @pytest.fixture
    def _JfifMarkers_(self, request, jfif_markers_):
        _JfifMarkers_ = class_mock(request, "docx.image.jpeg._JfifMarkers")
        _JfifMarkers_.from_stream.return_value = jfif_markers_
        return _JfifMarkers_

    @pytest.fixture
    def jfif_markers_(self, request):
        return instance_mock(request, _JfifMarkers)

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


class Describe_JfifMarkers:
    def it_can_construct_from_a_jfif_stream(
        self, stream_, _MarkerParser_, _JfifMarkers__init_, soi_, app0_, sof_, sos_
    ):
        marker_lst = [soi_, app0_, sof_, sos_]

        jfif_markers = _JfifMarkers.from_stream(stream_)

        _MarkerParser_.from_stream.assert_called_once_with(stream_)
        _JfifMarkers__init_.assert_called_once_with(ANY, marker_lst)
        assert isinstance(jfif_markers, _JfifMarkers)

    def it_can_find_the_APP0_marker(self, app0_fixture):
        jfif_markers, app0_ = app0_fixture
        app0 = jfif_markers.app0
        assert app0 is app0_

    def it_can_find_the_APP1_marker(self, app1_fixture):
        jfif_markers, app1_ = app1_fixture
        app1 = jfif_markers.app1
        assert app1 is app1_

    def it_raises_if_it_cant_find_the_APP0_marker(self, no_app0_fixture):
        jfif_markers = no_app0_fixture
        with pytest.raises(KeyError):
            jfif_markers.app0

    def it_raises_if_it_cant_find_the_APP1_marker(self, no_app1_fixture):
        jfif_markers = no_app1_fixture
        with pytest.raises(KeyError):
            jfif_markers.app1

    def it_can_find_the_SOF_marker(self, sof_fixture):
        jfif_markers, sof_ = sof_fixture
        sof = jfif_markers.sof
        assert sof is sof_

    def it_raises_if_it_cant_find_the_SOF_marker(self, no_sof_fixture):
        jfif_markers = no_sof_fixture
        with pytest.raises(KeyError):
            jfif_markers.sof

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

    @pytest.fixture
    def app0_(self, request):
        return instance_mock(request, _App0Marker, marker_code=JPEG_MARKER_CODE.APP0)

    @pytest.fixture
    def app1_(self, request):
        return instance_mock(request, _App1Marker, marker_code=JPEG_MARKER_CODE.APP1)

    @pytest.fixture
    def app0_fixture(self, soi_, app0_, eoi_):
        markers = (soi_, app0_, eoi_)
        jfif_markers = _JfifMarkers(markers)
        return jfif_markers, app0_

    @pytest.fixture
    def app1_fixture(self, soi_, app1_, eoi_):
        markers = (soi_, app1_, eoi_)
        jfif_markers = _JfifMarkers(markers)
        return jfif_markers, app1_

    @pytest.fixture
    def eoi_(self, request):
        return instance_mock(request, _SofMarker, marker_code=JPEG_MARKER_CODE.EOI)

    @pytest.fixture
    def _JfifMarkers__init_(self, request):
        return initializer_mock(request, _JfifMarkers)

    @pytest.fixture
    def marker_parser_(self, request, markers_all_):
        marker_parser_ = instance_mock(request, _MarkerParser)
        marker_parser_.iter_markers.return_value = markers_all_
        return marker_parser_

    @pytest.fixture
    def _MarkerParser_(self, request, marker_parser_):
        _MarkerParser_ = class_mock(request, "docx.image.jpeg._MarkerParser")
        _MarkerParser_.from_stream.return_value = marker_parser_
        return _MarkerParser_

    @pytest.fixture
    def markers_all_(self, request, soi_, app0_, sof_, sos_, eoi_):
        return [soi_, app0_, sof_, sos_, eoi_]

    @pytest.fixture
    def no_app0_fixture(self, soi_, eoi_):
        markers = (soi_, eoi_)
        return _JfifMarkers(markers)

    @pytest.fixture
    def no_app1_fixture(self, soi_, eoi_):
        markers = (soi_, eoi_)
        return _JfifMarkers(markers)

    @pytest.fixture
    def no_sof_fixture(self, soi_, eoi_):
        markers = (soi_, eoi_)
        return _JfifMarkers(markers)

    @pytest.fixture
    def sof_(self, request):
        return instance_mock(request, _SofMarker, marker_code=JPEG_MARKER_CODE.SOF0)

    @pytest.fixture
    def sof_fixture(self, soi_, sof_, eoi_):
        markers = (soi_, sof_, eoi_)
        jfif_markers = _JfifMarkers(markers)
        return jfif_markers, sof_

    @pytest.fixture
    def soi_(self, request):
        return instance_mock(request, _Marker, marker_code=JPEG_MARKER_CODE.SOI)

    @pytest.fixture
    def sos_(self, request):
        return instance_mock(request, _Marker, marker_code=JPEG_MARKER_CODE.SOS)

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


class Describe_Marker:
    def it_can_construct_from_a_stream_and_offset(self, from_stream_fixture):
        stream, marker_code, offset, _Marker__init_, length = from_stream_fixture

        marker = _Marker.from_stream(stream, marker_code, offset)

        _Marker__init_.assert_called_once_with(ANY, marker_code, offset, length)
        assert isinstance(marker, _Marker)

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

    @pytest.fixture(
        params=[
            (JPEG_MARKER_CODE.SOI, 2, 0),
            (JPEG_MARKER_CODE.APP0, 4, 16),
        ]
    )
    def from_stream_fixture(self, request, _Marker__init_):
        marker_code, offset, length = request.param
        bytes_ = b"\xFF\xD8\xFF\xE0\x00\x10"
        stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
        return stream_reader, marker_code, offset, _Marker__init_, length

    @pytest.fixture
    def _Marker__init_(self, request):
        return initializer_mock(request, _Marker)


class Describe_App0Marker:
    def it_can_construct_from_a_stream_and_offset(self, _App0Marker__init_):
        bytes_ = b"\x00\x10JFIF\x00\x01\x01\x01\x00\x2A\x00\x18"
        marker_code, offset, length = JPEG_MARKER_CODE.APP0, 0, 16
        density_units, x_density, y_density = 1, 42, 24
        stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

        app0_marker = _App0Marker.from_stream(stream, marker_code, offset)

        _App0Marker__init_.assert_called_once_with(
            ANY, marker_code, offset, length, density_units, x_density, y_density
        )
        assert isinstance(app0_marker, _App0Marker)

    def it_knows_the_image_dpi(self, dpi_fixture):
        density_units, x_density, y_density, horz_dpi, vert_dpi = dpi_fixture
        app0 = _App0Marker(None, None, None, density_units, x_density, y_density)
        assert app0.horz_dpi == horz_dpi
        assert app0.vert_dpi == vert_dpi

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

    @pytest.fixture
    def _App0Marker__init_(self, request):
        return initializer_mock(request, _App0Marker)

    @pytest.fixture(
        params=[
            (0, 100, 200, 72, 72),
            (1, 100, 200, 100, 200),
            (2, 100, 200, 254, 508),
        ]
    )
    def dpi_fixture(self, request):
        density_units, x_density, y_density, horz_dpi, vert_dpi = request.param
        return density_units, x_density, y_density, horz_dpi, vert_dpi


class Describe_App1Marker:
    def it_can_construct_from_a_stream_and_offset(
        self, _App1Marker__init_, _tiff_from_exif_segment_
    ):
        bytes_ = b"\x00\x42Exif\x00\x00"
        marker_code, offset, length = JPEG_MARKER_CODE.APP1, 0, 66
        horz_dpi, vert_dpi = 42, 24
        stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

        app1_marker = _App1Marker.from_stream(stream, marker_code, offset)

        _tiff_from_exif_segment_.assert_called_once_with(stream, offset, length)
        _App1Marker__init_.assert_called_once_with(
            ANY, marker_code, offset, length, horz_dpi, vert_dpi
        )
        assert isinstance(app1_marker, _App1Marker)

    def it_can_construct_from_non_Exif_APP1_segment(self, _App1Marker__init_):
        bytes_ = b"\x00\x42Foobar"
        marker_code, offset, length = JPEG_MARKER_CODE.APP1, 0, 66
        stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

        app1_marker = _App1Marker.from_stream(stream, marker_code, offset)

        _App1Marker__init_.assert_called_once_with(
            ANY, marker_code, offset, length, 72, 72
        )
        assert isinstance(app1_marker, _App1Marker)

    def it_gets_a_tiff_from_its_Exif_segment_to_help_construct(self, get_tiff_fixture):
        stream, offset, length = get_tiff_fixture[:3]
        BytesIO_, segment_bytes, substream_ = get_tiff_fixture[3:6]
        Tiff_, tiff_ = get_tiff_fixture[6:]
        tiff = _App1Marker._tiff_from_exif_segment(stream, offset, length)
        BytesIO_.assert_called_once_with(segment_bytes)
        Tiff_.from_stream.assert_called_once_with(substream_)
        assert tiff is tiff_

    def it_knows_the_image_dpi(self):
        horz_dpi, vert_dpi = 42, 24
        app1 = _App1Marker(None, None, None, horz_dpi, vert_dpi)
        assert app1.horz_dpi == horz_dpi
        assert app1.vert_dpi == vert_dpi

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

    @pytest.fixture
    def _App1Marker__init_(self, request):
        return initializer_mock(request, _App1Marker)

    @pytest.fixture
    def get_tiff_fixture(self, request, substream_, Tiff_, tiff_):
        bytes_ = b"xfillerxMM\x00*\x00\x00\x00\x42"
        stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
        BytesIO_ = class_mock(
            request, "docx.image.jpeg.io.BytesIO", return_value=substream_
        )
        offset, segment_length, segment_bytes = 0, 16, bytes_[8:]
        return (
            stream_reader,
            offset,
            segment_length,
            BytesIO_,
            segment_bytes,
            substream_,
            Tiff_,
            tiff_,
        )

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

    @pytest.fixture
    def Tiff_(self, request, tiff_):
        Tiff_ = class_mock(request, "docx.image.jpeg.Tiff")
        Tiff_.from_stream.return_value = tiff_
        return Tiff_

    @pytest.fixture
    def tiff_(self, request):
        return instance_mock(request, Tiff, horz_dpi=42, vert_dpi=24)

    @pytest.fixture
    def _tiff_from_exif_segment_(self, request, tiff_):
        return method_mock(
            request,
            _App1Marker,
            "_tiff_from_exif_segment",
            autospec=False,
            return_value=tiff_,
        )


class Describe_SofMarker:
    def it_can_construct_from_a_stream_and_offset(self, request, _SofMarker__init_):
        bytes_ = b"\x00\x11\x00\x00\x2A\x00\x18"
        marker_code, offset, length = JPEG_MARKER_CODE.SOF0, 0, 17
        px_width, px_height = 24, 42
        stream = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

        sof_marker = _SofMarker.from_stream(stream, marker_code, offset)

        _SofMarker__init_.assert_called_once_with(
            ANY, marker_code, offset, length, px_width, px_height
        )
        assert isinstance(sof_marker, _SofMarker)

    def it_knows_the_image_width_and_height(self):
        sof = _SofMarker(None, None, None, 42, 24)
        assert sof.px_width == 42
        assert sof.px_height == 24

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

    @pytest.fixture
    def _SofMarker__init_(self, request):
        return initializer_mock(request, _SofMarker)


class Describe_MarkerFactory:
    def it_constructs_the_appropriate_marker_object(self, call_fixture):
        marker_code, stream_, offset_, marker_cls_ = call_fixture
        marker = _MarkerFactory(marker_code, stream_, offset_)
        marker_cls_.from_stream.assert_called_once_with(stream_, marker_code, offset_)
        assert marker is marker_cls_.from_stream.return_value

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

    @pytest.fixture(
        params=[
            JPEG_MARKER_CODE.APP0,
            JPEG_MARKER_CODE.APP1,
            JPEG_MARKER_CODE.SOF0,
            JPEG_MARKER_CODE.SOF7,
            JPEG_MARKER_CODE.SOS,
        ]
    )
    def call_fixture(
        self,
        request,
        stream_,
        offset_,
        _App0Marker_,
        _App1Marker_,
        _SofMarker_,
        _Marker_,
    ):
        marker_code = request.param
        if marker_code == JPEG_MARKER_CODE.APP0:
            marker_cls_ = _App0Marker_
        elif marker_code == JPEG_MARKER_CODE.APP1:
            marker_cls_ = _App1Marker_
        elif marker_code in JPEG_MARKER_CODE.SOF_MARKER_CODES:
            marker_cls_ = _SofMarker_
        else:
            marker_cls_ = _Marker_
        return marker_code, stream_, offset_, marker_cls_

    @pytest.fixture
    def _App0Marker_(self, request):
        return class_mock(request, "docx.image.jpeg._App0Marker")

    @pytest.fixture
    def _App1Marker_(self, request):
        return class_mock(request, "docx.image.jpeg._App1Marker")

    @pytest.fixture
    def _Marker_(self, request):
        return class_mock(request, "docx.image.jpeg._Marker")

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

    @pytest.fixture
    def _SofMarker_(self, request):
        return class_mock(request, "docx.image.jpeg._SofMarker")

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


class Describe_MarkerFinder:
    def it_can_construct_from_a_stream(self, stream_, _MarkerFinder__init_):
        marker_finder = _MarkerFinder.from_stream(stream_)

        _MarkerFinder__init_.assert_called_once_with(ANY, stream_)
        assert isinstance(marker_finder, _MarkerFinder)

    def it_can_find_the_next_marker_after_a_given_offset(self, next_fixture):
        marker_finder, start, expected_code_and_offset = next_fixture
        marker_code, segment_offset = marker_finder.next(start)
        assert (marker_code, segment_offset) == expected_code_and_offset

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

    @pytest.fixture
    def _MarkerFinder__init_(self, request):
        return initializer_mock(request, _MarkerFinder)

    @pytest.fixture(
        params=[
            (0, JPEG_MARKER_CODE.SOI, 2),
            (1, JPEG_MARKER_CODE.APP0, 4),
            (2, JPEG_MARKER_CODE.APP0, 4),
            (3, JPEG_MARKER_CODE.EOI, 12),
            (4, JPEG_MARKER_CODE.EOI, 12),
            (6, JPEG_MARKER_CODE.EOI, 12),
            (8, JPEG_MARKER_CODE.EOI, 12),
        ]
    )
    def next_fixture(self, request):
        start, marker_code, segment_offset = request.param
        bytes_ = b"\xFF\xD8\xFF\xE0\x00\x01\xFF\x00\xFF\xFF\xFF\xD9"
        stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
        marker_finder = _MarkerFinder(stream_reader)
        expected_code_and_offset = (marker_code, segment_offset)
        return marker_finder, start, expected_code_and_offset

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


class Describe_MarkerParser:
    def it_can_construct_from_a_jfif_stream(
        self, stream_, StreamReader_, _MarkerParser__init_, stream_reader_
    ):
        marker_parser = _MarkerParser.from_stream(stream_)

        StreamReader_.assert_called_once_with(stream_, BIG_ENDIAN)
        _MarkerParser__init_.assert_called_once_with(ANY, stream_reader_)
        assert isinstance(marker_parser, _MarkerParser)

    def it_can_iterate_over_the_jfif_markers_in_its_stream(self, iter_markers_fixture):
        (
            marker_parser,
            stream_,
            _MarkerFinder_,
            marker_finder_,
            _MarkerFactory_,
            marker_codes,
            offsets,
            marker_lst,
        ) = iter_markers_fixture
        markers = list(marker_parser.iter_markers())
        _MarkerFinder_.from_stream.assert_called_once_with(stream_)
        assert marker_finder_.next.call_args_list == [call(0), call(2), call(20)]
        assert _MarkerFactory_.call_args_list == [
            call(marker_codes[0], stream_, offsets[0]),
            call(marker_codes[1], stream_, offsets[1]),
            call(marker_codes[2], stream_, offsets[2]),
        ]
        assert markers == marker_lst

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

    @pytest.fixture
    def app0_(self, request):
        return instance_mock(request, _App0Marker, segment_length=16)

    @pytest.fixture
    def eoi_(self, request):
        return instance_mock(request, _Marker, segment_length=0)

    @pytest.fixture
    def iter_markers_fixture(
        self,
        stream_reader_,
        _MarkerFinder_,
        marker_finder_,
        _MarkerFactory_,
        soi_,
        app0_,
        eoi_,
    ):
        marker_parser = _MarkerParser(stream_reader_)
        offsets = [2, 4, 22]
        marker_lst = [soi_, app0_, eoi_]
        marker_finder_.next.side_effect = [
            (JPEG_MARKER_CODE.SOI, offsets[0]),
            (JPEG_MARKER_CODE.APP0, offsets[1]),
            (JPEG_MARKER_CODE.EOI, offsets[2]),
        ]
        marker_codes = [
            JPEG_MARKER_CODE.SOI,
            JPEG_MARKER_CODE.APP0,
            JPEG_MARKER_CODE.EOI,
        ]
        return (
            marker_parser,
            stream_reader_,
            _MarkerFinder_,
            marker_finder_,
            _MarkerFactory_,
            marker_codes,
            offsets,
            marker_lst,
        )

    @pytest.fixture
    def _MarkerFactory_(self, request, soi_, app0_, eoi_):
        return class_mock(
            request, "docx.image.jpeg._MarkerFactory", side_effect=[soi_, app0_, eoi_]
        )

    @pytest.fixture
    def _MarkerFinder_(self, request, marker_finder_):
        _MarkerFinder_ = class_mock(request, "docx.image.jpeg._MarkerFinder")
        _MarkerFinder_.from_stream.return_value = marker_finder_
        return _MarkerFinder_

    @pytest.fixture
    def marker_finder_(self, request):
        return instance_mock(request, _MarkerFinder)

    @pytest.fixture
    def _MarkerParser__init_(self, request):
        return initializer_mock(request, _MarkerParser)

    @pytest.fixture
    def soi_(self, request):
        return instance_mock(request, _Marker, segment_length=0)

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

    @pytest.fixture
    def StreamReader_(self, request, stream_reader_):
        return class_mock(
            request, "docx.image.jpeg.StreamReader", return_value=stream_reader_
        )

    @pytest.fixture
    def stream_reader_(self, request):
        return instance_mock(request, StreamReader)
