using System;
using System.Collections;
using System.Globalization;

namespace MusicMetaTagger.Client.AllMusicGuide.Tests
{
	/// <summary>
	///   A static unit testing helper class that can test two objects
	///   to see if they are equal.
	/// </summary>
	/// <remarks>
	///   This class does not rely on Object.Equals to insure that reference types
	///   are equal.  Instead, it compares all public properties of 
	///   reference types recursively (eventually arriving at value types).  If the properties are the same,
	///   then the objects are assuemd to be equal.  This may be changed
	///   to an optional behavior later on (if anyone has a need for that kind of thing).
	/// </remarks>
	/// <author>MBH</author>
	/// <dateAuthored>11/29/04</dateAuthored>
	public class EqualityTester
	{
		/// <summary>
		///   When set to true, DateTime objects may differ by &lt; 1 second and still
		///   be considered equal.
		/// </summary>
		private static bool _fuzzyDateMatching;

		/// <summary>
		///   This is a hack to insure 100% test coverage
		/// </summary>
		static EqualityTester()
		{
			new EqualityTester();
		}

		/// <summary>
		///   This is a static class, so it cannot be instantiated directly
		/// </summary>
		private EqualityTester()
		{
		}

		/// <summary>
		///   When set to true, DateTime objects may differ by &lt; 1 second and still
		///   be considered equal.
		/// </summary>
		public static bool FuzzyDateMatching
		{
			get { return _fuzzyDateMatching; }
			set { _fuzzyDateMatching = value; }
		}

		/// <summary>
		///   Asserts that the result of enumerating two IEnumerable objects
		///   are identical.
		/// </summary>
		/// <param name = "o1">The first IEnumerable object</param>
		/// <param name = "o2">The second IEnumerable object</param>
		private static void AssertIEnumerablesEqual(object o1, object o2)
		{
			var enumerator1 = ((IEnumerable) o1).GetEnumerator();
			var enumerator2 = ((IEnumerable) o2).GetEnumerator();

			var enumerator1Valid = enumerator1.MoveNext();
			var enumerator2Valid = enumerator2.MoveNext();

			while (enumerator1Valid && enumerator2Valid)
			{
				//The two values from the enumerators

				var value1 = enumerator1.Current;

				var value2 = enumerator2.Current;

				AssertEqual(value1, value2);

				enumerator1Valid = enumerator1.MoveNext();
				enumerator2Valid = enumerator2.MoveNext();
			}

			//If an enumerator is still valid, that means it had more items in it than the other
			if (enumerator1Valid)
				throw (new NotSupportedException("The first IEnumerable object had more items in it than the second."));

			if (enumerator2Valid)
				throw (new NotSupportedException("The second IEnumerable object had more items in it than the first."));
		}

		/// <summary>
		///   Asserts that two reference-types are equal by comparing their properties.
		/// </summary>
		/// <param name = "o1">The first object.</param>
		/// <param name = "o2">The second object.</param>
		/// <exception cref = "NotSupportedException"></exception>
		private static void AssertReferencesEqual(object o1, object o2)
		{
			//At this point, it really shouldn't be possible for two things to be of differing types, so no check is performed.

			var t = o1.GetType();

			//Strings are a special case
			if (t == typeof (string))
			{
				//If the strings aren't equal, fail
				if (!o1.Equals(o2))
				{
					throw (new NotSupportedException("Strings are not equal: '" + o1 + "' != '" + o2 + "'"));
				}
			}

			if (t.GetInterface("IEnumerable") != null || t.IsArray)
				AssertIEnumerablesEqual(o1, o2);

			//Assert that all public properties are equal
			var properties = t.GetProperties();

			var isICollection = t.GetInterface("ICollection") != null;

			foreach (var property in properties)
			{
				//If this is a collection, skip the SyncRoot property since
				//that can vary even if collections have identicial members
				if (isICollection && property.Name == "SyncRoot")
					continue;

				//Skip Capacity on ICollections, too, since that can change
				if (isICollection && property.Name == "Capacity")
					continue;

				var getMethod = property.GetGetMethod();

				if (getMethod.GetParameters().Length > 0)
					//this property is an indexer and cannot be checked.  If the object implemeneted
					//IEnumerable, it was probably checked above.
					continue;

				{
					var value1 = getMethod.Invoke(o1, new object[] {});

					var value2 = getMethod.Invoke(o2, new object[] {});

					//See if either result is null
					if (value1 == null || value2 == null)
					{
						//If either one is null, they both better be or the test failed.
						if (value1 != null || value2 != null)
							throw (new NotSupportedException("Object property accessor returned one null, one not-null for " + getMethod.Name));
						continue;
					}

					if (value1.GetType().IsValueType)
					{
						try
						{
							AssertValuesEqual(value1, value2);
						}
						catch (NotSupportedException ex)
						{
							var errorMessage = String.Format("Property '{0}' failed equality check.  The inner exception is the reason.",
							                                 property.Name);
							throw (new NotSupportedException(errorMessage, ex));
						}
					}
						//Don't recurse down the array graph, it's self referencing and will overflow the stack.
					else if (!(t.IsArray && value1.GetType().IsArray))
					{
						try
						{
							AssertReferencesEqual(value1, value2);
						}
						catch (NotSupportedException ex)
						{
							var errorMessage = String.Format("Property '{0}' failed equality check.  The inner exception is the reason.",
							                                 property.Name);
							throw (new NotSupportedException(errorMessage, ex));
						}
					}
				}
			}
		}

