#if XUNIT_NULLABLE
#nullable enable
#else
// In case this is source-imported with global nullable enabled but no XUNIT_NULLABLE
#pragma warning disable CS8604
#endif

using System;
using System.Globalization;
using System.Linq;
using Xunit.Internal;

// An "ExceptionUtility" class exists in both xunit.assert (Xunit.Internal.ExceptionUtility) as well as xunit.execution.dotnet (Xunit.Sdk.ExceptionUtility)
// This causes an compile-time error in projects that reference both the xunit.assert.source and xunit.core packages.
// To resolve this issue, add an alias for the Xunit.Internal.ExceptionUtility to make sure, the xunit.assert core uses the right ExceptionUtility
using ExceptionUtilityInternal = Xunit.Internal.ExceptionUtility;

namespace Xunit.Sdk
{
	/// <summary>
	/// Exception thrown when Assert.Collection fails.
	/// </summary>
#if XUNIT_VISIBILITY_INTERNAL
	internal
#else
	public
#endif
	partial class CollectionException : XunitException
	{
		static readonly char[] crlfSeparators = new[] { '\r', '\n' };

		CollectionException(string message) :
			base(message)
		{ }

		static string FormatInnerException(Exception innerException)
		{
			var text = innerException.Message;
			var filteredStack = ExceptionUtilityInternal.TransformStackTrace(ExceptionUtilityInternal.FilterStackTrace(innerException.StackTrace), "  ");
			if (!string.IsNullOrWhiteSpace(filteredStack))
			{
				if (text.Length != 0)
					text += Environment.NewLine;

				text += "Stack Trace:" + Environment.NewLine + filteredStack;
			}

			var lines =
				text
					.Split(crlfSeparators, StringSplitOptions.RemoveEmptyEntries)
					.Select((value, idx) => idx > 0 ? "            " + value : value);

			return string.Join(Environment.NewLine, lines);
		}

		/// <summary>
		/// Creates an instance of the <see cref="CollectionException"/> class to be thrown
		/// when an item comparison failed
		/// </summary>
		/// <param name="exception">The exception that was thrown</param>
		/// <param name="indexFailurePoint">The item index for the failed item</param>
		/// <param name="failurePointerIndent">The number of spaces needed to indent the failure pointer</param>
		/// <param name="formattedCollection">The formatted collection</param>
		public static CollectionException ForMismatchedItem(
			Exception exception,
			int indexFailurePoint,
			int? failurePointerIndent,
			string formattedCollection)
		{
			Assert.GuardArgumentNotNull(nameof(exception), exception);

			var message = "Assert.Collection() Failure: Item comparison failure";

			if (failurePointerIndent.HasValue)
				message += string.Format(CultureInfo.CurrentCulture, "{0}            {1}\u2193 (pos {2})", Environment.NewLine, new string(' ', failurePointerIndent.Value), indexFailurePoint);

			message += string.Format(CultureInfo.CurrentCulture, "{0}Collection: {1}{2}Error:      {3}", Environment.NewLine, formattedCollection, Environment.NewLine, FormatInnerException(exception));

			return new CollectionException(message);
		}

		/// <summary>
		/// Creates an instance of the <see cref="CollectionException"/> class to be thrown
		/// when the item count in a collection does not match the expected count.
		/// </summary>
		/// <param name="expectedCount">The expected item count</param>
		/// <param name="actualCount">The actual item count</param>
		/// <param name="formattedCollection">The formatted collection</param>
		public static CollectionException ForMismatchedItemCount(
			int expectedCount,
			int actualCount,
			string formattedCollection) =>
				new CollectionException(
					string.Format(
						CultureInfo.CurrentCulture,
						"Assert.Collection() Failure: Mismatched item count{0}Collection:     {1}{2}Expected count: {3}{4}Actual count:   {5}",
						Environment.NewLine,
						Assert.GuardArgumentNotNull(nameof(formattedCollection), formattedCollection),
						Environment.NewLine,
						expectedCount,
						Environment.NewLine,
						actualCount
					)
				);
	}
}
