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

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

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;

using Object =
    UnityEngine.Object;


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

        private Scene _scene;

        private GameObject _root;

        private GameObject _child;

        private string _prefabPath;

        private string _materialPath;

        private string _substrateTestFolder;


        private sealed class TestKanbanTaskExecutionServices :
            IKanbanTaskExecutionServices,
            IServiceProvider
        {
            private readonly Func<object> _read;


            public TestKanbanTaskExecutionServices(
                Func<object> read)
            {
                _read = read;
            }


            public object ReadTaskContext()
            {
                return _read();
            }


            public object CreateChildTask(
                CreateKanbanChildTaskArguments arguments)
            {
                throw new InvalidOperationException(
                    "Child creation is not part of this test.");
            }


            public object CommitPlan()
            {
                throw new InvalidOperationException(
                    "Plan commit is not part of this test.");
            }


            public object GetService(
                Type serviceType)
            {
                return serviceType == typeof(IKanbanTaskExecutionServices)
                    ? this
                    : null;
            }
        }


        [Test]
        public void WorkflowCapabilities_DeclareGenericExecutionSemantics()
        {
            ICapabilityExecutionSemantics childCreation =
                new CreateKanbanChildTaskCapability();

            ICapabilityExecutionSemantics planCommit =
                new CommitKanbanPlanCapability();

            ICapabilityExecutionSemantics editModeTests =
                new RunEditModeTestsCapability();

            ICapabilityExecutionSemantics playModeTests =
                new RunPlayModeTestsCapability();

            ICapabilityObservationSemantics serializedProperties =
                new GetSerializedPropertiesCapability();

            ICapabilityObservationSemantics hierarchyTree =
                new GetHierarchyTreeCapability();


            Assert.AreEqual(
                CapabilityBatchBehavior.AllowSameCapabilityBatch,
                childCreation.BatchBehavior);

            Assert.AreEqual(
                CapabilityRepeatedCallBehavior.ReplayPreviousResult,
                childCreation.RepeatedCallBehavior);

            Assert.AreEqual(
                CapabilityRepeatedCallBehavior.ReplayPreviousResult,
                planCommit.RepeatedCallBehavior);

            Assert.AreEqual(
                CapabilityBatchBehavior.SingleMutationPerResponse,
                planCommit.BatchBehavior);

            Assert.AreEqual(
                CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult,
                editModeTests.RepeatedCallBehavior);

            Assert.AreEqual(
                CapabilityRepeatedCallBehavior.CompleteRunFromPreviousResult,
                playModeTests.RepeatedCallBehavior);

            Assert.AreEqual(
                CapabilityObservationBehavior.StableUntilMutation,
                serializedProperties.ObservationBehavior);

            Assert.AreEqual(
                CapabilityObservationBehavior.StableUntilMutation,
                hierarchyTree.ObservationBehavior);
        }


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

            _scene =
                EditorSceneManager.NewPreviewScene();

            _scene.name =
                "__AIRuntimeEditorCapabilityTests_" +
                Guid.NewGuid().ToString(
                    "N");

            _root =
                new GameObject(
                    "AutomationRoot");

            _child =
                new GameObject(
                    "AutomationChild");

            _child.transform.SetParent(
                _root.transform,
                false);

            SceneManager.MoveGameObjectToScene(
                _root,
                _scene);

            Selection.activeGameObject =
                _root;
        }


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

            if (!string.IsNullOrWhiteSpace(
                    _prefabPath))
            {
                AssetDatabase.DeleteAsset(
                    _prefabPath);
            }

            if (!string.IsNullOrWhiteSpace(
                    _materialPath))
            {
                AssetDatabase.DeleteAsset(
                    _materialPath);
            }

            if (!string.IsNullOrWhiteSpace(_substrateTestFolder))
                AssetDatabase.DeleteAsset(_substrateTestFolder);

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

            if (_scene.IsValid() &&
                _scene.isLoaded)
            {
                EditorSceneManager.ClosePreviewScene(
                    _scene);
            }
        }


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

            Register(
                new GetSerializedPropertiesCapability());

            Task<string> task =
                Execute(
                    "get_serialized_properties",
                    new
                    {
                        componentInstanceId =
                            collider.GetInstanceID(),

                        includeHidden =
                            false,

                        maxProperties =
                            100
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.AreEqual(
                collider.GetInstanceID(),
                (int)result["ObjectInstanceId"]);

            Assert.IsTrue(
                result["Properties"]
                    .Any(
                        property =>
                            string.Equals(
                                (string)property["Path"],
                                "m_IsTrigger",
                                StringComparison.Ordinal)));
        }


        [UnityTest]
        public IEnumerator GetSerializedProperties_DuplicateTypes_ReportExactInstanceIds()
        {
            CapsuleCollider first =
                _root.AddComponent<CapsuleCollider>();

            CapsuleCollider second =
                _root.AddComponent<CapsuleCollider>();

            Register(
                new GetSerializedPropertiesCapability());

            LogAssert.Expect(
                LogType.Error,
                $"[Capability] 'get_serialized_properties' failed: GameObject '{_root.name}' has 2 components matching 'UnityEngine.CapsuleCollider'. Matching ComponentInstanceIds: {first.GetInstanceID()}, {second.GetInstanceID()}. Retry with one exact ComponentInstanceId after using inspect_game_object or inspect_editor_component to identify the intended component.");

            Task<string> task =
                Execute(
                    "get_serialized_properties",
                    new
                    {
                        objectInstanceId =
                            _root.GetInstanceID(),

                        componentType =
                            "UnityEngine.CapsuleCollider"
                    });

            yield return AwaitTask(task);

            Assert.IsTrue(
                task.IsFaulted);

            string failure =
                task.Exception?.ToString() ??
                string.Empty;

            StringAssert.Contains(
                first.GetInstanceID().ToString(),
                failure);

            StringAssert.Contains(
                second.GetInstanceID().ToString(),
                failure);

            StringAssert.Contains(
                "Retry with one exact ComponentInstanceId",
                failure);
        }


        [Test]
        public void CreateEditorScene_RecognizesUntouchedDefaultSceneObjects()
        {
            Scene defaultScene =
                EditorSceneManager.NewPreviewScene();

            GameObject cameraObject =
                new GameObject(
                    "Main Camera",
                    typeof(Camera),
                    typeof(AudioListener));

            cameraObject.transform.position =
                new Vector3(
                    0f,
                    1f,
                    -10f);

            SceneManager.MoveGameObjectToScene(
                cameraObject,
                defaultScene);

            GameObject lightObject =
                new GameObject(
                    "Directional Light",
                    typeof(Light));

            lightObject.GetComponent<Light>().type =
                LightType.Directional;

            lightObject.transform.rotation =
                Quaternion.Euler(
                    50f,
                    -30f,
                    0f);

            SceneManager.MoveGameObjectToScene(
                lightObject,
                defaultScene);

            try
            {
                MethodInfo method =
                    typeof(CreateEditorSceneCapability)
                        .GetMethod(
                            "ContainsOnlyDefaultSceneObjects",
                            BindingFlags.NonPublic |
                            BindingFlags.Static);

                Assert.IsNotNull(
                    method);

                Assert.IsTrue(
                    (bool)method.Invoke(
                        null,
                        new object[]
                        {
                            defaultScene
                        }));

                cameraObject.transform.position =
                    Vector3.zero;

                Assert.IsTrue(
                    (bool)method.Invoke(
                        null,
                        new object[]
                        {
                            defaultScene
                        }),
                    "Unity-version-specific startup transforms must not prevent recognition of the default object pair.");

                cameraObject.AddComponent<BoxCollider>();

                Assert.IsFalse(
                    (bool)method.Invoke(
                        null,
                        new object[]
                        {
                            defaultScene
                        }),
                    "An extra component must preserve the untitled scene as user work.");
            }
            finally
            {
                EditorSceneManager.ClosePreviewScene(
                    defaultScene);
            }
        }


        [UnityTest]
        public IEnumerator SaveEditorScene_PersistsPopulatedUntitledSceneToExplicitPath()
        {
            if (!Application.isBatchMode)
            {
                Assert.Ignore(
                    "This scene-replacement test runs only in an isolated batch project so it can never replace a user's active untitled scene.");
            }

            Register(
                new SaveEditorSceneCapability(),
                CapabilityPermission.Write);

            _substrateTestFolder =
                "Assets/__AIRuntimeUntitledScene_" +
                Guid.NewGuid().ToString("N");
            string destinationScenePath =
                _substrateTestFolder +
                "/Recovered.unity";
            Scene untitledScene =
                EditorSceneManager.NewScene(
                    NewSceneSetup.EmptyScene,
                    NewSceneMode.Single);
            SceneManager.SetActiveScene(
                untitledScene);
            GameObject preserved =
                new GameObject(
                    "PreservedAgentHierarchy");

            try
            {
                Assert.AreEqual(
                    string.Empty,
                    untitledScene.path);

                Task<string> task =
                    Execute(
                        "save_editor_scene",
                        new
                        {
                            destinationScenePath
                        });

                yield return AwaitTask(
                    task);

                JObject result =
                    ParseSuccessful(
                        task);

                Assert.AreEqual(
                    destinationScenePath,
                    (string)result["ScenePath"]);
                Assert.IsTrue(
                    (bool)result["PersistedUntitledScene"]);
                Assert.IsTrue(
                    (bool)result["PersistenceVerified"]);
                Assert.IsFalse(
                    (bool)result["DirtyAfterSave"]);
                CollectionAssert.Contains(
                    result["PersistedGameObjectPaths"]
                        .Values<string>()
                        .ToArray(),
                    "PreservedAgentHierarchy");
                Assert.IsNotNull(
                    AssetDatabase.LoadAssetAtPath<SceneAsset>(
                        destinationScenePath));
            }
            finally
            {
                EditorSceneManager.NewScene(
                    NewSceneSetup.EmptyScene,
                    NewSceneMode.Single);
            }
        }


        [UnityTest]
        public IEnumerator GetKanbanTaskContext_ReturnsScopedAuthoritativeMetadata()
        {
            Func<object> read =
                () =>
                    new Dictionary<string, object>
                    {
                        { "TaskId", "child-1" },
                        { "Title", "Verify child" },
                        { "Status", "In Progress" },
                        { "CreatedStatus", "To Do" },
                        { "AssignedAgent", "DeveloperAgent" },
                        { "ParentTaskId", "parent-1" },
                        { "ParentTitle", "Verify parent" },
                        { "ParentAssignedAgent", "DeveloperAgent" },
                        { "IsChildTask", true },
                        { "CreationSource", "create_kanban_child_task" },
                        { "AgentAssignmentInherited", true },
                        { "AssignmentMatchesParent", true },
                        { "DirectChildCount", 2 },
                        {
                            "DirectChildren",
                            new object[]
                            {
                                new
                                {
                                    TaskId = "grandchild-1",
                                    Title = "Verify probe",
                                    ParentTaskId = "child-1",
                                    CreationSource = "create_kanban_child_task",
                                    CreatedStatus = "To Do",
                                    Status = "Done",
                                    AssignedAgent = "DeveloperAgent",
                                    AgentAssignmentInherited = true,
                                    AssignmentMatchesParent = true
                                }
                            }
                        },
                        { "ParentDirectChildCount", 1 },
                        { "IncompleteDescendantCount", 1 },
                        { "HasIncompleteDescendants", true },
                        { "AllDescendantsDone", false },
                        { "DescendantGateState", "BlockedInToDo" }
                    };

            TestKanbanTaskExecutionServices services =
                new TestKanbanTaskExecutionServices(
                    read);

            Register(
                AIRuntimeEditorPluginCatalog.CreateCapability(
                    "get_kanban_task_context",
                    AIRuntimeEditorPluginHost.Kanban,
                    services));

            Task<string> task =
                Execute(
                    "get_kanban_task_context",
                    new { });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

                Assert.AreEqual("child-1", (string)result["TaskId"]);
                Assert.AreEqual("parent-1", (string)result["ParentTaskId"]);
                Assert.AreEqual("To Do", (string)result["CreatedStatus"]);
                Assert.AreEqual(
                    "create_kanban_child_task",
                    (string)result["CreationSource"]);
                Assert.IsTrue((bool)result["AgentAssignmentInherited"]);
                Assert.IsTrue((bool)result["AssignmentMatchesParent"]);
                Assert.AreEqual(2, (int)result["DirectChildCount"]);
                Assert.AreEqual(
                    "grandchild-1",
                    (string)result["DirectChildren"][0]["TaskId"]);
                Assert.AreEqual(
                    "create_kanban_child_task",
                    (string)result["DirectChildren"][0]["CreationSource"]);
                Assert.IsTrue(
                    (bool)result["DirectChildren"][0]["AgentAssignmentInherited"]);
                Assert.AreEqual(1, (int)result["ParentDirectChildCount"]);
                Assert.AreEqual(1, (int)result["IncompleteDescendantCount"]);
                Assert.IsTrue((bool)result["HasIncompleteDescendants"]);
                Assert.IsFalse((bool)result["AllDescendantsDone"]);
            Assert.AreEqual(
                "BlockedInToDo",
                (string)result["DescendantGateState"]);
        }


        [Test]
        public void AddComponentVerification_CreatesDisposableTargetWhenSelectionIsEmpty()
        {
            Type runnerType =
                typeof(AIRuntimeKanbanSession)
                    .Assembly
                    .GetType(
                        "AIRuntime.Editor.AIRuntimeKanbanAgentRunner",
                        true);

            MethodInfo prepare =
                runnerType.GetMethod(
                    "PrepareDisposableAddComponentVerificationTarget",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.IsNotNull(prepare);

            Selection.objects =
                Array.Empty<Object>();

            GameObject target =
                null;

            try
            {
                target =
                    (GameObject)prepare.Invoke(
                        null,
                        null);

                Assert.AreSame(
                    target,
                    Selection.activeGameObject);
                StringAssert.StartsWith(
                    "AIRuntime Add Component Verification ",
                    target.name);
                Assert.IsNull(
                    target.GetComponent<BoxCollider>());
            }
            finally
            {
                Selection.activeGameObject =
                    _root;

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


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

            collider.isTrigger =
                false;

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            collider.GetInstanceID(),

                        propertyPath =
                            "m_IsTrigger",

                        valueJson =
                            "true",

                        useExpectedValue =
                            true,

                        expectedValueJson =
                            "false"
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.IsTrue(
                collider.isTrigger);

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

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

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

            Undo.PerformUndo();

            Assert.IsFalse(
                collider.isTrigger,
                "The serialized-property mutation should participate in Unity Undo.");
        }


        [UnityTest]
        public IEnumerator SetSerializedProperty_AcceptsEnumIndexObjectReturnedByInspection()
        {
            UnityEngine.UI.CanvasScaler scaler =
                _root.AddComponent<UnityEngine.UI.CanvasScaler>();

            scaler.uiScaleMode =
                UnityEngine.UI.CanvasScaler.ScaleMode.ConstantPixelSize;

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            scaler.GetInstanceID(),

                        propertyPath =
                            "m_UiScaleMode",

                        valueJson =
                            "{\"index\":1}"
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.AreEqual(
                UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize,
                scaler.uiScaleMode);

            Assert.AreEqual(
                "ScaleWithScreenSize",
                (string)result["CurrentValue"]?["name"]);

            Assert.IsTrue(
                (bool)result["Verified"]);
        }


        [UnityTest]
        public IEnumerator SetSerializedProperty_ResolvesUniqueTopLevelInspectorAliases()
        {
            Camera camera =
                _root.AddComponent<Camera>();

            camera.orthographic = false;
            camera.orthographicSize = 5f;

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> orthographicTask =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            camera.GetInstanceID(),
                        propertyPath =
                            "m_Orthographic",
                        valueJson =
                            "true"
                    });

            yield return AwaitTask(
                orthographicTask);

            JObject orthographicResult =
                ParseSuccessful(
                    orthographicTask);

            Assert.IsTrue(camera.orthographic);
            Assert.AreEqual(
                "orthographic",
                (string)orthographicResult["PropertyPath"]);

            Task<string> sizeTask =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            camera.GetInstanceID(),
                        propertyPath =
                            "m_OrthographicSize",
                        valueJson =
                            "7.5"
                    });

            yield return AwaitTask(
                sizeTask);

            JObject sizeResult =
                ParseSuccessful(
                    sizeTask);

            Assert.AreEqual(7.5f, camera.orthographicSize);
            Assert.AreEqual(
                "orthographic size",
                (string)sizeResult["PropertyPath"]);
        }


        [Test]
        public void BuiltInObjectReference_DoesNotAdvertiseUnresolvableGlobalId()
        {
            GameObject primitive =
                GameObject.CreatePrimitive(
                    PrimitiveType.Cube);

            try
            {
                Material builtInMaterial =
                    primitive
                        .GetComponent<MeshRenderer>()
                        .sharedMaterial;

                Assert.NotNull(builtInMaterial);

                Type utilityType =
                    typeof(AIRuntimeKanbanSession)
                        .Assembly
                        .GetType(
                            "AIRuntime.Editor.EditorCapabilityUtility",
                            true);

                MethodInfo method =
                    utilityType.GetMethod(
                        "GetGlobalObjectId",
                        BindingFlags.Public |
                        BindingFlags.Static);

                Assert.NotNull(method);

                string globalObjectId =
                    (string)method.Invoke(
                        null,
                        new object[]
                        {
                            builtInMaterial
                        });

                Assert.IsEmpty(globalObjectId);
                Assert.AreNotEqual(0, builtInMaterial.GetInstanceID());

                SerializedObject rendererObject =
                    new SerializedObject(
                        primitive.GetComponent<MeshRenderer>());

                SerializedProperty materialProperty =
                    rendererObject.FindProperty(
                        "m_Materials.Array.data[0]");

                Assert.NotNull(materialProperty);

                Type utilityTypeForValues =
                    typeof(AIRuntimeKanbanSession)
                        .Assembly
                        .GetType(
                            "AIRuntime.Editor.EditorCapabilityUtility",
                            true);

                MethodInfo readMethod =
                    utilityTypeForValues.GetMethod(
                        "ReadSerializedValue",
                        BindingFlags.Public |
                        BindingFlags.Static);

                Assert.NotNull(readMethod);

                JObject serializedReference =
                    (JObject)readMethod.Invoke(
                        null,
                        new object[]
                        {
                            materialProperty
                        });

                Assert.AreEqual(
                    string.Empty,
                    (string)serializedReference["globalObjectId"]);
            }
            finally
            {
                Object.DestroyImmediate(primitive);
            }
        }


        [Test]
        public void PersistentAssetGlobalId_ResolvesAfterAssetIsUnloaded()
        {
            _materialPath =
                "Assets/__AIRuntimeGlobalIdMaterialTest_" +
                Guid.NewGuid().ToString("N") +
                ".mat";

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

            AssetDatabase.CreateAsset(
                material,
                _materialPath);

            AssetDatabase.SaveAssets();

            string globalObjectId =
                UnityEditor.GlobalObjectId
                    .GetGlobalObjectIdSlow(
                        material)
                    .ToString();

            Resources.UnloadAsset(
                material);

            Type utilityType =
                typeof(AIRuntimeKanbanSession)
                    .Assembly
                    .GetType(
                        "AIRuntime.Editor.EditorCapabilityUtility",
                        true);

            MethodInfo method =
                utilityType.GetMethod(
                    "ResolveObject",
                    BindingFlags.Public |
                    BindingFlags.Static);

            Assert.NotNull(method);

            Object resolved =
                (Object)method.Invoke(
                    null,
                    new object[]
                    {
                        0,
                        globalObjectId,
                        false
                    });

            Assert.NotNull(resolved);
            Assert.IsInstanceOf<Material>(resolved);
            Assert.AreEqual(
                _materialPath,
                AssetDatabase.GetAssetPath(resolved));
        }


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

            Register(
                new InspectGameObjectCapability());

            Task<string> task =
                Execute(
                    "inspect_game_object",
                    new
                    {
                        objectInstanceId =
                            _root.GetInstanceID(),

                        includeComponents =
                            true
                    });

            yield return AwaitTask(
                task);

            JObject gameObject =
                ParseSuccessful(
                    task)["GameObject"] as JObject;

            Assert.IsNotNull(
                gameObject);

            Assert.AreEqual(
                "AutomationRoot",
                (string)gameObject["Name"]);

            Assert.AreEqual(
                1,
                (int)gameObject["ChildCount"]);

            Assert.IsTrue(
                gameObject["Components"]
                    .Any(
                        component =>
                            (int)component["InstanceId"] ==
                            collider.GetInstanceID()));
        }


        [UnityTest]
        public IEnumerator GetHierarchyTree_ReturnsNestedChildren()
        {
            Register(
                new GetHierarchyTreeCapability());

            Task<string> task =
                Execute(
                    "get_hierarchy_tree",
                    new
                    {
                        rootObjectInstanceId =
                            _root.GetInstanceID(),

                        maxDepth =
                            2,

                        maxObjects =
                            10
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

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

            Assert.AreEqual(
                "AutomationChild",
                (string)result["Roots"][0]["Children"][0]["Name"]);
        }


        [UnityTest]
        public IEnumerator FindGameObjects_AppliesAllFilters()
        {
            Register(
                new FindGameObjectsCapability());

            Task<string> task =
                Execute(
                    "find_game_objects",
                    new
                    {
                        name =
                            "AutomationChild",

                        exactName =
                            true,

                        componentType =
                            "UnityEngine.Transform",

                        scenePath =
                            _scene.name,

                        maxResults =
                            10
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.AreEqual(
                1,
                (int)result["MatchCount"]);

            Assert.AreEqual(
                _child.GetInstanceID(),
                (int)result["Objects"][0]["InstanceId"]);
        }


        [UnityTest]
        public IEnumerator FindGameObjects_UnknownLayerReturnsEmptyDiagnostic()
        {
            Register(
                new FindGameObjectsCapability());

            Task<string> task =
                Execute(
                    "find_game_objects",
                    new
                    {
                        name = "AutomationRoot",
                        exactName = true,
                        layerName = "__AIRuntimeMissingLayer__"
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            Assert.AreEqual(0, (int)result["MatchCount"]);
            Assert.IsFalse((bool)result["LayerExists"]);
            Assert.AreEqual(
                "__AIRuntimeMissingLayer__",
                (string)result["RequestedLayerName"]);
            StringAssert.Contains(
                "retry using the name filter",
                (string)result["Warning"]);
        }


        [UnityTest]
        public IEnumerator GetConsoleLogs_ReturnsCapturedMarker()
        {
            string marker =
                "__AIRuntimeConsoleCapability_" +
                Guid.NewGuid().ToString(
                    "N");

            Debug.Log(
                marker);

            Register(
                new GetConsoleLogsCapability());

            Task<string> task =
                Execute(
                    "get_console_logs",
                    new
                    {
                        contains =
                            marker,

                        includeLogs =
                            true,

                        includeWarnings =
                            false,

                        includeErrors =
                            false,

                        maxEntries =
                            10
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.AreEqual(
                1,
                (int)result["EntryCount"]);

            Assert.AreEqual(
                marker,
                (string)result["Entries"][0]["Message"]);
        }


        [UnityTest]
        public IEnumerator CompilerFeedback_DiagnosticsSurviveRestoreAndFiltersPreserveFingerprint()
        {
            Type buffer = typeof(GetCompilationErrorsCapability).Assembly.GetType("AIRuntime.Editor.EditorCompilationMessageBuffer", true);
            var flags = BindingFlags.NonPublic | BindingFlags.Static;
            MethodInfo restore = buffer.GetMethod("RestoreSnapshot", flags);
            MethodInfo serialize = buffer.GetMethod("SerializeSnapshot", flags);
            string original = (string)serialize.Invoke(null, null);
            try
            {
                string snapshot = new JObject {
                    ["CapturedSinceUtc"] = "2026-09-07T17:10:00Z",
                    ["Messages"] = new JArray(
                        new JObject { ["AssemblyPath"] = "Game.dll", ["Type"] = "Error", ["File"] = "Assets/Game/Hazard.cs", ["Line"] = 87, ["Column"] = 25, ["Message"] = "CS1061 missing GameManager API" },
                        new JObject { ["AssemblyPath"] = "Game.dll", ["Type"] = "Error", ["File"] = "Assets/Game/UI.cs", ["Line"] = 12, ["Column"] = 1, ["Message"] = "CS0246 unknown type" })
                }.ToString();
                restore.Invoke(null, new object[] { snapshot });
                string saved = (string)serialize.Invoke(null, null);
                buffer.GetMethod("OnCompilationStarted", flags).Invoke(null, new object[] { null });
                restore.Invoke(null, new object[] { saved });
                Register(new GetCompilationErrorsCapability());
                Task<string> first = Execute("get_compilation_errors", new { contains = "Hazard", maxMessages = 1 });
                yield return AwaitTask(first);
                Task<string> second = Execute("get_compilation_errors", new { contains = "UI", maxMessages = 10 });
                yield return AwaitTask(second);
                JObject a = ParseSuccessful(first), b = ParseSuccessful(second);
                Assert.AreEqual(2, (int)a["ErrorCount"]);
                Assert.AreEqual(DateTime.Parse("2026-09-07T17:10:00Z", null,
                    System.Globalization.DateTimeStyles.RoundtripKind), a["CapturedSinceUtc"].Value<DateTime>());
                Assert.AreEqual((string)a["ErrorFingerprint"], (string)b["ErrorFingerprint"]);
                Assert.AreEqual(87, (int)a["Messages"][0]["Line"]);
                Assert.AreEqual("Assets/Game/UI.cs", (string)b["Messages"][0]["File"]);

                Type taskType = typeof(AIRuntimeKanbanSession).GetNestedType("KanbanTask", BindingFlags.NonPublic);
                object card = Activator.CreateInstance(taskType);
                taskType.GetField("ProducedArtifactPaths").SetValue(card, new List<string> { "Assets/Game/Hazard.cs" });
                object diagnostics = buffer.GetMethod("Snapshot", flags).Invoke(null, null);
                string feedback = (string)typeof(AIRuntimeKanbanSession).GetMethod("BuildCompilerReworkFeedback", flags)
                    .Invoke(null, new[] { card, diagnostics });
                StringAssert.Contains("Hazard.cs(87,25)", feedback);
                StringAssert.Contains("CS1061", feedback);
                StringAssert.DoesNotContain("CS0246", feedback);
            }
            finally
            {
                restore.Invoke(null, new object[] { original });
                buffer.GetMethod("PersistSnapshot", flags).Invoke(null, null);
            }
        }

        [Test]
        public void CompilerFeedback_MissingEvidenceRequestsOnlyOneCompilationUntilStateChanges()
        {
            Type buffer = typeof(GetCompilationErrorsCapability).Assembly.GetType("AIRuntime.Editor.EditorCompilationMessageBuffer", true);
            var flags = BindingFlags.NonPublic | BindingFlags.Static;
            var method = buffer.GetMethod("TryRecaptureMissingDiagnostics", flags);
            FieldInfo requested = buffer.GetField("_missingDiagnosticsRecaptureRequested", flags);
            bool previous = (bool)requested.GetValue(null);
            string key = (string)buffer.GetProperty("SessionKey", flags).GetValue(null);
            try
            {
                requested.SetValue(null, false);
                int count = 0;
                Action capture = () => count++;
                Assert.IsTrue((bool)method.Invoke(null, new object[] { true, false, 0, capture }));
                Assert.IsFalse((bool)method.Invoke(null, new object[] { true, false, 0, capture }));
                Assert.AreEqual(1, count);
                Assert.IsFalse((bool)method.Invoke(null, new object[] { true, true, 0, capture }));
                method.Invoke(null, new object[] { false, false, 0, capture });
                Assert.IsTrue((bool)method.Invoke(null, new object[] { true, false, 0, capture }));
                Assert.AreEqual(2, count);
            }
            finally
            {
                requested.SetValue(null, previous);
                SessionState.SetBool(key + ".Recapture", previous);
            }
        }

        [UnityTest]
        public IEnumerator GetCompilationErrors_ReturnsGenerationState()
        {
            Register(
                new GetCompilationErrorsCapability());

            Task<string> task =
                Execute(
                    "get_compilation_errors",
                    new
                    {
                        includeWarnings =
                            true,

                        maxMessages =
                            25
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

            Assert.IsNotNull(
                result["IsCompiling"]);

            Assert.IsFalse(
                string.IsNullOrWhiteSpace(
                    (string)result["CapturedSinceUtc"]));

            Assert.IsNotNull(
                result["Messages"]);
        }


        [UnityTest]
        public IEnumerator RunEditModeTests_PreviewNormalizesWithoutRecursion()
        {
            Register(
                new RunEditModeTestsCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "run_edit_mode_tests",
                    new
                    {
                        previewOnly =
                            true,

                        testNames =
                            new[]
                            {
                                "  AIRuntime.Tests.Sample.Test  ",
                                "AIRuntime.Tests.Sample.Test"
                            },

                        assemblyNames =
                            new[]
                            {
                                "AIRuntime.Tests"
                            }
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

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

            Assert.IsFalse(
                (bool)result["RunScheduled"]);

            Assert.AreEqual(
                1,
                result["TestNames"].Count());

            Assert.AreEqual(
                "AIRuntime.Tests.Sample.Test",
                (string)result["TestNames"][0]);
        }


        [UnityTest]
        public IEnumerator RunPlayModeTests_PreviewUsesPlayModeContract()
        {
            Register(
                new RunPlayModeTestsCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "run_play_mode_tests",
                    new
                    {
                        previewOnly = true,
                        assemblyNames = new[] { "Game.PlayMode.Tests" }
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            Assert.IsTrue((bool)result["PreviewOnly"]);
            Assert.IsFalse((bool)result["RunScheduled"]);
            StringAssert.Contains(
                "PlayMode",
                (string)result["ResponseInstruction"]);
        }
        [UnityTest]
        public IEnumerator InspectScene_ReturnsLoadedHierarchyCounts()
        {
            Register(
                new InspectSceneCapability());

            Task<string> task =
                Execute(
                    "inspect_scene",
                    new
                    {
                        scenePath =
                            _scene.name,

                        includeRootObjects =
                            true
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

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

            Assert.AreEqual(
                1,
                (int)result["RootCount"]);

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

            Assert.AreEqual(
                "AutomationRoot",
                (string)result["RootObjects"][0]["Name"]);

            Assert.IsTrue(
                (bool)result["RootObjectsIncluded"]);
        }


        [UnityTest]
        public IEnumerator InspectScene_ResolvesUniqueFilenameFromWrongFolder()
        {
            Register(
                new InspectSceneCapability());

            Task<string> task =
                Execute(
                    "inspect_scene",
                    new
                    {
                        scenePath =
                            "Assets/__WrongSceneFolder/EmptyScene.unity",

                        includeRootObjects =
                            false
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

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

            Assert.AreEqual(
                TestAssetPaths.EmptyScene,
                (string)result["Path"]);

            StringAssert.Contains(
                "Assets/__WrongSceneFolder/EmptyScene.unity",
                (string)result["Warning"]);
        }


        [UnityTest]
        public IEnumerator CreateGameObject_CreatesPrimitiveInRequestedLoadedScene()
        {
            Register(
                new CreateGameObjectCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "create_game_object",
                    new
                    {
                        name = "AuthoredSphere",
                        scenePath = _scene.name,
                        objectType = "Sphere",
                        position = new
                        {
                            x = 2f,
                            y = 3f,
                            z = 0f
                        },
                        selectCreatedObject = false,
                        saveScene = false
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            GameObject created =
                EditorUtility.InstanceIDToObject(
                    (int)result["GameObjectInstanceId"])
                as GameObject;

            Assert.IsNotNull(created);
            Assert.AreEqual(_scene, created.scene);
            Assert.AreEqual(new Vector3(2f, 3f, 0f), created.transform.localPosition);
            Assert.IsNotNull(created.GetComponent<SphereCollider>());
            Assert.IsNotNull(created.GetComponent<MeshRenderer>());

            Object.DestroyImmediate(created);
        }


        [UnityTest]
        public IEnumerator UpdateGameObject_PathlessSceneRejectsAutomatedSaveBeforeMutation()
        {
            Register(
                new UpdateGameObjectCapability(),
                CapabilityPermission.Write);

            const string failure =
                "The target scene has no asset path. Automated capabilities cannot open Unity's Save As dialog. Create or open a persisted scene and pass its exact scenePath, or retry with saveScene=false.";

            LogAssert.Expect(
                LogType.Error,
                "[Capability] 'update_game_object' failed: " +
                failure);

            Task<string> task =
                Execute(
                    "update_game_object",
                    new
                    {
                        gameObjectInstanceId =
                            _root.GetInstanceID(),
                        newName =
                            "MustNotBeApplied"
                    });

            yield return AwaitTask(
                task);

            Assert.IsTrue(
                task.IsFaulted);

            StringAssert.Contains(
                failure,
                task.Exception?.ToString() ??
                string.Empty);

            Assert.AreEqual(
                "AutomationRoot",
                _root.name);
        }


        [Test]
        public void ControlPlayMode_EndOwnedSessionReleasesOnlyItsRun()
        {
            Type capabilityType =
                typeof(ControlPlayModeCapability);

            FieldInfo scopeActive =
                capabilityType.GetField(
                    "_scopeActive",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            FieldInfo scopeOwner =
                capabilityType.GetField(
                    "_scopeOwnerRunId",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            FieldInfo previousEnabled =
                capabilityType.GetField(
                    "_previousOptionsEnabled",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            FieldInfo previousOptions =
                capabilityType.GetField(
                    "_previousOptions",
                    BindingFlags.NonPublic |
                    BindingFlags.Static);

            Assert.NotNull(scopeActive);
            Assert.NotNull(scopeOwner);
            Assert.NotNull(previousEnabled);
            Assert.NotNull(previousOptions);

            bool originalEnabled =
                EditorSettings.enterPlayModeOptionsEnabled;

            EnterPlayModeOptions originalOptions =
                EditorSettings.enterPlayModeOptions;

            try
            {
                previousEnabled.SetValue(
                    null,
                    originalEnabled);

                previousOptions.SetValue(
                    null,
                    originalOptions);

                scopeOwner.SetValue(
                    null,
                    "owning-task");

                scopeActive.SetValue(
                    null,
                    true);

                Assert.IsFalse(
                    ControlPlayModeCapability.EndOwnedSession(
                        "other-task"));

                Assert.IsTrue(
                    (bool)scopeActive.GetValue(null));

                Assert.IsTrue(
                    ControlPlayModeCapability.EndOwnedSession(
                        "owning-task"));

                Assert.IsFalse(
                    (bool)scopeActive.GetValue(null));

                Assert.IsNull(
                    scopeOwner.GetValue(null));
            }
            finally
            {
                scopeActive.SetValue(
                    null,
                    false);

                scopeOwner.SetValue(
                    null,
                    null);

                EditorSettings.enterPlayModeOptions =
                    originalOptions;

                EditorSettings.enterPlayModeOptionsEnabled =
                    originalEnabled;
            }
        }


        [Test]
        public void ControlPlayMode_KanbanCatalogInjectsRunIdentity()
        {
            const string runId =
                "scoped-play-mode-owner";

            var services =
                new DelegateKanbanTaskExecutionServices(
                    () => new object(),
                    null,
                    runId);

            var capability =
                AIRuntimeEditorPluginCatalog.CreateCapability(
                    "control_play_mode",
                    AIRuntimeEditorPluginHost.Kanban,
                    services) as ControlPlayModeCapability;

            Assert.NotNull(
                capability);

            FieldInfo field =
                typeof(ControlPlayModeCapability).GetField(
                    "_runId",
                    BindingFlags.NonPublic |
                    BindingFlags.Instance);

            Assert.NotNull(
                field);

            Assert.AreEqual(
                runId,
                field.GetValue(capability));
        }


        [TestCase(
            true,
            true,
            true,
            true,
            ControlPlayModeCapability.ExitAfterStartupErrorAction)]
        [TestCase(
            false,
            true,
            true,
            true,
            ControlPlayModeCapability.ExitAfterStartupErrorAction)]
        [TestCase(
            true,
            false,
            true,
            true,
            ControlPlayModeCapability.ResumeUnexpectedPauseAction)]
        [TestCase(
            true,
            false,
            false,
            true,
            ControlPlayModeCapability.NoCorrectiveAction)]
        [TestCase(
            true,
            true,
            true,
            false,
            ControlPlayModeCapability.NoCorrectiveAction)]
        [TestCase(
            false,
            false,
            true,
            true,
            ControlPlayModeCapability.NoCorrectiveAction)]
        public void ControlPlayMode_SelectsSafeAutomaticCorrection(
            bool isPaused,
            bool hasStartupErrors,
            bool recoverUnexpectedPause,
            bool exitOnStartupError,
            string expected)
        {
            Assert.AreEqual(
                expected,
                ControlPlayModeCapability.SelectAutomaticCorrection(
                    isPaused,
                    hasStartupErrors,
                    recoverUnexpectedPause,
                    exitOnStartupError));
        }


        [Test]
        public void ControlPlayMode_DefaultsToBoundedCorrectiveEntry()
        {
            var arguments =
                new ControlPlayModeArguments();

            Assert.AreEqual(
                30,
                arguments.TransitionTimeoutSeconds);

            Assert.IsTrue(
                arguments.RecoverUnexpectedPause);

            Assert.IsTrue(
                arguments.ExitOnStartupError);
        }


        [UnityTest]
        public IEnumerator CreateGameObject_ConstructsUiObjectWithRectTransform()
        {
            Register(new CreateGameObjectCapability(), CapabilityPermission.Write);

            Task<string> task = Execute(
                "create_game_object",
                new
                {
                    name = "AuthoredCanvas",
                    scenePath = _scene.name,
                    objectType = "Empty",
                    componentTypes = new[]
                    {
                        "UnityEngine.RectTransform",
                        "UnityEngine.Canvas"
                    },
                    selectCreatedObject = false,
                    saveScene = false
                });

            yield return AwaitTask(task);
            JObject result = ParseSuccessful(task);
            GameObject created = EditorUtility.InstanceIDToObject(
                (int)result["GameObjectInstanceId"]) as GameObject;

            Assert.IsNotNull(created);
            Assert.IsInstanceOf<RectTransform>(created.transform);
            Assert.IsNotNull(created.GetComponent<Canvas>());
            Object.DestroyImmediate(created);
        }


        [UnityTest]
        public IEnumerator AddEditorComponent_ResolvesUniqueShortNameFromIncorrectNamespace()
        {
            Register(
                new AddEditorComponentCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "add_editor_component",
                    new
                    {
                        gameObjectInstanceId =
                            _root.GetInstanceID(),
                        componentType =
                            "UnityEngine.UI.Canvas"
                    });

            yield return AwaitTask(task);

            JObject result = ParseSuccessful(task);

            Assert.AreEqual(
                typeof(Canvas).FullName,
                (string)result["ComponentType"]);
            Assert.IsNotNull(
                _root.GetComponent<Canvas>());
        }


        [UnityTest]
        public IEnumerator UnityAssetLifecycle_PreservesIdentityAndUsesAssetDatabase()
        {
            _substrateTestFolder = "Assets/__AIRuntimeSubstrate_" + Guid.NewGuid().ToString("N");
            AssetDatabase.CreateFolder("Assets", System.IO.Path.GetFileName(_substrateTestFolder));
            string source = _substrateTestFolder + "/source.txt";
            System.IO.File.WriteAllText(System.IO.Path.Combine(Application.dataPath, source.Substring("Assets/".Length)), "substrate");
            AssetDatabase.ImportAsset(source, ImportAssetOptions.ForceSynchronousImport);
            string sourceGuid = AssetDatabase.AssetPathToGUID(source);

            Register(new CopyUnityAssetCapability(), CapabilityPermission.Write);
            Register(new MoveUnityAssetCapability(), CapabilityPermission.Write);
            Register(new InspectUnityAssetCapability());
            string copied = _substrateTestFolder + "/copied.txt";
            Task<string> copyTask = Execute("copy_unity_asset", new
            {
                sourceAssetPath = source,
                destinationAssetPath = copied,
                expectedSourceAssetPath = source
            });
            yield return AwaitTask(copyTask);
            string copiedGuid = AssetDatabase.AssetPathToGUID(copied);
            Assert.IsNotEmpty(copiedGuid);
            Assert.AreNotEqual(sourceGuid, copiedGuid);

            string moved = _substrateTestFolder + "/moved.txt";
            Task<string> moveTask = Execute("move_unity_asset", new
            {
                sourceAssetPath = copied,
                destinationAssetPath = moved,
                expectedSourceAssetPath = copied
            });
            yield return AwaitTask(moveTask);
            Assert.AreEqual(copiedGuid, AssetDatabase.AssetPathToGUID(moved));

            Task<string> inspectTask = Execute("inspect_unity_asset", new { assetGuid = copiedGuid });
            yield return AwaitTask(inspectTask);
            JObject inspected = ParseSuccessful(inspectTask);
            Assert.AreEqual(moved, (string)inspected["Asset"]["AssetPath"]);
        }


        [UnityTest]
        public IEnumerator RefreshUnityAssets_ReusesRecentForceUpdateForUnchangedContent()
        {
            _substrateTestFolder =
                "Assets/__AIRuntimeRefreshDedup_" +
                Guid.NewGuid().ToString("N");
            AssetDatabase.CreateFolder(
                "Assets",
                System.IO.Path.GetFileName(_substrateTestFolder));
            string assetPath =
                _substrateTestFolder +
                "/unchanged.txt";
            System.IO.File.WriteAllText(
                System.IO.Path.Combine(
                    Application.dataPath,
                    assetPath.Substring("Assets/".Length)),
                "unchanged refresh input");
            AssetDatabase.ImportAsset(
                assetPath,
                ImportAssetOptions.ForceSynchronousImport);

            Register(
                new RefreshUnityAssetsCapability(),
                CapabilityPermission.Write);

            object arguments = new
            {
                assetPaths = new[] { assetPath },
                forceUpdate = true
            };
            Task<string> firstTask =
                Execute("refresh_unity_assets", arguments);
            yield return AwaitTask(firstTask);
            JObject first = ParseSuccessful(firstTask);

            Task<string> secondTask =
                Execute("refresh_unity_assets", arguments);
            yield return AwaitTask(secondTask);
            JObject second = ParseSuccessful(secondTask);

            Assert.AreEqual(
                (string)first["OperationId"],
                (string)second["OperationId"]);
            Assert.AreEqual(
                (string)first["InputFingerprint"],
                (string)second["InputFingerprint"]);
            Assert.IsNotEmpty((string)first["InputFingerprint"]);
        }


        [UnityTest]
        public IEnumerator CreateMaterial_CreatesPersistentColoredAsset()
        {
            Register(
                new CreateMaterialCapability(),
                CapabilityPermission.Write);

            _materialPath =
                "Assets/__AIRuntimeMaterialTest_" +
                Guid.NewGuid().ToString("N") +
                ".mat";

            Task<string> task =
                Execute(
                    "create_material",
                    new
                    {
                        assetPath = _materialPath,
                        shaderName = "Standard",
                        color = new
                        {
                            r = 0.2f,
                            g = 0.4f,
                            b = 0.6f,
                            a = 1f
                        }
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            Assert.IsTrue((bool)result["Succeeded"]);
            Assert.IsTrue((bool)result["Created"]);
            Assert.AreEqual(_materialPath, (string)result["AssetPath"]);
            Assert.IsTrue((bool)result["ColorApplied"]);

            Material material =
                AssetDatabase.LoadAssetAtPath<Material>(
                    _materialPath);

            Assert.NotNull(material);
            Assert.AreEqual("Standard", material.shader.name);
        }


        [UnityTest]
        public IEnumerator SetSerializedProperty_AcceptsPascalCaseAssetPathFromCreateMaterialResult()
        {
            _materialPath =
                "Assets/__AIRuntimeMaterialReferenceTest_" +
                Guid.NewGuid().ToString("N") +
                ".mat";

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

            AssetDatabase.CreateAsset(
                material,
                _materialPath);

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

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            renderer.GetInstanceID(),

                        propertyPath =
                            "m_Materials.Array.data[0]",

                        valueJson =
                            JsonConvert.SerializeObject(
                                new
                                {
                                    Succeeded = true,
                                    AssetPath = _materialPath
                                })
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            Assert.NotNull(
                renderer.sharedMaterial);

            Assert.AreEqual(
                _materialPath,
                AssetDatabase.GetAssetPath(
                    renderer.sharedMaterial));

            Assert.AreEqual(
                AssetDatabase.AssetPathToGUID(
                    _materialPath),
                AssetDatabase.AssetPathToGUID(
                    AssetDatabase.GetAssetPath(
                        renderer.sharedMaterial)));

            Assert.IsTrue(
                (bool)result["Verified"]);
        }


        [UnityTest]
        public IEnumerator SetSerializedProperty_AcceptsStructuredObjectReferenceWithoutNestedJson()
        {
            _materialPath =
                "Assets/__AIRuntimeStructuredMaterialReferenceTest_" +
                Guid.NewGuid().ToString("N") +
                ".mat";

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

            AssetDatabase.CreateAsset(
                material,
                _materialPath);

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

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            renderer.GetInstanceID(),

                        propertyPath =
                            "m_Materials.Array.data[0]",

                        objectReference =
                            new
                            {
                                assetPath = _materialPath
                            }
                    });

            yield return AwaitTask(task);

            JObject result = ParseSuccessful(task);

            Assert.AreSame(
                AssetDatabase.LoadAssetAtPath<Material>(_materialPath),
                renderer.sharedMaterial);
            Assert.IsTrue((bool)result["Verified"]);
        }


        [UnityTest]
        public IEnumerator SetSerializedProperty_FlaggedIntegerUsesSemanticEnumNames()
        {
            Rigidbody rigidbody =
                _root.AddComponent<Rigidbody>();

            Register(
                new SetSerializedPropertyCapability(),
                CapabilityPermission.Write);

            Task<string> task =
                Execute(
                    "set_serialized_property",
                    new
                    {
                        componentInstanceId =
                            rigidbody.GetInstanceID(),
                        propertyPath =
                            "m_Constraints",
                        valueJson =
                            "{\"flags\":[\"FreezePositionZ\"]}"
                    });

            yield return AwaitTask(task);

            JObject result =
                ParseSuccessful(task);

            Assert.AreEqual(
                RigidbodyConstraints.FreezePositionZ,
                rigidbody.constraints);
            Assert.AreEqual(
                (int)RigidbodyConstraints.FreezePositionZ,
                (int)result["CurrentValue"]);
            Assert.IsTrue(
                (bool)result["Verified"]);
        }


        [UnityTest]
        public IEnumerator InspectPrefab_DescribesPrefabAsset()
        {
            _prefabPath =
                "Assets/__AIRuntimeInspectPrefab_" +
                Guid.NewGuid().ToString(
                    "N") +
                ".prefab";

            GameObject prefab =
                PrefabUtility.SaveAsPrefabAsset(
                    _root,
                    _prefabPath);

            Assert.IsNotNull(
                prefab);

            Register(
                new InspectPrefabCapability());

            Task<string> task =
                Execute(
                    "inspect_prefab",
                    new
                    {
                        assetPath =
                            _prefabPath
                    });

            yield return AwaitTask(
                task);

            JObject result =
                ParseSuccessful(
                    task);

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

            Assert.IsFalse(
                (bool)result["IsPartOfPrefabInstance"]);

            Assert.AreEqual(
                _prefabPath,
                (string)result["SourceAssetPath"]);
        }

        [UnityTest]
        public IEnumerator ProducedPrefabReview_CapturesCurrentRigidbodyConstraints()
        {
            _prefabPath = "Assets/__AIRuntimeReviewPrefab_" + Guid.NewGuid().ToString("N") + ".prefab";
            var body = _root.AddComponent<Rigidbody>();
            body.constraints = RigidbodyConstraints.FreezePositionZ | RigidbodyConstraints.FreezeRotationX;
            PrefabUtility.SaveAsPrefabAsset(_root, _prefabPath);
            Register(new InspectPrefabCapability());
            Register(new InspectGameObjectCapability());
            Register(new InspectEditorComponentCapability());
            Register(new InspectUnityAssetCapability());
            var runner = typeof(AIRuntimeKanbanSession).Assembly.GetType("AIRuntime.Editor.AIRuntimeKanbanAgentRunner");
            var task = (Task<string>)runner.GetMethod("CaptureProducedPrefabReviewEvidenceAsync",
                BindingFlags.NonPublic | BindingFlags.Static).Invoke(null,
                new object[] { _runtime, new[] { _prefabPath }, System.Threading.CancellationToken.None });
            yield return AwaitTask(task);
            JObject result = JObject.Parse(task.Result);
            var components = result["Prefabs"][0]["ComponentInspection"]["Components"];
            Assert.IsNotNull(components, task.Result);
            var inspected = components.First(c => c["Rigidbody"] != null && c["Rigidbody"].Type != JTokenType.Null)["Rigidbody"];
            Assert.IsTrue((bool)inspected["FreezePositionZ"]);
            Assert.IsTrue((bool)inspected["FreezeRotationX"]);
            Assert.IsFalse((bool)inspected["FreezeRotationZ"], "Review must receive the actual missing constraint, not a completion claim.");
        }


        [UnityTest]
        public IEnumerator CreatePrefab_CreatesRealPersistentPrefabFromSceneObject()
        {
            _prefabPath =
                "Assets/__AIRuntimeCreatePrefab_" +
                Guid.NewGuid().ToString("N") +
                ".prefab";

            Register(
                new CreatePrefabCapability(),
                CapabilityPermission.Write);

            Task<string> task = Execute(
                "create_prefab",
                new
                {
                    sourceGameObjectInstanceId = _root.GetInstanceID(),
                    assetPath = _prefabPath,
                    connectSource = false
                });

            yield return AwaitTask(task);

            JObject result = ParseSuccessful(task);
            GameObject prefab =
                AssetDatabase.LoadAssetAtPath<GameObject>(_prefabPath);

            Assert.IsTrue((bool)result["Succeeded"]);
            Assert.IsTrue((bool)result["Created"]);
            Assert.AreEqual(_prefabPath, (string)result["AssetPath"]);
            Assert.IsNotNull(prefab);
            Assert.IsTrue(PrefabUtility.IsPartOfPrefabAsset(prefab));
            Assert.AreEqual(System.IO.Path.GetFileNameWithoutExtension(_prefabPath), prefab.name);
            Assert.AreEqual(prefab.name, (string)result["RootName"]);
            Assert.AreEqual("AutomationRoot", _root.name, "Saving a disconnected prefab must preserve the source name.");
            Assert.AreEqual(1, prefab.transform.childCount);
            Assert.AreEqual(_child.name, prefab.transform.GetChild(0).name);
        }


        [Test]
        public void NewCapabilities_ExposeExpectedPermissionsAndSchemas()
        {
            Dictionary<ICapability, CapabilityPermission> capabilities =
                new Dictionary<ICapability, CapabilityPermission>
                {
                    {
                        new GetSerializedPropertiesCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new SetSerializedPropertyCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new InspectGameObjectCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new GetHierarchyTreeCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new FindGameObjectsCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new GetConsoleLogsCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new GetCompilationErrorsCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new RunEditModeTestsCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new InspectSceneCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new CreateEditorSceneCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new CreateGameObjectCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new SaveEditorSceneCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new CreatePrefabCapability(),
                        CapabilityPermission.Write
                    },
                    {
                        new InspectPrefabCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new ListCapabilitiesCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new RequestCapabilityCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new RunProcessCapability(),
                        CapabilityPermission.Process
                    },
                    {
                        new InspectAssetImporterCapability(),
                        CapabilityPermission.ReadOnly
                    },
                    {
                        new SetAssetImporterPropertyCapability(),
                        CapabilityPermission.Write
                    }
                };

            CapabilitySchemaExporter exporter =
                new CapabilitySchemaExporter();

            foreach (
                KeyValuePair<ICapability, CapabilityPermission> entry
                in capabilities)
            {
                Assert.AreEqual(
                    entry.Value,
                    entry.Key.Permission,
                    entry.Key.Name);

                JObject schema =
                    exporter.Export(
                        entry.Key);

                Assert.AreEqual(
                    "object",
                    (string)schema["type"],
                    entry.Key.Name);

                Assert.IsNotNull(
                    schema["properties"],
                    entry.Key.Name);
            }

            JObject setSchema =
                exporter.Export(
                    new SetSerializedPropertyCapability());

            CollectionAssert.Contains(
                setSchema["required"]
                    .Values<string>()
                    .ToArray(),
                "propertyPath");

            CollectionAssert.DoesNotContain(
                setSchema["required"]
                    .Values<string>()
                    .ToArray(),
                "valueJson");

            Assert.AreEqual(
                "object",
                (string)setSchema["properties"]?["objectReference"]?["type"]);
        }


        [Test]
        public void AutomatedPlayMode_EnablesSceneReloadWithoutRestoringDomainReload()
        {
            MethodInfo method =
                typeof(ControlPlayModeCapability)
                    .GetMethod(
                        "WithSafeAutomatedReloadOptions",
                        BindingFlags.NonPublic |
                        BindingFlags.Static);

            Assert.IsNotNull(method);

            EnterPlayModeOptions input =
                EnterPlayModeOptions.DisableDomainReload |
                EnterPlayModeOptions.DisableSceneReload;

            EnterPlayModeOptions result =
                (EnterPlayModeOptions)method.Invoke(
                    null,
                    new object[]
                    {
                        input
                    });

            Assert.IsTrue(
                (result & EnterPlayModeOptions.DisableDomainReload) != 0,
                "The active TestRunnerApi callback must survive PlayMode entry.");

            Assert.IsFalse(
                (result & EnterPlayModeOptions.DisableSceneReload) != 0,
                "AIRuntime test runs must not trigger Unity's blocking Reload Scene warning.");
        }


        [Test]
        public void AutomatedPlayMode_RestoresSettingsOnlyAfterEnteredEditMode()
        {
            Assert.IsFalse(
                ControlPlayModeCapability.ShouldRestoreSettings(
                    PlayModeStateChange.ExitingPlayMode),
                "The safe scene-reload setting must remain active for the full exit transition.");

            Assert.IsFalse(
                ControlPlayModeCapability.ShouldRestoreSettings(
                    PlayModeStateChange.ExitingEditMode));

            Assert.IsFalse(
                ControlPlayModeCapability.ShouldRestoreSettings(
                    PlayModeStateChange.EnteredPlayMode));

            Assert.IsTrue(
                ControlPlayModeCapability.ShouldRestoreSettings(
                    PlayModeStateChange.EnteredEditMode));
        }


        [Test]
        public void EditorPluginCatalog_RegistersAllAutomationCapabilities()
        {
            HashSet<string> names =
                new HashSet<string>(
                    AIRuntimeEditorPluginCatalog
                        .GetCapabilityPlugins(
                            AIRuntimeEditorPluginHost.ChatConsole)
                        .Select(plugin => plugin.Name),
                    StringComparer.Ordinal);

            string[] expected =
            {
                "create_editor_selection",
                "get_serialized_properties",
                "set_serialized_property",
                "create_material",
                "inspect_game_object",
                "get_hierarchy_tree",
                "find_game_objects",
                "get_console_logs",
                "get_compilation_errors",
                "run_edit_mode_tests",
                "inspect_scene",
                "create_editor_scene",
                "create_game_object",
                "save_editor_scene",
                "create_prefab",
                "instantiate_prefab",
                "inspect_prefab",
                "update_game_object",
                "duplicate_game_object",
                "destroy_game_object",
                "configure_project_settings",
                "search_workspace",
                "replace_text",
                "list_capabilities",
                "request_capability",
                "open_editor_scene",
                "close_editor_scene",
                "apply_prefab_overrides",
                "revert_or_unpack_prefab",
                "control_play_mode",
                "wait_for_editor_frames",
                "capture_camera_view",
                "capture_game_view",
                "build_player",
                "create_scriptable_object",
                "list_unity_packages",
                "run_process",
                "inspect_asset_importer",
                "set_asset_importer_property"
                ,"find_unity_assets"
                ,"inspect_unity_asset"
                ,"get_unity_asset_referencers"
                ,"move_unity_asset"
                ,"copy_unity_asset"
                ,"delete_unity_asset"
                ,"refresh_unity_assets"
                ,"get_unity_operation"
                ,"edit_serialized_collection"
            };

            CollectionAssert.DoesNotContain(names, "add_unity_package");
            CollectionAssert.DoesNotContain(names, "remove_unity_package");

            foreach (
                string name
                in expected)
            {
                CollectionAssert.Contains(
                    names,
                    name);
            }

            foreach (
                string name
                in expected)
            {
                ICapability capability =
                    AIRuntimeEditorPluginCatalog.CreateCapability(
                        name,
                        AIRuntimeEditorPluginHost.Kanban);

                Assert.IsNotNull(
                    capability,
                    name);

                Assert.AreEqual(
                    name,
                    capability.Name);
            }
        }


        private void Register(
            ICapability capability,
            CapabilityPermission permission =
                CapabilityPermission.ReadOnly)
        {
            _runtime.RegisterCapability(
                capability);

            _runtime.AllowPermission(
                permission);
        }


        private Task<string> Execute(
            string capabilityName,
            object arguments)
        {
            return _runtime.ExecuteCapabilityAsync(
                capabilityName,
                JsonConvert.SerializeObject(
                    arguments));
        }


        private static JObject ParseSuccessful(
            Task<string> task)
        {
            Assert.IsFalse(
                task.IsFaulted,
                task.Exception?.ToString());

            Assert.IsFalse(
                task.IsCanceled);

            return JObject.Parse(
                task.GetAwaiter()
                    .GetResult());
        }


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