using System;
using System.Diagnostics;
using System.Threading;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Renci.SshNet.Tests.Classes.Common
{
    [TestClass]
    public class CountdownEventTest
    {
        private Random _random;

        [TestInitialize]
        public void Init()
        {
            _random = new Random();
        }

        [TestMethod]
        public void Ctor_InitialCountGreatherThanZero()
        {
            var initialCount = _random.Next(1, 500);

            var countdownEvent = CreateCountdownEvent(initialCount);
            Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
            Assert.IsFalse(countdownEvent.IsSet);
            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Ctor_InitialCountZero()
        {
            const int initialCount = 0;

            var countdownEvent = CreateCountdownEvent(0);
            Assert.AreEqual(initialCount, countdownEvent.CurrentCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Signal_CurrentCountGreatherThanOne()
        {
            var initialCount = _random.Next(2, 1000);

            var countdownEvent = CreateCountdownEvent(initialCount);
            Assert.IsFalse(countdownEvent.Signal());
            Assert.AreEqual(--initialCount, countdownEvent.CurrentCount);
            Assert.IsFalse(countdownEvent.IsSet);
            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));
            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Signal_CurrentCountOne()
        {
            var countdownEvent = CreateCountdownEvent(1);
            Assert.IsTrue(countdownEvent.Signal());
            Assert.AreEqual(0, countdownEvent.CurrentCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Signal_CurrentCountZero()
        {
            var countdownEvent = CreateCountdownEvent(0);

            try
            {
                _ = countdownEvent.Signal();
                Assert.Fail();
            }
            catch (InvalidOperationException)
            {
                // Invalid attempt made to decrement the event's count below zero
            }
            finally
            {
                countdownEvent.Dispose();
            }
        }

        [TestMethod]
        public void Wait_TimeoutInfinite_ShouldBlockUntilCountdownEventIsSet()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = Session.InfiniteTimeSpan;

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = Interlocked.Increment(ref signalCount);
                        _ = countdownEvent.Signal();
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.Wait(timeout);
            watch.Stop();

            Assert.IsTrue(actual);
            Assert.AreEqual(expectedSignalCount, signalCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            Assert.IsTrue(watch.Elapsed >= sleep);
            Assert.IsTrue(watch.Elapsed <= sleep.Add(TimeSpan.FromMilliseconds(100)));

            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Wait_ShouldReturnTrueWhenCountdownEventIsSetBeforeTimeoutExpires()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = sleep.Add(TimeSpan.FromSeconds(2));

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = Interlocked.Increment(ref signalCount);
                        _ = countdownEvent.Signal();
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.Wait(timeout);
            watch.Stop();

            Assert.IsTrue(actual);
            Assert.AreEqual(expectedSignalCount, signalCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            Assert.IsTrue(watch.Elapsed >= sleep);
            Assert.IsTrue(watch.Elapsed <= timeout);

            countdownEvent.Dispose();
        }

        [TestMethod]
        public void Wait_ShouldReturnFalseWhenTimeoutExpiresBeforeCountdownEventIsSet()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = TimeSpan.FromMilliseconds(30);

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = countdownEvent.Signal();
                        _ = Interlocked.Increment(ref signalCount);
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.Wait(timeout);
            watch.Stop();

            Assert.IsFalse(actual);
            Assert.IsFalse(countdownEvent.IsSet);
            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));

            _ = countdownEvent.Wait(Session.InfiniteTimeSpan);
            countdownEvent.Dispose();
        }

        [TestMethod]
        public void WaitHandle_ShouldAlwaysReturnSameInstance()
        {
            var countdownEvent = CreateCountdownEvent(1);

            var waitHandleA = countdownEvent.WaitHandle;
            Assert.IsNotNull(waitHandleA);

            var waitHandleB = countdownEvent.WaitHandle;
            Assert.AreSame(waitHandleA, waitHandleB);
        }

        [TestMethod]
        public void WaitHandle_WaitOne_TimeoutInfinite_ShouldBlockUntilCountdownEventIsSet()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = Session.InfiniteTimeSpan;

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = Interlocked.Increment(ref signalCount);
                        _ = countdownEvent.Signal();
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.WaitHandle.WaitOne(timeout);
            watch.Stop();

            Assert.IsTrue(actual);
            Assert.AreEqual(expectedSignalCount, signalCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            Assert.IsTrue(watch.Elapsed >= sleep);
            Assert.IsTrue(watch.Elapsed <= sleep.Add(TimeSpan.FromMilliseconds(100)));

            countdownEvent.Dispose();
        }

        [TestMethod]
        public void WaitHandle_WaitOne_ShouldReturnTrueWhenCountdownEventIsSetBeforeTimeoutExpires()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = sleep.Add(TimeSpan.FromSeconds(2));

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = Interlocked.Increment(ref signalCount);
                        _ = countdownEvent.Signal();
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.Wait(timeout);
            watch.Stop();

            Assert.IsTrue(actual);
            Assert.AreEqual(expectedSignalCount, signalCount);
            Assert.IsTrue(countdownEvent.IsSet);
            Assert.IsTrue(countdownEvent.WaitHandle.WaitOne(0));
            Assert.IsTrue(watch.Elapsed >= sleep);
            Assert.IsTrue(watch.Elapsed <= timeout);

            countdownEvent.Dispose();
        }

        [TestMethod]
        public void WaitHandle_WaitOne_ShouldReturnFalseWhenTimeoutExpiresBeforeCountdownEventIsSet()
        {
            var sleep = TimeSpan.FromMilliseconds(100);
            var timeout = TimeSpan.FromMilliseconds(30);

            var countdownEvent = CreateCountdownEvent(1);
            var signalCount = 0;
            var expectedSignalCount = _random.Next(5, 20);

            for (var i = 0; i < (expectedSignalCount - 1); i++)
            {
                countdownEvent.AddCount();
            }

            var threads = new Thread[expectedSignalCount];
            for (var i = 0; i < expectedSignalCount; i++)
            {
                threads[i] = new Thread(() =>
                    {
                        Thread.Sleep(sleep);
                        _ = countdownEvent.Signal();
                        _ = Interlocked.Increment(ref signalCount);
                    });
                threads[i].Start();
            }

            var watch = new Stopwatch();
            watch.Start();
            var actual = countdownEvent.WaitHandle.WaitOne(timeout);
            watch.Stop();

            Assert.IsFalse(actual);
            Assert.IsFalse(countdownEvent.IsSet);
            Assert.IsFalse(countdownEvent.WaitHandle.WaitOne(0));

            _ = countdownEvent.Wait(Session.InfiniteTimeSpan);
            countdownEvent.Dispose();
        }

        private static CountdownEvent CreateCountdownEvent(int initialCount)
        {
            return new CountdownEvent(initialCount);
        }
    }
}
