﻿// 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.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 virtual void EmptyTest()
        {
            this.EmptyTestHelper(Empty<int, bool>(), 5);
        }

        [Fact]
        public void ContainsTest()
        {
            ContainsTestHelper(Empty<int, string>(), 5, "foo");
        }

        [Fact]
        public void RemoveTest()
        {
            RemoveTestHelper(Empty<int, GenericParameterHelper?>(), 5);
        }

        [Fact]
        public void SetItemTest()
        {
            var map = this.Empty<string, int>()
                .SetItem("Microsoft", 100)
                .SetItem("Corporation", 50);
            Assert.Equal(2, map.Count);

            map = map.SetItem("Microsoft", 200);
            Assert.Equal(2, map.Count);
            Assert.Equal(200, map["Microsoft"]);

            // Set it to the same thing again and make sure it's all good.
            var sameMap = map.SetItem("Microsoft", 200);
            Assert.True(IsSame(map, sameMap));
        }

        [Fact]
        public void SetItemsTest()
        {
            var template = new Dictionary<string, int>
            {
                { "Microsoft", 100 },
                { "Corporation", 50 },
            };
            var map = this.Empty<string, int>().SetItems(template);
            Assert.Equal(2, map.Count);

            var changes = new Dictionary<string, int>
            {
                { "Microsoft", 150 },
                { "Dogs", 90 },
            };
            map = map.SetItems(changes);
            Assert.Equal(3, map.Count);
            Assert.Equal(150, map["Microsoft"]);
            Assert.Equal(50, map["Corporation"]);
            Assert.Equal(90, map["Dogs"]);

            map = map.SetItems(
                new[] {
                    new KeyValuePair<string, int>("Microsoft", 80),
                    new KeyValuePair<string, int>("Microsoft", 70),
                });
            Assert.Equal(3, map.Count);
            Assert.Equal(70, map["Microsoft"]);
            Assert.Equal(50, map["Corporation"]);
            Assert.Equal(90, map["Dogs"]);

            map = this.Empty<string, int>().SetItems(new[] { // use an array for code coverage
                new KeyValuePair<string, int>("a", 1), new KeyValuePair<string, int>("b", 2),
                new KeyValuePair<string, int>("a", 3),
            });
            Assert.Equal(2, map.Count);
            Assert.Equal(3, map["a"]);
            Assert.Equal(2, map["b"]);
        }

        [Fact]
        public void ContainsKeyTest()
        {
            ContainsKeyTestHelper(Empty<int, GenericParameterHelper>(), 1, new GenericParameterHelper());
        }

        [Fact]
        public void IndexGetNonExistingKeyThrowsTest()
        {
            Assert.Throws<KeyNotFoundException>(() => this.Empty<int, int>()[3]);
        }

        [Fact]
        public void IndexGetTest()
        {
            var map = this.Empty<int, int>().Add(3, 5);
            Assert.Equal(5, map[3]);
        }

        /// <summary>
        /// Verifies that the GetHashCode method returns the standard one.
        /// </summary>
        [Fact]
        public void GetHashCodeTest()
        {
            var dictionary = Empty<string, int>();
            Assert.Equal(EqualityComparer<object>.Default.GetHashCode(dictionary), dictionary.GetHashCode());
        }

        [Fact]
        public void ICollectionOfKVMembers()
        {
            var dictionary = (ICollection<KeyValuePair<string, int>>)Empty<string, int>();
            Assert.Throws<NotSupportedException>(() => dictionary.Add(new KeyValuePair<string, int>()));
            Assert.Throws<NotSupportedException>(() => dictionary.Remove(new KeyValuePair<string, int>()));
            Assert.Throws<NotSupportedException>(() => dictionary.Clear());
            Assert.True(dictionary.IsReadOnly);
        }

        [Fact]
        public void ICollectionMembers()
        {
            ((ICollection)Empty<string, int>()).CopyTo(Array.Empty<object>(), 0);

            var dictionary = (ICollection)Empty<string, int>().Add("a", 1);
            Assert.True(dictionary.IsSynchronized);
            Assert.NotNull(dictionary.SyncRoot);
            Assert.Same(dictionary.SyncRoot, dictionary.SyncRoot);

            var array = new DictionaryEntry[2];
            dictionary.CopyTo(array, 1);
            Assert.Null(array[0].Value);
            Assert.Equal(new DictionaryEntry("a", 1), (DictionaryEntry)array[1]);
        }

        [Fact]
        public void IDictionaryOfKVMembers()
        {
            var dictionary = (IDictionary<string, int>)Empty<string, int>().Add("c", 3);
            Assert.Throws<NotSupportedException>(() => dictionary.Add("a", 1));
            Assert.Throws<NotSupportedException>(() => dictionary.Remove("a"));
            Assert.Throws<NotSupportedException>(() => dictionary["a"] = 2);
            Assert.Throws<KeyNotFoundException>(() => dictionary["a"]);
            Assert.Equal(3, dictionary["c"]);
        }

        [Fact]
        public void IDictionaryMembers()
        {
            var dictionary = (IDictionary)Empty<string, int>().Add("c", 3);
            Assert.Throws<NotSupportedException>(() => dictionary.Add("a", 1));
            Assert.Throws<NotSupportedException>(() => dictionary.Remove("a"));
            Assert.Throws<NotSupportedException>(() => dictionary["a"] = 2);
            Assert.Throws<NotSupportedException>(() => dictionary.Clear());
            Assert.False(dictionary.Contains("a"));
            Assert.True(dictionary.Contains("c"));
            Assert.Null(dictionary["a"]);
            Assert.Equal(3, dictionary["c"]);
            Assert.True(dictionary.IsFixedSize);
            Assert.True(dictionary.IsReadOnly);
            Assert.Equal(new[] { "c" }, dictionary.Keys.Cast<string>().ToArray());
            Assert.Equal(new[] { 3 }, dictionary.Values.Cast<int>().ToArray());
        }

        [Fact]
        public void IDictionaryEnumerator()
        {
            var dictionary = (IDictionary)Empty<string, int>().Add("a", 1);
            var enumerator = dictionary.GetEnumerator();
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Current);
            Assert.Null(enumerator.Key);
            Assert.Equal(0, enumerator.Value);
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Entry);
            Assert.True(enumerator.MoveNext());
            Assert.Equal(enumerator.Entry, enumerator.Current);
            Assert.Equal(enumerator.Key, enumerator.Entry.Key);
            Assert.Equal(enumerator.Value, enumerator.Entry.Value);
            Assert.Equal("a", enumerator.Key);
            Assert.Equal(1, enumerator.Value);
            Assert.False(enumerator.MoveNext());
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Current);
            Assert.Null(enumerator.Key);
            Assert.Equal(0, enumerator.Value);
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Entry);
            Assert.False(enumerator.MoveNext());

            enumerator.Reset();
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Current);
            Assert.Null(enumerator.Key);
            Assert.Equal(0, enumerator.Value);
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Entry);
            Assert.True(enumerator.MoveNext());
            Assert.Equal(enumerator.Key, ((DictionaryEntry)enumerator.Current).Key);
            Assert.Equal(enumerator.Value, ((DictionaryEntry)enumerator.Current).Value);
            Assert.Equal("a", enumerator.Key);
            Assert.Equal(1, enumerator.Value);
            Assert.False(enumerator.MoveNext());
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Current);
            Assert.Null(enumerator.Key);
            Assert.Equal(0, enumerator.Value);
            Assert.Equal(new DictionaryEntry(null!, 0), enumerator.Entry);
            Assert.False(enumerator.MoveNext());
        }

        [Fact]
        public void TryGetKey()
        {
            var dictionary = Empty<int>(StringComparer.OrdinalIgnoreCase)
                .Add("a", 1);
            Assert.True(dictionary.TryGetKey("a", out string actualKey));
            Assert.Equal("a", actualKey);

            Assert.True(dictionary.TryGetKey("A", out actualKey));
            Assert.Equal("a", actualKey);

            Assert.False(dictionary.TryGetKey("b", out actualKey));
            Assert.Equal("b", actualKey);
        }

        protected void EmptyTestHelper<K, V>(IImmutableDictionary<K, V?> empty, K someKey)
            where K : notnull
        {
            Assert.True(IsSame(empty, empty.Clear()));
            Assert.Equal(0, empty.Count);
            Assert.Equal(0, empty.Count());
            Assert.Equal(0, empty.Keys.Count());
            Assert.Equal(0, empty.Values.Count());
            Assert.Same(EqualityComparer<V>.Default, GetValueComparer(empty));
            Assert.False(empty.ContainsKey(someKey));
            Assert.False(empty.Contains(new KeyValuePair<K, V?>(someKey, default(V))));
            Assert.Equal(default(V), empty.GetValueOrDefault(someKey));

            Assert.False(empty.TryGetValue(someKey, out V? value));
            Assert.Equal(default(V), value);
        }

        protected void AddExistingKeySameValueTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue> map, TKey key, TValue value1, TValue value2)
            where TKey : notnull
        {
            Assert.NotNull(map);
            Assert.NotNull(key);
            Assert.True(GetValueComparer(map).Equals(value1, value2));

            map = map.Add(key, value1);
            Assert.True(IsSame(map, map.Add(key, value2)));
            Assert.True(IsSame(map, map.AddRange(new[] { new KeyValuePair<TKey, TValue>(key, value2) })));
        }

        /// <summary>
        /// Verifies that adding a key-value pair where the key already is in the map but with a different value throws.
        /// </summary>
        /// <typeparam name="TKey">The type of key in the map.</typeparam>
        /// <typeparam name="TValue">The type of value in the map.</typeparam>
        /// <param name="map">The map to manipulate.</param>
        /// <param name="key">The key to add.</param>
        /// <param name="value1">The first value to add.</param>
        /// <param name="value2">The second value to add.</param>
        /// <remarks>
        /// Adding a key-value pair to a map where that key already exists, but with a different value, cannot fit the
        /// semantic of "adding", either by just returning or mutating the value on the existing key.  Throwing is the only reasonable response.
        /// </remarks>
        protected void AddExistingKeyDifferentValueTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue> map, TKey key, TValue value1, TValue value2)
            where TKey : notnull
        {
            Assert.NotNull(map);
            Assert.NotNull(key);
            Assert.False(GetValueComparer(map).Equals(value1, value2));

            var map1 = map.Add(key, value1);
            var map2 = map.Add(key, value2);
            Assert.Throws<ArgumentException>(null, () => map1.Add(key, value2));
            Assert.Throws<ArgumentException>(null, () => map2.Add(key, value1));
        }

        protected static void ContainsKeyTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue> map, TKey key, TValue value)
        {
            Assert.False(map.ContainsKey(key));
            Assert.True(map.Add(key, value).ContainsKey(key));
        }

        protected static void ContainsTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue> map, TKey key, TValue value)
            where TKey : notnull
        {
            Assert.False(map.Contains(new KeyValuePair<TKey, TValue>(key, value)));
            Assert.False(map.Contains(key, value));
            Assert.True(map.Add(key, value).Contains(new KeyValuePair<TKey, TValue>(key, value)));
            Assert.True(map.Add(key, value).Contains(key, value));
        }

        protected void RemoveTestHelper<TKey, TValue>(IImmutableDictionary<TKey, TValue?> map, TKey key)
            where TKey : notnull
        {
            // no-op remove
            Assert.True(IsSame(map, map.Remove(key)));
            Assert.True(IsSame(map, map.RemoveRange(Enumerable.Empty<TKey>())));

            // substantial remove
            var addedMap = map.Add(key, default(TValue));
            var removedMap = addedMap.Remove(key);
            Assert.NotSame(addedMap, removedMap);
            Assert.False(removedMap.ContainsKey(key));
        }

        protected abstract IImmutableDictionary<TKey, TValue> Empty<TKey, TValue>()
            where TKey : notnull;

        protected abstract IImmutableDictionary<string, TValue> Empty<TValue>(StringComparer comparer);

        protected abstract IEqualityComparer<TValue> GetValueComparer<TKey, TValue>(IImmutableDictionary<TKey, TValue> dictionary)
            where TKey : notnull;
    }
}
