using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security;
using NUnit.Framework;
using UnityEditor;

namespace eagle0.Tests {
    public static class DynamicTextBindingTestRunner {
        public static void Run() {
            var resultsPath = CommandLineValue("-testResults");
            if (string.IsNullOrEmpty(resultsPath)) {
                resultsPath = Path.Combine(
                        Directory.GetCurrentDirectory(),
                        "dynamic-text-binding-test-results.xml");
            }

            var results = DiscoverTests().Select(test => RunTest(test.Name, test.Run)).ToList();

            WriteResults(resultsPath, results);

            foreach (var result in results) {
                if (!result.Passed) {
                    UnityEngine.Debug.LogError($"{result.Name} failed: {result.Exception}");
                }
            }

            EditorApplication.Exit(results.Exists(result => !result.Passed) ? 1 : 0);
        }

        private static TestResult RunTest(string name, Action test) {
            var stopwatch = Stopwatch.StartNew();
            try {
                test();
                stopwatch.Stop();
                return new TestResult(name, stopwatch.Elapsed.TotalSeconds, null);
            } catch (Exception exception) {
                stopwatch.Stop();
                return new TestResult(
                        name,
                        stopwatch.Elapsed.TotalSeconds,
                        exception is TargetInvocationException targetInvocationException &&
                                        targetInvocationException.InnerException != null
                                ? targetInvocationException.InnerException
                                : exception);
            }
        }

        private static IReadOnlyList<TestCase> DiscoverTests() {
            return typeof(DynamicTextBindingTestRunner)
                    .Assembly.GetTypes()
                    .Where(type => type.Namespace == typeof(DynamicTextBindingTests).Namespace)
                    .SelectMany(
                            type => type.GetMethods(BindingFlags.Instance | BindingFlags.Public)
                                            .Where(method => method.GetCustomAttributes(
                                                                           typeof(TestAttribute),
                                                                           false)
                                                                     .Length > 0)
                                            .Select(method => new TestCase(type, method)))
                    .OrderBy(test => test.Name)
                    .ToList();
        }

        private static string CommandLineValue(string name) {
            var args = Environment.GetCommandLineArgs();
            for (int i = 0; i < args.Length - 1; i++) {
                if (args[i] == name) { return args[i + 1]; }
            }

            return null;
        }

        private static void WriteResults(string path, IReadOnlyList<TestResult> results) {
            Directory.CreateDirectory(Path.GetDirectoryName(path));

            var failed = 0;
            foreach (var result in results) {
                if (!result.Passed) { failed++; }
            }

            using var writer = new StreamWriter(path);
            writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
            writer.WriteLine(
                    $"<test-run testcasecount=\"{results.Count}\" result=\"{(failed == 0 ? "Passed" : "Failed")}\" total=\"{results.Count}\" passed=\"{results.Count - failed}\" failed=\"{failed}\" skipped=\"0\" asserts=\"0\">");
            writer.WriteLine(
                    $"  <test-suite type=\"Assembly\" name=\"DynamicTextEditorTests\" result=\"{(failed == 0 ? "Passed" : "Failed")}\" total=\"{results.Count}\" passed=\"{results.Count - failed}\" failed=\"{failed}\" skipped=\"0\" asserts=\"0\">");

            foreach (var result in results) {
                writer.WriteLine(
                        $"    <test-case name=\"{Escape(result.Name)}\" result=\"{(result.Passed ? "Passed" : "Failed")}\" duration=\"{result.DurationSeconds:0.000}\" asserts=\"0\">");
                if (!result.Passed) {
                    writer.WriteLine("      <failure>");
                    writer.WriteLine(
                            $"        <message>{Escape(result.Exception.Message)}</message>");
                    writer.WriteLine(
                            $"        <stack-trace>{Escape(result.Exception.StackTrace)}</stack-trace>");
                    writer.WriteLine("      </failure>");
                }
                writer.WriteLine("    </test-case>");
            }

            writer.WriteLine("  </test-suite>");
            writer.WriteLine("</test-run>");
        }

        private static string Escape(string value) { return SecurityElement.Escape(value ?? ""); }

        private class TestCase {
            private readonly Type type;
            private readonly MethodInfo method;

            public string Name => $"{type.Name}.{method.Name}";

            public TestCase(Type type, MethodInfo method) {
                this.type = type;
                this.method = method;
            }

            public void Run() {
                var tests = Activator.CreateInstance(type);
                method.Invoke(tests, Array.Empty<object>());
            }
        }

        private class TestResult {
            public string Name { get; }
            public double DurationSeconds { get; }
            public Exception Exception { get; }
            public bool Passed => Exception == null;

            public TestResult(string name, double durationSeconds, Exception exception) {
                Name = name;
                DurationSeconds = durationSeconds;
                Exception = exception;
            }
        }
    }
}
