// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Net.Http;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core.Features;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http3;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.Logging;
using Moq;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Tests;

public class Http3TimeoutTests : Http3TestBase
{
    [Fact]
    public async Task KeepAliveTimeout_ControlStreamNotReceived_ConnectionClosed()
    {
        var limits = _serviceContext.ServerOptions.Limits;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1));

        await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError);
    }

    [Fact]
    public async Task KeepAliveTimeout_RequestNotReceived_ConnectionClosed()
    {
        var limits = _serviceContext.ServerOptions.Limits;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();
        await Http3Api.CreateControlStream();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        Http3Api.AdvanceTime(limits.KeepAliveTimeout + TimeSpan.FromTicks(1));

        await Http3Api.WaitForConnectionStopAsync(0, false, expectedErrorCode: Http3ErrorCode.NoError);
    }

    [Fact]
    public async Task KeepAliveTimeout_AfterRequestComplete_ConnectionClosed()
    {
        var requestHeaders = new[]
        {
            new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
            new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
            new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
        };

        var limits = _serviceContext.ServerOptions.Limits;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        await Http3Api.CreateControlStream();
        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();
        var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true);
        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectReceiveEndOfStream();
        await requestStream.OnDisposedTask.DefaultTimeout();

        Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1));

        await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError);
    }

    [Fact]
    public async Task KeepAliveTimeout_LongRunningRequest_KeepsConnectionAlive()
    {
        var requestHeaders = new[]
        {
            new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
            new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
            new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
        };

        var limits = _serviceContext.ServerOptions.Limits;
        var requestReceivedTcs = new TaskCompletionSource();
        var requestFinishedTcs = new TaskCompletionSource();

        await Http3Api.InitializeConnectionAsync(_ =>
        {
            requestReceivedTcs.SetResult();
            return requestFinishedTcs.Task;
        }).DefaultTimeout();

        await Http3Api.CreateControlStream();
        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();
        var requestStream = await Http3Api.CreateRequestStream(requestHeaders, endStream: true);

        await requestReceivedTcs.Task;

        Http3Api.AdvanceTime(limits.KeepAliveTimeout);
        Http3Api.AdvanceTime(limits.KeepAliveTimeout);
        Http3Api.AdvanceTime(limits.KeepAliveTimeout);
        Http3Api.AdvanceTime(limits.KeepAliveTimeout);
        Http3Api.AdvanceTime(limits.KeepAliveTimeout);

        requestFinishedTcs.SetResult();

        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectReceiveEndOfStream();
        await requestStream.OnDisposedTask.DefaultTimeout();

        Http3Api.AdvanceTime(limits.KeepAliveTimeout + Heartbeat.Interval + TimeSpan.FromTicks(1));

        await Http3Api.WaitForConnectionStopAsync(4, false, expectedErrorCode: Http3ErrorCode.NoError);
    }

    [Fact]
    public async Task HEADERS_IncompleteFrameReceivedWithinRequestHeadersTimeout_StreamError()
    {
        var timeProvider = _serviceContext.MockTimeProvider;
        var timestamp = timeProvider.GetTimestamp();
        var limits = _serviceContext.ServerOptions.Limits;

        var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, null).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        await requestStream.SendHeadersPartialAsync().DefaultTimeout();

        await requestStream.OnStreamCreatedTask;

        var serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId];

        Http3Api.TriggerTick();
        Http3Api.TriggerTick(limits.RequestHeadersTimeout);

        Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverRequestStream.StreamTimeoutTimestamp);

        Http3Api.TriggerTick(TimeSpan.FromTicks(1));

        await requestStream.WaitForStreamErrorAsync(
            Http3ErrorCode.RequestRejected,
            AssertExpectedErrorMessages,
            CoreStrings.BadRequest_RequestHeadersTimeout);
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task HEADERS_HeaderFrameReceivedWithinRequestHeadersTimeout_Success(bool pendingStreamsEnabled)
    {
        Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = pendingStreamsEnabled;

        var timestamp = _serviceContext.MockTimeProvider.GetTimestamp();
        var limits = _serviceContext.ServerOptions.Limits;
        var headers = new[]
        {
            new KeyValuePair<string, string>(InternalHeaderNames.Method, "Custom"),
            new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
            new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
            new KeyValuePair<string, string>(InternalHeaderNames.Authority, "localhost:80"),
        };

        var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(_noopApplication, null).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        dynamic serverRequestStream;

        if (pendingStreamsEnabled)
        {
            await requestStream.OnUnidentifiedStreamCreatedTask.DefaultTimeout();

            serverRequestStream = Http3Api.Connection._unidentifiedStreams[requestStream.StreamId];
        }
        else
        {
            await requestStream.OnStreamCreatedTask.DefaultTimeout();

            serverRequestStream = Http3Api.Connection._streams[requestStream.StreamId];
        }

        Http3Api.TriggerTick();
        Http3Api.AdvanceTime(limits.RequestHeadersTimeout);

        Assert.Equal(_serviceContext.TimeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverRequestStream.StreamTimeoutTimestamp);

        await requestStream.SendHeadersAsync(headers).DefaultTimeout();

        await requestStream.OnHeaderReceivedTask.DefaultTimeout();

        Http3Api.AdvanceTime(TimeSpan.FromTicks(1));

        await requestStream.SendDataAsync(Memory<byte>.Empty, endStream: true);

        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectReceiveEndOfStream();
    }

    [Fact]
    public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_StreamError_PendingStreamsEnabled()
    {
        Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = true;

        var timeProvider = _serviceContext.MockTimeProvider;
        var timestamp = timeProvider.GetTimestamp();
        var limits = _serviceContext.ServerOptions.Limits;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        var outboundControlStream = await Http3Api.CreateControlStream(id: null);

        await outboundControlStream.OnUnidentifiedStreamCreatedTask.DefaultTimeout();
        var serverInboundControlStream = Http3Api.Connection._unidentifiedStreams[outboundControlStream.StreamId];

        Http3Api.TriggerTick();
        Http3Api.AdvanceTime(limits.RequestHeadersTimeout);

        Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverInboundControlStream.StreamTimeoutTimestamp);

        Http3Api.AdvanceTime(TimeSpan.FromTicks(1));
    }

    [Fact]
    public async Task ControlStream_HeaderNotReceivedWithinRequestHeadersTimeout_StreamError()
    {
        Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = false;

        var timeProvider = _serviceContext.MockTimeProvider;
        var timestamp = timeProvider.GetTimestamp();
        Http3Api._timeoutControl.Initialize();
        var limits = _serviceContext.ServerOptions.Limits;
        var headers = new[]
        {
            new KeyValuePair<string, string>(InternalHeaderNames.Method, "Custom"),
            new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
            new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
            new KeyValuePair<string, string>(InternalHeaderNames.Authority, "localhost:80"),
        };

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        var outboundControlStream = await Http3Api.CreateControlStream(id: null);

        await outboundControlStream.OnStreamCreatedTask.DefaultTimeout();

        var serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId];

        Http3Api.TriggerTick();
        Http3Api.TriggerTick(limits.RequestHeadersTimeout);

        Assert.Equal(timeProvider.GetTimestamp(timestamp, limits.RequestHeadersTimeout), serverInboundControlStream.StreamTimeoutTimestamp);

        Http3Api.TriggerTick(TimeSpan.FromTicks(1));

        await outboundControlStream.WaitForStreamErrorAsync(
            Http3ErrorCode.StreamCreationError,
            AssertExpectedErrorMessages,
            CoreStrings.Http3ControlStreamHeaderTimeout);
    }

    [Fact]
    public async Task ControlStream_HeaderReceivedWithinRequestHeadersTimeout_StreamError()
    {
        var limits = _serviceContext.ServerOptions.Limits;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        Http3Api.TriggerTick();
        Http3Api.TriggerTick(limits.RequestHeadersTimeout + TimeSpan.FromTicks(1));

        var outboundControlStream = await Http3Api.CreateControlStream(id: 0);

        await outboundControlStream.OnStreamCreatedTask.DefaultTimeout();

        Http3Api.TriggerTick();
    }

    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task ControlStream_RequestHeadersTimeoutMaxValue_ExpirationIsMaxValue(bool pendingStreamEnabled)
    {
        Http3Api._serviceContext.ServerOptions.EnableWebTransportAndH3Datagrams = pendingStreamEnabled;

        var timeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;
        limits.RequestHeadersTimeout = TimeSpan.MaxValue;

        await Http3Api.InitializeConnectionAsync(_noopApplication).DefaultTimeout();

        var controlStream = await Http3Api.GetInboundControlStream().DefaultTimeout();
        await controlStream.ExpectSettingsAsync().DefaultTimeout();

        var outboundControlStream = await Http3Api.CreateControlStream(id: null);

        dynamic serverInboundControlStream;
        if (pendingStreamEnabled)
        {
            await outboundControlStream.OnUnidentifiedStreamCreatedTask.DefaultTimeout();
            serverInboundControlStream = Http3Api.Connection._unidentifiedStreams[outboundControlStream.StreamId];
        }
        else
        {
            await outboundControlStream.OnStreamCreatedTask.DefaultTimeout();
            serverInboundControlStream = Http3Api.Connection._streams[outboundControlStream.StreamId];
        }

        Http3Api.TriggerTick();

        Assert.Equal(TimeSpan.MaxValue.ToTicks(timeProvider), serverInboundControlStream.StreamTimeoutTimestamp);
    }

    [Fact]
    public async Task DATA_Received_TooSlowlyOnSmallRead_AbortsConnectionAfterGracePeriod()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        Http3Api._timeoutControl.Initialize();

        await Http3Api.InitializeConnectionAsync(_readRateApplication);

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        // _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period.
        var requestStream = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_helloWorldBytes.Length), endStream: false);
        await requestStream.SendDataAsync(_helloWorldBytes, endStream: false);

        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectDataAsync();

        // Don't send any more data and advance just to and then past the grace period.
        Http3Api.AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromTicks(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);

        await Http3Api.WaitForConnectionErrorAsync<ConnectionAbortedException>(
            ignoreNonGoAwayFrames: false,
            expectedLastStreamId: 4,
            Http3ErrorCode.InternalError,
            null);

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task ResponseDrain_SlowerThanMinimumDataRate_AbortsConnection()
    {
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        await Http3Api.InitializeConnectionAsync(_noopApplication);

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        var requestStream = await Http3Api.CreateRequestStream(new[]
        {
            new KeyValuePair<string, string>(InternalHeaderNames.Path, "/"),
            new KeyValuePair<string, string>(InternalHeaderNames.Scheme, "http"),
            new KeyValuePair<string, string>(InternalHeaderNames.Method, "GET"),
            new KeyValuePair<string, string>(InternalHeaderNames.Authority, "localhost:80"),
        }, null, true, new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously));

        await requestStream.OnDisposingTask.DefaultTimeout();

        Http3Api.TriggerTick();
        Assert.Null(requestStream.StreamContext._error);

        Http3Api.TriggerTick(TimeSpan.FromTicks(1));
        Assert.Null(requestStream.StreamContext._error);

        Http3Api.TriggerTick(limits.MinResponseDataRate.GracePeriod);

        requestStream.StartStreamDisposeTcs.TrySetResult();

        await Http3Api.WaitForConnectionErrorAsync<Http3ConnectionErrorException>(
            ignoreNonGoAwayFrames: false,
            expectedLastStreamId: 4,
            Http3ErrorCode.InternalError,
            matchExpectedErrorMessage: AssertExpectedErrorMessages,
            expectedErrorMessage: CoreStrings.ConnectionTimedBecauseResponseMininumDataRateNotSatisfied);

        Assert.Contains(TestSink.Writes, w => w.EventId.Name == "ResponseMinimumDataRateNotSatisfied");
    }

    private class EchoAppWithNotification
    {
        private readonly TaskCompletionSource _writeStartedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

        public Task WriteStartedTask => _writeStartedTcs.Task;

        public async Task RunApp(HttpContext context)
        {
            await context.Response.Body.FlushAsync();

            var buffer = new byte[16 * 1024];
            int received;

            while ((received = await context.Request.Body.ReadAsync(buffer, 0, buffer.Length)) > 0)
            {
                var writeTask = context.Response.Body.WriteAsync(buffer, 0, received);
                _writeStartedTcs.TrySetResult();

                await writeTask;
            }
        }
    }

    [Fact]
    public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnSmallWrite_AbortsConnectionAfterGracePeriod()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        // Disable response buffering so "socket" backpressure is observed immediately.
        limits.MaxResponseBufferSize = 0;

        Http3Api._timeoutControl.Initialize();

        var app = new EchoAppWithNotification();
        var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp, _browserRequestHeaders, endStream: false);
        await requestStream.SendDataAsync(_helloWorldBytes, endStream: true);

        await requestStream.ExpectHeadersAsync();

        await app.WriteStartedTask.DefaultTimeout();

        // Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
        Http3Api._timeoutControl.Tick(mockTimeProvider.GetTimestamp());

        // Don't read data frame to induce "socket" backpressure.
        Http3Api.AdvanceTime(TimeSpan.FromSeconds((requestStream.BytesReceived + _helloWorldBytes.Length) / limits.MinResponseDataRate.BytesPerSecond) +
            limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromSeconds(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);

        // The "hello, world" bytes are buffered from before the timeout, but not an END_STREAM data frame.
        var data = await requestStream.ExpectDataAsync();
        Assert.Equal(_helloWorldBytes.Length, data.Length);

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task DATA_Sent_TooSlowlyDueToSocketBackPressureOnLargeWrite_AbortsConnectionAfterRateTimeout()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinResponseDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        // Disable response buffering so "socket" backpressure is observed immediately.
        limits.MaxResponseBufferSize = 0;

        Http3Api._timeoutControl.Initialize();

        var app = new EchoAppWithNotification();
        var requestStream = await Http3Api.InitializeConnectionAndStreamsAsync(app.RunApp, _browserRequestHeaders, endStream: false);
        await requestStream.SendDataAsync(_maxData, endStream: true);

        await requestStream.ExpectHeadersAsync();

        await app.WriteStartedTask.DefaultTimeout();

        // Complete timing of the request body so we don't induce any unexpected request body rate timeouts.
        Http3Api._timeoutControl.Tick(mockTimeProvider.GetTimestamp());

        var timeToWriteMaxData = TimeSpan.FromSeconds((requestStream.BytesReceived + _maxData.Length) / limits.MinResponseDataRate.BytesPerSecond) +
            limits.MinResponseDataRate.GracePeriod + Heartbeat.Interval - TimeSpan.FromSeconds(.5);

        // Don't read data frame to induce "socket" backpressure.
        Http3Api.AdvanceTime(timeToWriteMaxData);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromSeconds(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.WriteDataRate), Times.Once);

        // The _maxData bytes are buffered from before the timeout, but not an END_STREAM data frame.
        await requestStream.ExpectDataAsync();

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task DATA_Received_TooSlowlyOnLargeRead_AbortsConnectionAfterRateTimeout()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        Http3Api._timeoutControl.Initialize();

        await Http3Api.InitializeConnectionAsync(_readRateApplication);

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
        var requestStream = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_maxData.Length), endStream: false);
        await requestStream.SendDataAsync(_maxData, endStream: false);

        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectDataAsync();

        // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
        // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
        var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5);

        // Don't send any more data and advance just to and then past the rate timeout.
        Http3Api.AdvanceTime(timeToReadMaxData);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromSeconds(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);

        await Http3Api.WaitForConnectionErrorAsync<ConnectionAbortedException>(
            ignoreNonGoAwayFrames: false,
            expectedLastStreamId: null,
            Http3ErrorCode.InternalError,
            null);

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task DATA_Received_TooSlowlyOnMultipleStreams_AbortsConnectionAfterAdditiveRateTimeout()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        Http3Api._timeoutControl.Initialize();

        await Http3Api.InitializeConnectionAsync(_readRateApplication);

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
        var requestStream1 = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_maxData.Length), endStream: false);
        await requestStream1.SendDataAsync(_maxData, endStream: false);

        await requestStream1.ExpectHeadersAsync();
        await requestStream1.ExpectDataAsync();

        var requestStream2 = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_maxData.Length), endStream: false);
        await requestStream2.SendDataAsync(_maxData, endStream: false);

        await requestStream2.ExpectHeadersAsync();
        await requestStream2.ExpectDataAsync();

        var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond);
        // Double the timeout for the second stream.
        timeToReadMaxData += timeToReadMaxData;

        // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
        // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
        timeToReadMaxData -= TimeSpan.FromSeconds(.5);

        // Don't send any more data and advance just to and then past the rate timeout.
        Http3Api.AdvanceTime(timeToReadMaxData);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromSeconds(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);

        await Http3Api.WaitForConnectionErrorAsync<ConnectionAbortedException>(
            ignoreNonGoAwayFrames: false,
            expectedLastStreamId: null,
            Http3ErrorCode.InternalError,
            null);

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task DATA_Received_TooSlowlyOnSecondStream_AbortsConnectionAfterNonAdditiveRateTimeout()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        Http3Api._timeoutControl.Initialize();

        await Http3Api.InitializeConnectionAsync(_readRateApplication);

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        Logger.LogInformation("Sending first request");

        // _maxData is 16 KiB, and 16 KiB / 240 bytes/sec ~= 68 secs which is far above the grace period.
        var requestStream1 = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_maxData.Length), endStream: false);
        await requestStream1.SendDataAsync(_maxData, endStream: true);

        await requestStream1.ExpectHeadersAsync();
        await requestStream1.ExpectDataAsync();

        await requestStream1.ExpectReceiveEndOfStream();

        Logger.LogInformation("Sending second request");
        var requestStream2 = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_maxData.Length), endStream: false);
        await requestStream2.SendDataAsync(_maxData, endStream: false);

        await requestStream2.ExpectHeadersAsync();
        await requestStream2.ExpectDataAsync();

        // Due to the imprecision of floating point math and the fact that TimeoutControl derives rate from elapsed
        // time for reads instead of vice versa like for writes, use a half-second instead of single-tick cushion.
        var timeToReadMaxData = TimeSpan.FromSeconds(_maxData.Length / limits.MinRequestBodyDataRate.BytesPerSecond) - TimeSpan.FromSeconds(.5);

        // Don't send any more data and advance just to and then past the rate timeout.
        Http3Api.AdvanceTime(timeToReadMaxData);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromSeconds(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(TimeoutReason.ReadDataRate), Times.Once);

        await Http3Api.WaitForConnectionErrorAsync<ConnectionAbortedException>(
            ignoreNonGoAwayFrames: false,
            expectedLastStreamId: null,
            Http3ErrorCode.InternalError,
            null);

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }

    [Fact]
    public async Task DATA_Received_SlowlyWhenRateLimitDisabledPerRequest_DoesNotAbortConnection()
    {
        var mockTimeProvider = _serviceContext.MockTimeProvider;
        var limits = _serviceContext.ServerOptions.Limits;

        // Use non-default value to ensure the min request and response rates aren't mixed up.
        limits.MinRequestBodyDataRate = new MinDataRate(480, TimeSpan.FromSeconds(2.5));

        Http3Api._timeoutControl.Initialize();

        await Http3Api.InitializeConnectionAsync(context =>
        {
            // Completely disable rate limiting for this stream.
            context.Features.Get<IHttpMinRequestBodyDataRateFeature>().MinDataRate = null;
            return _readRateApplication(context);
        });

        var inboundControlStream = await Http3Api.GetInboundControlStream();
        await inboundControlStream.ExpectSettingsAsync();

        Http3Api.OutboundControlStream = await Http3Api.CreateControlStream();

        // _helloWorldBytes is 12 bytes, and 12 bytes / 240 bytes/sec = .05 secs which is far below the grace period.
        var requestStream = await Http3Api.CreateRequestStream(ReadRateRequestHeaders(_helloWorldBytes.Length), endStream: false);
        await requestStream.SendDataAsync(_helloWorldBytes, endStream: false);

        await requestStream.ExpectHeadersAsync();

        await requestStream.ExpectDataAsync();

        // Don't send any more data and advance just to and then past the grace period.
        Http3Api.AdvanceTime(limits.MinRequestBodyDataRate.GracePeriod);

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        Http3Api.AdvanceTime(TimeSpan.FromTicks(1));

        _mockTimeoutHandler.Verify(h => h.OnTimeout(It.IsAny<TimeoutReason>()), Times.Never);

        await requestStream.SendDataAsync(_helloWorldBytes, endStream: true);

        await requestStream.ExpectReceiveEndOfStream();

        _mockTimeoutHandler.VerifyNoOtherCalls();
    }
}
