﻿// 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/ImmutableDictionaryBuilderTestBase.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 ImmutableDictionaryBuilderTestBase : ImmutablesTestBase
    {
        [Fact]
        public void Add()
        {
            var builder = this.GetBuilder<string, int>();
            builder.Add("five", 5);
            builder.Add(new KeyValuePair<string, int>("six", 6));
            Assert.Equal(5, builder["five"]);
            Assert.Equal(6, builder["six"]);
            Assert.False(builder.ContainsKey("four"));
        }

        /// <summary>
        /// Verifies that "adding" an entry to the dictionary that already exists
        /// with exactly the same key and value will *not* throw an exception.
        /// </summary>
        /// <remarks>
        /// The BCL Dictionary type would throw in this circumstance.
        /// But in an immutable world, not only do we not care so much since the result is the same.
        /// </remarks>
        [Fact]
        public void AddExactDuplicate()
        {
            var builder = this.GetBuilder<string, int>();
            builder.Add("five", 5);
            builder.Add("five", 5);
            Assert.Equal(1, builder.Count);
        }

        [Fact]
        public void AddExistingKeyWithDifferentValue()
        {
            var builder = this.GetBuilder<string, int>();
            builder.Add("five", 5);
            Assert.Throws<ArgumentException>(null, () => builder.Add("five", 6));
        }

        [Fact]
        public void Indexer()
        {
            var builder = this.GetBuilder<string, int>();

            // Set and set again.
            builder["five"] = 5;
            Assert.Equal(5, builder["five"]);
            builder["five"] = 5;
            Assert.Equal(5, builder["five"]);

            // Set to a new value.
            builder["five"] = 50;
            Assert.Equal(50, builder["five"]);

            // Retrieve an invalid value.
            Assert.Throws<KeyNotFoundException>(() => builder["foo"]);
        }

        [Fact]
        public void ContainsPair()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5);
            var builder = this.GetBuilder(map);
            Assert.True(builder.Contains(new KeyValuePair<string, int>("five", 5)));
        }

        [Fact]
        public void RemovePair()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            Assert.True(builder.Remove(new KeyValuePair<string, int>("five", 5)));
            Assert.False(builder.Remove(new KeyValuePair<string, int>("foo", 1)));
            Assert.Equal(1, builder.Count);
            Assert.Equal(6, builder["six"]);
        }

        [Fact]
        public void RemoveKey()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            builder.Remove("five");
            Assert.Equal(1, builder.Count);
            Assert.Equal(6, builder["six"]);
        }

        [Fact]
        public void CopyTo()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5);
            var builder = this.GetBuilder(map);
            var array = new KeyValuePair<string, int>[2]; // intentionally larger than source.
            builder.CopyTo(array, 1);
            Assert.Equal(new KeyValuePair<string, int>(), array[0]);
            Assert.Equal(new KeyValuePair<string, int>("five", 5), array[1]);

            Assert.Throws<ArgumentNullException>("array", () => builder.CopyTo(null!, 0));
        }

        [Fact]
        public void IsReadOnly()
        {
            var builder = this.GetBuilder<string, int>();
            Assert.False(builder.IsReadOnly);
        }

        [Fact]
        public void Keys()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            CollectionAssertAreEquivalent(new[] { "five", "six" }, builder.Keys);
            CollectionAssertAreEquivalent(new[] { "five", "six" }, ((IReadOnlyDictionary<string, int>)builder).Keys.ToArray());
        }

        [Fact]
        public void Values()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            CollectionAssertAreEquivalent(new[] { 5, 6 }, builder.Values);
            CollectionAssertAreEquivalent(new[] { 5, 6 }, ((IReadOnlyDictionary<string, int>)builder).Values.ToArray());
        }

        [Fact]
        public void TryGetValue()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            Assert.True(builder.TryGetValue("five", out int value) && value == 5);
            Assert.True(builder.TryGetValue("six", out value) && value == 6);
            Assert.False(builder.TryGetValue("four", out value));
            Assert.Equal(0, value);
        }

        [Fact]
        public void EnumerateTest()
        {
            var map = this.GetEmptyImmutableDictionary<string, int>().Add("five", 5).Add("six", 6);
            var builder = this.GetBuilder(map);
            using (var enumerator = builder.GetEnumerator())
            {
                Assert.True(enumerator.MoveNext());
                Assert.True(enumerator.MoveNext());
                Assert.False(enumerator.MoveNext());
            }

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

        [Fact]
        public void IDictionaryMembers()
        {
            var builder = this.GetBuilder<string, int>();
            var dictionary = (IDictionary)builder;
            dictionary.Add("a", 1);
            Assert.True(dictionary.Contains("a"));
            Assert.Equal(1, dictionary["a"]);
            Assert.Equal(new[] { "a" }, dictionary.Keys.Cast<string>().ToArray());
            Assert.Equal(new[] { 1 }, dictionary.Values.Cast<int>().ToArray());
            dictionary["a"] = 2;
            Assert.Equal(2, dictionary["a"]);
            dictionary.Remove("a");
            Assert.False(dictionary.Contains("a"));
            Assert.False(dictionary.IsFixedSize);
            Assert.False(dictionary.IsReadOnly);
        }

        [Fact]
        public void IDictionaryEnumerator()
        {
            var builder = this.GetBuilder<string, int>();
            var dictionary = (IDictionary)builder;
            dictionary.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 ICollectionMembers()
        {
            var builder = this.GetBuilder<string, int>();
            var collection = (ICollection)builder;

            collection.CopyTo(Array.Empty<object>(), 0);

            builder.Add("b", 2);
            Assert.True(builder.ContainsKey("b"));

            var array = new object[builder.Count + 1];
            collection.CopyTo(array, 1);
            Assert.Null(array[0]);
            Assert.Equal(new object?[] { null, new KeyValuePair<string, int>("b", 2) }, array);

            var entryArray = new DictionaryEntry[builder.Count + 1];
            collection.CopyTo(entryArray, 1);
            Assert.Equal(default(DictionaryEntry), entryArray[0]);
            Assert.Equal(new DictionaryEntry[] { default, new DictionaryEntry("b", 2) }, entryArray);

            Assert.False(collection.IsSynchronized);
            Assert.NotNull(collection.SyncRoot);
            Assert.Same(collection.SyncRoot, collection.SyncRoot);
        }

        protected abstract bool TryGetKeyHelper<TKey, TValue>(IDictionary<TKey, TValue> dictionary, TKey equalKey, out TKey actualKey)
            where TKey : notnull;

        /// <summary>
        /// Gets the Builder for a given dictionary instance.
        /// </summary>
        /// <typeparam name="TKey">The type of key.</typeparam>
        /// <typeparam name="TValue">The type of value.</typeparam>
        /// <returns>The builder.</returns>
        protected abstract IDictionary<TKey, TValue> GetBuilder<TKey, TValue>(IImmutableDictionary<TKey, TValue>? basis = null)
            where TKey : notnull;

        /// <summary>
        /// Gets an empty immutable dictionary.
        /// </summary>
        /// <typeparam name="TKey">The type of key.</typeparam>
        /// <typeparam name="TValue">The type of value.</typeparam>
        /// <returns>The immutable dictionary.</returns>
        protected abstract IImmutableDictionary<TKey, TValue> GetEmptyImmutableDictionary<TKey, TValue>()
            where TKey : notnull;

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