using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;

using Unity.Mathematics;

namespace GamecraftModdingAPI.Tests
{
    /// <summary>
    /// API test system assertion utilities.
    /// </summary>
    public static class Assert
    {
		private static StreamWriter logFile = null;

		private static ConcurrentDictionary<string, string> callbacks = new ConcurrentDictionary<string, string>();

		private const string PASS = "SUCCESS: ";

		private const string FAIL = "FAILURE: ";

		private const string WARN = "WARNING: ";

		private const string INFO = "DEBUG: ";

        /// <summary>
        /// Log a message to the test log.
        /// </summary>
        /// <param name="msg">Message.</param>
        /// <param name="end">Message ending.</param>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void Log(string msg, string end = "\n")
		{
			if (logFile == null) openTestLog();
			logFile.Write(msg + end);
			logFile.Flush();
		}

        /// <summary>
        /// Asserts that the event receives a callback... eventually.
		/// Add the eventhandler returned by this method to the relevant event.
		/// This does not assert that the callback happens under that event's intended circumstances.
		/// Add another event handler to assert specific circumstance requirements.
        /// </summary>
        /// <returns>The callback event handler.</returns>
        /// <param name="eventName">Event name.</param>
        /// <param name="eventMsg">Event error message.</param>
        /// <typeparam name="T">The event handler callback argument object.</typeparam>
		public static EventHandler<T> CallsBack<T>(string eventName, string eventMsg = null)
		{
			if (eventMsg == null) eventMsg = $"expected callback to {eventName} but it never occurred...";
			callbacks[eventName] = eventMsg;
            
			return (sender, args) =>
			{
				string value = null;
				if (!callbacks.TryRemove(eventName, out value)) { Log(WARN + $"callback to {eventName} occurred again or a related error occurred... (Received '{args.ToString()}' from '{(sender == null ? (string)sender : sender.ToString())}')"); }
				Log(PASS + $"callback to {eventName} occurred... (Received '{args.ToString()}' from '{(sender == null ? (string)sender : sender.ToString())}')");
				TestRoot.TestsPassed = true;
			};
		}

		public static bool NotNull<T>(T obj, string err = null, string success = null)
		{
			if (err == null) err = $"{nameof(T)} object was null.";
			if (success == null) success = $"{nameof(T)} '{obj}' not null";
            if (obj == null)
			{
				Log(FAIL + err);
				TestRoot.TestsPassed = false;
				return false;
			}
			else
			{
				Log(PASS + success);
				TestRoot.TestsPassed = true;
				return true;
			}
		}

		public static bool Equal<T>(T obj1, T obj2, string err = null, string success = null)
        {
			if (err == null) err = $"{nameof(T)} '{obj1}' is not equal to '{obj2}'.";
			if (success == null) success = $"{nameof(T)} '{obj1}' is equal to '{obj2}'.";
			if ((obj1 == null && obj2 == null)
				|| (obj1 != null && obj2 != null && obj1.Equals(obj2) && obj2.Equals(obj1)))
			{
                // pass
				Log(PASS + success);
                TestRoot.TestsPassed = true;
                return true;
			}
			else
			{
                // fail
				Log(FAIL + err);
                TestRoot.TestsPassed = false;
                return false;
			}
        }

		public static bool Errorless(Action tryThis, string err = null, string success = null)
		{
			if (err == null) err = $"{tryThis} raised an exception: ";
			if (success == null) success = $"{tryThis} completed without raising an exception.";
            try
			{
				tryThis();
			}
			catch (Exception e)
			{
				Log(FAIL + err + e);
				TestRoot.TestsPassed = false;
				return false;
			}
			TestRoot.TestsPassed = true;
			Log(PASS + success);
			return true;
		}

		public static bool CloseTo(float a, float b, string err = null, string success = null, float delta = float.Epsilon)
		{
			if (err == null) err = $"{a} is not within {delta} of {b}.";
			if (success == null) success = $"{a} is close enough to {b}.";
			if (Math.Abs(a - b) > delta)
			{
				Log(FAIL + err);
                TestRoot.TestsPassed = false;
                return false;
			}
			else
			{
				TestRoot.TestsPassed = true;
				Log(PASS + success);
                return true;
			}
		}

		public static bool CloseTo(double a, double b, string err = null, string success = null, double delta = double.Epsilon)
        {
			if (err == null) err = $"{a} is not within {delta} of {b}.";
			if (success == null) success = $"{a} is close enough to {b}.";
			if (Math.Abs(a - b) > delta)
            {
				Log(FAIL + err);
                TestRoot.TestsPassed = false;
                return false;
            }
            else
            {
                TestRoot.TestsPassed = true;
				Log(PASS + success);
                return true;
            }
        }

		public static bool CloseTo(float3 a, float3 b, string err = null, string success = null, float delta = float.Epsilon)
		{
			if (err == null) err = $"{a} is not within {delta} of {b} in every direction.";
			if (success == null) success = $"{a} is close enough to {b}.";
			bool xClose = CloseTo(a.x, b.x, err, success, delta);
			bool yClose = CloseTo(a.y, b.y, err, success, delta);
			bool zClose = CloseTo(a.z, b.z, err, success, delta);
			if (xClose && yClose && zClose)
			{
				//TestRoot.TestsPassed = true;
				//Log(PASS + success);
				return true;
			}
			else
			{
				//Log(FAIL + err);
                //TestRoot.TestsPassed = false;
				return false;
			}
		}

        public static void Fail(string msg = null)
		{
			if (msg == null) msg = $"Manual test failure with no message provided.";
			Log(FAIL + msg);
			TestRoot.TestsPassed = false;
		}

		public static void Pass(string msg = null)
        {
			if (msg == null) msg = $"Manual test pass with no message provided.";
            Log(PASS + msg);
            TestRoot.TestsPassed = true;
        }

		public static void Warn(string msg = null)
        {
            if (msg == null) msg = $"Manual test warning with no message provided.";
            Log(WARN + msg);
            TestRoot.TestsPassed = true;
        }

        internal static void CallsComplete()
		{
			foreach(string key in callbacks.Keys)
			{
				Log(FAIL + callbacks[key]);
				TestRoot.TestsPassed = false;
			}
		}

        internal static void CloseLog()
		{
			if (logFile != null) logFile.Close();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void openTestLog()
		{
			logFile = File.CreateText(TestRoot.ReportFile);
		}
    }
}