# -*- coding: utf-8 -*-
"""
test_informational_responses
~~~~~~~~~~~~~~~~~~~~~~~~~~

Tests that validate that hyper-h2 correctly handles informational (1XX)
responses in its state machine.
"""
import pytest

import jh2.config
import jh2.connection
import jh2.events
import jh2.exceptions


class TestReceivingInformationalResponses(object):
    """
    Tests for receiving informational responses.
    """
    example_request_headers = [
        (b':authority', b'example.com'),
        (b':path', b'/'),
        (b':scheme', b'https'),
        (b':method', b'GET'),
        (b'expect', b'100-continue'),
    ]
    example_informational_headers = [
        (b':status', b'100'),
        (b'server', b'fake-serv/0.1.0')
    ]
    example_response_headers = [
        (b':status', b'200'),
        (b'server', b'fake-serv/0.1.0')
    ]
    example_trailers = [
        (b'trailer', b'you-bet'),
    ]

    @pytest.mark.parametrize('end_stream', (True, False))
    def test_single_informational_response(self, frame_factory, end_stream):
        """
        When receiving a informational response, the appropriate event is
        signaled.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()
        c.send_headers(
            stream_id=1,
            headers=self.example_request_headers,
            end_stream=end_stream
        )

        f = frame_factory.build_headers_frame(
            headers=self.example_informational_headers,
            stream_id=1,
        )
        events = c.receive_data(f.serialize())

        assert len(events) == 1
        event = events[0]

        assert isinstance(event, jh2.events.InformationalResponseReceived)
        assert event.headers == self.example_informational_headers
        assert event.stream_id == 1

    @pytest.mark.parametrize('end_stream', (True, False))
    def test_receiving_multiple_header_blocks(self, frame_factory, end_stream):
        """
        At least three header blocks can be received: informational, headers,
        trailers.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()
        c.send_headers(
            stream_id=1,
            headers=self.example_request_headers,
            end_stream=end_stream
        )

        f1 = frame_factory.build_headers_frame(
            headers=self.example_informational_headers,
            stream_id=1,
        )
        f2 = frame_factory.build_headers_frame(
            headers=self.example_response_headers,
            stream_id=1,
        )
        f3 = frame_factory.build_headers_frame(
            headers=self.example_trailers,
            stream_id=1,
            flags=['END_STREAM'],
        )
        events = c.receive_data(
            f1.serialize() + f2.serialize() + f3.serialize()
        )

        assert len(events) == 4

        assert isinstance(events[0], jh2.events.InformationalResponseReceived)
        assert events[0].headers == self.example_informational_headers
        assert events[0].stream_id == 1

        assert isinstance(events[1], jh2.events.ResponseReceived)
        assert events[1].headers == self.example_response_headers
        assert events[1].stream_id == 1

        assert isinstance(events[2], jh2.events.TrailersReceived)
        assert events[2].headers == self.example_trailers
        assert events[2].stream_id == 1

    @pytest.mark.parametrize('end_stream', (True, False))
    def test_receiving_multiple_informational_responses(self,
                                                        frame_factory,
                                                        end_stream):
        """
        More than one informational response is allowed.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()
        c.send_headers(
            stream_id=1,
            headers=self.example_request_headers,
            end_stream=end_stream
        )

        f1 = frame_factory.build_headers_frame(
            headers=self.example_informational_headers,
            stream_id=1,
        )
        f2 = frame_factory.build_headers_frame(
            headers=[(':status', '101')],
            stream_id=1,
        )
        events = c.receive_data(f1.serialize() + f2.serialize())

        assert len(events) == 2

        assert isinstance(events[0], jh2.events.InformationalResponseReceived)
        assert events[0].headers == self.example_informational_headers
        assert events[0].stream_id == 1

        assert isinstance(events[1], jh2.events.InformationalResponseReceived)
        assert events[1].headers == [(b':status', b'101')]
        assert events[1].stream_id == 1

    @pytest.mark.parametrize('end_stream', (True, False))
    def test_receive_provisional_response_with_end_stream(self,
                                                          frame_factory,
                                                          end_stream):
        """
        Receiving provisional responses with END_STREAM set causes
        ProtocolErrors.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()
        c.send_headers(
            stream_id=1,
            headers=self.example_request_headers,
            end_stream=end_stream
        )
        c.clear_outbound_data_buffer()

        f = frame_factory.build_headers_frame(
            headers=self.example_informational_headers,
            stream_id=1,
            flags=['END_STREAM']
        )

        with pytest.raises(jh2.exceptions.ProtocolError):
            c.receive_data(f.serialize())

        expected = frame_factory.build_goaway_frame(
            last_stream_id=0,
            error_code=1,
        )
        assert c.data_to_send() == expected.serialize()

    @pytest.mark.parametrize('end_stream', (True, False))
    def test_receiving_out_of_order_headers(self, frame_factory, end_stream):
        """
        When receiving a informational response after the actual response
        headers we consider it a ProtocolError and raise it.
        """
        c = jh2.connection.H2Connection()
        c.initiate_connection()
        c.send_headers(
            stream_id=1,
            headers=self.example_request_headers,
            end_stream=end_stream
        )

        f1 = frame_factory.build_headers_frame(
            headers=self.example_response_headers,
            stream_id=1,
        )
        f2 = frame_factory.build_headers_frame(
            headers=self.example_informational_headers,
            stream_id=1,
        )
        c.receive_data(f1.serialize())
        c.clear_outbound_data_buffer()

        with pytest.raises(jh2.exceptions.ProtocolError):
            c.receive_data(f2.serialize())

        expected = frame_factory.build_goaway_frame(
            last_stream_id=0,
            error_code=1,
        )
        assert c.data_to_send() == expected.serialize()


