// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

// NOTE: This code is derived from an implementation originally in dotnet/runtime:
// https://github.com/dotnet/runtime/blob/v8.0.3/src/libraries/Common/tests/System/Collections/ICollection.Generic.Tests.cs
//
// See the commentary in https://github.com/dotnet/roslyn/pull/50156 for notes on incorporating changes made to the
// reference implementation.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Collections
{
    /// <summary>
    /// Contains tests that ensure the correctness of any class that implements the generic
    /// ICollection interface
    /// </summary>
    public abstract class ICollection_Generic_Tests<T> : IEnumerable_Generic_Tests<T>
        where T : notnull
    {
        #region ICollection<T> Helper Methods

        /// <summary>
        /// Creates an instance of an ICollection{T} that can be used for testing.
        /// </summary>
        /// <returns>An instance of an ICollection{T} that can be used for testing.</returns>
        protected abstract ICollection<T> GenericICollectionFactory();

        /// <summary>
        /// Creates an instance of an ICollection{T} that can be used for testing.
        /// </summary>
        /// <param name="count">The number of unique items that the returned ICollection{T} contains.</param>
        /// <returns>An instance of an ICollection{T} that can be used for testing.</returns>
        protected virtual ICollection<T> GenericICollectionFactory(int count)
        {
            ICollection<T> collection = GenericICollectionFactory();
            AddToCollection(collection, count);
            return collection;
        }

        protected virtual bool DuplicateValuesAllowed => true;
        protected virtual bool DefaultValueWhenNotAllowed_Throws => true;
        protected virtual bool IsReadOnly => false;
        protected virtual bool IsReadOnly_ValidityValue => IsReadOnly;
        protected virtual bool AddRemoveClear_ThrowsNotSupported => false;
        protected virtual bool DefaultValueAllowed => true;
        protected virtual IEnumerable<T> InvalidValues => Array.Empty<T>();

        protected virtual void AddToCollection(ICollection<T> collection, int numberOfItemsToAdd)
        {
            int seed = 9600;
            IEqualityComparer<T> comparer = GetIEqualityComparer();
            while (collection.Count < numberOfItemsToAdd)
            {
                T toAdd = CreateT(seed++);
                while (collection.Contains(toAdd, comparer) || InvalidValues.Contains(toAdd, comparer))
                    toAdd = CreateT(seed++);
                collection.Add(toAdd);
            }
        }

        protected virtual Type ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType => typeof(ArgumentException);

        #endregion

        #region IEnumerable<T> Helper Methods

        protected override IEnumerable<T> GenericIEnumerableFactory(int count)
        {
            return GenericICollectionFactory(count);
        }

        /// <summary>
        /// Returns a set of ModifyEnumerable delegates that modify the enumerable passed to them.
        /// </summary>
        protected override IEnumerable<ModifyEnumerable> GetModifyEnumerables(ModifyOperation operations)
        {
            if (!AddRemoveClear_ThrowsNotSupported && (operations & ModifyOperation.Add) == ModifyOperation.Add)
            {
                yield return (IEnumerable<T> enumerable) =>
                {
                    var casted = (ICollection<T>)enumerable;
                    casted.Add(CreateT(2344));
                    return true;
                };
            }
            if (!AddRemoveClear_ThrowsNotSupported && (operations & ModifyOperation.Remove) == ModifyOperation.Remove)
            {
                yield return (IEnumerable<T> enumerable) =>
                {
                    var casted = (ICollection<T>)enumerable;
                    if (casted.Count() > 0)
                    {
                        casted.Remove(casted.ElementAt(0));
                        return true;
                    }
                    return false;
                };
            }
            if (!AddRemoveClear_ThrowsNotSupported && (operations & ModifyOperation.Clear) == ModifyOperation.Clear)
            {
                yield return (IEnumerable<T> enumerable) =>
                {
                    var casted = (ICollection<T>)enumerable;
                    if (casted.Count() > 0)
                    {
                        casted.Clear();
                        return true;
                    }
                    return false;
                };
            }
        }

        #endregion

        #region IsReadOnly

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_IsReadOnly_Validity(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            Assert.Equal(IsReadOnly_ValidityValue, collection.IsReadOnly);
        }

        #endregion

        #region Count

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Count_Validity(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            Assert.Equal(count, collection.Count);
        }

        #endregion

        #region Add

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public virtual void ICollection_Generic_Add_DefaultValue(int count)
        {
            if (DefaultValueAllowed && !IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T?> collection = GenericICollectionFactory(count)!;
                collection.Add(default(T));
                Assert.Equal(count + 1, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_InvalidValueToMiddleOfCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                Assert.All(InvalidValues, invalidValue =>
                {
                    ICollection<T> collection = GenericICollectionFactory(count);
                    collection.Add(invalidValue);
                    for (int i = 0; i < count; i++)
                        collection.Add(CreateT(i));
                    Assert.Equal(count * 2, collection.Count);
                });
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_InvalidValueToBeginningOfCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                Assert.All(InvalidValues, invalidValue =>
                {
                    ICollection<T> collection = GenericICollectionFactory(0);
                    collection.Add(invalidValue);
                    for (int i = 0; i < count; i++)
                        collection.Add(CreateT(i));
                    Assert.Equal(count, collection.Count);
                });
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_InvalidValueToEndOfCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                Assert.All(InvalidValues, invalidValue =>
                {
                    ICollection<T> collection = GenericICollectionFactory(count);
                    collection.Add(invalidValue);
                    Assert.Equal(count, collection.Count);
                });
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_DuplicateValue(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DuplicateValuesAllowed)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                T duplicateValue = CreateT(700);
                collection.Add(duplicateValue);
                collection.Add(duplicateValue);
                Assert.Equal(count + 2, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_AfterCallingClear(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                collection.Clear();
                AddToCollection(collection, 5);
                Assert.Equal(5, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_AfterRemovingAnyValue(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                int seed = 840;
                ICollection<T> collection = GenericICollectionFactory(count);
                List<T> items = collection.ToList();
                T toAdd = CreateT(seed++);
                while (collection.Contains(toAdd))
                    toAdd = CreateT(seed++);
                collection.Add(toAdd);
                collection.Remove(toAdd);

                toAdd = CreateT(seed++);
                while (collection.Contains(toAdd))
                    toAdd = CreateT(seed++);

                collection.Add(toAdd);
                items.Add(toAdd);
                CollectionAsserts.EqualUnordered(items, collection);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_AfterRemovingAllItems(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                List<T> itemsToRemove = collection.ToList();
                for (int i = 0; i < count; i++)
                    collection.Remove(collection.ElementAt(0));
                collection.Add(CreateT(254));
                Assert.Equal(1, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_ToReadOnlyCollection(int count)
        {
            if (IsReadOnly || AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                Assert.Throws<NotSupportedException>(() => collection.Add(CreateT(0)));
                Assert.Equal(count, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Add_AfterRemoving(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                int seed = 840;
                ICollection<T> collection = GenericICollectionFactory(count);
                T toAdd = CreateT(seed++);
                while (collection.Contains(toAdd))
                    toAdd = CreateT(seed++);
                collection.Add(toAdd);
                collection.Remove(toAdd);
                collection.Add(toAdd);
            }
        }

        #endregion

        #region Clear

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Clear(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            if (IsReadOnly || AddRemoveClear_ThrowsNotSupported)
            {
                Assert.Throws<NotSupportedException>(() => collection.Clear());
                Assert.Equal(count, collection.Count);
            }
            else
            {
                collection.Clear();
                Assert.Equal(0, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Clear_Repeatedly(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            if (IsReadOnly || AddRemoveClear_ThrowsNotSupported)
            {
                Assert.Throws<NotSupportedException>(() => collection.Clear());
                Assert.Throws<NotSupportedException>(() => collection.Clear());
                Assert.Throws<NotSupportedException>(() => collection.Clear());
                Assert.Equal(count, collection.Count);
            }
            else
            {
                collection.Clear();
                collection.Clear();
                collection.Clear();
                Assert.Equal(0, collection.Count);
            }
        }

        [Theory]
        [InlineData(false)]
        [InlineData(true)]
        public void ICollection_Generic_Remove_ReferenceRemovedFromCollection(bool useRemove)
        {
            if (typeof(T).IsValueType || IsReadOnly || AddRemoveClear_ThrowsNotSupported)
            {
                return;
            }

            ICollection<T> collection = GenericICollectionFactory();

            WeakReference<object> wr = populateAndRemove(collection, useRemove);
            Assert.True(SpinWait.SpinUntil(() =>
            {
                GC.Collect();
                return !wr.TryGetTarget(out _);
            }, 30_000));
            GC.KeepAlive(collection);

            [MethodImpl(MethodImplOptions.NoInlining)]
            WeakReference<object> populateAndRemove(ICollection<T> collection, bool useRemove)
            {
                AddToCollection(collection, 1);
                T value = collection.First();

                if (useRemove)
                {
                    Assert.True(collection.Remove(value));
                }
                else
                {
                    collection.Clear();
                    Assert.Equal(0, collection.Count);
                }

                return new WeakReference<object>(value);
            }
        }

        #endregion

        #region Contains

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Contains_ValidValueOnCollectionNotContainingThatValue(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            int seed = 4315;
            T item = CreateT(seed++);
            while (collection.Contains(item))
                item = CreateT(seed++);
            Assert.False(collection.Contains(item));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Contains_ValidValueOnCollectionContainingThatValue(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            foreach (T item in collection)
                Assert.True(collection.Contains(item));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Contains_DefaultValueOnCollectionNotContainingDefaultValue(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            if (DefaultValueAllowed && default(T) is null) // it's true only for reference types and for Nullable<T>
            {
                Assert.False(collection.Contains(default(T)!));
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public virtual void ICollection_Generic_Contains_DefaultValueOnCollectionContainingDefaultValue(int count)
        {
            if (DefaultValueAllowed && !IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T?> collection = GenericICollectionFactory(count)!;
                collection.Add(default(T));
                Assert.True(collection.Contains(default(T)));
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Contains_ValidValueThatExistsTwiceInTheCollection(int count)
        {
            if (DuplicateValuesAllowed && !IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                T item = CreateT(12);
                collection.Add(item);
                collection.Add(item);
                Assert.Equal(count + 2, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Contains_InvalidValue_ThrowsArgumentException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            Assert.All(InvalidValues, invalidValue =>
                Assert.Throws<ArgumentException>(() => collection.Contains(invalidValue))
            );
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public virtual void ICollection_Generic_Contains_DefaultValueWhenNotAllowed(int count)
        {
            if (!DefaultValueAllowed && !IsReadOnly)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                if (DefaultValueWhenNotAllowed_Throws)
                    Assert.Throws<ArgumentNullException>("item", () => collection.Contains(default(T)!));
                else
                    Assert.False(collection.Contains(default(T)!));
            }
        }

        #endregion

        #region CopyTo

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_NullArray_ThrowsArgumentNullException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            Assert.Throws<ArgumentNullException>(() => collection.CopyTo(null!, 0));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_NegativeIndex_ThrowsArgumentOutOfRangeException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            T[] array = new T[count];
            Assert.Throws<ArgumentOutOfRangeException>(() => collection.CopyTo(array, -1));
            Assert.Throws<ArgumentOutOfRangeException>(() => collection.CopyTo(array, int.MinValue));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_IndexEqualToArrayCount_ThrowsArgumentException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            T[] array = new T[count];
            if (count > 0)
                Assert.ThrowsAny<ArgumentException>(() => collection.CopyTo(array, count));
            else
                collection.CopyTo(array, count); // does nothing since the array is empty
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowsAnyArgumentException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            T[] array = new T[count];
            Assert.Throws(ICollection_Generic_CopyTo_IndexLargerThanArrayCount_ThrowType, () => collection.CopyTo(array, count + 1));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_NotEnoughSpaceInOffsettedArray_ThrowsArgumentException(int count)
        {
            if (count > 0) // Want the T array to have at least 1 element
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                T[] array = new T[count];
                Assert.ThrowsAny<ArgumentException>(() => collection.CopyTo(array, 1));
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_ExactlyEnoughSpaceInArray(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            T[] array = new T[count];
            collection.CopyTo(array, 0);
            Assert.True(Enumerable.SequenceEqual(collection, array));
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_CopyTo_ArrayIsLargerThanCollection(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            T[] array = new T[count * 3 / 2];
            collection.CopyTo(array, 0);
            Assert.True(Enumerable.SequenceEqual(collection, array.Take(count)));
        }

        #endregion

        #region Remove

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_OnReadOnlyCollection_ThrowsNotSupportedException(int count)
        {
            if (IsReadOnly || AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                Assert.Throws<NotSupportedException>(() => collection.Remove(CreateT(34543)));
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_DefaultValueNotContainedInCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DefaultValueAllowed && !Enumerable.Contains(InvalidValues, default(T)))
            {
                int seed = count * 21;
                ICollection<T?> collection = GenericICollectionFactory(count)!;
                T? value = default(T);
                while (collection.Contains(value))
                {
                    collection.Remove(value);
                    count--;
                }
                Assert.False(collection.Remove(value));
                Assert.Equal(count, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_NonDefaultValueNotContainedInCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                int seed = count * 251;
                ICollection<T> collection = GenericICollectionFactory(count);
                T value = CreateT(seed++);
                while (collection.Contains(value) || Enumerable.Contains(InvalidValues, value))
                    value = CreateT(seed++);
                Assert.False(collection.Remove(value));
                Assert.Equal(count, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public virtual void ICollection_Generic_Remove_DefaultValueContainedInCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DefaultValueAllowed && !Enumerable.Contains(InvalidValues, default(T)))
            {
                int seed = count * 21;
                ICollection<T?> collection = GenericICollectionFactory(count)!;
                T? value = default(T);
                if (!collection.Contains(value))
                {
                    collection.Add(value);
                    count++;
                }
                Assert.True(collection.Remove(value));
                Assert.Equal(count - 1, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_NonDefaultValueContainedInCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                int seed = count * 251;
                ICollection<T> collection = GenericICollectionFactory(count);
                T value = CreateT(seed++);
                if (!collection.Contains(value))
                {
                    collection.Add(value);
                    count++;
                }
                Assert.True(collection.Remove(value));
                Assert.Equal(count - 1, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_ValueThatExistsTwiceInCollection(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported && DuplicateValuesAllowed)
            {
                int seed = count * 90;
                ICollection<T> collection = GenericICollectionFactory(count);
                T value = CreateT(seed++);
                collection.Add(value);
                collection.Add(value);
                count += 2;
                Assert.True(collection.Remove(value));
                Assert.True(collection.Contains(value));
                Assert.Equal(count - 1, collection.Count);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_EveryValue(int count)
        {
            if (!IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                Assert.All(collection.ToList(), value =>
                {
                    Assert.True(collection.Remove(value));
                });
                Assert.Empty(collection);
            }
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_InvalidValue_ThrowsArgumentException(int count)
        {
            ICollection<T> collection = GenericICollectionFactory(count);
            Assert.All(InvalidValues, value =>
            {
                Assert.Throws<ArgumentException>(() => collection.Remove(value));
            });
            Assert.Equal(count, collection.Count);
        }

        [Theory]
        [MemberData(nameof(ValidCollectionSizes))]
        public void ICollection_Generic_Remove_DefaultValueWhenNotAllowed(int count)
        {
            if (!DefaultValueAllowed && !IsReadOnly && !AddRemoveClear_ThrowsNotSupported)
            {
                ICollection<T> collection = GenericICollectionFactory(count);
                if (DefaultValueWhenNotAllowed_Throws)
                    Assert.Throws<ArgumentNullException>(() => collection.Remove(default(T)!));
                else
                    Assert.False(collection.Remove(default(T)!));
            }
        }

        #endregion
    }
}
