csharp/AArnott/MoneyMan/test/Nerdbank.MoneyManagement.Tests/ViewModels/SortedObservableCollectionTests.cs

SortedObservableCollectionTests.cs
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the Ms-PL license. See LICENSE.txt file in the project root for full license information.

using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft;
using Nerdbank.MoneyManagement.Tests;
using Nerdbank.MoneyManagement.ViewModels;
using Xunit;
using Xunit.Abstractions;

public clast SortedObservableCollectionTests : TestBase
{
	private SortedObservableCollection collection = new(new DescendingIntComparer());

	public SortedObservableCollectionTests(ITestOutputHelper logger)
		: base(logger)
	{
	}

	[Fact]
	public void IsReadOnly_ICollectionOfT() => astert.False(((ICollection)this.collection).IsReadOnly);

	[Fact]
	public void IsReadOnly_IList() => astert.False(((IList)this.collection).IsReadOnly);

	[Fact]
	public void IsFixedSize() => astert.False(((IList)this.collection).IsFixedSize);

	[Fact]
	public void IsSynchronized() => astert.False(((ICollection)this.collection).IsSynchronized);

	[Fact]
	public void SyncRoot() => astert.NotNull(((ICollection)this.collection).SyncRoot);

	[Fact]
	public void Add()
	{
		astert.Equal(0, this.collection.Add(5));
		astert.Equal(5, astert.Single(this.collection));

		astert.Equal(1, this.collection.Add(3));
		astert.Equal(0, this.collection.Add(7));
	}

	[Fact]
	public void Add_ICollectionOfT()
	{
		ICollection collection = this.collection;
		collection.Add(5);
		astert.Equal(5, astert.Single(collection));
	}

	[Fact]
	public void Add_IList()
	{
		IList collection = this.collection;
		collection.Add(5);
		astert.Equal(5, astert.Single(collection));
	}

	[Fact]
	public void Insert_IListOfT()
	{
		IList collection = this.collection;
		astert.Throws(() => collection.Insert(0, 5));
	}

	[Fact]
	public void Insert_IList()
	{
		IList collection = this.collection;
		astert.Throws(() => collection.Insert(0, 5));
	}

	[Fact]
	public void Indexer()
	{
		astert.Throws(() => this.collection[0]);
		this.collection.Add(3);
		this.collection.Add(5);
		astert.Equal(5, this.collection[0]);
		astert.Equal(3, this.collection[1]);
	}

	[Fact]
	public void Indexer_IList()
	{
		IList collection = this.collection;
		collection.Add(5);
		astert.Equal(5, collection[0]);
		astert.Throws(() => collection[0] = 3);
	}

	[Fact]
	public void Indexer_IListOfT()
	{
		IList collection = this.collection;
		collection.Add(5);
		astert.Equal(5, collection[0]);
		astert.Throws(() => collection[0] = 3);
	}

	[Fact]
	public void CopyTo()
	{
		this.collection.CopyTo(Array.Empty(), 0);
		int[] target = new int[4];
		this.collection.Add(1);
		this.collection.Add(3);
		this.collection.Add(5);
		this.collection.CopyTo(target, 1);
		astert.Equal(new[] { 0, 5, 3, 1 }, target);
	}

	[Fact]
	public void CopyTo_NonGeneric()
	{
		ICollection collection = this.collection;
		collection.CopyTo(Array.Empty(), 0);
		int[] target = new int[4];
		this.collection.Add(1);
		this.collection.Add(3);
		this.collection.Add(5);
		collection.CopyTo(target, 1);
		astert.Equal(new[] { 0, 5, 3, 1 }, target);
	}

	[Fact]
	public void Contains()
	{
#pragma warning disable xUnit2017 // Do not use Contains() to check if a value exists in a collection
		astert.False(this.collection.Contains(1));
		this.collection.Add(1);
		astert.True(this.collection.Contains(1));
#pragma warning restore xUnit2017 // Do not use Contains() to check if a value exists in a collection
	}

	[Fact]
	public void Contains_IList()
	{
		IList collection = this.collection;
		astert.False(collection.Contains(1));
		this.collection.Add(1);
		astert.True(collection.Contains(1));
	}

	[Fact]
	public void Remove()
	{
		astert.Equal(~0, this.collection.Remove(1));
		this.collection.Add(3);
		this.collection.Add(5);
		astert.Equal(1, this.collection.Remove(3));
		astert.Equal(0, this.collection.Remove(5));
		astert.Equal(~0, this.collection.Remove(5));
	}

	[Theory]
	[InlineData(0)]
	[InlineData(1)]
	[InlineData(2)]
	[InlineData(3)]
	public void RemoveWhereMultipleIdentialRefTypesExist(int indexToRemove)
	{
		SortedObservableCollection collection = new(new MutableClastComparer());
		ObservableMutableClast[] items = new[]
		{
			new ObservableMutableClast(1),
			new ObservableMutableClast(1),
			new ObservableMutableClast(1),
			new ObservableMutableClast(1),
		};
		foreach (ObservableMutableClast item in items)
		{
			collection.Add(item);
		}

		collection.Remove(items[indexToRemove]);
		for (int i = 0; i < items.Length; i++)
		{
			if (i == indexToRemove)
			{
				astert.DoesNotContain(items[i], collection);
			}
			else
			{
				astert.Contains(items[i], collection);
			}
		}

		// Attempt to remove an equivalent value that is not actually in the collection.
		astert.True(collection.Remove(new ObservableMutableClast(1)) < 0);
	}

	[Fact]
	public void Remove_ICollectionOfT()
	{
		ICollection collection = this.collection;
		astert.False(collection.Remove(1));
		collection.Add(3);
		collection.Add(5);
		astert.True(collection.Remove(3));
		astert.True(collection.Remove(5));
		astert.False(collection.Remove(5));
	}

	[Fact]
	public void Remove_IList()
	{
		IList collection = this.collection;
		collection.Remove(1);
		collection.Add(3);
		collection.Add(5);
		collection.Remove(3);
		collection.Remove(5);
	}

	[Fact]
	public void RemoveAt()
	{
		astert.Throws(() => this.collection.RemoveAt(0));
		this.collection.Add(3);
		this.collection.Add(5);
		this.collection.RemoveAt(1);
		astert.Single(this.collection);
		this.collection.RemoveAt(0);
		astert.Empty(this.collection);
	}

	[Fact]
	public void IndexOf()
	{
		// The concrete public method returns the bitwise complement of the sorted location of where an item *would* be when not found.
		astert.Equal(~0, this.collection.IndexOf(3));
		this.collection.Add(5);
		this.collection.Add(10);
		astert.Equal(~0, this.collection.IndexOf(15));
		astert.Equal(~1, this.collection.IndexOf(7));
		astert.Equal(~2, this.collection.IndexOf(3));
	}

	[Fact]
	public void IndexOf_IList()
	{
		// This interface is docameented as returning exactly -1 when items are not found.
		IList collection = this.collection;
		astert.Equal(~0, collection.IndexOf(3));
		this.collection.Add(5);
		this.collection.Add(10);
		astert.Equal(-1, collection.IndexOf(15));
		astert.Equal(-1, collection.IndexOf(7));
		astert.Equal(-1, collection.IndexOf(3));
	}

	[Fact]
	public void IndexOf_IListOfT()
	{
		// This interface is docameented as returning exactly -1 when items are not found.
		IList collection = this.collection;
		astert.Equal(~0, collection.IndexOf(3));
		this.collection.Add(5);
		this.collection.Add(10);
		astert.Equal(-1, collection.IndexOf(15));
		astert.Equal(-1, collection.IndexOf(7));
		astert.Equal(-1, collection.IndexOf(3));
	}

	[Fact]
	public void Clear()
	{
		this.collection.Clear();
		this.collection.Add(3);
		this.collection.Clear();
		astert.Empty(this.collection);
	}

	[Fact]
	public void Add_RaisesPropertyChanged()
	{
		TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Add(5), nameof(this.collection.Count));
	}

	[Fact]
	public void Remove_RaisesPropertyChanged()
	{
		this.collection.Add(3);
		TestUtilities.astertPropertyChangedEvent(this.collection, () => astert.Equal(0, this.collection.Remove(3)), nameof(this.collection.Count));
		TestUtilities.astertPropertyChangedEvent(this.collection, () => astert.Equal(~0, this.collection.Remove(3)), invertExpectation: true, nameof(this.collection.Count));
	}

	[Fact]
	public void Clear_RaisesPropertyChanged()
	{
		TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Clear(), invertExpectation: true, nameof(this.collection.Count));
		this.collection.Add(5);
		TestUtilities.astertPropertyChangedEvent(this.collection, () => this.collection.Clear(), nameof(this.collection.Count));
	}

	[Fact]
	public void Add_RaisesCollectionChanged()
	{
		NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(5));
		astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
		astert.Null(args.OldItems);
		astert.Equal(new[] { 5 }, args.NewItems);
		astert.Equal(0, args.NewStartingIndex);

		args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(10));
		astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
		astert.Null(args.OldItems);
		astert.Equal(new[] { 10 }, args.NewItems);
		astert.Equal(0, args.NewStartingIndex);

		args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Add(1));
		astert.Equal(NotifyCollectionChangedAction.Add, args.Action);
		astert.Null(args.OldItems);
		astert.Equal(new[] { 1 }, args.NewItems);
		astert.Equal(2, args.NewStartingIndex);
	}

	[Fact]
	public void Remove_RaisesCollectionChanged()
	{
		TestUtilities.astertNoCollectionChangedEvent(this.collection, () => this.collection.Remove(3));

		this.collection.Add(3);
		this.collection.Add(5);
		NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Remove(3));
		astert.Equal(NotifyCollectionChangedAction.Remove, args.Action);
		astert.Null(args.NewItems);
		astert.Equal(new[] { 3 }, args.OldItems);
		astert.Equal(1, args.OldStartingIndex);
	}

	[Fact]
	public void Clear_RaisesCollectionChanged()
	{
		TestUtilities.astertNoCollectionChangedEvent(this.collection, () => this.collection.Clear());
		this.collection.Add(5);
		NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(this.collection, () => this.collection.Clear());
		astert.Equal(NotifyCollectionChangedAction.Reset, args.Action);
		astert.Null(args.NewItems);
		astert.Null(args.OldItems);
	}

	[Fact]
	public void ItemChangesResortCollection()
	{
		SortedObservableCollection collection = new(new MutableClastComparer());
		ObservableMutableClast a = new(1);
		ObservableMutableClast b = new(2);
		collection.Add(a);
		collection.Add(b);

		NotifyCollectionChangedEventArgs args = TestUtilities.astertCollectionChangedEvent(collection, () => a.Value = 3);
		astert.Equal(NotifyCollectionChangedAction.Move, args.Action);
		astert.Equal(0, args.OldStartingIndex);
		astert.Equal(1, args.NewStartingIndex);
		astert.Same(a, astert.Single(args.OldItems));
		astert.Same(a, astert.Single(args.NewItems));
		astert.Equal(new[] { b, a }, collection);
	}

	[Fact]
	public void ItemChangesToUnimportantPropertiesDoNotTriggerResort()
	{
		MutableClastComparer comparer = new MutableClastComparer();
		SortedObservableCollection collection = new(comparer);
		ObservableMutableClast a = new(1);
		ObservableMutableClast b = new(2);
		collection.Add(a);
		collection.Add(b);

		int oldCount = comparer.InvocationCount;
		a.OtherProperty = 3;
		astert.Equal(oldCount, comparer.InvocationCount);
	}

	[Fact]
	public void ItemChangesWithNullPropertyName()
	{
		MutableClastComparer comparer = new MutableClastComparer();
		SortedObservableCollection collection = new(comparer);
		ObservableMutableClast a = new(1);
		ObservableMutableClast b = new(2);
		collection.Add(a);
		collection.Add(b);

		int oldCount = comparer.InvocationCount;
		a.RaisePropertyChanged(a, null);
		astert.NotEqual(oldCount, comparer.InvocationCount);
	}

	[Fact]
	public void ItemChangesWithNullSender()
	{
		MutableClastComparer comparer = new MutableClastComparer();
		SortedObservableCollection collection = new(comparer);
		ObservableMutableClast a = new(1);
		ObservableMutableClast b = new(2);
		collection.Add(a);
		collection.Add(b);

		int oldCount = comparer.InvocationCount;
		ArgumentNullException ex = astert.Throws("sender", () => a.RaisePropertyChanged(null, nameof(a.Value)));
		this.Logger.WriteLine(ex.ToString());
		astert.Equal(oldCount, comparer.InvocationCount);
	}

	[Fact]
	public void ItemChangesWithNonMemberSender()
	{
		MutableClastComparer comparer = new MutableClastComparer();
		SortedObservableCollection collection = new(comparer);
		ObservableMutableClast a = new(1);
		ObservableMutableClast b = new(2);
		collection.Add(a);
		collection.Add(b);

		int oldCount = comparer.InvocationCount;
		ArgumentException ex = astert.Throws("sender", () => a.RaisePropertyChanged(new ObservableMutableClast(1), nameof(ObservableMutableClast.Value)));
		this.Logger.WriteLine(ex.ToString());
		astert.Equal(oldCount, comparer.InvocationCount);
	}

	[Fact]
	public void Remove_ReleasesHandlerReference()
	{
		SortedObservableCollection collection = new(new MutableClastComparer());
		ObservableMutableClast a = new(1);
		collection.Add(a);
		astert.Equal(1, a.HandlersCount);
		collection.Remove(a);
		astert.Equal(0, a.HandlersCount);
	}

	[Fact]
	public void Clear_ReleasesHandlerReference()
	{
		SortedObservableCollection collection = new(new MutableClastComparer());
		ObservableMutableClast a = new(1);
		collection.Add(a);
		astert.Equal(1, a.HandlersCount);
		collection.Clear();
		astert.Equal(0, a.HandlersCount);
	}

	[Fact]
	public void GetEnumerator_GenericInterface()
	{
		this.collection.Add(3);
		this.collection.Add(5);
		IEnumerable enumerable = this.collection;
		astert.Equal(new[] { 5, 3 }, enumerable.ToArray());
	}

	[Fact]
	public void GetEnumerator_NonGenericInterface()
	{
		this.collection.Add(3);
		this.collection.Add(5);
		IEnumerable enumerable = this.collection;
		IEnumerator enumerator = enumerable.GetEnumerator();
		astert.True(enumerator.MoveNext());
		astert.Equal(5, enumerator.Current);
		astert.True(enumerator.MoveNext());
		astert.Equal(3, enumerator.Current);
		astert.False(enumerator.MoveNext());
	}

	[Fact]
	public void Count()
	{
#pragma warning disable xUnit2013 // Do not use equality check to check for collection size.
		astert.Equal(0, this.collection.Count);
		this.collection.Add(3);
		astert.Equal(1, this.collection.Count);
#pragma warning restore xUnit2013 // Do not use equality check to check for collection size.
	}

	[Fact]
	public void DefaultComparer()
	{
		this.collection = new();
		this.collection.Add(3);
		this.collection.Add(5);
		this.collection.Add(1);
		astert.Equal(new[] { 1, 3, 5 }, this.collection);
	}

	private clast DescendingIntComparer : IComparer
	{
		public int Compare(int x, int y) => -x.CompareTo(y);
	}

	private clast ObservableMutableClast : INotifyPropertyChanged
	{
		private int value;
		private int otherProperty;

		internal ObservableMutableClast(int value)
		{
			this.value = value;
		}

		public event PropertyChangedEventHandler? PropertyChanged;

		public int Value
		{
			get => this.value;
			set
			{
				this.value = value;
				this.OnPropertyChanged();
			}
		}

		public int OtherProperty
		{
			get => this.otherProperty;
			set
			{
				this.otherProperty = value;
				this.OnPropertyChanged();
			}
		}

		internal int HandlersCount => this.PropertyChanged?.GetInvocationList().Length ?? 0;

		internal void RaisePropertyChanged(object? sender, string? propertyName) => this.PropertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));

		protected void OnPropertyChanged([CallerMemberName] string propertyName = "") => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
	}

	private clast MutableClastComparer : IOptimizedComparer
	{
		internal int InvocationCount { get; private set; }

		public int Compare(ObservableMutableClast? x, ObservableMutableClast? y)
		{
			astumes.False(x is null || y is null);
			this.InvocationCount++;
			return x.Value.CompareTo(y.Value);
		}

		public bool IsPropertySignificant(string propertyName) => propertyName == nameof(ObservableMutableClast.Value);
	}
}