﻿// 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/v5.0.2/src/libraries/System.Collections.Immutable/tests/ImmutableDictionaryTestBase.nonnetstandard.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Collections
{
    public abstract partial class ImmutableDictionaryTestBase : ImmutablesTestBase
    {
        [Fact]
        public void EnumeratorTest()
        {
            EnumeratorTestHelper(this.Empty<int, GenericParameterHelper>());
        }

        [Fact]
        public void KeysTest()
        {
            KeysTestHelper(Empty<int, bool>(), 5);
        }

        [Fact]
        public void ValuesTest()
        {
            ValuesTestHelper(Empty<int, bool>(), 5);
        }

        [Fact]
        public void AddAscendingTest()
        {
            AddAscendingTestHelper(Empty<int, GenericParameterHelper>());
        }

        [Fact]
        public void DictionaryRemoveThrowsTest()
        {
            IDictionary<int, int> map = this.Empty<int, int>().Add(5, 3).ToReadOnlyDictionary();
            Assert.Throws<NotSupportedException>(() => map.Remove(5));
        }

        [Fact]
        public void DictionaryAddThrowsTest()
        {
            IDictionary<int, int> map = this.Empty<int, int>().ToReadOnlyDictionary();
            Assert.Throws<NotSupportedException>(() => map.Add(5, 3));
        }

        [Fact]
        public void DictionaryIndexSetThrowsTest()
        {
            IDictionary<int, int> map = this.Empty<int, int>().ToReadOnlyDictionary();
            Assert.Throws<NotSupportedException>(() => map[3] = 5);
        }

        [Fact]
        public void EqualsTest()
        {
            Assert.False(Empty<int, int>().Equals(null));
            Assert.False(Empty<int, int>().Equals("hi"));
            Assert.True(Empty<int, int>().Equals(Empty<int, int>()));
            Assert.False(Empty<int, int>().Add(3, 2).Equals(Empty<int, int>().Add(3, 2)));
            Assert.False(Empty<int, int>().Add(3, 2).Equals(Empty<int, int>().Add(3, 1)));
            Assert.False(Empty<int, int>().Add(5, 1).Equals(Empty<int, int>().Add(3, 1)));
            Assert.False(Empty<int, int>().Add(3, 1).Add(5, 1).Equals(Empty<int, int>().Add(3, 1)));
            Assert.False(Empty<int, int>().Add(3, 1).Equals(Empty<int, int>().Add(3, 1).Add(5, 1)));

            Assert.True(Empty<int, int>().ToReadOnlyDictionary().Equals(Empty<int, int>()));
            Assert.True(Empty<int, int>().Equals(Empty<int, int>().ToReadOnlyDictionary()));
            Assert.True(Empty<int, int>().ToReadOnlyDictionary().Equals(Empty<int, int>().ToReadOnlyDictionary()));
            Assert.False(Empty<int, int>().Add(3, 1).ToReadOnlyDictionary().Equals(Empty<int, int>()));
            Assert.False(Empty<int, int>().Equals(Empty<int, int>().Add(3, 1).ToReadOnlyDictionary()));
            Assert.False(Empty<int, int>().ToReadOnlyDictionary().Equals(Empty<int, int>().Add(3, 1).ToReadOnlyDictionary()));
        }

        [Fact]
        public void AddRangeTest()
        {
            var map = Empty<int, GenericParameterHelper>();
            map = map.AddRange(Enumerable.Range(1, 100).Select(n => new KeyValuePair<int, GenericParameterHelper>(n, new GenericParameterHelper())));
            CollectionAssertAreEquivalent(map.Select(kv => kv.Key).ToList(), Enumerable.Range(1, 100).ToList());
            Assert.Equal(100, map.Count);

            // Test optimization for empty map.
            var map2 = Empty<int, GenericParameterHelper>();
            var jointMap = map2.AddRange(map);
            Assert.True(IsSame(map, jointMap));

            jointMap = map2.AddRange(map.ToReadOnlyDictionary());
            Assert.True(IsSame(map, jointMap));

            jointMap = map2.AddRange(map.ToBuilder());
            Assert.True(IsSame(map, jointMap));
        }

        [Fact]
        public void AddDescendingTest()
        {
            AddDescendingTestHelper(Empty<int, GenericParameterHelper>());
        }

        [Fact]
        public void AddRemoveRandomDataTest()
        {
            AddRemoveRandomDataTestHelper(Empty<double, GenericParameterHelper>());
        }

        [Fact]
        public void AddRemoveEnumerableTest()
        {
            AddRemoveEnumerableTestHelper(Empty<int, int>());
        }

        private static IImmutableDictionary<TKey, TValue> AddTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue> map, TKey key, TValue value) where TKey : IComparable<TKey>
        {
            Assert.NotNull(map);
            Assert.NotNull(key);

            IImmutableDictionary<TKey, TValue> addedMap = map.Add(key, value);
            Assert.NotSame(map, addedMap);
            ////Assert.Equal(map.Count + 1, addedMap.Count);
            Assert.False(map.ContainsKey(key));
            Assert.True(addedMap.ContainsKey(key));
            AssertAreSame(value, addedMap.GetValueOrDefault(key));

            return addedMap;
        }

        protected static void AddAscendingTestHelper(IImmutableDictionary<int, GenericParameterHelper> map)
        {
            Assert.NotNull(map);

            for (int i = 0; i < 10; i++)
            {
                map = AddTestHelper(map, i, new GenericParameterHelper(i));
            }

            Assert.Equal(10, map.Count);
            for (int i = 0; i < 10; i++)
            {
                Assert.True(map.ContainsKey(i));
            }
        }

        protected static void AddDescendingTestHelper(IImmutableDictionary<int, GenericParameterHelper> map)
        {
            for (int i = 10; i > 0; i--)
            {
                map = AddTestHelper(map, i, new GenericParameterHelper(i));
            }

            Assert.Equal(10, map.Count);
            for (int i = 10; i > 0; i--)
            {
                Assert.True(map.ContainsKey(i));
            }
        }

        protected static void AddRemoveRandomDataTestHelper(IImmutableDictionary<double, GenericParameterHelper> map)
        {
            Assert.NotNull(map);

            double[] inputs = GenerateDummyFillData();
            for (int i = 0; i < inputs.Length; i++)
            {
                map = AddTestHelper(map, inputs[i], new GenericParameterHelper());
            }

            Assert.Equal(inputs.Length, map.Count);
            for (int i = 0; i < inputs.Length; i++)
            {
                Assert.True(map.ContainsKey(inputs[i]));
            }

            for (int i = 0; i < inputs.Length; i++)
            {
                map = map.Remove(inputs[i]);
            }

            Assert.Equal(0, map.Count);
        }

        protected void AddRemoveEnumerableTestHelper(IImmutableDictionary<int, int> empty)
        {
            Assert.NotNull(empty);

            Assert.True(IsSame(empty, empty.RemoveRange(Enumerable.Empty<int>())));
            Assert.True(IsSame(empty, empty.AddRange(Enumerable.Empty<KeyValuePair<int, int>>())));
            var list = new List<KeyValuePair<int, int>> { new KeyValuePair<int, int>(3, 5), new KeyValuePair<int, int>(8, 10) };
            var nonEmpty = empty.AddRange(list);
            var halfRemoved = nonEmpty.RemoveRange(Enumerable.Range(1, 5));
            Assert.Equal(1, halfRemoved.Count);
            Assert.True(halfRemoved.ContainsKey(8));
        }

        protected static void KeysTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue?> map, TKey key)
            where TKey : notnull
        {
            Assert.Equal(0, map.Keys.Count());
            Assert.Equal(0, map.ToReadOnlyDictionary().Keys.Count());

            var nonEmpty = map.Add(key, default(TValue));
            Assert.Equal(1, nonEmpty.Keys.Count());
            Assert.Equal(1, nonEmpty.ToReadOnlyDictionary().Keys.Count());
            KeysOrValuesTestHelper(((IDictionary<TKey, TValue>)nonEmpty).Keys, key);
        }

        protected static void ValuesTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue?> map, TKey key)
        {
            Assert.Equal(0, map.Values.Count());
            Assert.Equal(0, map.ToReadOnlyDictionary().Values.Count());

            var nonEmpty = map.Add(key, default(TValue));
            Assert.Equal(1, nonEmpty.Values.Count());
            Assert.Equal(1, nonEmpty.ToReadOnlyDictionary().Values.Count());
            KeysOrValuesTestHelper(((IDictionary<TKey, TValue?>)nonEmpty).Values, default(TValue));
        }

        protected static void EnumeratorTestHelper(IImmutableDictionary<int, GenericParameterHelper> map)
        {
            for (int i = 0; i < 10; i++)
            {
                map = AddTestHelper(map, i, new GenericParameterHelper(i));
            }

            int j = 0;
            foreach (KeyValuePair<int, GenericParameterHelper> pair in map)
            {
                Assert.Equal(j, pair.Key);
                Assert.Equal(j, pair.Value.Data);
                j++;
            }

            var list = map.ToList();
            Assert.Equal<KeyValuePair<int, GenericParameterHelper>>(list, ToListNonGeneric<KeyValuePair<int, GenericParameterHelper>>(map));

            // Apply some less common uses to the enumerator to test its metal.
            using (var enumerator = map.GetEnumerator())
            {
                enumerator.Reset(); // reset isn't usually called before MoveNext
                ManuallyEnumerateTest(list, enumerator);
                enumerator.Reset();
                ManuallyEnumerateTest(list, enumerator);

                // this time only partially enumerate
                enumerator.Reset();
                enumerator.MoveNext();
                enumerator.Reset();
                ManuallyEnumerateTest(list, enumerator);
            }

            var manualEnum = map.GetEnumerator();
            Assert.Equal(default(KeyValuePair<int, GenericParameterHelper>), manualEnum.Current);
            while (manualEnum.MoveNext()) { }
            Assert.False(manualEnum.MoveNext());
            Assert.Equal(default(KeyValuePair<int, GenericParameterHelper>), manualEnum.Current);
        }

        private static List<T> ToListNonGeneric<T>(IEnumerable sequence)
        {
            Assert.NotNull(sequence);

            var list = new List<T>();
            var enumerator = sequence.GetEnumerator();
            while (enumerator.MoveNext())
            {
                list.Add((T)enumerator.Current);
            }

            return list;
        }

        private static void KeysOrValuesTestHelper<T>(ICollection<T> collection, T containedValue)
        {
            if (collection is null)
                throw new ArgumentNullException(nameof(collection));

            Assert.True(collection.Contains(containedValue));
            Assert.Throws<NotSupportedException>(() => collection.Add(default(T)!));
            Assert.Throws<NotSupportedException>(() => collection.Clear());

            var nonGeneric = (ICollection)collection;
            Assert.NotNull(nonGeneric.SyncRoot);
            Assert.Same(nonGeneric.SyncRoot, nonGeneric.SyncRoot);
            Assert.True(nonGeneric.IsSynchronized);
            Assert.True(collection.IsReadOnly);

            Assert.Throws<ArgumentNullException>("array", () => nonGeneric.CopyTo(null!, 0));
            var array = new T[collection.Count + 1];
            nonGeneric.CopyTo(array, 1);
            Assert.Equal(default(T), array[0]);
            Assert.Equal(array.Skip(1), nonGeneric.Cast<T>().ToArray());
        }
    }
}
