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

using AIRuntime.Editor;
using AIRuntime.Runtime.Capabilities;
using AIRuntime.Runtime.Capabilities.Permissions;

using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.TestTools;


namespace AIRuntime.Tests
{
    public sealed class EditorComponentCapabilityTests
    {
        private AIRuntime.Runtime.AIRuntime _runtime;

        private GameObject _gameObject;

        private GameObject _secondGameObject;

        private string _folderPath;


        [SetUp]
        public void SetUp()
        {
            _runtime =
                new AIRuntime.Runtime.AIRuntime(
                    Application.dataPath);

            _gameObject =
                new GameObject(
                    "EditorComponentCapabilityTest");

            _secondGameObject =
                new GameObject(
                    "EditorSelectionSecondTarget");
        }


        [TearDown]
        public void TearDown()
        {
            Selection.activeObject =
                null;

            if (_gameObject != null)
            {
                Object.DestroyImmediate(
                    _gameObject);
            }

            if (_secondGameObject != null)
            {
                Object.DestroyImmediate(
                    _secondGameObject);
            }

            if (!string.IsNullOrWhiteSpace(
                    _folderPath) &&
                AssetDatabase.IsValidFolder(
                    _folderPath))
            {
                AssetDatabase.DeleteAsset(
                    _folderPath);
            }
        }

        [UnityTest]
        public IEnumerator GetSelection_DescribesSelectedProjectFolder()
        {
            string folderName =
                "__AIRuntimeSelectedFolderTest";

            _folderPath =
                $"Assets/{folderName}";

            if (AssetDatabase.IsValidFolder(
                    _folderPath))
            {
                AssetDatabase.DeleteAsset(
                    _folderPath);
            }

            string guid =
                AssetDatabase.CreateFolder(
                    "Assets",
                    folderName);

            Assert.IsFalse(
                string.IsNullOrWhiteSpace(
                    guid));

            Object folder =
                AssetDatabase.LoadMainAssetAtPath(
                    _folderPath);

            Assert.IsNotNull(
                folder);

            Selection.activeObject =
                folder;

            _runtime.RegisterCapability(
                new GetEditorSelectionCapability());

            _runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "get_editor_selection",
                    "{\"includeComponents\":true}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            JObject result =
                JObject.Parse(
                    task.GetAwaiter()
                        .GetResult());

            JObject selected =
                (JObject)result["Objects"]?[0];

            Assert.IsNotNull(
                selected);

            Assert.AreEqual(
                folderName,
                (string)selected["Name"]);

            Assert.AreEqual(
                _folderPath,
                (string)selected["AssetPath"]);

            Assert.IsTrue(
                (bool)selected["IsAsset"]);

            Assert.IsTrue(
                (bool)selected["IsFolder"]);

            Assert.AreEqual(
                _folderPath,
                (string)selected["FolderPath"]);

            Assert.AreEqual(
                "Assets",
                (string)selected["ParentFolderPath"]);

            Assert.IsFalse(
                (bool)selected["IsGameObject"]);
        }


        [UnityTest]
        public IEnumerator AddComponent_AddsExactRequestedType()
        {
            _runtime.RegisterCapability(
                new AddEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Write);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "add_editor_component",
                    $@"{{
                        ""gameObjectInstanceId"": {_gameObject.GetInstanceID()},
                        ""componentType"": ""UnityEngine.BoxCollider""
                    }}");


            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.IsNotNull(
                _gameObject.GetComponent<BoxCollider>());

