using System;
using System.Collections;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using MCPForUnity.Editor.Tools;
using MCPForUnityTests.Editor.Tools.Fixtures;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageScriptableObjectTests
{
private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests";
private const double UnityReadyTimeoutSeconds = 180.0;
private string _runRoot;
private string _nestedFolder;
private string _createdAssetPath;
private string _createdGuid;
private string _matAPath;
private string _matBPath;
[UnitySetUp]
public IEnumerator SetUp()
{
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
EnsureFolder("Assets/Temp");
// Avoid deleting/recreating the entire TempRoot each test (can trigger heavy reimport churn).
// Instead, isolate each test in its own unique subfolder under TempRoot.
EnsureFolder(TempRoot);
_runRoot = $"{TempRoot}/Run_{Guid.NewGuid():N}";
EnsureFolder(_runRoot);
_nestedFolder = _runRoot + "/Nested/Deeper";
_createdAssetPath = null;
_createdGuid = null;
// Create two Materials we can reference by guid/path.
_matAPath = $"{TempRoot}/MatA_{Guid.NewGuid():N}.mat";
_matBPath = $"{TempRoot}/MatB_{Guid.NewGuid():N}.mat";
var shader = FindFallbackShader();
Assert.IsNotNull(shader, "A fallback shader must be available for creating Material assets in tests.");
AssetDatabase.CreateAsset(new Material(shader), _matAPath);
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
}
[TearDown]
public void TearDown()
{
// Best-effort cleanup
if (!string.IsNullOrEmpty(_createdAssetPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_createdAssetPath) != null)
{
AssetDatabase.DeleteAsset(_createdAssetPath);
}
if (!string.IsNullOrEmpty(_matAPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matAPath) != null)
{
AssetDatabase.DeleteAsset(_matAPath);
}
if (!string.IsNullOrEmpty(_matBPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matBPath) != null)
{
AssetDatabase.DeleteAsset(_matBPath);
}
if (!string.IsNullOrEmpty(_runRoot) && AssetDatabase.IsValidFolder(_runRoot))
{
AssetDatabase.DeleteAsset(_runRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
[Test]
public void Create_CreatesNestedFolders_PlacesAssetCorrectly()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = _nestedFolder,
["assetName"] = "My_Test_Def_Placement",
["overwrite"] = true,
};
var raw = ManageScriptableObject.HandleCommand(create);
var result = raw as JObject ?? JObject.FromObject(raw);
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Expected data payload");
_createdGuid = data!["guid"]?.ToString();
_createdAssetPath = data["path"]?.ToString();
Assert.IsTrue(AssetDatabase.IsValidFolder(_nestedFolder), "Nested folder should be created.");
Assert.IsTrue(_createdAssetPath!.StartsWith(_nestedFolder, StringComparison.Ordinal), $"Asset should be created under {_nestedFolder}: {_createdAssetPath}");
Assert.IsTrue(_createdAssetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase), "Asset should have .asset extension.");
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
}
[Test]
public void Create_AppliesPatches_ToCreatedAsset()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
// Patching correctness does not depend on nested folder creation; keep this lightweight.
["folderPath"] = _runRoot,
["assetName"] = "My_Test_Def_Patches",
["overwrite"] = true,
["patches"] = new JArray
{
new JObject { ["propertyPath"] = "displayName", ["op"] = "set", ["value"] = "Hello" },
new JObject { ["propertyPath"] = "baseNumber", ["op"] = "set", ["value"] = 42 },
new JObject { ["propertyPath"] = "nested.note", ["op"] = "set", ["value"] = "note!" }
}
};
var raw = ManageScriptableObject.HandleCommand(create);
var result = raw as JObject ?? JObject.FromObject(raw);
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Expected data payload");
_createdGuid = data!["guid"]?.ToString();
_createdAssetPath = data["path"]?.ToString();
Assert.IsTrue(_createdAssetPath!.StartsWith(_runRoot, StringComparison.Ordinal), $"Asset should be created under {_runRoot}: {_createdAssetPath}");
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
Assert.AreEqual("Hello", asset!.DisplayName, "Private [SerializeField] string should be set via SerializedProperty.");
Assert.AreEqual(42, asset.BaseNumber, "Inherited serialized field should be set via SerializedProperty.");
Assert.AreEqual("note!", asset.NestedNote, "Nested struct field should be set via SerializedProperty path.");
}
[Test]
public void Modify_ArrayResize_ThenAssignObjectRefs_ByGuidAndByPath()
{
// Create base asset first with no patches.
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = _runRoot,
["assetName"] = "Modify_Target",
["overwrite"] = true
};
var createRes = ToJObject(ManageScriptableObject.HandleCommand(create));
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
_createdGuid = createRes["data"]?["guid"]?.ToString();
_createdAssetPath = createRes["data"]?["path"]?.ToString();
var matAGuid = AssetDatabase.AssetPathToGUID(_matAPath);
var modify = new JObject
{
["action"] = "modify",
["target"] = new JObject { ["guid"] = _createdGuid },
["patches"] = new JArray
{
// Resize list to 2
new JObject { ["propertyPath"] = "materials.Array.size", ["op"] = "array_resize", ["value"] = 2 },
// Assign element 0 by guid
new JObject
{
["propertyPath"] = "materials.Array.data[0]",
["op"] = "set",
["ref"] = new JObject { ["guid"] = matAGuid }
},
// Assign element 1 by path
new JObject
{
["propertyPath"] = "materials.Array.data[1]",
["op"] = "set",
["ref"] = new JObject { ["path"] = _matBPath }
}
}
};
var modRes = ToJObject(ManageScriptableObject.HandleCommand(modify));
Assert.IsTrue(modRes.Value<bool>("success"), modRes.ToString());
// Assert patch results are ok so failures are visible even if the tool returns success.
var results = modRes["data"]?["results"] as JArray;
Assert.IsNotNull(results, "Expected per-patch results in response.");
foreach (var r in results!)
{
Assert.IsTrue(r.Value<bool>("ok"), $"Patch failed: {r}");
}
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset);
Assert.AreEqual(2, asset!.Materials.Count, "List should be resized to 2.");
var matA = AssetDatabase.LoadAssetAtPath<Material>(_matAPath);
var matB = AssetDatabase.LoadAssetAtPath<Material>(_matBPath);
Assert.AreEqual(matA, asset.Materials[0], "Element 0 should be set by GUID ref.");
Assert.AreEqual(matB, asset.Materials[1], "Element 1 should be set by path ref.");
}
[Test]
public void Errors_InvalidAction_TypeNotFound_TargetNotFound()
{
// invalid action
var badAction = ToJObject(ManageScriptableObject.HandleCommand(new JObject { ["action"] = "nope" }));
Assert.IsFalse(badAction.Value<bool>("success"));
Assert.AreEqual("invalid_params", badAction.Value<string>("error"));
// type not found
var badType = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = "Nope.MissingType",
["folderPath"] = TempRoot,
["assetName"] = "X",
}));
Assert.IsFalse(badType.Value<bool>("success"));
Assert.AreEqual("type_not_found", badType.Value<string>("error"));
// target not found
var badTarget = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "modify",
["target"] = new JObject { ["guid"] = "00000000000000000000000000000000" },
["patches"] = new JArray(),
}));
Assert.IsFalse(badTarget.Value<bool>("success"));
Assert.AreEqual("target_not_found", badTarget.Value<string>("error"));
}
[Test]
public void Create_RejectsNonAssetsRootFolders()
{
var badPackages = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "Packages/NotAllowed",
["assetName"] = "BadFolder",
["overwrite"] = true,
}));
Assert.IsFalse(badPackages.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badPackages.Value<string>("error"));
var badAbsolute = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "/tmp/not_allowed",
["assetName"] = "BadFolder2",
["overwrite"] = true,
}));
Assert.IsFalse(badAbsolute.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badAbsolute.Value<string>("error"));
var badFileUri = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "file:///tmp/not_allowed",
["assetName"] = "BadFolder3",
["overwrite"] = true,
}));
Assert.IsFalse(badFileUri.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badFileUri.Value<string>("error"));
}
[Test]
public void Create_NormalizesRelativeAndBackslashPaths_AndAvoidsDoubleSlashesInResult()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = @"Temp\ManageScriptableObjectTests\SlashProbe\\Deep",
["assetName"] = "SlashProbe",
["overwrite"] = true,
};
var res = ToJObject(ManageScriptableObject.HandleCommand(create));
Assert.IsTrue(res.Value<bool>("success"), res.ToString());
var path = res["data"]?["path"]?.ToString();
Assert.IsNotNull(path, "Expected path in response.");
Assert.IsTrue(path!.StartsWith("Assets/Temp/ManageScriptableObjectTests/SlashProbe/Deep", StringComparison.Ordinal),
$"Expected sanitized Assets-rooted path, got: {path}");
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
}
}
}