using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace NitroxModel.DataStructures
{
    [TestClass]
    public class ThreadSafeSetTest
    {
        private ThreadSafeSet<string> set;

        [TestInitialize]
        public void Setup()
        {
            set = new ThreadSafeSet<string>();
            for (int i = 0; i < 10; i++)
            {
                set.Add($"test {i}");
            }
        }

        [TestMethod]
        public void Remove()
        {
            set.Should().Contain("test 0");
            set.Remove("test 0");
            set.Should().NotContain("test 0");
        }

        [TestMethod]
        public void Contains()
        {
            set.Contains("test 1").Should().BeTrue();
        }

        [TestMethod]
        public void Except()
        {
            string[] exclude = { "test 0", "test 5", "test 9" };
            set.Should().Contain(exclude);
            set.ExceptWith(exclude);
            set.Should().NotContain(exclude);
        }
        
        [TestMethod]
        public void ReadAndWriteSimultaneous()
        {
            int iterations = 500000;

            ThreadSafeSet<string> comeGetMe = new();
            List<long> countsRead = new();
            long addCount = 0;

            Random r = new Random();
            DoReaderWriter(() =>
                {
                    countsRead.Add(Interlocked.Read(ref addCount));
                },
                i =>
                {
                    comeGetMe.Add(new string(Enumerable.Repeat(' ', 10).Select(c => (char)r.Next('A', 'Z')).ToArray()));
                    Interlocked.Increment(ref addCount);
                },
                iterations);

            addCount.Should().Be(iterations);
            countsRead.Count.Should().BeGreaterThan(0);
            countsRead.Last().Should().Be(iterations);
            comeGetMe.Count.Should().Be(iterations);
        }

        [TestMethod]
        public void IterateAndAddSimultaneous()
        {
            int iterations = 500000;

            ThreadSafeSet<int> comeGetMe = new();
            long addCount = 0;
            long iterationsReadMany = 0;

            Random r = new();
            DoReaderWriter(() =>
                {
                    foreach (int item in comeGetMe)
                    {
                        Interlocked.Increment(ref iterationsReadMany);
                    }
                },
                i =>
                {
                    comeGetMe.Add(r.Next());
                    Interlocked.Increment(ref addCount);
                },
                iterations);

            addCount.Should().Be(iterations);
            iterationsReadMany.Should().BePositive();
        }

        [TestMethod]
        public void IterateAndAdd()
        {
            ThreadSafeSet<int> nums = new()
            {
                1,2,3,4,5
            };

            foreach (int num in nums)
            {
                if (num == 3)
                {
                    nums.Add(10);
                }
            }

            nums.Count.Should().Be(6);
            nums.Last().Should().Be(10);
        }

        private void DoReaderWriter(Action reader, Action<int> writer, int iterators)
        {
            ManualResetEventSlim barrier = new(false);
            Thread readerThread = new(() =>
            {
                while (!barrier.IsSet)
                {
                    reader();
                    Thread.Yield();
                }

                // Read one last time after writer finishes
                reader();
            });
            Thread writerThread = new(() =>
            {
                for (int i = 0; i < iterators; i++)
                {
                    writer(i);
                }
                barrier.Set();
            });

            readerThread.Start();
            writerThread.Start();
            barrier.Wait();
        }
    }
}