class TestSendingInformationalResponses(object):
    """
    Tests for sending informational responses.
    """
    example_request_headers = [
        (b':authority', b'example.com'),
        (b':path', b'/'),
        (b':scheme', b'https'),
        (b':method', b'GET'),
        (b'expect', b'100-continue'),
    ]
    unicode_informational_headers = [
        (u':status', u'100'),
        (u'server', u'fake-serv/0.1.0')
    ]
    bytes_informational_headers = [
        (b':status', b'100'),
        (b'server', b'fake-serv/0.1.0')
    ]
    example_response_headers = [
        (b':status', b'200'),
        (b'server', b'fake-serv/0.1.0')
    ]
    example_trailers = [
        (b'trailer', b'you-bet'),
    ]
    server_config = jh2.config.H2Configuration(client_side=False)

    @pytest.mark.parametrize(
        'hdrs', (unicode_informational_headers, bytes_informational_headers),
    )
    @pytest.mark.parametrize('end_stream', (True, False))
    def test_single_informational_response(self,
                                           frame_factory,
                                           hdrs,
                                           end_stream):
        """
        When sending a informational response, the appropriate frames are
        emitted.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.initiate_connection()
        c.receive_data(frame_factory.preamble())
        flags = ['END_STREAM'] if end_stream else []
        f = frame_factory.build_headers_frame(
            headers=self.example_request_headers,
            stream_id=1,
            flags=flags,
        )
        c.receive_data(f.serialize())
        c.clear_outbound_data_buffer()
        frame_factory.refresh_encoder()

        c.send_headers(
            stream_id=1,
            headers=hdrs
        )

        f = frame_factory.build_headers_frame(
            headers=hdrs,
            stream_id=1,
        )
        assert c.data_to_send() == f.serialize()

    @pytest.mark.parametrize(
        'hdrs', (unicode_informational_headers, bytes_informational_headers),
    )
    @pytest.mark.parametrize('end_stream', (True, False))
    def test_sending_multiple_header_blocks(self,
                                            frame_factory,
                                            hdrs,
                                            end_stream):
        """
        At least three header blocks can be sent: informational, headers,
        trailers.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.initiate_connection()
        c.receive_data(frame_factory.preamble())
        flags = ['END_STREAM'] if end_stream else []
        f = frame_factory.build_headers_frame(
            headers=self.example_request_headers,
            stream_id=1,
            flags=flags,
        )
        c.receive_data(f.serialize())
        c.clear_outbound_data_buffer()
        frame_factory.refresh_encoder()

        # Send the three header blocks.
        c.send_headers(
            stream_id=1,
            headers=hdrs
        )
        c.send_headers(
            stream_id=1,
            headers=self.example_response_headers
        )
        c.send_headers(
            stream_id=1,
            headers=self.example_trailers,
            end_stream=True
        )

        # Check that we sent them properly.
        f1 = frame_factory.build_headers_frame(
            headers=hdrs,
            stream_id=1,
        )
        f2 = frame_factory.build_headers_frame(
            headers=self.example_response_headers,
            stream_id=1,
        )
        f3 = frame_factory.build_headers_frame(
            headers=self.example_trailers,
            stream_id=1,
            flags=['END_STREAM']
        )
        assert (
            c.data_to_send() ==
            f1.serialize() + f2.serialize() + f3.serialize()
        )

    @pytest.mark.parametrize(
        'hdrs', (unicode_informational_headers, bytes_informational_headers),
    )
    @pytest.mark.parametrize('end_stream', (True, False))
    def test_sending_multiple_informational_responses(self,
                                                      frame_factory,
                                                      hdrs,
                                                      end_stream):
        """
        More than one informational response is allowed.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.initiate_connection()
        c.receive_data(frame_factory.preamble())
        flags = ['END_STREAM'] if end_stream else []
        f = frame_factory.build_headers_frame(
            headers=self.example_request_headers,
            stream_id=1,
            flags=flags,
        )
        c.receive_data(f.serialize())
        c.clear_outbound_data_buffer()
        frame_factory.refresh_encoder()

        # Send two informational responses.
        c.send_headers(
            stream_id=1,
            headers=hdrs,
        )
        c.send_headers(
            stream_id=1,
            headers=[(':status', '101')]
        )

        # Check we sent them both.
        f1 = frame_factory.build_headers_frame(
            headers=hdrs,
            stream_id=1,
        )
        f2 = frame_factory.build_headers_frame(
            headers=[(':status', '101')],
            stream_id=1,
        )
        assert c.data_to_send() == f1.serialize() + f2.serialize()

    @pytest.mark.parametrize(
        'hdrs', (unicode_informational_headers, bytes_informational_headers),
    )
    @pytest.mark.parametrize('end_stream', (True, False))
    def test_send_provisional_response_with_end_stream(self,
                                                       frame_factory,
                                                       hdrs,
                                                       end_stream):
        """
        Sending provisional responses with END_STREAM set causes
        ProtocolErrors.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.initiate_connection()
        c.receive_data(frame_factory.preamble())
        flags = ['END_STREAM'] if end_stream else []
        f = frame_factory.build_headers_frame(
            headers=self.example_request_headers,
            stream_id=1,
            flags=flags,
        )
        c.receive_data(f.serialize())

        with pytest.raises(jh2.exceptions.ProtocolError):
            c.send_headers(
                stream_id=1,
                headers=hdrs,
                end_stream=True,
            )

    @pytest.mark.parametrize(
        'hdrs', (unicode_informational_headers, bytes_informational_headers),
    )
    @pytest.mark.parametrize('end_stream', (True, False))
    def test_reject_sending_out_of_order_headers(self,
                                                 frame_factory,
                                                 hdrs,
                                                 end_stream):
        """
        When sending an informational response after the actual response
        headers we consider it a ProtocolError and raise it.
        """
        c = jh2.connection.H2Connection(config=self.server_config)
        c.initiate_connection()
        c.receive_data(frame_factory.preamble())
        flags = ['END_STREAM'] if end_stream else []
        f = frame_factory.build_headers_frame(
            headers=self.example_request_headers,
            stream_id=1,
            flags=flags,
        )
        c.receive_data(f.serialize())

        c.send_headers(
            stream_id=1,
            headers=self.example_response_headers
        )

        with pytest.raises(jh2.exceptions.ProtocolError):
            c.send_headers(
                stream_id=1,
                headers=hdrs
            )
