// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Http.Connections.Client;
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
using Microsoft.AspNetCore.SignalR.Tests;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Newtonsoft.Json;
using Xunit;

namespace Microsoft.AspNetCore.SignalR.Client.Tests
{
    public partial class HttpConnectionTests
    {
        public class Negotiate
        {
            [Theory]
            [InlineData("")]
            [InlineData("Not Json")]
            public Task StartThrowsFormatExceptionIfNegotiationResponseIsInvalid(string negotiatePayload)
            {
                return RunInvalidNegotiateResponseTest<InvalidDataException>(negotiatePayload, "Invalid negotiation response received.");
            }

            [Fact]
            public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId()
            {
                return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
            }

            [Fact]
            public Task NegotiateResponseWithNegotiateVersionRequiresConnectionToken()
            {
                return RunInvalidNegotiateResponseTest<InvalidDataException>(ResponseUtils.CreateNegotiationContent(negotiateVersion: 1, connectionToken: null), "Invalid negotiation response received.");
            }

            [Fact]
            public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
            {
                return RunInvalidNegotiateResponseTest<AggregateException>(ResponseUtils.CreateNegotiationContent(transportTypes: HttpTransportType.ServerSentEvents),
                    "Unable to connect to the server with any of the available transports. (ServerSentEvents failed: The transport is disabled by the client.)");
            }

            [Fact]
            public Task ConnectionCannotBeStartedIfNoTransportProvidedByServer()
            {
                return RunInvalidNegotiateResponseTest<NoTransportSupportedException>(ResponseUtils.CreateNegotiationContent(transportTypes: HttpTransportType.None), "None of the transports supported by the client are supported by the server.");
            }

            [Theory]
            [InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
            [InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
            [InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
            [InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
            [InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
            [InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0&negotiateVersion=1")]
            public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);

                var negotiateUrlTcs = new TaskCompletionSource<string>();
                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnLongPollDelete(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    negotiateUrlTcs.TrySetResult(request.RequestUri.ToString());
                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                        ResponseUtils.CreateNegotiationContent());
                });

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, url: requestedUrl, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                        });
                }

                Assert.Equal(expectedNegotiate, await negotiateUrlTcs.Task.OrTimeout());
            }

