using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Linq; // welcome to the dark side

using Svelto.Tasks;
using Svelto.Tasks.Lean;
using Svelto.Tasks.Enumerators;
using UnityEngine;

using GamecraftModdingAPI.App;
using GamecraftModdingAPI.Tasks;
using GamecraftModdingAPI.Utility;

namespace GamecraftModdingAPI.Tests
{
    /// <summary>
    /// API test system root class.
    /// </summary>
	public static class TestRoot
    {
		public static bool AutoShutdown = true;

		public const string ReportFile = "GamecraftModdingAPI_tests.log";

		private static bool _testsPassed = false;

		private static uint _testsCount = 0;

		private static uint _testsCountPassed = 0;

		private static uint _testsCountFailed = 0;

		private static string state = "StartingUp";

		private static Stopwatch timer;

		private static List<Type> testTypes = null;

        public static bool TestsPassed
		{
			get => _testsPassed;
			set
			{
				_testsPassed = _testsPassed && value;
				_testsCount++;
				if (value)
				{
					_testsCountPassed++;
				}
				else
				{
					_testsCountFailed++;
				}
			}
		}

		private static void StartUp()
		{
            // init
			timer = Stopwatch.StartNew();
			_testsPassed = true;
			_testsCount = 0;
			_testsCountPassed = 0;
			_testsCountFailed = 0;
			// flow control
			Game.Enter += (sender, args) => { GameTests().RunOn(RobocraftX.Schedulers.Lean.EveryFrameStepRunner_RUNS_IN_TIME_STOPPED_AND_RUNNING); };
			Game.Exit += (s, a) => state = "ReturningFromGame";
			Client.EnterMenu += (sender, args) => 
			{
				if (state == "EnteringMenu") 
				{
					MenuTests().RunOn(Scheduler.leanRunner);
					state = "EnteringGame";
				}
				if (state == "ReturningFromGame")
				{
					TearDown().RunOn(Scheduler.leanRunner);
					state = "ShuttingDown";
				}
			};
            // init tests here
			foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
					if (m.GetCustomAttribute<APITestStartUpAttribute>() != null)
                    {
						try
                        {
                            m.Invoke(null, new object[0]);
                        }
                        catch (Exception e)
                        {
							Assert.Fail($"Start up method '{m}' raised an exception: {e.ToString()}");
                        }
                    }
                }
            }
			state = "EnteringMenu";
		}

        private static IEnumerator<TaskContract> MenuTests()
		{
			yield return Yield.It;
			// menu tests
			foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
                    APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
					if (a != null && a.TestType == TestType.Menu)
                    {
						try
						{
							m.Invoke(null, new object[0]);
						}
						catch (Exception e)
						{
							Assert.Fail($"Menu test '{m}' raised an exception: {e.ToString()}");
						}
                        yield return Yield.It;
                    }
                }
            }
			// load game
			yield return GoToGameTests().Continue();
		}

		private static IEnumerator<TaskContract> GoToGameTests()
		{
			Client app = new Client();
			int oldLength = 0;
			while (app.MyGames.Length == 0 || oldLength != app.MyGames.Length)
			{
				oldLength = app.MyGames.Length;
				yield return new WaitForSecondsEnumerator(1).Continue();
			}
			yield return Yield.It;
			app.MyGames[0].EnterGame();
			/*Game newGame = Game.NewGame();
			yield return new WaitForSecondsEnumerator(5).Continue(); // wait for sync
			newGame.EnterGame();*/
		}

		private static IEnumerator<TaskContract> GameTests()
		{
			yield return Yield.It;
			Game currentGame = Game.CurrentGame();
			// in-game tests
			yield return new WaitForSecondsEnumerator(5).Continue(); // wait for game to finish loading
			foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
					APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
					if (a != null && a.TestType == TestType.Game)
                    {
						try
                        {
                            m.Invoke(null, new object[0]);
                        }
                        catch (Exception e)
                        {
							Assert.Fail($"Game test '{m}' raised an exception: {e.ToString()}");
                        }
						yield return Yield.It;
                    }
                }
            }
			currentGame.ToggleTimeMode();
			yield return new WaitForSecondsEnumerator(5).Continue();
            // simulation tests
			foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
                    APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
					if (a != null && a.TestType == TestType.SimulationMode)
                    {
						try
                        {
                            m.Invoke(null, new object[0]);
                        }
                        catch (Exception e)
                        {
							Assert.Fail($"Simulation test '{m}' raised an exception: {e.ToString()}");
                        }
                        yield return Yield.It;
                    }
                }
            }
			currentGame.ToggleTimeMode();
			yield return new WaitForSecondsEnumerator(5).Continue();
            // build tests
			foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
                    APITestCaseAttribute a = m.GetCustomAttribute<APITestCaseAttribute>();
					if (a != null && a.TestType == TestType.EditMode)
                    {
						try
                        {
                            m.Invoke(null, new object[0]);
                        }
                        catch (Exception e)
                        {
                            Assert.Fail($"Build test '{m}' raised an exception: {e.ToString()}");
                        }
						yield return Yield.It;
                    }
                }
            }
			// exit game
			yield return new WaitForSecondsEnumerator(5).Continue();
			yield return ReturnToMenu().Continue();
		}

		private static IEnumerator<TaskContract> ReturnToMenu()
		{
			Logging.MetaLog("Returning to main menu");
			yield return Yield.It;
			Game.CurrentGame().ExitGame();
		}

		private static IEnumerator<TaskContract> TearDown()
		{
			yield return new WaitForSecondsEnumerator(5).Continue();
			Logging.MetaLog("Tearing down test run");
			// dispose tests here
            foreach (Type t in testTypes)
            {
                foreach (MethodBase m in t.GetMethods())
                {
                    if (m.GetCustomAttribute<APITestTearDownAttribute>() != null)
                    {
						try
                        {
                            m.Invoke(null, new object[0]);
                        }
                        catch (Exception e)
                        {
                            Assert.Warn($"Tear down method '{m}' raised an exception: {e.ToString()}");
                        }
                        yield return Yield.It;
                    }
                }
            }
            // finish up
			Assert.CallsComplete();
			timer.Stop();
			string verdict = _testsPassed ? "--- PASSED :) ---" : "--- FAILED :( ---";
			Assert.Log($"VERDICT: {verdict} ({_testsCountPassed}/{_testsCountFailed}/{_testsCount} P/F/T in {timer.ElapsedMilliseconds}ms)");
			yield return Yield.It;
			// end game
			Logging.MetaLog("Completed test run: " + verdict);
            yield return Yield.It;
			Assert.CloseLog();
            if (AutoShutdown) Application.Quit();
		}

		private static void FindTests(Assembly asm)
		{
			testTypes = new List<Type>();
			foreach (Type t in asm.GetTypes())
			{
				if (t.GetCustomAttribute<APITestClassAttribute>() != null)
				{
					testTypes.Add(t);
				}
			}
		}

        /// <summary>
        /// Runs the tests.
        /// </summary>
        /// <param name="asm">Assembly to search for tests. When set to null, this uses the GamecraftModdingAPI assembly. </param>
		public static void RunTests(Assembly asm = null)
		{
			if (asm == null) asm = Assembly.GetExecutingAssembly();
			FindTests(asm);
			Logging.MetaLog("Starting test run");
			// log metadata
			Assert.Log($"Unity {Application.unityVersion}");
			Assert.Log($"Gamecraft {Application.version}");
			Assert.Log($"GamecraftModdingAPI {Assembly.GetExecutingAssembly().GetName().Version}");
			Assert.Log($"Testing {asm.GetName().Name} {asm.GetName().Version}");
			Assert.Log($"START: --- {DateTime.Now.ToString()} --- ({testTypes.Count} tests classes detected)");
			StartUp();
			Logging.MetaLog("Test StartUp complete");
		}
    }
}