		/// <summary>
		///   Asserts that two value types are equal.
		/// </summary>
		/// <param name = "o1">The first value.</param>
		/// <param name = "o2">The second value</param>
		/// <exception cref = "NotSupportedException">Thrown if the values aren't equal.</exception>
		private static void AssertValuesEqual(object o1, object o2)
		{
			if (o1 is DateTime)
			{
				AssertDateTimesEqual((DateTime) o1, (DateTime) o2);
			}
			else if (!o1.Equals(o2))
				throw (new NotSupportedException(String.Format("Values not equal: '{0}' != '{1}'", o1, o2)));
		}

		/// <summary>
		///   Asserts that two DateTime objects are equal.
		/// </summary>
		/// <param name = "dateTime1"></param>
		/// <param name = "dateTime2"></param>
		private static void AssertDateTimesEqual(DateTime dateTime1, DateTime dateTime2)
		{
			if (_fuzzyDateMatching)
			{
				if (dateTime1 - dateTime2 > TimeSpan.FromSeconds(1) || dateTime1 - dateTime2 < TimeSpan.FromSeconds(-1))
					throw (new NotSupportedException(
						String.Format("DateTime's differ by more than 1 second with fuzzy matching enabled: '{0}' != '{1}'.", dateTime1,
						              dateTime2)));
			}
			else if (dateTime1 != dateTime2)
				throw (new NotSupportedException(String.Format("Values not equal: '{0}' != '{1}'", dateTime1.ToString(CultureInfo.InvariantCulture),
				                                               dateTime2.ToString(CultureInfo.InvariantCulture))));
		}

		/// <summary>
		///   Asserts that all public properties on the specified objects
		///   are equal.
		/// </summary>
		/// <param name = "o1">The first object</param>
		/// <param name = "o2">The second object</param>
		/// <exception cref = "InvalidOperationException">Thrown if the two objects are of different types, indicating an internal error.</exception>
		/// <exception cref = "NotSupportedException">Thrown if the objects aren't equal.</exception>
		/// <remarks>
		///   o1 and o2 must be of the same type.
		/// </remarks>
		public static void AssertEqual(object o1, object o2)
		{
			if (o1 == null || o2 == null)
			{
				//Both values better be null...
				if (o1 != null || o2 != null)
					throw (new NotSupportedException("One value type was null while the other was not."));
				return;
			}

			if (o1.GetType() != o2.GetType())
				throw (new InvalidOperationException("AssertEqual called with objects of differing types: " + o1.GetType().Name +
				                                     ", " + o2.GetType().Name));

			var t = o1.GetType();

			if (t.IsValueType)
				AssertValuesEqual(o1, o2);
			else
				AssertReferencesEqual(o1, o2);
		}

		/// <summary>
		///   Recursively tests that all public properties on the specified objects are equal.
		/// </summary>
		/// <param name = "o1"></param>
		/// <param name = "o2"></param>
		/// <exception cref = "InvalidOperationException">Thrown if the two objects are not of the same type.</exception>
		/// <returns>true if the objects are equal, false otherwise.</returns>
		public static bool AreEqual(object o1, object o2)
		{
			try
			{
				AssertEqual(o1, o2);
			}
			catch (NotSupportedException)
			{
				return false;
			}

			return true;
		}
	}
}