            StringAssert.Contains(
                "UnityEngine.BoxCollider",
                task.GetAwaiter()
                    .GetResult());
        }


        [UnityTest]
        public IEnumerator CreateSelection_ReplacesWithExactInstanceIds()
        {
            Selection.activeObject =
                null;

            _runtime.RegisterCapability(
                new CreateEditorSelectionCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Write);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "create_editor_selection",
                    $@"{{
                        ""instanceIds"": [
                            {_gameObject.GetInstanceID()},
                            {_secondGameObject.GetInstanceID()}
                        ]
                    }}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            CollectionAssert.AreEquivalent(
                new[]
                {
                    _gameObject,
                    _secondGameObject
                },
                Selection.objects);

            Assert.AreEqual(
                _gameObject,
                Selection.activeObject);

            JObject result =
                JObject.Parse(
                    task.GetAwaiter()
                        .GetResult());

            Assert.IsTrue(
                (bool)result["Succeeded"]);

            Assert.AreEqual(
                2,
                (int)result["Count"]);
        }


        [UnityTest]
        public IEnumerator CreateSelection_AddsToCurrentSelection()
        {
            Selection.objects =
                new Object[]
                {
                    _gameObject
                };

            _runtime.RegisterCapability(
                new CreateEditorSelectionCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Write);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "create_editor_selection",
                    $@"{{
                        ""instanceIds"": [{_secondGameObject.GetInstanceID()}],
                        ""additive"": true
                    }}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            CollectionAssert.AreEquivalent(
                new[]
                {
                    _gameObject,
                    _secondGameObject
                },
                Selection.objects);

            Assert.AreEqual(
                _gameObject,
                Selection.activeObject,
                "Additive selection should preserve the existing object as Unity's active selection.");
        }


        [UnityTest]
        public IEnumerator CreateSelection_SelectsProjectFolderByAssetPath()
        {
            string folderName =
                "__AIRuntimeCreateSelectionFolderTest";

            _folderPath =
                $"Assets/{folderName}";

            string guid =
                AssetDatabase.CreateFolder(
                    "Assets",
                    folderName);

            Assert.IsFalse(
                string.IsNullOrWhiteSpace(
                    guid));

            _runtime.RegisterCapability(
                new CreateEditorSelectionCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Write);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "create_editor_selection",
                    $@"{{""assetPaths"": [""{_folderPath}""]}}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.AreEqual(
                _folderPath,
                AssetDatabase.GetAssetPath(
                    Selection.activeObject));

            JObject result =
                JObject.Parse(
                    task.GetAwaiter()
                        .GetResult());

            Assert.IsTrue(
                (bool)result["Objects"]?[0]?["IsFolder"]);
        }


        [UnityTest]
        public IEnumerator CreateSelection_ClearsCurrentSelection()
        {
            Selection.activeObject =
                _gameObject;

            _runtime.RegisterCapability(
                new CreateEditorSelectionCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Write);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "create_editor_selection",
                    "{\"clearSelection\":true}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.IsEmpty(
                Selection.objects);

            Assert.IsNull(
                Selection.activeObject);
        }


        [Test]
        public void CreateSelection_ExposesWriteTargetSchema()
        {
            CreateEditorSelectionCapability capability =
                new CreateEditorSelectionCapability();

            JObject schema =
                new CapabilitySchemaExporter()
                    .Export(
                        capability);

            Assert.AreEqual(
                CapabilityPermission.Write,
                capability.Permission);

            Assert.IsNotNull(
                schema["properties"]?["instanceIds"]);

            Assert.IsNotNull(
                schema["properties"]?["assetPaths"]);

            Assert.IsNotNull(
                schema["properties"]?["additive"]);

            Assert.IsNotNull(
                schema["properties"]?["clearSelection"]);
        }


        [UnityTest]
        public IEnumerator RemoveComponent_RemovesExactInstance()
        {
            BoxCollider collider =
                _gameObject.AddComponent<BoxCollider>();

            int componentInstanceId =
                collider.GetInstanceID();

            _runtime.RegisterCapability(
                new RemoveEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Destructive);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "remove_editor_component",
                    $@"{{
                        ""componentInstanceId"": {componentInstanceId}
                    }}");


            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.IsNull(
                _gameObject.GetComponent<BoxCollider>());

            StringAssert.Contains(
                "Removed",
                task.GetAwaiter()
                    .GetResult());
        }


        [UnityTest]
        public IEnumerator RemoveComponent_CanResolveUniqueTypeOnGameObject()
        {
            _gameObject.AddComponent<BoxCollider>();

            _runtime.RegisterCapability(
                new RemoveEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.Destructive);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "remove_editor_component",
                    $@"{{
                        ""gameObjectInstanceId"": {_gameObject.GetInstanceID()},
                        ""componentType"": ""UnityEngine.BoxCollider""
                    }}");


            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.IsNull(
                _gameObject.GetComponent<BoxCollider>());
        }

        [UnityTest]
        public IEnumerator InspectComponent_DiagnosesMeshColliderWithoutMesh()
        {
            MeshCollider collider =
                _gameObject.AddComponent<MeshCollider>();

            Selection.activeGameObject =
                _gameObject;

            _runtime.RegisterCapability(
                new InspectEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "inspect_editor_component",
                    $@"{{
                        ""gameObjectInstanceId"": {_gameObject.GetInstanceID()},
                        ""componentInstanceId"": {collider.GetInstanceID()},
                        ""includeSerializedProperties"": true
                    }}");


            yield return AwaitTask(
                task);


            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            JObject result =
                JObject.Parse(
                    task.GetAwaiter()
                        .GetResult());

            JObject inspected =
                result["Components"]?
                    .OfType<JObject>()
                    .Single();

            Assert.IsNotNull(
                inspected);

            Assert.AreEqual(
                "UnityEngine.MeshCollider",
                (string)inspected["Type"]);

            Assert.IsFalse(
                (bool)inspected["Collider"]["HasSharedMesh"]);

            Assert.IsTrue(
                inspected["Diagnostics"]?
                    .Any(
                        diagnostic =>
                            string.Equals(
                                (string)diagnostic["Code"],
                                "MeshColliderMissingMesh",
                                System.StringComparison.Ordinal)) ??
                false);

            Assert.IsTrue(
                inspected["Properties"]?
                    .Any(
                        property =>
                            string.Equals(
                                (string)property["Path"],
                                "m_Mesh",
                                System.StringComparison.Ordinal)) ??
                false);
        }


        [UnityTest]
        public IEnumerator InspectComponent_ExposesRendererMaterialsAndRigidbodyConstraints()
        {
            string folderName =
                "__AIRuntimeComponentEvidence_" +
                System.Guid.NewGuid().ToString("N");

            _folderPath =
                "Assets/" + folderName;

            AssetDatabase.CreateFolder(
                "Assets",
                folderName);

            string materialPath =
                _folderPath + "/Mat_Ball.mat";

            Material material =
                new Material(
                    Shader.Find("Standard"));

            AssetDatabase.CreateAsset(
                material,
                materialPath);

            MeshRenderer renderer =
                _gameObject.AddComponent<MeshRenderer>();

            renderer.sharedMaterial =
                material;

            Rigidbody rigidbody =
                _gameObject.AddComponent<Rigidbody>();

            rigidbody.constraints =
                RigidbodyConstraints.FreezePositionZ |
                RigidbodyConstraints.FreezeRotationX |
                RigidbodyConstraints.FreezeRotationY;

            Selection.activeGameObject =
                _gameObject;

            _runtime.RegisterCapability(
                new InspectEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "inspect_editor_component",
                    $@"{{
                        ""gameObjectInstanceId"": {_gameObject.GetInstanceID()},
                        ""includeSerializedProperties"": true
                    }}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            JObject result =
                JObject.Parse(
                    task.GetAwaiter().GetResult());

            JObject rendererResult =
                result["Components"]
                    .OfType<JObject>()
                    .Single(component =>
                        (string)component["Type"] ==
                        "UnityEngine.MeshRenderer");

            Assert.AreEqual(
                materialPath,
                (string)rendererResult["Renderer"]
                    ["Materials"][0]["AssetPath"]);

            Assert.IsTrue(
                rendererResult["Properties"]
                    .Any(property =>
                        (string)property["Path"] ==
                            "m_Materials.Array.data[0]" &&
                        (string)property["AssetPath"] ==
                            materialPath));

            JObject rigidbodyResult =
                result["Components"]
                    .OfType<JObject>()
                    .Single(component =>
                        (string)component["Type"] ==
                        "UnityEngine.Rigidbody");

            Assert.IsTrue(
                (bool)rigidbodyResult["Rigidbody"]
                    ["FreezePositionZ"]);

            Assert.AreEqual(
                (int)rigidbody.constraints,
                (int)rigidbodyResult["Rigidbody"]
                    ["ConstraintsValue"]);

            Assert.IsTrue(
                rigidbodyResult["Properties"]
                    .Any(property =>
                        (string)property["Path"] ==
                        "m_Constraints"));
        }


        [UnityTest]
        public IEnumerator InspectComponent_ExposesMonoBehaviourScriptAssetPath()
        {
            EventSystem component =
                _gameObject.AddComponent<EventSystem>();

            _runtime.RegisterCapability(
                new InspectEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "inspect_editor_component",
                    $@"{{
                        ""gameObjectInstanceId"": {_gameObject.GetInstanceID()},
                        ""componentInstanceId"": {component.GetInstanceID()},
                        ""includeSerializedProperties"": true
                    }}");

            yield return AwaitTask(
                task);

            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            JObject result =
                JObject.Parse(
                    task.GetAwaiter().GetResult());

            string scriptAssetPath =
                result["Components"]?[0]?["ScriptAssetPath"]
                    ?.Value<string>();

            Assert.IsNotEmpty(
                scriptAssetPath,
                "The inspected MonoBehaviour must expose its script asset path.");

            Assert.IsTrue(
                scriptAssetPath.StartsWith(
                    "Assets/") ||
                scriptAssetPath.StartsWith(
                    "Packages/"),
                $"Expected a Unity asset path, but received '{scriptAssetPath}'.");

            Assert.That(
                scriptAssetPath,
                Does.EndWith(
                    ".cs"));
        }


        [UnityTest]
        public IEnumerator InspectComponent_UsesOnlySelectedGameObjectWhenIdOmitted()
        {
            BoxCollider collider =
                _gameObject.AddComponent<BoxCollider>();

            collider.enabled =
                false;

            Selection.activeGameObject =
                _gameObject;

            _runtime.RegisterCapability(
                new InspectEditorComponentCapability());

            _runtime.AllowPermission(
                CapabilityPermission.ReadOnly);

            Task<string> task =
                _runtime.ExecuteCapabilityAsync(
                    "inspect_editor_component",
                    "{\"componentType\":\"UnityEngine.BoxCollider\"}");


            yield return AwaitTask(
                task);


            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            JObject result =
                JObject.Parse(
                    task.GetAwaiter()
                        .GetResult());

            Assert.AreEqual(
                _gameObject.GetInstanceID(),
                (int)result["GameObjectInstanceId"]);

            Assert.IsTrue(
                result["Diagnostics"]?
                    .Any(
                        diagnostic =>
                            string.Equals(
                                (string)diagnostic["Code"],
                                "ComponentDisabled",
                                System.StringComparison.Ordinal)) ??
                false);
        }

        [Test]
        public void InspectComponent_ExposesReadOnlyTargetSchema()
        {
            InspectEditorComponentCapability capability =
                new InspectEditorComponentCapability();

            CapabilitySchemaExporter exporter =
                new CapabilitySchemaExporter();

            JObject schema =
                exporter.Export(
                    capability);

            Assert.AreEqual(
                CapabilityPermission.ReadOnly,
                capability.Permission);

            Assert.AreEqual(
                "inspect_editor_component",
                capability.Name);

            Assert.IsNotNull(
                schema["properties"]?["gameObjectInstanceId"]);

            Assert.IsNotNull(
                schema["properties"]?["componentInstanceId"]);

            Assert.IsNotNull(
                schema["properties"]?["componentType"]);

            Assert.IsNotNull(
                schema["properties"]?["includeSerializedProperties"]);
        }


        private static IEnumerator AwaitTask(
            Task task)
        {
            while (!task.IsCompleted)
            {
                yield return null;
            }
        }
    }
}
