using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Attributes;
using AIRuntime.Runtime.Capabilities.Permissions;

using UnityEditor;
using UnityEditor.TestTools.TestRunner.Api;
using UnityEngine;

using Object =
    UnityEngine.Object;

using CapabilityRange =
    AIRuntime.Runtime.Capabilities.Attributes.RangeAttribute;


namespace AIRuntime.Editor
{
    [Serializable]
    public sealed class RunEditModeTestsArguments
    {
        [Description("Optional exact fully-qualified test names.")]
        public List<string> TestNames { get; set; } =
            new List<string>();

        [Description("Optional regular-expression group or namespace filters.")]
        public List<string> GroupNames { get; set; } =
            new List<string>();

        [Description("Optional NUnit category names.")]
        public List<string> CategoryNames { get; set; } =
            new List<string>();

        [Description("Optional test assembly names without .dll.")]
        public List<string> AssemblyNames { get; set; } =
            new List<string>();

        [DefaultValue(false)]
        [Description("Return the normalized test filter without starting a test run.")]
        public bool PreviewOnly { get; set; }

        [DefaultValue(false)]
        [Description("Include passed and skipped leaf-test details. Failures are always included.")]
        public bool IncludePassedTests { get; set; }

        [DefaultValue(200)]
        [CapabilityRange(1, 2000)]
        [Description("Maximum leaf-test result records to return.")]
        public int MaxResults { get; set; } =
            200;
    }


    [Serializable]
    internal sealed class EditorTestResultRecord
    {
        public string FullName;

        public string ResultState;

        public double DurationSeconds;

        public string Message;

        public string StackTrace;

        public string Output;
    }


    [Serializable]
    internal sealed class RunEditModeTestsResult
    {
        public bool PreviewOnly;

        public bool RunScheduled;

        public string RunId;

        public List<string> TestNames =
            new List<string>();

        public List<string> GroupNames =
            new List<string>();

        public List<string> CategoryNames =
            new List<string>();

        public List<string> AssemblyNames =
            new List<string>();

        public string ResultState;

        public double DurationSeconds;

        public int PassCount;

        public int FailCount;

        public int SkipCount;

        public int InconclusiveCount;

        public int ReturnedResultCount;

        public bool ResultsTruncated;

        public List<EditorTestResultRecord> Results =
            new List<EditorTestResultRecord>();

        public string ResponseInstruction =
            "RunScheduled=true means Unity completed this EditMode test run and returned aggregate results. FailCount greater than zero is a failed verification even though the capability call itself succeeded. PreviewOnly=true does not execute tests.";
    }


