// 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.Buffers;
using System.Net;
using System.Net.WebSockets;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Connections.Client;
using Microsoft.AspNetCore.Http.Connections.Client.Internal;
using Microsoft.AspNetCore.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;

namespace Microsoft.AspNetCore.SignalR.Tests;

[Collection(EndToEndTestsCollection.Name)]
public class WebSocketsTransportTests : FunctionalTestBase
{
    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task HttpOptionsSetOntoWebSocketOptions()
    {
        ClientWebSocketOptions webSocketsOptions = null;

        var httpOptions = new HttpConnectionOptions();
        httpOptions.Cookies.Add(new Cookie("Name", "Value", string.Empty, "fakeuri.org"));
        var clientCertificate = new X509Certificate(Array.Empty<byte>());
        httpOptions.ClientCertificates.Add(clientCertificate);
        httpOptions.UseDefaultCredentials = false;
        httpOptions.Credentials = Mock.Of<ICredentials>();
        httpOptions.Proxy = Mock.Of<IWebProxy>();
        httpOptions.WebSocketConfiguration = options => webSocketsOptions = options;

        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: httpOptions, loggerFactory: null, accessTokenProvider: null, httpClient: null);
            Assert.NotNull(webSocketsTransport);

            // we need to open a connection so it would apply httpOptions to webSocketOptions
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echo"),
                TransferFormat.Binary).DefaultTimeout();
            await webSocketsTransport.StopAsync().DefaultTimeout();
        }

        Assert.NotNull(webSocketsOptions);
        Assert.Equal(1, webSocketsOptions.Cookies.Count);
        Assert.Single(webSocketsOptions.ClientCertificates);
        Assert.Same(clientCertificate, webSocketsOptions.ClientCertificates[0]);
        Assert.False(webSocketsOptions.UseDefaultCredentials);
        Assert.Same(httpOptions.Proxy, webSocketsOptions.Proxy);
        Assert.Same(httpOptions.Credentials, webSocketsOptions.Credentials);
    }

    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task HttpOptionsWebSocketFactoryIsUsed()
    {
        var httpOptions = new HttpConnectionOptions();
        var webSocketMock = new Mock<WebSocket>();
        bool factoryWasUsed = false;

        // we emulate that connection is closed right away after it was established
        webSocketMock.Setup(socket => socket.CloseStatus).Returns(WebSocketCloseStatus.NormalClosure);
        webSocketMock.Setup(socket => socket.ReceiveAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new ValueWebSocketReceiveResult(0, WebSocketMessageType.Close, true));

        httpOptions.WebSocketFactory = (context, token) =>
        {
            factoryWasUsed = true;
            return ValueTask.FromResult(webSocketMock.Object);
        };

        var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: httpOptions, loggerFactory: null, accessTokenProvider: null, httpClient: null);
        await webSocketsTransport.StartAsync(new Uri("http://FakeEndpot.com/echo"), TransferFormat.Binary).DefaultTimeout();
        await webSocketsTransport.StopAsync().DefaultTimeout();

        webSocketMock.Verify((socket) => socket.ReceiveAsync(It.IsAny<Memory<byte>>(), It.IsAny<CancellationToken>()), Times.Once());
        Assert.True(factoryWasUsed);
    }

    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task WebSocketsTransportStopsSendAndReceiveLoopsWhenTransportIsStopped()
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echo"),
                TransferFormat.Binary).DefaultTimeout();
            await webSocketsTransport.StopAsync().DefaultTimeout();
            await webSocketsTransport.Running.DefaultTimeout();
        }
    }

    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task WebSocketsTransportSendsUserAgent()
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/httpheader"),
                TransferFormat.Binary).DefaultTimeout();

            await webSocketsTransport.Output.WriteAsync(Encoding.UTF8.GetBytes("User-Agent"));

            // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport
            await webSocketsTransport.Running.DefaultTimeout();

            Assert.True(webSocketsTransport.Input.TryRead(out var result));

            var userAgent = Encoding.UTF8.GetString(result.Buffer.ToArray());

            // user agent version should come from version embedded in assembly metadata
            var assemblyVersion = typeof(Constants)
                .Assembly
                .GetCustomAttribute<AssemblyInformationalVersionAttribute>();

            var majorVersion = typeof(HttpConnection).Assembly.GetName().Version.Major;
            var minorVersion = typeof(HttpConnection).Assembly.GetName().Version.Minor;

            Assert.StartsWith($"Microsoft SignalR/{majorVersion}.{minorVersion} ({assemblyVersion.InformationalVersion}; ", userAgent);
        }
    }

    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task WebSocketsTransportSendsXRequestedWithHeader()
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/httpheader"),
                TransferFormat.Binary).DefaultTimeout();

            await webSocketsTransport.Output.WriteAsync(Encoding.UTF8.GetBytes(HeaderNames.XRequestedWith));

            // The HTTP header endpoint closes the connection immediately after sending response which should stop the transport
            await webSocketsTransport.Running.DefaultTimeout();

            Assert.True(webSocketsTransport.Input.TryRead(out var result));

            var headerValue = Encoding.UTF8.GetString(result.Buffer.ToArray());

            Assert.Equal("XMLHttpRequest", headerValue);
        }
    }

    [ConditionalFact]
    [WebSocketsSupportedCondition]
    public async Task WebSocketsTransportStopsWhenConnectionChannelClosed()
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echo"),
                TransferFormat.Binary);
            webSocketsTransport.Output.Complete();
            await webSocketsTransport.Running.DefaultTimeout(TimeSpan.FromSeconds(10));
        }
    }

    [ConditionalTheory]
    [WebSocketsSupportedCondition]
    [InlineData(TransferFormat.Text)]
    [InlineData(TransferFormat.Binary)]
    public async Task WebSocketsTransportStopsWhenConnectionClosedByTheServer(TransferFormat transferFormat)
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);
            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echoAndClose"), transferFormat);

            await webSocketsTransport.Output.WriteAsync(new byte[] { 0x42 });

            // The echoAndClose endpoint closes the connection immediately after sending response which should stop the transport
            await webSocketsTransport.Running.DefaultTimeout();

            Assert.True(webSocketsTransport.Input.TryRead(out var result));
            Assert.Equal(new byte[] { 0x42 }, result.Buffer.ToArray());
            webSocketsTransport.Input.AdvanceTo(result.Buffer.End);
        }
    }

    [ConditionalTheory]
    [WebSocketsSupportedCondition]
    [InlineData(TransferFormat.Text)]
    [InlineData(TransferFormat.Binary)]
    public async Task WebSocketsTransportSetsTransferFormat(TransferFormat transferFormat)
    {
        await using (var server = await StartServer<Startup>())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, loggerFactory: LoggerFactory, accessTokenProvider: null, httpClient: null);

            await webSocketsTransport.StartAsync(new Uri(server.WebSocketsUrl + "/echo"),
                transferFormat).DefaultTimeout();

            await webSocketsTransport.StopAsync().DefaultTimeout();
            await webSocketsTransport.Running.DefaultTimeout();
        }
    }

    [ConditionalTheory]
    [InlineData(TransferFormat.Text | TransferFormat.Binary)] // Multiple values not allowed
    [InlineData((TransferFormat)42)] // Unexpected value
    [WebSocketsSupportedCondition]
    public async Task WebSocketsTransportThrowsForInvalidTransferFormat(TransferFormat transferFormat)
    {
        using (StartVerifiableLog())
        {
            var webSocketsTransport = new WebSocketsTransport(httpConnectionOptions: null, LoggerFactory, accessTokenProvider: null, httpClient: null);
            var exception = await Assert.ThrowsAsync<ArgumentException>(() =>
                webSocketsTransport.StartAsync(new Uri("http://fakeuri.org"), transferFormat));

            Assert.Contains($"The '{transferFormat}' transfer format is not supported by this transport.", exception.Message);
            Assert.Equal("transferFormat", exception.ParamName);
        }
    }
}
