#if NETCOREAPP3_0_OR_GREATER

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NSubstitute;
using Xunit;
using Xunit.Sdk;

public class AsyncCollectionAssertsTests
{
	public class All
	{
		[Fact]
		public static void GuardClauses()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.All(default(IAsyncEnumerable<object>)!, _ => { }));
			Assert.Throws<ArgumentNullException>("action", () => Assert.All(new object[0].ToAsyncEnumerable(), (Action<object>)null!));
			Assert.Throws<ArgumentNullException>("action", () => Assert.All(new object[0].ToAsyncEnumerable(), (Action<object, int>)null!));
		}

		[Fact]
		public static void Success()
		{
			var items = new[] { 1, 1, 1, 1, 1, 1 }.ToAsyncEnumerable();

			Assert.All(items, x => Assert.Equal(1, x));
		}

		[Fact]
		public static void Failure()
		{
			var items = new[] { 1, 1, 42, 2112, 1, 1 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.All(items, item => Assert.Equal(1, item)));

			Assert.IsType<AllException>(ex);
			Assert.Equal(
				"Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine +
				"[2]: Item:  42" + Environment.NewLine +
				"     Error: Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 1" + Environment.NewLine +
				"            Actual:   42" + Environment.NewLine +
				"[3]: Item:  2112" + Environment.NewLine +
				"     Error: Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 1" + Environment.NewLine +
				"            Actual:   2112",
				ex.Message
			);
		}

		[Fact]
		public static void ActionCanReceiveIndex()
		{
			var items = new[] { 1, 1, 2, 2, 1, 1 };
			var indices = new List<int>();

			Assert.All(items, (_, idx) => indices.Add(idx));

			Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices);
		}
	}

	public class AllAsync
	{
		[Fact]
		public static async Task GuardClauses()
		{
			await Assert.ThrowsAsync<ArgumentNullException>("collection", () => Assert.AllAsync(default(IAsyncEnumerable<object>)!, async _ => await Task.Yield()));
			await Assert.ThrowsAsync<ArgumentNullException>("action", () => Assert.AllAsync(new object[0].ToAsyncEnumerable(), (Func<object, Task>)null!));
			await Assert.ThrowsAsync<ArgumentNullException>("action", () => Assert.AllAsync(new object[0].ToAsyncEnumerable(), (Func<object, int, Task>)null!));
		}

		[Fact]
		public static async Task Success()
		{
			var items = new[] { 1, 1, 1, 1, 1, 1 }.ToAsyncEnumerable();

			await Assert.AllAsync(items, async item => { await Task.Yield(); Assert.Equal(1, item); });
		}

		[Fact]
		public static void Failure()
		{
			var items = new[] { 1, 1, 42, 2112, 1, 1 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.All(items, x => Assert.Equal(1, x)));

			Assert.IsType<AllException>(ex);
			Assert.Equal(
				"Assert.All() Failure: 2 out of 6 items in the collection did not pass." + Environment.NewLine +
				"[2]: Item:  42" + Environment.NewLine +
				"     Error: Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 1" + Environment.NewLine +
				"            Actual:   42" + Environment.NewLine +
				"[3]: Item:  2112" + Environment.NewLine +
				"     Error: Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 1" + Environment.NewLine +
				"            Actual:   2112",
				ex.Message
			);
		}

		[Fact]
		public static async Task ActionCanReceiveIndex()
		{
			var items = new[] { 1, 1, 2, 2, 1, 1 }.ToAsyncEnumerable();
			var indices = new List<int>();

			await Assert.AllAsync(items, async (_, idx) => { await Task.Yield(); indices.Add(idx); });

			Assert.Equal(new[] { 0, 1, 2, 3, 4, 5 }, indices);
		}
	}

	public class Collection
	{
		[Fact]
		public static void EmptyCollection()
		{
			var list = new List<int>().ToAsyncEnumerable();

#pragma warning disable xUnit2011 // Do not use empty collection check
			Assert.Collection(list);
#pragma warning restore xUnit2011 // Do not use empty collection check
		}

		[Fact]
		public static void MismatchedElementCount()
		{
			var list = new List<int>().ToAsyncEnumerable();

			var ex = Record.Exception(
				() => Assert.Collection(list,
					item => Assert.True(false)
				)
			);

			var collEx = Assert.IsType<CollectionException>(ex);
			Assert.Equal(
				"Assert.Collection() Failure: Mismatched item count" + Environment.NewLine +
				"Collection:     []" + Environment.NewLine +
				"Expected count: 1" + Environment.NewLine +
				"Actual count:   0",
				collEx.Message
			);
			Assert.Null(collEx.InnerException);
		}

		[Fact]
		public static void NonEmptyCollection()
		{
			var list = new List<int> { 42, 2112 }.ToAsyncEnumerable();

			Assert.Collection(list,
				item => Assert.Equal(42, item),
				item => Assert.Equal(2112, item)
			);
		}

		[Fact]
		public static void MismatchedElement()
		{
			var list = new List<int> { 42, 2112 }.ToAsyncEnumerable();

			var ex = Record.Exception(() =>
				Assert.Collection(list,
					item => Assert.Equal(42, item),
					item => Assert.Equal(2113, item)
				)
			);

			var collEx = Assert.IsType<CollectionException>(ex);
			Assert.StartsWith(
				"Assert.Collection() Failure: Item comparison failure" + Environment.NewLine +
				"                 ↓ (pos 1)" + Environment.NewLine +
				"Collection: [42, 2112]" + Environment.NewLine +
				"Error:      Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 2113" + Environment.NewLine +
				"            Actual:   2112" + Environment.NewLine +
				"            Stack Trace:",
				ex.Message
			);
		}
	}

	public class CollectionAsync
	{
		[Fact]
		public static async Task EmptyCollection()
		{
			var list = new List<int>().ToAsyncEnumerable();

#pragma warning disable xUnit2011 // Do not use empty collection check
			await Assert.CollectionAsync(list);
#pragma warning restore xUnit2011 // Do not use empty collection check
		}

		[Fact]
		public static async Task MismatchedElementCountAsync()
		{
			var list = new List<int>().ToAsyncEnumerable();

			var ex = await Record.ExceptionAsync(
				() => Assert.CollectionAsync(list,
					async item => await Task.Yield()
				)
			);

			var collEx = Assert.IsType<CollectionException>(ex);
			Assert.Equal(
				"Assert.Collection() Failure: Mismatched item count" + Environment.NewLine +
				"Collection:     []" + Environment.NewLine +
				"Expected count: 1" + Environment.NewLine +
				"Actual count:   0",
				collEx.Message
			);
			Assert.Null(collEx.InnerException);
		}

		[Fact]
		public static async Task NonEmptyCollectionAsync()
		{
			var list = new List<int> { 42, 2112 }.ToAsyncEnumerable();

			await Assert.CollectionAsync(list,
				async item =>
				{
					await Task.Yield();
					Assert.Equal(42, item);
				},
				async item =>
				{
					await Task.Yield();
					Assert.Equal(2112, item);
				}
			);
		}

		[Fact]
		public static async Task MismatchedElementAsync()
		{
			var list = new List<int> { 42, 2112 }.ToAsyncEnumerable();

			var ex = await Record.ExceptionAsync(() =>
				 Assert.CollectionAsync(list,
					 async item =>
					 {
						 await Task.Yield();
						 Assert.Equal(42, item);
					 },
					 async item =>
					 {
						 await Task.Yield();
						 Assert.Equal(2113, item);
					 }
				 )
			);

			var collEx = Assert.IsType<CollectionException>(ex);
			Assert.StartsWith(
				"Assert.Collection() Failure: Item comparison failure" + Environment.NewLine +
				"                 ↓ (pos 1)" + Environment.NewLine +
				"Collection: [42, 2112]" + Environment.NewLine +
				"Error:      Assert.Equal() Failure: Values differ" + Environment.NewLine +
				"            Expected: 2113" + Environment.NewLine +
				"            Actual:   2112" + Environment.NewLine +
				"            Stack Trace:",
				ex.Message
			);
		}
	}

	public class Contains
	{
		[Fact]
		public static void GuardClause()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Contains(14, default(IAsyncEnumerable<int>)!));
		}

		[Fact]
		public static void CanFindNullInContainer()
		{
			var list = new List<object?> { 16, null, "Hi there" }.ToAsyncEnumerable();

			Assert.Contains(null, list);
		}

		[Fact]
		public static void ItemInContainer()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			Assert.Contains(42, list);
		}

		[Fact]
		public static void ItemNotInContainer()
		{
			var list = new List<int> { 41, 43 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Contains(42, list));

			Assert.IsType<ContainsException>(ex);
			Assert.Equal(
				"Assert.Contains() Failure: Item not found in collection" + Environment.NewLine +
				"Collection: [41, 43]" + Environment.NewLine +
				"Not found:  42",
				ex.Message
			);
		}

		[Fact]
		public static void NullsAllowedInContainer()
		{
			var list = new List<object?> { null, 16, "Hi there" }.ToAsyncEnumerable();

			Assert.Contains("Hi there", list);
		}
	}

	public class Contains_Comparer
	{
		[Fact]
		public static void GuardClauses()
		{
			var comparer = Substitute.For<IEqualityComparer<int>>();

			Assert.Throws<ArgumentNullException>("collection", () => Assert.Contains(14, default(IAsyncEnumerable<int>)!, comparer));
			Assert.Throws<ArgumentNullException>("comparer", () => Assert.Contains(14, new int[0].ToAsyncEnumerable(), null!));
		}

		[Fact]
		public static void CanUseComparer()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			Assert.Contains(43, list, new MyComparer());
		}

		class MyComparer : IEqualityComparer<int>
		{
			public bool Equals(int x, int y) => true;

			public int GetHashCode(int obj) => throw new NotImplementedException();
		}
	}

	public class Contains_Predicate
	{
		[Fact]
		public static void GuardClauses()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Contains(default(IAsyncEnumerable<int>)!, item => true));
			Assert.Throws<ArgumentNullException>("filter", () => Assert.Contains(new int[0].ToAsyncEnumerable(), (Predicate<int>)null!));
		}

		[Fact]
		public static void ItemFound()
		{
			var list = new[] { "Hello", "world" }.ToAsyncEnumerable();

			Assert.Contains(list, item => item.StartsWith("w"));
		}

		[Fact]
		public static void ItemNotFound()
		{
			var list = new[] { "Hello", "world" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Contains(list, item => item.StartsWith("q")));

			Assert.IsType<ContainsException>(ex);
			Assert.Equal(
				"Assert.Contains() Failure: Filter not matched in collection" + Environment.NewLine +
				"Collection: [\"Hello\", \"world\"]",
				ex.Message
			);
		}
	}

	public class Distinct
	{
		[Fact]
		public static void GuardClauses()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Distinct(default(IAsyncEnumerable<int>)!));
			Assert.Throws<ArgumentNullException>("comparer", () => Assert.Distinct(new object[0].ToAsyncEnumerable(), null!));
		}

		[Fact]
		public static void WithNull()
		{
			var list = new List<object?> { 16, "Hi there", null }.ToAsyncEnumerable();

			Assert.Distinct(list);
		}

		[Fact]
		public static void TwoItems()
		{
			var list = new List<int> { 42, 42 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Distinct(list));

			Assert.IsType<DistinctException>(ex);
			Assert.Equal(
				"Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine +
				"Collection: [42, 42]" + Environment.NewLine +
				"Item:       42",
				ex.Message
			);
		}

		[Fact]
		public static void TwoNulls()
		{
			var list = new List<string?> { "a", null, "b", null, "c", "d" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Distinct(list));

			Assert.IsType<DistinctException>(ex);
			Assert.Equal(
				"Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine +
				$"Collection: [\"a\", null, \"b\", null, \"c\", {ArgumentFormatter.Ellipsis}]" + Environment.NewLine +
				"Item:       null",
				ex.Message
			);
		}

		[Fact]
		public static void CaseSensitiveStrings()
		{
			var list = new string[] { "a", "b", "A" }.ToAsyncEnumerable();

			Assert.Distinct(list);
			Assert.Distinct(list, StringComparer.Ordinal);
		}

		[Fact]
		public static void CaseInsensitiveStrings()
		{
			var list = new string[] { "a", "b", "A" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Distinct(list, StringComparer.OrdinalIgnoreCase));

			Assert.IsType<DistinctException>(ex);
			Assert.Equal(
				"Assert.Distinct() Failure: Duplicate item found" + Environment.NewLine +
				"Collection: [\"a\", \"b\", \"A\"]" + Environment.NewLine +
				"Item:       \"A\"",
				ex.Message
			);
		}
	}

	public class DoesNotContain
	{
		[Fact]
		public static void GuardClause()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.DoesNotContain(14, default(IAsyncEnumerable<int>)!));
		}

		[Fact]
		public static void CanSearchForNullInContainer()
		{
			var list = new List<object?> { 16, "Hi there" }.ToAsyncEnumerable();

			Assert.DoesNotContain(null, list);
		}

		[Fact]
		public static void ItemInContainer()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.DoesNotContain(42, list));

			Assert.IsType<DoesNotContainException>(ex);
			Assert.Equal(
				"Assert.DoesNotContain() Failure: Item found in collection" + Environment.NewLine +
				"             ↓ (pos 0)" + Environment.NewLine +
				"Collection: [42]" + Environment.NewLine +
				"Found:      42",
				ex.Message
			);
		}

		[Fact]
		public static void ItemNotInContainer()
		{
			var list = new List<int>().ToAsyncEnumerable();

			Assert.DoesNotContain(42, list);
		}

		[Fact]
		public static void NullsAllowedInContainer()
		{
			var list = new List<object?> { null, 16, "Hi there" }.ToAsyncEnumerable();

			Assert.DoesNotContain(42, list);
		}
	}

	public class DoesNotContain_Comparer
	{
		[Fact]
		public static void GuardClauses()
		{
			var comparer = Substitute.For<IEqualityComparer<int>>();

			Assert.Throws<ArgumentNullException>("collection", () => Assert.DoesNotContain(14, default(IAsyncEnumerable<int>)!, comparer));
			Assert.Throws<ArgumentNullException>("comparer", () => Assert.DoesNotContain(14, new int[0].ToAsyncEnumerable(), null!));
		}

		[Fact]
		public static void CanUseComparer()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			Assert.DoesNotContain(42, list, new MyComparer());
		}

		class MyComparer : IEqualityComparer<int>
		{
			public bool Equals(int x, int y) => false;

			public int GetHashCode(int obj) => throw new NotImplementedException();
		}
	}

	public class DoesNotContain_Predicate
	{
		[Fact]
		public static void GuardClauses()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.DoesNotContain(default(IAsyncEnumerable<int>)!, item => true));
			Assert.Throws<ArgumentNullException>("filter", () => Assert.DoesNotContain(new int[0].ToAsyncEnumerable(), (Predicate<int>)null!));
		}

		[Fact]
		public static void ItemFound()
		{
			var list = new[] { "Hello", "world" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.DoesNotContain(list, item => item.StartsWith("w")));

			Assert.IsType<DoesNotContainException>(ex);
			Assert.Equal(
				"Assert.DoesNotContain() Failure: Filter matched in collection" + Environment.NewLine +
				"                      ↓ (pos 1)" + Environment.NewLine +
				"Collection: [\"Hello\", \"world\"]",
				ex.Message
			);
		}

		[Fact]
		public static void ItemNotFound()
		{
			var list = new[] { "Hello", "world" }.ToAsyncEnumerable();

			Assert.DoesNotContain(list, item => item.StartsWith("q"));
		}
	}

	public class Empty
	{
		[Fact]
		public static void GuardClause()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Empty(default(IAsyncEnumerable<int>)!));
		}

		[Fact]
		public static void EmptyCollection()
		{
			var list = new List<int>().ToAsyncEnumerable();

			Assert.Empty(list);
		}

		[Fact]
		public static void NonEmptyCollection()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Empty(list));

			Assert.IsType<EmptyException>(ex);
			Assert.Equal(
				"Assert.Empty() Failure: Collection was not empty" + Environment.NewLine +
				"Collection: [42]",
				ex.Message
			);
		}

		[Fact]
		public static void CollectionEnumeratorDisposed()
		{
			var enumerator = new SpyEnumerator<int>(Enumerable.Empty<int>().ToAsyncEnumerable());

			Assert.Empty(enumerator);

			Assert.True(enumerator.IsDisposed);
		}
	}

	public class Equal
	{
		public class Null
		{
			[Fact]
			public static void BothNull()
			{
				var nullEnumerable = default(IEnumerable<int>);
				var nullAsyncEnumerable = default(IAsyncEnumerable<int>);

				Assert.Equal(nullEnumerable, nullAsyncEnumerable);
				Assert.Equal(nullAsyncEnumerable, nullAsyncEnumerable);
			}

			[Fact]
			public static void EmptyExpectedNullActual()
			{
				var expected = new int[0];
				var actual = default(IAsyncEnumerable<int>);

				void validateError(
					Action action,
					string expectedType)
				{
					var ex = Record.Exception(action);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Collections differ" + Environment.NewLine +
						"Expected: " + expectedType + " []" + Environment.NewLine +
						"Actual:   " + new string(' ', expectedType.Length) + " null",
						ex.Message
					);
				}

				validateError(() => Assert.Equal(expected, actual), "int[]");
				validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual), "<generated>");
			}

			[Fact]
			public static void NullExpectedEmptyActual()
			{
				var actual = new int[0].ToAsyncEnumerable();

				void validateError(Action action)
				{
					var ex = Record.Exception(action);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Collections differ" + Environment.NewLine +
						"Expected:             null" + Environment.NewLine +
						"Actual:   <generated> []",
						ex.Message
					);
				}

				validateError(() => Assert.Equal(default(IEnumerable<int>), actual));
				validateError(() => Assert.Equal(default(IAsyncEnumerable<int>), actual));
			}
		}

		public class Collections
		{
			[Fact]
			public static void Equal()
			{
				var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
				var actual = expected.ToAsyncEnumerable();

				Assert.Equal(expected, actual);
				Assert.Equal(expected.ToAsyncEnumerable(), actual);
			}

			[Theory]
			// Nulls
			[InlineData(
				null, null, "null",
				new[] { 1, 2, 3, 4, 5, 6, 7 }, "[1, 2, 3, 4, 5, $$ELLIPSIS$$]", null
			)]
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[1, 2, 3, 4, 5, $$ELLIPSIS$$]",
				null, "null", null
			)]
			// Start of array
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, " ↓ (pos 0)", "[1, 2, 3, 4, 5, $$ELLIPSIS$$]",
				new[] { 99, 2, 3, 4, 5, 6, 7 }, "[99, 2, 3, 4, 5, $$ELLIPSIS$$]", " ↑ (pos 0)"
			)]
			// Middle of array
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, "            ↓ (pos 3)", "[$$ELLIPSIS$$, 2, 3, 4, 5, 6, $$ELLIPSIS$$]",
				new[] { 1, 2, 3, 99, 5, 6, 7 }, "[$$ELLIPSIS$$, 2, 3, 99, 5, 6, $$ELLIPSIS$$]", "            ↑ (pos 3)"
			)]
			// End of array
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, "                  ↓ (pos 6)", "[$$ELLIPSIS$$, 3, 4, 5, 6, 7]",
				new[] { 1, 2, 3, 4, 5, 6, 99 }, "[$$ELLIPSIS$$, 3, 4, 5, 6, 99]", "                  ↑ (pos 6)"
			)]
			//// Overruns
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 4, 5, 6, 7]",
				new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]", "                  ↑ (pos 7)"
			)]
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7 }, null, "[$$ELLIPSIS$$, 6, 7]",
				new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]", "            ↑ (pos 7)"
			)]
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7, 8 }, "                  ↓ (pos 7)", "[$$ELLIPSIS$$, 4, 5, 6, 7, 8]",
				new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 4, 5, 6, 7]", null
			)]
			[InlineData(
				new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 }, "            ↓ (pos 7)", "[$$ELLIPSIS$$, 6, 7, 8, 9, 10, $$ELLIPSIS$$]",
				new[] { 1, 2, 3, 4, 5, 6, 7 }, "[$$ELLIPSIS$$, 6, 7]", null
			)]
			public void NotEqual(
				int[]? expected,
				string? expectedPointer,
				string expectedDisplay,
				int[]? actualArray,
				string actualDisplay,
				string? actualPointer)
			{
				var actual = actualArray is null ? null : new List<int>(actualArray).ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType)
				{
					var message = "Assert.Equal() Failure: Collections differ";
					var actualType = actualArray is null ? "" : "<generated> ";

					if (actualType == expectedType)
					{
						actualType = "";
						expectedType = "";
					}

					var padding = Math.Max(expectedType.Length, actualType.Length);
					var paddingBlanks = new string(' ', padding);

					if (expectedPointer is not null)
						message += Environment.NewLine + "          " + paddingBlanks + expectedPointer;

					message +=
						Environment.NewLine + "Expected: " + expectedType.PadRight(padding) + expectedDisplay +
						Environment.NewLine + "Actual:   " + actualType.PadRight(padding) + actualDisplay;

					if (actualPointer is not null)
						message += Environment.NewLine + "          " + paddingBlanks + actualPointer;

					var ex = Record.Exception(action);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(message.Replace("$$ELLIPSIS$$", ArgumentFormatter.Ellipsis), ex.Message);
				}

				validateError(() => Assert.Equal(expected, actual), expected is null ? "" : "int[] ");
				validateError(() => Assert.Equal(expected?.ToAsyncEnumerable(), actual), expected is null ? "" : "<generated> ");
			}
		}

		public class CollectionsWithComparer
		{
			[Fact]
			public static void AlwaysFalse()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = expected.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Collections differ" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"Actual:   " + actualType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
				}

				validateError(() => Assert.Equal(expected, actual, new IntComparer(false)), "int[] ", "<generated> ");
				validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, new IntComparer(false)), "", "");
			}

			[Fact]
			public static void AlwaysTrue()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new int[] { 0, 0, 0, 0, 0 }.ToAsyncEnumerable();

				Assert.Equal(expected, actual, new IntComparer(true));
				Assert.Equal(expected.ToAsyncEnumerable(), actual, new IntComparer(true));
			}

			class IntComparer : IEqualityComparer<int>
			{
				readonly bool answer;

				public IntComparer(bool answer)
				{
					this.answer = answer;
				}

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

				public int GetHashCode(int obj) => throw new NotImplementedException();
			}

			// https://github.com/xunit/xunit/issues/2795
			[Fact]
			public void CollectionItemIsEnumerable()
			{
				List<EnumerableItem> actual = new List<EnumerableItem> { new(0), new(2) };
				List<EnumerableItem> expected = new List<EnumerableItem> { new(1), new(3) };

				Assert.Equal(expected, actual.ToAsyncEnumerable(), new EnumerableItemComparer());
				Assert.Equal(expected.ToAsyncEnumerable(), actual.ToAsyncEnumerable(), new EnumerableItemComparer());
			}

			public class EnumerableItemComparer : IEqualityComparer<EnumerableItem>
			{
				public bool Equals(EnumerableItem? x, EnumerableItem? y) =>
					x?.Value / 2 == y?.Value / 2;

				public int GetHashCode(EnumerableItem obj) =>
					throw new NotImplementedException();
			}

			public sealed class EnumerableItem : IEnumerable<string>
			{
				public int Value { get; }

				public EnumerableItem(int value) => Value = value;

				public IEnumerator<string> GetEnumerator() => Enumerable.Repeat("", Value).GetEnumerator();

				IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
			}

			[Fact]
			public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException()
			{
				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"Actual:   " + actualType.PadRight(padding) + "[1, 3]" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
					Assert.IsType<DivideByZeroException>(ex.InnerException);
				}

				validateError(() => Assert.Equal(new[] { 1, 2 }, new[] { 1, 3 }.ToAsyncEnumerable(), new ThrowingComparer()), "int[] ", "<generated> ");
				validateError(() => Assert.Equal(new[] { 1, 2 }.ToAsyncEnumerable(), new[] { 1, 3 }.ToAsyncEnumerable(), new ThrowingComparer()), "", "");
			}

			public class ThrowingComparer : IEqualityComparer<int>
			{
				public bool Equals(int x, int y) =>
					throw new DivideByZeroException();

				public int GetHashCode(int obj) =>
					throw new NotImplementedException();
			}
		}

		public class CollectionsWithEquatable
		{
			[Fact]
			public void Equal()
			{
				var expected = new[] { new EquatableObject { Char = 'a' } };
				var actual = new[] { new EquatableObject { Char = 'a' } }.ToAsyncEnumerable();

				Assert.Equal(expected, actual);
				Assert.Equal(expected.ToAsyncEnumerable(), actual);
			}

			[Fact]
			public void NotEqual()
			{
				var expected = new[] { new EquatableObject { Char = 'a' } };
				var actual = new[] { new EquatableObject { Char = 'b' } }.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Collections differ" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: " + expectedType.PadRight(padding) + "[EquatableObject { Char = 'a' }]" + Environment.NewLine +
						"Actual:   " + actualType.PadRight(padding) + "[EquatableObject { Char = 'b' }]" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
				}

				validateError(() => Assert.Equal(expected, actual), "EquatableObject[] ", "<generated> ");
				validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual), "", "");
			}

			public class EquatableObject : IEquatable<EquatableObject>
			{
				public char Char { get; set; }

				public bool Equals(EquatableObject? other) =>
					other != null && other.Char == Char;
			}
		}

		public class CollectionsWithFunc
		{
			[Fact]
			public static void AlwaysFalse()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 1, 2, 3, 4, 5 }).ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Collections differ" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"Actual:   " + actualType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
				}

				validateError(() => Assert.Equal(expected, actual, (x, y) => false), "int[] ", "<generated> ");
				validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, (int x, int y) => false), "", "");
			}

			[Fact]
			public static void AlwaysTrue()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 0, 0, 0, 0, 0 }).ToAsyncEnumerable();

				Assert.Equal(expected, actual, (x, y) => true);
				Assert.Equal(expected.ToAsyncEnumerable(), actual, (int x, int y) => true);
			}

			// https://github.com/xunit/xunit/issues/2795
			[Fact]
			public void CollectionItemIsEnumerable()
			{
				var expected = new List<EnumerableItem> { new(1), new(3) };
				var actual = new List<EnumerableItem> { new(0), new(2) }.ToAsyncEnumerable();

				Assert.Equal(expected, actual, (x, y) => x.Value / 2 == y.Value / 2);
				Assert.Equal(expected.ToAsyncEnumerable(), actual, (x, y) => x.Value / 2 == y.Value / 2);
			}

			public sealed class EnumerableItem : IEnumerable<string>
			{
				public int Value { get; }

				public EnumerableItem(int value) => Value = value;

				public IEnumerator<string> GetEnumerator() => Enumerable.Repeat("", Value).GetEnumerator();

				IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
			}

			[Fact]
			public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException()
			{
				var expected = new[] { 1, 2 };
				var actual = new[] { 1, 3 }.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<EqualException>(ex);
					Assert.Equal(
						"Assert.Equal() Failure: Exception thrown during comparison" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"Actual:   " + actualType.PadRight(padding) + "[1, 3]" + Environment.NewLine +
						"          " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
					Assert.IsType<DivideByZeroException>(ex.InnerException);
				}

				validateError(() => Assert.Equal(expected, actual, (int e, int a) => throw new DivideByZeroException()), "int[] ", "<generated> ");
				validateError(() => Assert.Equal(expected.ToAsyncEnumerable(), actual, (int e, int a) => throw new DivideByZeroException()), "", "");
			}
		}
	}

	public class NotEmpty
	{
		[Fact]
		public static void EmptyContainer()
		{
			var list = new List<int>().ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.NotEmpty(list));

			Assert.IsType<NotEmptyException>(ex);
			Assert.Equal(
				"Assert.NotEmpty() Failure: Collection was empty",
				ex.Message
			);
		}

		[Fact]
		public static void NonEmptyContainer()
		{
			var list = new List<int> { 42 }.ToAsyncEnumerable();

			Assert.NotEmpty(list);
		}

		[Fact]
		public static void EnumeratorDisposed()
		{
			var enumerator = new SpyEnumerator<int>(Enumerable.Range(0, 1).ToAsyncEnumerable());

			Assert.NotEmpty(enumerator);

			Assert.True(enumerator.IsDisposed);
		}
	}

	public class NotEqual
	{
		public class Null
		{
			[Fact]
			public static void BothNull()
			{
				var nullEnumerable = default(IEnumerable<int>);
				var nullAsyncEnumerable = default(IAsyncEnumerable<int>);

				void validateError(Action action)
				{
					var ex = Record.Exception(action);
					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Values are equal" + Environment.NewLine +
						"Expected: Not null" + Environment.NewLine +
						"Actual:       null",
						ex.Message
					);
				}

				validateError(() => Assert.NotEqual(nullEnumerable, nullAsyncEnumerable));
				validateError(() => Assert.NotEqual(nullAsyncEnumerable, nullAsyncEnumerable));
			}

			[Fact]
			public static void EmptyExpectedNullActual()
			{
				var expected = new int[0];
				var actual = default(IAsyncEnumerable<int>);

				Assert.NotEqual(expected, actual);
				Assert.NotEqual(expected.ToAsyncEnumerable(), actual);
			}

			[Fact]
			public static void NullExpectedEmptyActual()
			{
				var actual = new int[0].ToAsyncEnumerable();

				Assert.NotEqual(default(IEnumerable<int>), actual);
				Assert.NotEqual(default(IAsyncEnumerable<int>), actual);
			}
		}

		public class Collections
		{
			[Fact]
			public static void Equal()
			{
				var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
				var actual = new List<int>(expected).ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine +
						$"Expected: Not {expectedType.PadRight(padding)}[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine +
						$"Actual:       {actualType.PadRight(padding)}[1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]",
						ex.Message
					);
				}

				validateError(() => Assert.NotEqual(expected, actual), "int[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual), "", "");
			}

			[Fact]
			public static void NotEqual()
			{
				var expected = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
				var actual = new List<int>(new[] { 1, 2, 3, 4, 0, 6, 7, 8, 9, 10 }).ToAsyncEnumerable();

				Assert.NotEqual(expected, actual);
				Assert.NotEqual(expected.ToAsyncEnumerable(), actual);
			}
		}

		public class CollectionsWithComparer
		{
			[Fact]
			public static void AlwaysFalse()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 1, 2, 3, 4, 5 }).ToAsyncEnumerable();

				Assert.NotEqual(expected, actual, new IntComparer(false));
				Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new IntComparer(false));
			}

			[Fact]
			public static void AlwaysTrue()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 0, 0, 0, 0, 0 }).ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine +
						"Expected: Not " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"Actual:       " + actualType.PadRight(padding) + "[0, 0, 0, 0, 0]",
						ex.Message
					);
				}

				validateError(() => Assert.NotEqual(expected, actual, new IntComparer(true)), "int[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new IntComparer(true)), "", "");
			}

			class IntComparer : IEqualityComparer<int>
			{
				readonly bool answer;

				public IntComparer(bool answer)
				{
					this.answer = answer;
				}

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

				public int GetHashCode(int obj) => throw new NotImplementedException();
			}

			[Fact]
			public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException()
			{
				var expected = new[] { 1, 2 };
				var actual = new[] { 1, 2 }.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine +
						"              " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: Not " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"Actual:       " + actualType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"              " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
					Assert.IsType<DivideByZeroException>(ex.InnerException);
				}

				validateError(() => Assert.NotEqual(expected, actual, new ThrowingComparer()), "int[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, new ThrowingComparer()), "", "");
			}

			public class ThrowingComparer : IEqualityComparer<int>
			{
				public bool Equals(int x, int y) =>
					throw new DivideByZeroException();

				public int GetHashCode(int obj) =>
					throw new NotImplementedException();
			}
		}

		public class CollectionsWithEquatable
		{
			[Fact]
			public void Equal()
			{
				var expected = new[] { new EquatableObject { Char = 'a' } };
				var actual = new[] { new EquatableObject { Char = 'a' } }.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine +
						"Expected: Not " + expectedType.PadRight(padding) + "[EquatableObject { Char = 'a' }]" + Environment.NewLine +
						"Actual:       " + actualType.PadRight(padding) + "[EquatableObject { Char = 'a' }]",
						ex.Message
					);
				}

				validateError(() => Assert.NotEqual(expected, actual), "EquatableObject[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual), "", "");
			}

			[Fact]
			public void NotEqual()
			{
				var expected = new[] { new EquatableObject { Char = 'a' } };
				var actual = new[] { new EquatableObject { Char = 'b' } }.ToAsyncEnumerable();

				Assert.NotEqual(expected, actual);
				Assert.NotEqual(expected.ToAsyncEnumerable(), actual);
			}

			public class EquatableObject : IEquatable<EquatableObject>
			{
				public char Char { get; set; }

				public bool Equals(EquatableObject? other) =>
					other != null && other.Char == Char;
			}
		}

		public class CollectionsWithFunc
		{
			[Fact]
			public static void AlwaysFalse()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 1, 2, 3, 4, 5 }).ToAsyncEnumerable();

				Assert.NotEqual(expected, actual, (x, y) => false);
				Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int x, int y) => false);
			}

			[Fact]
			public static void AlwaysTrue()
			{
				var expected = new[] { 1, 2, 3, 4, 5 };
				var actual = new List<int>(new int[] { 0, 0, 0, 0, 0 }).ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Collections are equal" + Environment.NewLine +
						"Expected: Not " + expectedType.PadRight(padding) + "[1, 2, 3, 4, 5]" + Environment.NewLine +
						"Actual:       " + actualType.PadRight(padding) + "[0, 0, 0, 0, 0]",
						ex.Message
					);
				}

				validateError(() => Assert.NotEqual(expected, actual, (x, y) => true), "int[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int x, int y) => true), "", "");
			}

			[Fact]
			public void WithThrow_PrintsPointerWhereThrowOccurs_RecordsInnerException()
			{
				var expected = new[] { 1, 2 };
				var actual = new[] { 1, 2 }.ToAsyncEnumerable();

				void validateError(
					Action action,
					string expectedType,
					string actualType)
				{
					var ex = Record.Exception(action);
					var padding = Math.Max(expectedType.Length, actualType.Length);

					Assert.IsType<NotEqualException>(ex);
					Assert.Equal(
						"Assert.NotEqual() Failure: Exception thrown during comparison" + Environment.NewLine +
						"              " + new string(' ', padding) + " ↓ (pos 0)" + Environment.NewLine +
						"Expected: Not " + expectedType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"Actual:       " + actualType.PadRight(padding) + "[1, 2]" + Environment.NewLine +
						"              " + new string(' ', padding) + " ↑ (pos 0)",
						ex.Message
					);
					Assert.IsType<DivideByZeroException>(ex.InnerException);
				}

				validateError(() => Assert.NotEqual(expected, actual, (int e, int a) => throw new DivideByZeroException()), "int[] ", "<generated> ");
				validateError(() => Assert.NotEqual(expected.ToAsyncEnumerable(), actual, (int e, int a) => throw new DivideByZeroException()), "", "");
			}
		}
	}

	public class Single
	{
		[Fact]
		public static void GuardClause()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Single(default(IAsyncEnumerable<object>)!));
		}

		[Fact]
		public static void EmptyCollection()
		{
			var collection = new object[0].ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection));

			Assert.IsType<SingleException>(ex);
			Assert.Equal("Assert.Single() Failure: The collection was empty", ex.Message);
		}

		[Fact]
		public static void SingleItemCollection()
		{
			var collection = new[] { "Hello" }.ToAsyncEnumerable();

			var item = Assert.Single(collection);

			Assert.Equal("Hello", item);
		}

		[Fact]
		public static void MultiItemCollection()
		{
			var collection = new[] { "Hello", "World" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection contained 2 items" + Environment.NewLine +
				"Collection: [\"Hello\", \"World\"]",
				ex.Message
			);
		}

		[Fact]
		public static void Truncation()
		{
			var collection = new[] { 1, 2, 3, 4, 5, 6, 7 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection contained 7 items" + Environment.NewLine +
				$"Collection: [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]",
				ex.Message
			);
		}

		[Fact]
		public static void StringAsCollection_Match()
		{
			var collection = "H".ToAsyncEnumerable();

			var value = Assert.Single(collection);

			Assert.Equal('H', value);
		}

		[Fact]
		public static void StringAsCollection_NoMatch()
		{
			var collection = "Hello".ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection contained 5 items" + Environment.NewLine +
				"Collection: ['H', 'e', 'l', 'l', 'o']",
				ex.Message
			);
		}
	}

	public class Single_WithPredicate
	{
		[Fact]
		public static void GuardClauses()
		{
			Assert.Throws<ArgumentNullException>("collection", () => Assert.Single(default(IAsyncEnumerable<object>)!, _ => true));
			Assert.Throws<ArgumentNullException>("predicate", () => Assert.Single(new object[0].ToAsyncEnumerable(), null!));
		}

		[Fact]
		public static void SingleMatch()
		{
			var collection = new[] { "Hello", "World" }.ToAsyncEnumerable();

			var result = Assert.Single(collection, item => item.StartsWith("H"));

			Assert.Equal("Hello", result);
		}

		[Fact]
		public static void NoMatches()
		{
			var collection = new[] { "Hello", "World" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection, item => false));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine +
				"Expected:   (predicate expression)" + Environment.NewLine +
				"Collection: [\"Hello\", \"World\"]",
				ex.Message
			);
		}

		[Fact]
		public static void TooManyMatches()
		{
			var collection = new[] { "Hello", "World" }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection, item => true));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine +
				"Expected:      (predicate expression)" + Environment.NewLine +
				"Collection:    [\"Hello\", \"World\"]" + Environment.NewLine +
				"Match indices: 0, 1",
				ex.Message
			);
		}

		[Fact]
		public static void Truncation()
		{
			var collection = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 4 }.ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection, item => item == 4));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection contained 2 matching items" + Environment.NewLine +
				"Expected:      (predicate expression)" + Environment.NewLine +
				$"Collection:    [1, 2, 3, 4, 5, {ArgumentFormatter.Ellipsis}]" + Environment.NewLine +
				"Match indices: 3, 8",
				ex.Message
			);
		}

		[Fact]
		public static void StringAsCollection_Match()
		{
			var collection = "H".ToAsyncEnumerable();

			var value = Assert.Single(collection, c => c != 'Q');

			Assert.Equal('H', value);
		}

		[Fact]
		public static void StringAsCollection_NoMatch()
		{
			var collection = "H".ToAsyncEnumerable();

			var ex = Record.Exception(() => Assert.Single(collection, c => c == 'Q'));

			Assert.IsType<SingleException>(ex);
			Assert.Equal(
				"Assert.Single() Failure: The collection did not contain any matching items" + Environment.NewLine +
				"Expected:   (predicate expression)" + Environment.NewLine +
				"Collection: ['H']",
				ex.Message
			);
		}
	}

	sealed class SpyEnumerator<T> : IAsyncEnumerable<T>, IAsyncEnumerator<T>
	{
		IAsyncEnumerator<T>? innerEnumerator;

		public SpyEnumerator(IAsyncEnumerable<T> enumerable)
		{
			innerEnumerator = enumerable.GetAsyncEnumerator();
		}

		public T Current =>
			GuardNotNull("Tried to get Current on a disposed enumerator", innerEnumerator).Current;

		public bool IsDisposed => innerEnumerator is null;

		public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) => this;

		public ValueTask<bool> MoveNextAsync() =>
			GuardNotNull("Tried to call MoveNext() on a disposed enumerator", innerEnumerator).MoveNextAsync();

		public async ValueTask DisposeAsync()
		{
			if (innerEnumerator is not null)
				await innerEnumerator.DisposeAsync();

			innerEnumerator = null;
		}

		/// <summary/>
		static T2 GuardNotNull<T2>(
			string message,
			[NotNull] T2? value)
				where T2 : class
		{
			if (value is null)
				throw new InvalidOperationException(message);

			return value;
		}
	}
}

#endif