    public class RunEditModeTestsCapability :
        CapabilityBase<RunEditModeTestsArguments>,
        ICapabilityExecutionSemantics
    {
        private static readonly object RunSync =
            new object();

        private static bool _capabilityRunActive;


        public override string Name =>
            "run_edit_mode_tests";


        public override string Description =>
            "Runs Unity EditMode tests through the public Test Runner API using optional test-name, regex group, category, and assembly filters, then returns aggregate and bounded leaf results.";


        protected virtual TestMode RunnerTestMode =>
            TestMode.EditMode;


        protected virtual string TestModeLabel =>
            "EditMode";


        protected virtual IDisposable PrepareRunEnvironment()
        {
            return null;
        }


        public override CapabilityPermission Permission =>
            CapabilityPermission.Write;


        public CapabilityBatchBehavior BatchBehavior =>
            CapabilityBatchBehavior.SingleMutationPerResponse;


        public CapabilityRepeatedCallBehavior RepeatedCallBehavior =>
            CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult;


        protected override async Task<object> ExecuteAsync(
            RunEditModeTestsArguments arguments,
            CapabilityContext context)
        {
            context.CancellationToken
                .ThrowIfCancellationRequested();

            RunEditModeTestsArguments request =
                arguments ??
                new RunEditModeTestsArguments();

            Filter filter =
                CreateFilter(
                    request,
                    RunnerTestMode);

            RunEditModeTestsResult preview =
                CreateBaseResult(
                    request,
                    TestModeLabel);

            if (request.PreviewOnly)
            {
                preview.PreviewOnly =
                    true;

                return preview;
            }

            if (EditorApplication.isCompiling)
            {
                throw new InvalidOperationException(
                    $"Unity is compiling scripts. Wait for compilation to finish before running {TestModeLabel} tests.");
            }

            if (EditorUtility.scriptCompilationFailed)
            {
                throw new InvalidOperationException(
                    $"Unity has script compilation errors. Resolve them before running {TestModeLabel} tests; no test scene was created.");
            }

            lock (RunSync)
            {
                if (_capabilityRunActive)
                {
                    throw new InvalidOperationException(
                        "Another Unity test capability call is already active.");
                }

                _capabilityRunActive =
                    true;
            }

            TestRunnerApi api =
                ScriptableObject.CreateInstance<TestRunnerApi>();

            IDisposable runEnvironment =
                null;

            try
            {
                runEnvironment =
                    PrepareRunEnvironment();
            }
            catch
            {
                Object.DestroyImmediate(
                    api);
                ReleaseRun();
                throw;
            }

            TestRunCallbacks callbacks =
                new TestRunCallbacks(
                    api,
                    request,
                    preview,
                    () =>
                    {
                        try
                        {
                            runEnvironment?.Dispose();
                        }
                        finally
                        {
                            ReleaseRun();
                        }
                    });

            api.RegisterCallbacks(
                callbacks);

            try
            {
                ExecutionSettings settings =
                    new ExecutionSettings(
                        filter)
                    {
                        runSynchronously =
                            false
                    };

                string runId =
                    api.Execute(
                        settings);

                callbacks.RunId =
                    runId;

                preview.RunId =
                    runId;

                using (
                    context.CancellationToken.Register(
                        callbacks.RequestCancellation))
                {
                    return await callbacks
                        .Completion
                        .Task;
                }
            }
            catch
            {
                callbacks.Dispose();

                throw;
            }
        }


        internal static Filter CreateFilter(
            RunEditModeTestsArguments arguments)
        {
            return CreateFilter(
                arguments,
                TestMode.EditMode);
        }


        internal static Filter CreateFilter(
            RunEditModeTestsArguments arguments,
            TestMode testMode)
        {
            return new Filter
            {
                testMode =
                    testMode,

                testNames =
                    Normalize(
                        arguments.TestNames),

                groupNames =
                    Normalize(
                        arguments.GroupNames),

                categoryNames =
                    Normalize(
                        arguments.CategoryNames),

                assemblyNames =
                    Normalize(
                        arguments.AssemblyNames)
            };
        }


        private static RunEditModeTestsResult CreateBaseResult(
            RunEditModeTestsArguments request,
            string testModeLabel)
        {
            RunEditModeTestsResult result =
                new RunEditModeTestsResult
            {
                TestNames =
                    Normalize(
                        request.TestNames)
                    .ToList(),

                GroupNames =
                    Normalize(
                        request.GroupNames)
                    .ToList(),

                CategoryNames =
                    Normalize(
                        request.CategoryNames)
                    .ToList(),

                AssemblyNames =
                    Normalize(
                        request.AssemblyNames)
                    .ToList()
            };

            result.ResponseInstruction =
                $"RunScheduled=true means Unity completed this {testModeLabel} test run and returned aggregate results. FailCount greater than zero is a failed verification even though the capability call itself succeeded. PreviewOnly=true does not execute tests.";

            return result;
        }


        private static string[] Normalize(
            IEnumerable<string> values)
        {
            return (values ??
                    Enumerable.Empty<string>())
                .Where(
                    value =>
                        !string.IsNullOrWhiteSpace(
                            value))
                .Select(
                    value =>
                        value.Trim())
                .Distinct(
                    StringComparer.Ordinal)
                .ToArray();
        }


        private static void ReleaseRun()
        {
            lock (RunSync)
            {
                _capabilityRunActive =
                    false;
            }
        }


        private sealed class TestRunCallbacks :
            ICallbacks,
            IDisposable
        {
            private readonly TestRunnerApi _api;

            private readonly RunEditModeTestsArguments _request;

            private readonly RunEditModeTestsResult _result;

            private readonly Action _onDisposed;

            private readonly List<EditorTestResultRecord> _leafResults =
                new List<EditorTestResultRecord>();

            private bool _disposed;


            public TestRunCallbacks(
                TestRunnerApi api,
                RunEditModeTestsArguments request,
                RunEditModeTestsResult result,
                Action onDisposed)
            {
                _api =
                    api;

                _request =
                    request;

                _result =
                    result;

                _onDisposed =
                    onDisposed;
            }


            public TaskCompletionSource<object> Completion
            {
                get;
            } =
                new TaskCompletionSource<object>(
                    TaskCreationOptions
                        .RunContinuationsAsynchronously);


            public string RunId
            {
                get;
                set;
            }


            public void RunStarted(
                ITestAdaptor testsToRun)
            {
            }


            public void RunFinished(
                ITestResultAdaptor result)
            {
                if (_disposed)
                    return;

                _result.PreviewOnly =
                    false;

                _result.RunScheduled =
                    true;

                _result.RunId =
                    RunId;

                _result.ResultState =
                    result.ResultState;

                _result.DurationSeconds =
                    result.Duration;

                _result.PassCount =
                    result.PassCount;

                _result.FailCount =
                    result.FailCount;

                _result.SkipCount =
                    result.SkipCount;

                _result.InconclusiveCount =
                    result.InconclusiveCount;

                _result.ResultsTruncated =
                    _leafResults.Count >
                    _request.MaxResults;

                _result.Results =
                    _leafResults
                        .Take(
                            _request.MaxResults)
                        .ToList();

                _result.ReturnedResultCount =
                    _result.Results.Count;

                Completion.TrySetResult(
                    _result);

                Dispose();
            }


            public void TestStarted(
                ITestAdaptor test)
            {
            }


            public void TestFinished(
                ITestResultAdaptor result)
            {
                if (_disposed ||
                    result.HasChildren)
                {
                    return;
                }

                bool failed =
                    result.FailCount >
                        0 ||
                    result.ResultState.StartsWith(
                        "Failed",
                        StringComparison.OrdinalIgnoreCase);

                if (!failed &&
                    !_request.IncludePassedTests)
                {
                    return;
                }

                if (_leafResults.Count >=
                    _request.MaxResults + 1)
                {
                    return;
                }

                _leafResults.Add(
                    new EditorTestResultRecord
                    {
                        FullName =
                            result.FullName,

                        ResultState =
                            result.ResultState,

                        DurationSeconds =
                            result.Duration,

                        Message =
                            result.Message,

                        StackTrace =
                            result.StackTrace,

                        Output =
                            result.Output
                    });
            }


            public void RequestCancellation()
            {
                EditorApplication.delayCall +=
                    CancelOnMainThread;
            }


            public void Dispose()
            {
                if (_disposed)
                    return;

                _disposed =
                    true;

                _api.UnregisterCallbacks(
                    this);

                Object.DestroyImmediate(
                    _api);

                _onDisposed();
            }


            private void CancelOnMainThread()
            {
                EditorApplication.delayCall -=
                    CancelOnMainThread;

                if (_disposed)
                    return;

                Completion.TrySetCanceled();

                Dispose();
            }
        }
    }


