from unittest.mock import Mock

import pytest

from aioquic.h3.connection import H3Connection
from aioquic.h3.events import DataReceived, HeadersReceived
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.connection import QuicConnection
from aioquic.quic.events import ProtocolNegotiated

from sanic import Request, Sanic
from sanic.compat import Header
from sanic.config import DEFAULT_CONFIG
from sanic.exceptions import BadRequest, PayloadTooLarge
from sanic.http.constants import Stage
from sanic.http.http3 import Http3, HTTPReceiver
from sanic.models.server_types import ConnInfo
from sanic.response import empty, json
from sanic.server.protocols.http_protocol import Http3Protocol


try:
    from unittest.mock import AsyncMock
except ImportError:
    from tests.asyncmock import AsyncMock  # type: ignore

pytestmark = pytest.mark.asyncio


@pytest.fixture(autouse=True)
async def setup(app: Sanic):
    @app.get("/")
    async def handler(*_):
        return empty()

    app.router.finalize()
    app.signal_router.finalize()
    app.signal_router.allow_fail_builtin = False


@pytest.fixture
def http_request(app):
    return Request(b"/", Header({}), "3", "GET", Mock(), app)


def generate_protocol(app):
    connection = QuicConnection(configuration=QuicConfiguration())
    connection._ack_delay = 0
    connection._loss = Mock()
    connection._loss.spaces = []
    connection._loss.get_loss_detection_time = lambda: None
    connection.datagrams_to_send = Mock(return_value=[])  # type: ignore
    return Http3Protocol(
        connection,
        app=app,
        stream_handler=None,
    )


def generate_http_receiver(app, http_request) -> HTTPReceiver:
    protocol = generate_protocol(app)
    receiver = HTTPReceiver(
        protocol.transmit,
        protocol,
        http_request,
    )
    http_request.stream = receiver
    return receiver


def test_http_receiver_init(app: Sanic, http_request: Request):
    receiver = generate_http_receiver(app, http_request)
    assert receiver.request_body is None
    assert receiver.stage is Stage.IDLE
    assert receiver.headers_sent is False
    assert receiver.response is None
    assert receiver.request_max_size == DEFAULT_CONFIG["REQUEST_MAX_SIZE"]
    assert receiver.request_bytes == 0


async def test_http_receiver_run_request(app: Sanic, http_request: Request):
    handler = AsyncMock()

    class mock_handle(Sanic):
        handle_request = handler

    app.__class__ = mock_handle
    receiver = generate_http_receiver(app, http_request)
    receiver.protocol.quic_event_received(
        ProtocolNegotiated(alpn_protocol="h3")
    )
    await receiver.run()
    handler.assert_awaited_once_with(receiver.request)


async def test_http_receiver_run_exception(app: Sanic, http_request: Request):
    handler = AsyncMock()

    class mock_handle(Sanic):
        handle_exception = handler

    app.__class__ = mock_handle
    receiver = generate_http_receiver(app, http_request)
    receiver.protocol.quic_event_received(
        ProtocolNegotiated(alpn_protocol="h3")
    )
    exception = Exception("Oof")
    await receiver.run(exception)
    handler.assert_awaited_once_with(receiver.request, exception)

    handler.reset_mock()
    receiver.stage = Stage.REQUEST
    await receiver.run(exception)
    handler.assert_awaited_once_with(receiver.request, exception)


def test_http_receiver_respond(app: Sanic, http_request: Request):
    receiver = generate_http_receiver(app, http_request)
    response = empty()

    receiver.stage = Stage.RESPONSE
    with pytest.raises(RuntimeError, match="Response already started"):
        receiver.respond(response)

    receiver.stage = Stage.HANDLER
    receiver.response = Mock()
    resp = receiver.respond(response)

    assert receiver.response is resp
    assert resp is response
    assert response.stream is receiver


def test_http_receiver_receive_body(app: Sanic, http_request: Request):
    receiver = generate_http_receiver(app, http_request)
    receiver.request_max_size = 4

    receiver.receive_body(b"..")
    assert receiver.request.body == b".."

    receiver.receive_body(b"..")
    assert receiver.request.body == b"...."

    with pytest.raises(
        PayloadTooLarge, match="Request body exceeds the size limit"
    ):
        receiver.receive_body(b"..")


