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

using System.Collections.Immutable;
using System.IO.Pipes;
using System.Runtime.Serialization;
using System.Text;

namespace Microsoft.VisualBasic.ApplicationServices.Tests;

public class SingleInstanceTests
{
    private const int SendTimeout = 10000;

    private sealed class ReceivedArgs
    {
        private List<string[]> _received = [];

        internal void Add(string[] args)
        {
            _received.Add(args);
        }

        internal ImmutableArray<string[]> Freeze()
        {
            var received = _received;
            Interlocked.CompareExchange(ref _received, null, received);
            return [.. received];
        }
    }

    private readonly dynamic _testAccessor = GetTestHelper();

    private static dynamic GetTestHelper()
    {
        var assembly = typeof(WindowsFormsApplicationBase).Assembly;
        var type = assembly.GetType("Microsoft.VisualBasic.ApplicationServices.SingleInstanceHelpers");
        return type.TestAccessor.Dynamic;
    }

    private bool TryCreatePipeServer(string pipeName, out NamedPipeServerStream pipeServer)
    {
        return _testAccessor.TryCreatePipeServer(pipeName, out pipeServer);
    }

    private Task WaitForClientConnectionsAsync(NamedPipeServerStream pipeServer, Action<string[]> callback, CancellationToken cancellationToken = default)
    {
        return _testAccessor.WaitForClientConnectionsAsync(pipeServer, callback, cancellationToken);
    }

    private Task SendSecondInstanceArgsAsync(string pipeName, string[] args, CancellationToken cancellationToken)
    {
        return _testAccessor.SendSecondInstanceArgsAsync(pipeName, args, cancellationToken);
    }

    private bool SendSecondInstanceArgs(string pipeName, int timeout, string[] args)
    {
        CancellationTokenSource tokenSource = new();
        tokenSource.CancelAfter(timeout);
        try
        {
            var awaitable = SendSecondInstanceArgsAsync(pipeName, args, tokenSource.Token).ConfigureAwait(false);
            awaitable.GetAwaiter().GetResult();
        }
        catch (Exception)
        {
            return false;
        }

        return true;
    }

    private static string GetUniqueName() => Guid.NewGuid().ToString();

    [Fact]
    public void MultipleDistinctServers()
    {
        List<NamedPipeServerStream> pipeServers = [];
        int n = 5;
        try
        {
            for (int i = 0; i < n; i++)
            {
                Assert.True(TryCreatePipeServer(GetUniqueName(), out var pipeServer));
                Assert.NotNull(pipeServer);
                pipeServers.Add(pipeServer);
            }
        }
        finally
        {
            foreach (var pipeServer in pipeServers)
            {
                pipeServer.Dispose();
            }
        }
    }

    [Fact]
    public async Task MultipleServers_Overlapping()
    {
        string pipeName = GetUniqueName();
        const int n = 10;
        int completed = 0;
        int created = 0;
        var tasks = Enumerable.Range(0, n).Select(i => Task.Factory.StartNew(() =>
        {
            Thread.Sleep(100);
            if (TryCreatePipeServer(pipeName, out var pipeServer))
            {
                Interlocked.Increment(ref created);
            }

            using (pipeServer)
            {
                Thread.Sleep(10);
            }

            Interlocked.Increment(ref completed);
        }, cancellationToken: default, creationOptions: default, scheduler: TaskScheduler.Default)).ToArray();
        await Task.WhenAll(tasks);
        Assert.Equal(n, completed);
        Assert.True(created >= 1);
    }

