﻿// 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/ImmutableDictionaryTest.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.Generic;
using System.Linq;
using Microsoft.CodeAnalysis.Collections;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests.Collections
{
    public class ImmutableSegmentedHashSetBuilderTest : ImmutablesTestBase
    {
        [Fact]
        public void CreateBuilder()
        {
            var builder = ImmutableSegmentedHashSet.CreateBuilder<string>();
            Assert.Same(EqualityComparer<string>.Default, builder.KeyComparer);

            builder = ImmutableSegmentedHashSet.CreateBuilder<string>(StringComparer.OrdinalIgnoreCase);
            Assert.Same(StringComparer.OrdinalIgnoreCase, builder.KeyComparer);
        }

        [Fact]
        public void ToBuilder()
        {
            var builder = ImmutableSegmentedHashSet<int>.Empty.ToBuilder();
            Assert.True(builder.Add(3));
            Assert.True(builder.Add(5));
            Assert.False(builder.Add(5));
            Assert.Equal(2, builder.Count);
            Assert.True(builder.Contains(3));
            Assert.True(builder.Contains(5));
            Assert.False(builder.Contains(7));

            var set = builder.ToImmutable();
            Assert.Equal(builder.Count, set.Count);
            Assert.True(builder.Add(8));
            Assert.Equal(3, builder.Count);
            Assert.Equal(2, set.Count);
            Assert.True(builder.Contains(8));
            Assert.False(set.Contains(8));
        }

        [Fact]
        public void BuilderFromSet()
        {
            var set = ImmutableSegmentedHashSet<int>.Empty.Add(1);
            var builder = set.ToBuilder();
            Assert.True(builder.Contains(1));
            Assert.True(builder.Add(3));
            Assert.True(builder.Add(5));
            Assert.False(builder.Add(5));
            Assert.Equal(3, builder.Count);
            Assert.True(builder.Contains(3));
            Assert.True(builder.Contains(5));
            Assert.False(builder.Contains(7));

            var set2 = builder.ToImmutable();
            Assert.Equal(builder.Count, set2.Count);
            Assert.True(set2.Contains(1));
            Assert.True(builder.Add(8));
            Assert.Equal(4, builder.Count);
            Assert.Equal(3, set2.Count);
            Assert.True(builder.Contains(8));

            Assert.False(set.Contains(8));
            Assert.False(set2.Contains(8));
        }

        [Fact]
        public void EnumerateBuilderWhileMutating()
        {
            var builder = ImmutableSegmentedHashSet<int>.Empty.Union(Enumerable.Range(1, 10)).ToBuilder();
            CollectionAssertAreEquivalent(Enumerable.Range(1, 10).ToArray(), builder.ToArray());

            var enumerator = builder.GetEnumerator();
            Assert.True(enumerator.MoveNext());
            builder.Add(11);

            // Verify that a new enumerator will succeed.
            CollectionAssertAreEquivalent(Enumerable.Range(1, 11).ToArray(), builder.ToArray());

            // Try enumerating further with the previous enumerable now that we've changed the collection.
            Assert.Throws<InvalidOperationException>(() => enumerator.MoveNext());
            enumerator.Reset();
            enumerator.MoveNext(); // resetting should fix the problem.

            // Verify that by obtaining a new enumerator, we can enumerate all the contents.
            CollectionAssertAreEquivalent(Enumerable.Range(1, 11).ToArray(), builder.ToArray());
        }

        [Fact]
        public void BuilderReusesUnchangedImmutableInstances()
        {
            var collection = ImmutableSegmentedHashSet<int>.Empty.Add(1);
            var builder = collection.ToBuilder();
            Assert.True(IsSame(collection, builder.ToImmutable())); // no changes at all.
            builder.Add(2);

            var newImmutable = builder.ToImmutable();
            Assert.False(IsSame(collection, newImmutable)); // first ToImmutable with changes should be a new instance.
            Assert.True(IsSame(newImmutable, builder.ToImmutable())); // second ToImmutable without changes should be the same instance.
        }

        [Fact]
        public void EnumeratorTest()
        {
            var builder = ImmutableSegmentedHashSet.Create(1).ToBuilder();
            ManuallyEnumerateTest(new[] { 1 }, ((IEnumerable<int>)builder).GetEnumerator());
        }

        [Fact]
        public void Clear()
        {
            var set = ImmutableSegmentedHashSet.Create(1);
            var builder = set.ToBuilder();
            builder.Clear();
            Assert.Equal(0, builder.Count);
        }

        [Fact]
        public void KeyComparer()
        {
            var builder = ImmutableSegmentedHashSet.Create("a", "B").ToBuilder();
            Assert.Same(EqualityComparer<string>.Default, builder.KeyComparer);
            Assert.True(builder.Contains("a"));
            Assert.False(builder.Contains("A"));

            builder.KeyComparer = StringComparer.OrdinalIgnoreCase;
            Assert.Same(StringComparer.OrdinalIgnoreCase, builder.KeyComparer);
            Assert.Equal(2, builder.Count);
            Assert.True(builder.Contains("a"));
            Assert.True(builder.Contains("A"));

            var set = builder.ToImmutable();
            Assert.Same(StringComparer.OrdinalIgnoreCase, set.KeyComparer);
        }

        [Fact]
        public void KeyComparerCollisions()
        {
            var builder = ImmutableSegmentedHashSet.Create("a", "A").ToBuilder();
            builder.KeyComparer = StringComparer.OrdinalIgnoreCase;
            Assert.Equal(1, builder.Count);
            Assert.True(builder.Contains("a"));

            var set = builder.ToImmutable();
            Assert.Same(StringComparer.OrdinalIgnoreCase, set.KeyComparer);
            Assert.Equal(1, set.Count);
            Assert.True(set.Contains("a"));
        }

        [Fact]
        public void KeyComparerEmptyCollection()
        {
            var builder = ImmutableSegmentedHashSet.Create<string>().ToBuilder();
            Assert.Same(EqualityComparer<string>.Default, builder.KeyComparer);
            builder.KeyComparer = StringComparer.OrdinalIgnoreCase;
            Assert.Same(StringComparer.OrdinalIgnoreCase, builder.KeyComparer);
            var set = builder.ToImmutable();
            Assert.Same(StringComparer.OrdinalIgnoreCase, set.KeyComparer);
        }

        [Fact]
        public void UnionWith()
        {
            var builder = ImmutableSegmentedHashSet.Create(1, 2, 3).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.UnionWith(null!));
            builder.UnionWith(new[] { 2, 3, 4 });
            Assert.Equal(new[] { 1, 2, 3, 4 }, builder);
        }

        [Fact]
        public void ExceptWith()
        {
            var builder = ImmutableSegmentedHashSet.Create(1, 2, 3).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.ExceptWith(null!));
            builder.ExceptWith(new[] { 2, 3, 4 });
            Assert.Equal(new[] { 1 }, builder);
        }

        [Fact]
        public void SymmetricExceptWith()
        {
            var builder = ImmutableSegmentedHashSet.Create(1, 2, 3).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.SymmetricExceptWith(null!));
            builder.SymmetricExceptWith(new[] { 2, 3, 4 });
            Assert.Equal(new[] { 1, 4 }, builder);
        }

        [Fact]
        public void IntersectWith()
        {
            var builder = ImmutableSegmentedHashSet.Create(1, 2, 3).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.IntersectWith(null!));
            builder.IntersectWith(new[] { 2, 3, 4 });
            Assert.Equal(new[] { 2, 3 }, builder);
        }

        [Fact]
        public void IsProperSubsetOf()
        {
            var builder = ImmutableSegmentedHashSet.CreateRange(Enumerable.Range(1, 3)).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.IsProperSubsetOf(null!));
            Assert.False(builder.IsProperSubsetOf(Enumerable.Range(1, 3)));
            Assert.True(builder.IsProperSubsetOf(Enumerable.Range(1, 5)));
        }

        [Fact]
        public void IsProperSupersetOf()
        {
            var builder = ImmutableSegmentedHashSet.CreateRange(Enumerable.Range(1, 3)).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.IsProperSupersetOf(null!));
            Assert.False(builder.IsProperSupersetOf(Enumerable.Range(1, 3)));
            Assert.True(builder.IsProperSupersetOf(Enumerable.Range(1, 2)));
        }

        [Fact]
        public void IsSubsetOf()
        {
            var builder = ImmutableSegmentedHashSet.CreateRange(Enumerable.Range(1, 3)).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.IsSubsetOf(null!));
            Assert.False(builder.IsSubsetOf(Enumerable.Range(1, 2)));
            Assert.True(builder.IsSubsetOf(Enumerable.Range(1, 3)));
            Assert.True(builder.IsSubsetOf(Enumerable.Range(1, 5)));
        }

        [Fact]
        public void IsSupersetOf()
        {
            var builder = ImmutableSegmentedHashSet.CreateRange(Enumerable.Range(1, 3)).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.IsSupersetOf(null!));
            Assert.False(builder.IsSupersetOf(Enumerable.Range(1, 4)));
            Assert.True(builder.IsSupersetOf(Enumerable.Range(1, 3)));
            Assert.True(builder.IsSupersetOf(Enumerable.Range(1, 2)));
        }

        [Fact]
        public void Overlaps()
        {
            var builder = ImmutableSegmentedHashSet.CreateRange(Enumerable.Range(1, 3)).ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.Overlaps(null!));
            Assert.True(builder.Overlaps(Enumerable.Range(3, 2)));
            Assert.False(builder.Overlaps(Enumerable.Range(4, 3)));
        }

        [Fact]
        public void Remove()
        {
            var builder = ImmutableSegmentedHashSet.Create("a").ToBuilder();
            Assert.False(builder.Remove("b"));
            Assert.True(builder.Remove("a"));
        }

        [Fact]
        public void SetEquals()
        {
            var builder = ImmutableSegmentedHashSet.Create("a").ToBuilder();
            Assert.Throws<ArgumentNullException>("other", () => builder.SetEquals(null!));
            Assert.False(builder.SetEquals(new[] { "b" }));
            Assert.True(builder.SetEquals(new[] { "a" }));
            Assert.True(builder.SetEquals(builder));
        }

        [Fact]
        public void ICollectionOfTMethods()
        {
            ICollection<string> builder = ImmutableSegmentedHashSet.Create("a").ToBuilder();
            builder.Add("b");
            Assert.True(builder.Contains("b"));

            var array = new string[3];
            builder.CopyTo(array, 1);
            Assert.Null(array[0]);
            CollectionAssertAreEquivalent(new[] { null, "a", "b" }, array);

            Assert.False(builder.IsReadOnly);

            CollectionAssertAreEquivalent(new[] { "a", "b" }, builder.ToArray()); // tests enumerator
        }

        [Fact]
        public void NullHandling()
        {
            var builder = ImmutableSegmentedHashSet<string?>.Empty.ToBuilder();
            Assert.True(builder.Add(null));
            Assert.False(builder.Add(null));
            Assert.True(builder.Contains(null));
            Assert.True(builder.Remove(null));

            builder.UnionWith(new[] { null, "a" });
            Assert.True(builder.IsSupersetOf(new[] { null, "a" }));
            Assert.True(builder.IsSubsetOf(new[] { null, "a" }));
            Assert.True(builder.IsProperSupersetOf(new[] { (string?)null }));
            Assert.True(builder.IsProperSubsetOf(new[] { null, "a", "b" }));

            builder.IntersectWith(new[] { (string?)null });
            Assert.Equal(1, builder.Count);

            builder.ExceptWith(new[] { (string?)null });
            Assert.False(builder.Remove(null));
        }

        [Fact(Skip = "https://github.com/dotnet/roslyn/issues/54716")]
        public void DebuggerAttributesValid()
        {
            DebuggerAttributes.ValidateDebuggerDisplayReferences(ImmutableSegmentedHashSet.CreateBuilder<int>());
        }

        [Fact]
        public void ToImmutableHashSet()
        {
            ImmutableSegmentedHashSet<int>.Builder builder = ImmutableSegmentedHashSet.CreateBuilder<int>();
            builder.Add(1);
            builder.Add(2);
            builder.Add(3);

            var set = System.Collections.Immutable.ImmutableSortedSet.ToImmutableSortedSet(builder);
            Assert.True(builder.Contains(1));
            Assert.True(builder.Contains(2));
            Assert.True(builder.Contains(3));

            builder.Remove(3);
            Assert.False(builder.Contains(3));
            Assert.True(set.Contains(3));

            builder.Clear();
            Assert.True(builder.ToImmutableSegmentedHashSet().IsEmpty);
            Assert.False(set.IsEmpty);

            ImmutableSegmentedHashSet<int>.Builder? nullBuilder = null;
            Assert.Throws<ArgumentNullException>("builder", () => nullBuilder!.ToImmutableSegmentedHashSet());
        }

        [Fact]
        public void TryGetValue()
        {
            var builder = ImmutableSegmentedHashSet.Create(1, 2, 3).ToBuilder();
            Assert.True(builder.TryGetValue(2, out _));

            builder = ImmutableSegmentedHashSet.Create(CustomEqualityComparer.Instance, 1, 2, 3, 4).ToBuilder();
            var existing = 0;
            Assert.True(builder.TryGetValue(5, out existing));
            Assert.Equal(4, existing);
        }

        private class CustomEqualityComparer : IEqualityComparer<int>
        {
            private CustomEqualityComparer()
            {
            }

            public static CustomEqualityComparer Instance { get; } = new CustomEqualityComparer();

            public bool Equals(int x, int y) => x >> 1 == y >> 1;

            public int GetHashCode(int obj) => (obj >> 1).GetHashCode();
        }
    }
}