    public sealed class RunPlayModeTestsCapability :
        RunEditModeTestsCapability
    {
        private sealed class EnterPlayModeScope :
            IDisposable
        {
            private readonly bool _previousEnabled;

            private readonly EnterPlayModeOptions _previousOptions;

            private bool _disposed;


            public EnterPlayModeScope()
            {
                _previousEnabled =
                    EditorSettings.enterPlayModeOptionsEnabled;

                _previousOptions =
                    EditorSettings.enterPlayModeOptions;

                // A normal PlayMode entry reloads the Editor domain. That
                // destroys the awaiting Kanban run before TestRunnerApi can
                // return its result, so auto-resume starts the same test again
                // and repeatedly creates InitTestScene assets. Force scene
                // reload on for test isolation and to avoid Unity's blocking
                // "Reload Scene" warning when the user's normal Enter Play
                // Mode options disable it. Preserve only the domain containing
                // the capability and its completion callback.
                ControlPlayModeCapability
                    .ApplySafeAutomatedReloadOptions(
                        _previousOptions);
            }


            public void Dispose()
            {
                if (_disposed)
                {
                    return;
                }

                _disposed =
                    true;

                EditorSettings.enterPlayModeOptions =
                    _previousOptions;

                EditorSettings.enterPlayModeOptionsEnabled =
                    _previousEnabled;
            }
        }


        public override string Name =>
            "run_play_mode_tests";


        public override string Description =>
            "Runs Unity PlayMode tests through the public Test Runner API using optional test-name, regex group, category, and assembly filters, then returns aggregate and bounded leaf results.";


        protected override TestMode RunnerTestMode =>
            TestMode.PlayMode;


        protected override string TestModeLabel =>
            "PlayMode";


        protected override IDisposable PrepareRunEnvironment()
        {
            return new EnterPlayModeScope();
        }
    }
}