            [Fact]
            public async Task NegotiateReturnedConnectionIdIsSetOnConnection()
            {
                string connectionId = null;

                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
                    JsonConvert.SerializeObject(new
                    {
                        connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                        availableTransports = new object[]
                        {
                            new
                            {
                                transport = "LongPolling",
                                transferFormats = new[] { "Text" }
                            },
                        }
                    })));
                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                            connectionId = connection.ConnectionId;
                        });
                }

                Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
            }

            [Fact]
            public async Task NegotiateCanHaveNewFields()
            {
                string connectionId = null;

                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
                    JsonConvert.SerializeObject(new
                    {
                        connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                        availableTransports = new object[]
                        {
                            new
                            {
                                transport = "LongPolling",
                                transferFormats = new[] { "Text" }
                            },
                        },
                        newField = "ignore this",
                    })));
                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                            connectionId = connection.ConnectionId;
                        });
                }

                Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
            }

            [Fact]
            public async Task ConnectionIdGetsSetWithNegotiateProtocolGreaterThanZero()
            {
                string connectionId = null;

                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
                    JsonConvert.SerializeObject(new
                    {
                        connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                        negotiateVersion = 1,
                        connectionToken = "different-id",
                        availableTransports = new object[]
                        {
                            new
                            {
                                transport = "LongPolling",
                                transferFormats = new[] { "Text" }
                            },
                        },
                        newField = "ignore this",
                    })));
                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                            connectionId = connection.ConnectionId;
                        });
                }

                Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
                Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
                Assert.Equal("http://fakeuri.org/?id=different-id", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
            }

            [Fact]
            public async Task ConnectionTokenFieldIsIgnoredForNegotiateIdLessThanOne()
            {
                string connectionId = null;

                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
                    JsonConvert.SerializeObject(new
                    {
                        connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                        connectionToken = "different-id",
                        availableTransports = new object[]
                        {
                            new
                            {
                                transport = "LongPolling",
                                transferFormats = new[] { "Text" }
                            },
                        },
                        newField = "ignore this",
                    })));
                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                            connectionId = connection.ConnectionId;
                        });
                }

                Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
                Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
                Assert.Equal("http://fakeuri.org/?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
            }

            [Fact]
            public async Task NegotiateThatReturnsUrlGetFollowed()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                var firstNegotiate = true;
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    if (firstNegotiate)
                    {
                        firstNegotiate = false;
                        return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                            JsonConvert.SerializeObject(new
                            {
                                url = "https://another.domain.url/chat"
                            }));
                    }

                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                        JsonConvert.SerializeObject(new
                        {
                            connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                            availableTransports = new object[]
                            {
                                new
                                {
                                    transport = "LongPolling",
                                    transferFormats = new[] { "Text" }
                                },
                            }
                        }));
                });

                testHttpHandler.OnLongPoll((token) =>
                {
                    var tcs = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);

                    token.Register(() => tcs.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)));

                    return tcs.Task;
                });

                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                        });
                }

                Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
                Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
            }

            [Fact]
            public async Task NegotiateThatReturnsRedirectUrlForeverThrowsAfter100Tries()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                            JsonConvert.SerializeObject(new
                            {
                                url = "https://another.domain.url/chat"
                            }));
                });

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => connection.StartAsync().OrTimeout());
                            Assert.Equal("Negotiate redirection limit exceeded.", exception.Message);
                        });
                }
            }

            [Fact]
            public async Task NegotiateThatReturnsUrlGetFollowedWithAccessToken()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                var firstNegotiate = true;
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    if (firstNegotiate)
                    {
                        firstNegotiate = false;

                        // The first negotiate requires an access token
                        if (request.Headers.Authorization?.Parameter != "firstSecret")
                        {
                            return ResponseUtils.CreateResponse(HttpStatusCode.Unauthorized);
                        }

                        return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                            JsonConvert.SerializeObject(new
                            {
                                url = "https://another.domain.url/chat",
                                accessToken = "secondSecret"
                            }));
                    }

                    // All other requests require an access token
                    if (request.Headers.Authorization?.Parameter != "secondSecret")
                    {
                        return ResponseUtils.CreateResponse(HttpStatusCode.Unauthorized);
                    }

                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                        JsonConvert.SerializeObject(new
                        {
                            connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                            availableTransports = new object[]
                            {
                                new
                                {
                                    transport = "LongPolling",
                                    transferFormats = new[] { "Text" }
                                },
                            }
                        }));
                });

                testHttpHandler.OnLongPoll((request, token) =>
                {
                    // All other requests require an access token
                    if (request.Headers.Authorization?.Parameter != "secondSecret")
                    {
                        return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Unauthorized));
                    }
                    var tcs = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);

                    token.Register(() => tcs.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)));

                    return tcs.Task;
                });

                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                Task<string> AccessTokenProvider() => Task.FromResult<string>("firstSecret");

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory, accessTokenProvider: AccessTokenProvider),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                        });
                }

                Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
                // Delete request
                Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
            }

            [Fact]
            public async Task NegotiateThatReturnsRedirectUrlDoesNotAddAnotherNegotiateVersionQueryString()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                var negotiateCount = 0;
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    negotiateCount++;
                    if (negotiateCount == 1)
                    {
                        return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                                JsonConvert.SerializeObject(new
                                {
                                    url = "https://another.domain.url/chat?negotiateVersion=1"
                                }));
                    }
                    else
                    {
                        return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                                JsonConvert.SerializeObject(new
                                {
                                    connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
                                    availableTransports = new object[]
                                    {
                                        new
                                        {
                                            transport = "LongPolling",
                                            transferFormats = new[] { "Text" }
                                        },
                                    }
                                }));
                    }
                });

                testHttpHandler.OnLongPoll((token) =>
                {
                    var tcs = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);

                    token.Register(() => tcs.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)));

                    return tcs.Task;
                });

                testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                        });
                }

                Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
                Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
                Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
            }

            [Fact]
            public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);

                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                        JsonConvert.SerializeObject(new
                        {
                            connectionId = "00000000-0000-0000-0000-000000000000",
                            availableTransports = new object[]
                            {
                                new
                                {
                                    transport = "QuantumEntanglement",
                                    transferFormats = new[] { "Qbits" },
                                },
                                new
                                {
                                    transport = "CarrierPigeon",
                                    transferFormats = new[] { "Text" },
                                },
                                new
                                {
                                    transport = "LongPolling",
                                    transferFormats = new[] { "Text", "Binary" }
                                },
                            }
                        }));
                });

                var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);

                transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling))
                    .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));

                using (var noErrorScope = new VerifyNoErrorsScope())
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, transportFactory: transportFactory.Object, loggerFactory: noErrorScope.LoggerFactory, transferFormat: TransferFormat.Binary),
                        async (connection) =>
                        {
                            await connection.StartAsync().OrTimeout();
                        });
                }
            }

            [Fact]
            public async Task StartSkipsOverTransportsThatDoNotSupportTheRequredTransferFormat()
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);

                testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                        JsonConvert.SerializeObject(new
                        {
                            connectionId = "00000000-0000-0000-0000-000000000000",
                            availableTransports = new object[]
                            {
                                new
                                {
                                    transport = "WebSockets",
                                    transferFormats = new[] { "Qbits" },
                                },
                                new
                                {
                                    transport = "ServerSentEvents",
                                    transferFormats = new[] { "Text" },
                                },
                                new
                                {
                                    transport = "LongPolling",
                                    transferFormats = new[] { "Text", "Binary" }
                                },
                            }
                        }));
                });

                var transportFactory = new Mock<ITransportFactory>(MockBehavior.Strict);

                transportFactory.Setup(t => t.CreateTransport(HttpTransportType.LongPolling))
                    .Returns(new TestTransport(transferFormat: TransferFormat.Text | TransferFormat.Binary));

                await WithConnectionAsync(
                    CreateConnection(testHttpHandler, transportFactory: transportFactory.Object, transferFormat: TransferFormat.Binary),
                    async (connection) =>
                    {
                        await connection.StartAsync().OrTimeout();
                    });
            }

            [Fact]
            public async Task NegotiateThatReturnsErrorThrowsFromStart()
            {
                bool ExpectedError(WriteContext writeContext)
                {
                    return writeContext.LoggerName == typeof(HttpConnection).FullName &&
                        writeContext.EventId.Name == "ErrorWithNegotiation";
                }

                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
                testHttpHandler.OnNegotiate((request, cancellationToken) =>
                {
                    return ResponseUtils.CreateResponse(HttpStatusCode.OK,
                            JsonConvert.SerializeObject(new
                            {
                                error = "Test error."
                            }));
                });

                using (var noErrorScope = new VerifyNoErrorsScope(expectedErrorsFilter: ExpectedError))
                {
                    await WithConnectionAsync(
                        CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
                        async (connection) =>
                        {
                            var exception = await Assert.ThrowsAsync<Exception>(() => connection.StartAsync().OrTimeout());
                            Assert.Equal("Test error.", exception.Message);
                        });
                }
            }

            private async Task RunInvalidNegotiateResponseTest<TException>(string negotiatePayload, string expectedExceptionMessage) where TException : Exception
            {
                var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);

                testHttpHandler.OnNegotiate((_, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK, negotiatePayload));

                await WithConnectionAsync(
                    CreateConnection(testHttpHandler),
                    async (connection) =>
                    {
                        var exception = await Assert.ThrowsAsync<TException>(
                            () => connection.StartAsync().OrTimeout());

                        Assert.Equal(expectedExceptionMessage, exception.Message);
                    });
            }
        }
    }
}