    [Fact]
    public void MultipleClients_Sequential()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            const int n = 5;
            string[][] sentArgs = Enumerable.Range(0, n).Select(i => Enumerable.Range(0, i).Select(i => i.ToString()).ToArray()).ToArray();
            ReceivedArgs receivedArgs = new();
            WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);
            for (int i = 0; i < n; i++)
            {
                Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, sentArgs[i]));
            }

            FlushLastConnection(pipeName);
            Assert.Equal(sentArgs, receivedArgs.Freeze());
        }
    }

    [Fact]
    public async Task MultipleClients_Overlapping()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            const int n = 5;
            string[][] sentArgs = Enumerable.Range(0, n).Select(i => Enumerable.Range(0, i).Select(i => i.ToString()).ToArray()).ToArray();
            ReceivedArgs receivedArgs = new();
            _ = WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);
            var tasks = Enumerable.Range(0, n).Select(i => Task.Factory.StartNew(() => { Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, sentArgs[i])); }, cancellationToken: default, creationOptions: default, scheduler: TaskScheduler.Default)).ToArray();
            await Task.WhenAll(tasks);
            FlushLastConnection(pipeName);
            var receivedSorted = receivedArgs.Freeze().Sort((x, y) => x.Length - y.Length);
            Assert.Equal(sentArgs, receivedSorted);
        }
    }

    // Message that exceeds the buffer size in SingleInstanceHelpers.ReadArgsAsync.
    [Fact]
    public void ManyArgs()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            string[] expectedArgs = getStrings(20000).ToArray();
            ReceivedArgs receivedArgs = new();
            WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);
            Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, expectedArgs));
            FlushLastConnection(pipeName);
            string[] actualArgs = receivedArgs.Freeze().Single();
            Assert.Equal(expectedArgs, actualArgs);
        }

        static IEnumerable<string> getStrings(int maxTotalLength)
        {
            Random r = new();
            int n = 0;
            while (n < maxTotalLength)
            {
                string str = getString(r);
                n += str.Length;
                yield return str;
            }
        }

        static string getString(Random r)
        {
            int n = r.Next(1000);
            StringBuilder builder = new();
            for (int i = 0; i < n; i++)
            {
                builder.Append((char)('a' + r.Next(26)));
            }

            return builder.ToString();
        }
    }

    [Fact]
    public async Task ClientConnectionTimeout()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            var task = Task.Factory.StartNew(() => SendSecondInstanceArgs(pipeName, timeout: 300, []), cancellationToken: TestContext.Current.CancellationToken, creationOptions: default, scheduler: TaskScheduler.Default);
            bool result = await task;
            Assert.False(result);
        }
    }

    // Corresponds to second instance crash sending incomplete args.
    [Fact]
    public async Task ClientConnectBeforeWaitForClientConnection()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            ReceivedArgs receivedArgs = new();
            var task = Task.Factory.StartNew(() => SendSecondInstanceArgs(pipeName, SendTimeout, ["1", "ABC"]), cancellationToken: TestContext.Current.CancellationToken, creationOptions: default, scheduler: TaskScheduler.Default);
            // Allow time for connection.
            Thread.Sleep(100);
            _ = WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);
            await task;
            FlushLastConnection(pipeName);
            Assert.Equal(new[] { new[] { "1", "ABC" } }, receivedArgs.Freeze());
        }
    }

    // Send data other than string[].
    [Fact]
    public void InvalidClientData()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            ReceivedArgs receivedArgs = new();
            WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);

            sendData(pipeName, Array.Empty<string>()); // valid
            sendData(pipeName, 3); // invalid
            sendData(pipeName, new[] { "ABC" }); // valid
            sendData(pipeName, new int[] { 1, 2, 3 }); // invalid
            sendData(pipeName, new[] { "", "" }); // valid
            sendData(pipeName, new char[] { '1', '2', '3' }); // invalid

            FlushLastConnection(pipeName);

            Assert.Equal(new[] { (string[])[], ["ABC"], ["", ""] }, receivedArgs.Freeze());
        }

        static void sendData<T>(string pipeName, T obj)
        {
            var pipeClient = CreateClientConnection(pipeName, SendTimeout);
            if (pipeClient is null)
            {
                return;
            }

            using (pipeClient)
            {
                DataContractSerializer serializer = new(typeof(T));
                serializer.WriteObject(pipeClient, obj);
            }
        }
    }

    // Corresponds to second instance crash sending incomplete args.
    [Fact]
    public void CloseClientAfterClientConnect()
    {
        string pipeName = GetUniqueName();
        Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
        using (pipeServer)
        {
            ReceivedArgs receivedArgs = new();
            WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);

            // Send valid args.
            Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, []));
            Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, ["1", "ABC"]));

            // Send bad args: close client after connect.
            closeAfterConnect(pipeName);

            // Send invalid args.
            sendUnexpectedArgs(pipeName);

            // Send valid args.
            Assert.True(SendSecondInstanceArgs(pipeName, SendTimeout, ["DEF", "2"]));

            FlushLastConnection(pipeName);

            Assert.Equal(new[] { (string[])[], ["1", "ABC"], ["DEF", "2"] }, receivedArgs.Freeze());
        }

        static void closeAfterConnect(string pipeName)
        {
            using var pipeClient = CreateClientConnection(pipeName, SendTimeout);
        }

        static void sendUnexpectedArgs(string pipeName)
        {
            var pipeClient = CreateClientConnection(pipeName, SendTimeout);
            if (pipeClient is null)
            {
                return;
            }

            using (pipeClient)
            {
                pipeClient.Write([1, 2, 3], 0, 3);
            }
        }
    }

    // Corresponds to first instance closing while second instance is sending args.
    [Fact]
    public void CloseServerAfterClientConnect()
    {
        string pipeName = GetUniqueName();
        NamedPipeClientStream pipeClient = null;
        try
        {
            ReceivedArgs receivedArgs = new();
            Assert.True(TryCreatePipeServer(pipeName, out var pipeServer));
            using (pipeServer)
            {
                WaitForClientConnectionsAsync(pipeServer, receivedArgs.Add, TestContext.Current.CancellationToken);
                pipeClient = CreateClientConnection(pipeName, SendTimeout);
            }

            Thread.Sleep(500);
            Assert.Empty(receivedArgs.Freeze());
        }
        finally
        {
            pipeClient?.Dispose();
        }
    }

    // Ensure the previous client connection has been consumed completely by the
    // server by creating an extra client connection that closes after connecting.
    private static void FlushLastConnection(string pipeName)
    {
        using var _ = CreateClientConnection(pipeName, SendTimeout);
    }

    private static NamedPipeClientStream CreateClientConnection(string pipeName, int timeout)
    {
        NamedPipeClientStream pipeClient = new(".", pipeName, PipeDirection.Out);
        try
        {
            pipeClient.Connect(timeout);
        }
        catch (TimeoutException)
        {
            pipeClient.Dispose();
            return null;
        }

        return pipeClient;
    }
}