def test_http3_events(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    http3.http_event_received(
        HeadersReceived(
            [
                (b":method", b"GET"),
                (b":path", b"/location"),
                (b":scheme", b"https"),
                (b":authority", b"localhost:8443"),
                (b"foo", b"bar"),
            ],
            1,
            False,
        )
    )
    http3.http_event_received(DataReceived(b"foobar", 1, False))
    receiver = http3.receivers[1]

    assert len(http3.receivers) == 1
    assert receiver.request.stream_id == 1
    assert receiver.request.path == "/location"
    assert receiver.request.method == "GET"
    assert receiver.request.headers["foo"] == "bar"
    assert receiver.request.body == b"foobar"


async def test_send_headers(app: Sanic, http_request: Request):
    send_headers_mock = Mock()
    existing_send_headers = H3Connection.send_headers
    receiver = generate_http_receiver(app, http_request)
    receiver.protocol.quic_event_received(
        ProtocolNegotiated(alpn_protocol="h3")
    )

    http_request._protocol = receiver.protocol

    def send_headers(*args, **kwargs):
        send_headers_mock(*args, **kwargs)
        return existing_send_headers(
            receiver.protocol.connection, *args, **kwargs
        )

    receiver.protocol.connection.send_headers = send_headers
    receiver.head_only = False
    response = json({}, status=201, headers={"foo": "bar"})

    with pytest.raises(RuntimeError, match="no response"):
        receiver.send_headers()

    receiver.response = response
    receiver.send_headers()

    assert receiver.headers_sent
    assert receiver.stage is Stage.RESPONSE
    send_headers_mock.assert_called_once_with(
        stream_id=0,
        headers=[
            (b":status", b"201"),
            (b"foo", b"bar"),
            (b"content-length", b"2"),
            (b"content-type", b"application/json"),
        ],
    )


def test_multiple_streams(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    http3.http_event_received(
        HeadersReceived(
            [
                (b":method", b"GET"),
                (b":path", b"/location"),
                (b":scheme", b"https"),
                (b":authority", b"localhost:8443"),
                (b"foo", b"bar"),
            ],
            1,
            False,
        )
    )
    http3.http_event_received(
        HeadersReceived(
            [
                (b":method", b"GET"),
                (b":path", b"/location"),
                (b":scheme", b"https"),
                (b":authority", b"localhost:8443"),
                (b"foo", b"bar"),
            ],
            2,
            False,
        )
    )

    receiver1 = http3.get_receiver_by_stream_id(1)
    receiver2 = http3.get_receiver_by_stream_id(2)
    assert len(http3.receivers) == 2
    assert isinstance(receiver1, HTTPReceiver)
    assert isinstance(receiver2, HTTPReceiver)
    assert receiver1 is not receiver2


def test_request_stream_id(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    http3.http_event_received(
        HeadersReceived(
            [
                (b":method", b"GET"),
                (b":path", b"/location"),
                (b":scheme", b"https"),
                (b":authority", b"localhost:8443"),
                (b"foo", b"bar"),
            ],
            1,
            False,
        )
    )
    receiver = http3.get_receiver_by_stream_id(1)

    assert isinstance(receiver.request, Request)
    assert receiver.request.stream_id == 1


def test_request_conn_info(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    http3.http_event_received(
        HeadersReceived(
            [
                (b":method", b"GET"),
                (b":path", b"/location"),
                (b":scheme", b"https"),
                (b":authority", b"localhost:8443"),
                (b"foo", b"bar"),
            ],
            1,
            False,
        )
    )
    receiver = http3.get_receiver_by_stream_id(1)

    assert isinstance(receiver.request.conn_info, ConnInfo)


def test_request_header_encoding(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    with pytest.raises(BadRequest) as exc_info:
        http3.http_event_received(
            HeadersReceived(
                [
                    (b":method", b"GET"),
                    (b":path", b"/location"),
                    (b":scheme", b"https"),
                    (b":authority", b"localhost:8443"),
                    ("foo\u00A0".encode(), b"bar"),
                ],
                1,
                False,
            )
        )
    assert exc_info.value.status_code == 400
    assert (
        str(exc_info.value)
        == "Header names may only contain US-ASCII characters."
    )


def test_request_url_encoding(app):
    protocol = generate_protocol(app)
    http3 = Http3(protocol, protocol.transmit)
    with pytest.raises(BadRequest) as exc_info:
        http3.http_event_received(
            HeadersReceived(
                [
                    (b":method", b"GET"),
                    (b":path", b"/location\xA0"),
                    (b":scheme", b"https"),
                    (b":authority", b"localhost:8443"),
                    (b"foo", b"bar"),
                ],
                1,
                False,
            )
        )
    assert exc_info.value.status_code == 400
    assert str(exc_info.value) == "URL may only contain US-ASCII characters."
