using System.Runtime.Serialization; using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Interfaces; using AyCode.Core.Loggers; using AyCode.Core.Serializers.Jsons; using Newtonsoft.Json; using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests; [TestClass] public sealed class JsonExtensionTests { [TestInitialize] public void TestInit() { TestDataFactory.ResetIdCounter(); } #region Deep Hierarchy Tests (5 Levels) [TestMethod] public void DeepHierarchy_5Levels_MergePreservesAllReferences() { // Arrange: Create 5-level deep hierarchy var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 3); // Store original references at all levels var originalItem = order.Items[0]; var originalPallet = order.Items[0].Pallets[0]; var originalMeasurement = order.Items[0].Pallets[0].Measurements[0]; var originalPoint = order.Items[0].Pallets[0].Measurements[0].Points[0]; var updateJson = $@"{{ ""Id"": {order.Id}, ""OrderNumber"": ""ORD-UPDATED"", ""Items"": [{{ ""Id"": {originalItem.Id}, ""ProductName"": ""Updated-Product"", ""Pallets"": [{{ ""Id"": {originalPallet.Id}, ""PalletCode"": ""PLT-UPDATED"", ""Measurements"": [{{ ""Id"": {originalMeasurement.Id}, ""Name"": ""Measurement-UPDATED"", ""Points"": [{{ ""Id"": {originalPoint.Id}, ""Label"": ""Point-UPDATED"", ""Value"": 999.99 }}] }}] }}] }}] }}"; // Act updateJson.JsonTo(order); // Assert: All references preserved Assert.AreSame(originalItem, order.Items[0], "Level 2: Item reference must be preserved"); Assert.AreSame(originalPallet, order.Items[0].Pallets[0], "Level 3: Pallet reference must be preserved"); Assert.AreSame(originalMeasurement, order.Items[0].Pallets[0].Measurements[0], "Level 4: Measurement reference must be preserved"); Assert.AreSame(originalPoint, order.Items[0].Pallets[0].Measurements[0].Points[0], "Level 5: Point reference must be preserved"); // Assert: Values updated Assert.AreEqual("ORD-UPDATED", order.OrderNumber); Assert.AreEqual("Updated-Product", order.Items[0].ProductName); Assert.AreEqual("PLT-UPDATED", order.Items[0].Pallets[0].PalletCode); Assert.AreEqual("Measurement-UPDATED", order.Items[0].Pallets[0].Measurements[0].Name); Assert.AreEqual("Point-UPDATED", order.Items[0].Pallets[0].Measurements[0].Points[0].Label); Assert.AreEqual(999.99, order.Items[0].Pallets[0].Measurements[0].Points[0].Value); } [TestMethod] public void DeepHierarchy_5Levels_InsertAndKeepLogic() { // Arrange var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 2); var originalItemCount = order.Items.Count; var originalItem2 = order.Items[1]; var existingPointId = order.Items[0].Pallets[0].Measurements[0].Points[0].Id; var updateJson = $@"{{ ""Id"": {order.Id}, ""Items"": [{{ ""Id"": {order.Items[0].Id}, ""Pallets"": [{{ ""Id"": {order.Items[0].Pallets[0].Id}, ""Measurements"": [{{ ""Id"": {order.Items[0].Pallets[0].Measurements[0].Id}, ""Points"": [ {{ ""Id"": {existingPointId}, ""Label"": ""Updated-Point"" }}, {{ ""Id"": 9999, ""Label"": ""NEW-Point"", ""Value"": 123.45 }} ] }}] }}] }}] }}"; // Act updateJson.JsonTo(order); // Assert Assert.AreEqual(originalItemCount, order.Items.Count, "Items count should be preserved (KEEP logic)"); Assert.AreSame(originalItem2, order.Items[1], "Second item reference should be preserved"); Assert.IsTrue(order.Items[0].Pallets[0].Measurements[0].Points.Any(p => p.Id == 9999), "New point should be inserted"); } #endregion #region Semantic Reference Tests (IId types with $id/$ref) [TestMethod] public void SemanticReference_SharedTag_SerializesWithSemanticId() { // Arrange var sharedTag = TestDataFactory.CreateTag("SharedTag"); var order = TestDataFactory.CreateOrder(itemCount: 2, sharedTag: sharedTag); // Act var json = order.ToJson(); Console.WriteLine($"Semantic Reference JSON:\n{json}"); // Assert Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id for IId types"); Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for shared references"); } [TestMethod] public void SemanticReference_DeserializeAndMerge_PreservesSharedReferences() { // Arrange var sharedTag = TestDataFactory.CreateTag("OriginalKey"); var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", PrimaryTag = sharedTag, SecondaryTag = sharedTag, Tags = [sharedTag] }; var originalTagRef = order.PrimaryTag; var updateJson = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-UPDATED"", ""PrimaryTag"": { ""Id"": 1, ""Name"": ""UpdatedKey"" } }"; // Act updateJson.JsonTo(order); // Assert Assert.AreEqual("ORD-UPDATED", order.OrderNumber); Assert.AreEqual("UpdatedKey", order.PrimaryTag?.Name); Assert.AreSame(originalTagRef, order.SecondaryTag, "SecondaryTag reference should be preserved"); } #endregion #region Newtonsoft Reference Tests (Non-IId types) [TestMethod] public void NewtonsoftReference_SharedMetadata_SerializesWithNumericId() { // Arrange var sharedMeta = TestDataFactory.CreateMetadata(withChild: true); var order = TestDataFactory.CreateOrder(itemCount: 2, sharedMetadata: sharedMeta); // Act var json = order.ToJson(); Console.WriteLine($"Newtonsoft Reference JSON:\n{json}"); // Assert Assert.IsTrue(json.Contains("\"$id\""), "Should contain $id"); Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for shared references"); } [TestMethod] public void NewtonsoftReference_DeepNestedNonId_HandlesCorrectly() { // Arrange var rootMeta = new MetadataInfo { Key = "Root", Value = "RootValue", ChildMetadata = new MetadataInfo { Key = "Child", Value = "ChildValue", ChildMetadata = new MetadataInfo { Key = "GrandChild", Value = "GrandChildValue" } } }; var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", OrderMetadata = rootMeta, AuditMetadata = rootMeta }; // Act var json = order.ToJson(); // Assert Assert.IsTrue(json.Contains("Root")); Assert.IsTrue(json.Contains("Child")); Assert.IsTrue(json.Contains("GrandChild")); Assert.IsTrue(json.Contains("\"$ref\""), "Should contain $ref for duplicate reference"); } #endregion #region Hybrid Reference Tests (Mixed IId and Non-IId) [TestMethod] public void HybridReference_MixedTypes_BothRefSystemsWork() { // Arrange var sharedTag = TestDataFactory.CreateTag(); var sharedMeta = TestDataFactory.CreateMetadata(); sharedTag.Description = sharedMeta.Key; // Link them var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", PrimaryTag = sharedTag, SecondaryTag = sharedTag, OrderMetadata = sharedMeta, AuditMetadata = sharedMeta, Tags = [sharedTag], Items = [new TestOrderItem { Id = 10, ProductName = "A", Tag = sharedTag, ItemMetadata = sharedMeta }] }; var json = order.ToJson(); // Assert var refCount = json.Split("\"$ref\"").Length - 1; Assert.IsTrue(refCount >= 2, $"Should have multiple $ref tokens. Found: {refCount}"); } #endregion #region NoMerge Collection Tests [TestMethod] public void NoMergeCollection_ReplacesEntireCollection() { // Arrange var order = TestDataFactory.CreateOrder(itemCount: 1); order.NoMergeItems = [ new TestOrderItem { Id = 100, ProductName = "NoMerge-A" }, new TestOrderItem { Id = 101, ProductName = "NoMerge-B" } ]; var originalRef = order.NoMergeItems; var updateJson = $@"{{ ""Id"": {order.Id}, ""NoMergeItems"": [ {{ ""Id"": 200, ""ProductName"": ""NoMerge-NEW"" }} ] }}"; // Act order.DeepPopulateWithMerge(updateJson); // Assert Assert.AreNotSame(originalRef, order.NoMergeItems); Assert.AreEqual(1, order.NoMergeItems.Count); Assert.AreEqual(200, order.NoMergeItems[0].Id); } #endregion #region Non-IId Collection Tests [TestMethod] public void NonIdCollection_ReplacesContent() { // Arrange var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", MetadataList = [ new MetadataInfo { Key = "Old-A" }, new MetadataInfo { Key = "Old-B" } ] }; var updateJson = @"{ ""Id"": 1, ""MetadataList"": [ { ""Key"": ""New-X"" }, { ""Key"": ""New-Y"" } ] }"; // Act order.DeepPopulateWithMerge(updateJson); // Assert Assert.AreEqual(2, order.MetadataList.Count); Assert.IsTrue(order.MetadataList.Any(m => m.Key == "New-X")); Assert.IsFalse(order.MetadataList.Any(m => m.Key == "Old-A")); } #endregion #region Guid IId Tests [TestMethod] public void GuidId_DeepPopulate_ReferencePreserved() { var order = new TestGuidOrder { Id = Guid.NewGuid(), Code = "ORD-001", Items = [ new TestGuidItem { Id = Guid.NewGuid(), Name = "Apple", Qty = 5 }, new TestGuidItem { Id = Guid.NewGuid(), Name = "Orange", Qty = 3 } ] }; var originalItemsRef = order.Items; var originalAppleRef = order.Items[0]; var appleId = order.Items[0].Id; var json = new { Id = order.Id, Code = "ORD-UPDATED", Items = new[] { new { Id = appleId, Name = "Apple", Qty = 7 }, new { Id = Guid.NewGuid(), Name = "Banana", Qty = 4 } } }.ToJson(); json.JsonTo(order); // List reference preserved Assert.AreSame(originalItemsRef, order.Items, "List reference must be preserved"); // Apple reference preserved (updated in-place) Assert.AreSame(originalAppleRef, order.Items[0], "Apple reference must be preserved"); Assert.AreEqual(7, order.Items[0].Qty, "Apple Qty should be updated"); // Count: Apple (updated) + Orange (kept) + Banana (new) Assert.AreEqual(3, order.Items.Count, "Should have 3 items: Apple updated, Orange kept, Banana added"); Assert.AreEqual("ORD-UPDATED", order.Code); } #endregion #region Round-Trip Serialization Tests [TestMethod] public void RoundTrip_DeepHierarchy_PreservesData() { // Arrange var sharedTag = TestDataFactory.CreateTag(); var sharedMeta = TestDataFactory.CreateMetadata(withChild: true); var order = TestDataFactory.CreateOrder(itemCount: 2, palletsPerItem: 2, sharedTag: sharedTag, sharedMetadata: sharedMeta); // Act var json = order.ToJson(); var deserialized = json.JsonTo(); // Assert Assert.IsNotNull(deserialized); Assert.AreEqual(order.Id, deserialized.Id); Assert.AreEqual(order.OrderNumber, deserialized.OrderNumber); Assert.AreEqual(order.Items.Count, deserialized.Items.Count); } #endregion #region Primitive Array Tests (SignalR IdMessage pattern) [TestMethod] public void PrimitiveArray_BooleanTrue_RoundTrips() { var jsonString = (new[] { true }).ToJson(); var result = jsonString.JsonTo(typeof(bool[])) as bool[]; Assert.IsNotNull(result); Assert.IsTrue(result[0], "Boolean true should deserialize as true!"); } [TestMethod] public void PrimitiveArray_AllTypes_RoundTrip() { var testCases = new (Type type, object value)[] { (typeof(bool), true), (typeof(int), 42), (typeof(long), 123456789L), (typeof(double), 3.14159), (typeof(decimal), 99.99m), (typeof(string), "test"), (typeof(Guid), Guid.NewGuid()), (typeof(DateTime), DateTime.UtcNow), (typeof(TestStatus), TestStatus.Processing) }; foreach (var (type, value) in testCases) { var wrapped = Array.CreateInstance(type, 1); wrapped.SetValue(value, 0); var json = wrapped.ToJson(); var result = json.JsonTo(type.MakeArrayType()) as Array; Assert.IsNotNull(result, $"Failed for {type.Name}"); Assert.AreEqual(value, result.GetValue(0), $"Value mismatch for {type.Name}"); } } [TestMethod] public void IdMessage_MultipleParameters_SimulateSignalR() { var @params = new (Type t, object v)[] { (typeof(bool), true), (typeof(string), "filter"), (typeof(int), 100) }; foreach (var (type, value) in @params) { var wrapped = Array.CreateInstance(type, 1); wrapped.SetValue(value, 0); var json = wrapped.ToJson(); var arr = json.JsonTo(type.MakeArrayType()) as Array; Assert.AreEqual(value, arr?.GetValue(0)); } } #endregion #region WASM Compatibility Tests [TestMethod] public void WasmCompat_AcJsonSerializer_SimpleObject() { var item = new TestOrderItem { Id = 1, ProductName = "Test", Quantity = 10, UnitPrice = 99.99m, Status = TestStatus.Processing }; var json = AcJsonSerializer.Serialize(item); Assert.IsTrue(json.Contains("\"Id\":1")); Assert.IsTrue(json.Contains("\"ProductName\":\"Test\"")); } [TestMethod] public void WasmCompat_AcJsonDeserializer_RoundTrip() { var original = new TestOrderItem { Id = 42, ProductName = "WASM Test", Quantity = 5, UnitPrice = 25.50m, Status = TestStatus.Shipped }; var json = AcJsonSerializer.Serialize(original); var deserialized = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(deserialized); Assert.AreEqual(42, deserialized.Id); Assert.AreEqual("WASM Test", deserialized.ProductName); Assert.AreEqual(TestStatus.Shipped, deserialized.Status); } [TestMethod] public void WasmCompat_AllPrimitiveTypes() { var testData = TestDataFactory.CreatePrimitiveTestData(); var json = AcJsonSerializer.Serialize(testData); var deserialized = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(deserialized); Assert.AreEqual(testData.IntValue, deserialized.IntValue); Assert.AreEqual(testData.LongValue, deserialized.LongValue); Assert.AreEqual(testData.BoolValue, deserialized.BoolValue); Assert.AreEqual(testData.StringValue, deserialized.StringValue); Assert.AreEqual(testData.GuidValue, deserialized.GuidValue); Assert.AreEqual(testData.EnumValue, deserialized.EnumValue); } [TestMethod] public void WasmCompat_EmptyCollections_HandleCorrectly() { var order = new TestOrder { Id = 1, OrderNumber = "EMPTY-TEST", Items = [], Tags = [] }; var json = AcJsonSerializer.Serialize(order); Assert.IsTrue(json.Contains("\"Items\":[]"), "Empty Items should serialize as []"); Assert.IsTrue(json.Contains("\"Tags\":[]"), "Empty Tags should serialize as []"); var deserialized = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(deserialized?.Items); Assert.AreEqual(0, deserialized.Items.Count); Assert.IsNotNull(deserialized?.Tags); Assert.AreEqual(0, deserialized.Tags.Count); } [TestMethod] public void Serialize_NullCollection_IsOmitted() { var order = new TestOrderWithNullableCollections { Id = 1, OrderNumber = "TEST", Items = null, Tags = null }; var json = AcJsonSerializer.Serialize(order); Assert.IsFalse(json.Contains("\"Items\""), "Null Items should not be serialized"); Assert.IsFalse(json.Contains("\"Tags\""), "Null Tags should not be serialized"); } [TestMethod] public void WasmCompat_SharedReferences_IdRefResolution() { var sharedTag = new SharedTag { Id = 999, Name = "SharedKey" }; var order = new TestOrder { Id = 1, OrderNumber = "REF-TEST", PrimaryTag = sharedTag, SecondaryTag = sharedTag, Tags = [sharedTag] }; var json = AcJsonSerializer.Serialize(order); Assert.IsTrue(json.Contains("\"$id\"")); Assert.IsTrue(json.Contains("\"$ref\"")); var nativeSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }; var deserialized = JsonConvert.DeserializeObject(json, nativeSettings); Assert.IsNotNull(deserialized); Assert.AreSame(deserialized.PrimaryTag, deserialized.SecondaryTag); Assert.AreSame(deserialized.PrimaryTag, deserialized.Tags[0]); } #endregion #region Cross-Serializer Compatibility Tests [TestMethod] public void CrossSerializer_MixedReferences_CompatibleWithNewtonsoft() { // Arrange var sharedTag = new SharedTag { Id = 100, Name = "SharedKey", CreatedAt = DateTime.UtcNow }; var sharedMeta = new MetadataInfo { Key = "SharedMeta", Value = "MetaValue", ChildMetadata = new MetadataInfo { Key = "Child" } }; var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", Status = TestStatus.Processing, PrimaryTag = sharedTag, SecondaryTag = sharedTag, OrderMetadata = sharedMeta, AuditMetadata = sharedMeta, Tags = [sharedTag], Items = [new TestOrderItem { Id = 10, ProductName = "Product-A", Tag = sharedTag, ItemMetadata = sharedMeta }] }; // Act - Serialize with AyCode var json = order.ToJson(); // Deserialize with native Newtonsoft var nativeSettings = new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Ignore, NullValueHandling = NullValueHandling.Ignore }; var deserialized = JsonConvert.DeserializeObject(json, nativeSettings); // Assert Assert.IsNotNull(deserialized); Assert.AreSame(deserialized.PrimaryTag, deserialized.SecondaryTag); Assert.AreSame(deserialized.OrderMetadata, deserialized.AuditMetadata); Assert.AreSame(deserialized.PrimaryTag, deserialized.Items[0].Tag); } #endregion #region Populate $ref Handling Tests [TestMethod] public void Populate_RefNode_ShouldSetPropertyToReferencedObject() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" }, ""SecondaryTag"": { ""$ref"": ""1"" } }"; var order = new TestOrder { Id = 1, OrderNumber = "OLD" }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from $ref"); Assert.AreEqual(100, order.PrimaryTag.Id); Assert.AreEqual("SharedTag", order.PrimaryTag.Name); Assert.AreSame(order.PrimaryTag, order.SecondaryTag, "SecondaryTag should reference the same object as PrimaryTag via $ref"); } [TestMethod] public void Populate_RefNodeInCollection_ShouldSetPropertyToReferencedObject() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" }, ""Tags"": [ { ""$ref"": ""1"" }, { ""$id"": ""2"", ""Id"": 200, ""Name"": ""OtherTag"" } ] }"; var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List() }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.AreEqual(2, order.Tags.Count, "Tags should have 2 items"); Assert.AreSame(order.PrimaryTag, order.Tags[0], "Tags[0] should reference the same object as PrimaryTag via $ref"); Assert.AreEqual(200, order.Tags[1].Id); Assert.AreNotSame(order.PrimaryTag, order.Tags[1]); } [TestMethod] public void Populate_NestedRefNode_ShouldResolveCorrectly() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""Items"": [{ ""Id"": 10, ""ProductName"": ""Product-A"", ""Tag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""ItemTag"" } }], ""PrimaryTag"": { ""$ref"": ""1"" } }"; var order = new TestOrder { Id = 1, OrderNumber = "OLD", Items = new List { new TestOrderItem { Id = 10, ProductName = "OLD" } } }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.Items[0].Tag, "Item's Tag should be set"); Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from $ref"); Assert.AreSame(order.Items[0].Tag, order.PrimaryTag, "PrimaryTag should reference the same object as Items[0].Tag via $ref"); } [TestMethod] public void Populate_ForwardRef_ShouldResolveDeferredReference() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""SecondaryTag"": { ""$ref"": ""1"" }, ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" } }"; var order = new TestOrder { Id = 1, OrderNumber = "OLD" }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set"); Assert.IsNotNull(order.SecondaryTag, "SecondaryTag should be set from forward $ref"); Assert.AreSame(order.PrimaryTag, order.SecondaryTag, "Forward $ref should resolve to the same object"); } [TestMethod] public void Populate_MultipleRefsToSameId_AllShouldResolveToSameObject() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""SharedTag"" }, ""SecondaryTag"": { ""$ref"": ""1"" }, ""Tags"": [ { ""$ref"": ""1"" }, { ""$ref"": ""1"" }, { ""$ref"": ""1"" } ] }"; var order = new TestOrder { Id = 1, OrderNumber = "OLD", Tags = new List() }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag); Assert.AreSame(order.PrimaryTag, order.SecondaryTag); Assert.AreEqual(3, order.Tags.Count); Assert.AreSame(order.PrimaryTag, order.Tags[0]); Assert.AreSame(order.PrimaryTag, order.Tags[1]); Assert.AreSame(order.PrimaryTag, order.Tags[2]); } [TestMethod] public void Populate_DeeplyNestedRef_ShouldResolveAcrossLevels() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""Items"": [{ ""Id"": 10, ""ProductName"": ""Product-A"", ""Tag"": { ""$id"": ""deep1"", ""Id"": 999, ""Name"": ""DeepTag"" } }], ""PrimaryTag"": { ""$ref"": ""deep1"" } }"; var order = new TestOrder { Id = 1, Items = new List { new TestOrderItem { Id = 10 } } }; // Act json.JsonTo(order); // Assert var deepTag = order.Items[0].Tag; Assert.IsNotNull(deepTag, "Item's Tag should be set"); Assert.IsNotNull(order.PrimaryTag, "PrimaryTag should be set from deep $ref"); Assert.AreSame(deepTag, order.PrimaryTag, "Root PrimaryTag should reference the nested Item's Tag via $ref"); } [TestMethod] public void Populate_RefInNestedObject_ShouldResolveFromParentContext() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""RootTag"" }, ""Items"": [{ ""Id"": 10, ""ProductName"": ""Product-A"", ""Tag"": { ""$ref"": ""1"" } }] }"; var order = new TestOrder { Id = 1, Items = new List { new TestOrderItem { Id = 10 } } }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag); Assert.IsNotNull(order.Items[0].Tag); Assert.AreSame(order.PrimaryTag, order.Items[0].Tag, "Nested Tag should reference root PrimaryTag via $ref"); } [TestMethod] public void Deserialize_RefOnly_ShouldCreateDeferredAndResolve() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""SecondaryTag"": { ""$ref"": ""1"" }, ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""Tag"" } }"; // Act var order = AcJsonDeserializer.Deserialize(json); // Assert Assert.IsNotNull(order); Assert.IsNotNull(order.PrimaryTag); Assert.IsNotNull(order.SecondaryTag); Assert.AreSame(order.PrimaryTag, order.SecondaryTag); } [TestMethod] public void Deserialize_MultipleIdRefs_ComplexGraph() { var json = @"{ ""Id"": 1, ""OrderNumber"": ""ORD-001"", ""PrimaryTag"": { ""$id"": ""tag1"", ""Id"": 100, ""Name"": ""Tag1"" }, ""SecondaryTag"": { ""$id"": ""tag2"", ""Id"": 200, ""Name"": ""Tag2"" }, ""Tags"": [ { ""$ref"": ""tag1"" }, { ""$ref"": ""tag2"" }, { ""$ref"": ""tag1"" } ], ""Items"": [{ ""Id"": 10, ""ProductName"": ""Product-A"", ""Tag"": { ""$ref"": ""tag2"" } }] }"; // Act var order = AcJsonDeserializer.Deserialize(json); // Assert Assert.IsNotNull(order); Assert.AreEqual(3, order.Tags.Count); Assert.AreSame(order.PrimaryTag, order.Tags[0]); Assert.AreSame(order.PrimaryTag, order.Tags[2]); Assert.AreSame(order.SecondaryTag, order.Tags[1]); Assert.AreSame(order.SecondaryTag, order.Items[0].Tag); Assert.AreNotSame(order.PrimaryTag, order.SecondaryTag); } [TestMethod] public void Populate_RefWithExistingTarget_ShouldOverwriteWithReference() { var json = @"{ ""Id"": 1, ""PrimaryTag"": { ""$id"": ""1"", ""Id"": 100, ""Name"": ""NewTag"" }, ""SecondaryTag"": { ""$ref"": ""1"" } }"; var existingTag = new SharedTag { Id = 999, Name = "ExistingTag" }; var order = new TestOrder { Id = 1, SecondaryTag = existingTag }; // Act json.JsonTo(order); // Assert Assert.IsNotNull(order.PrimaryTag); Assert.AreSame(order.PrimaryTag, order.SecondaryTag, "SecondaryTag should be overwritten with $ref reference"); Assert.AreNotSame(existingTag, order.SecondaryTag, "Original SecondaryTag should be replaced"); } #endregion #region AcJsonSerializer Complex Object Tests [TestMethod] public void Serialize_ObjectWithDictionaryProperty_SerializesDictionaryCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", Counts = new Dictionary { { "apple", 5 }, { "banana", 3 } } }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"Counts\":{")); Assert.IsTrue(json.Contains("\"apple\":5")); Assert.IsTrue(json.Contains("\"banana\":3")); } [TestMethod] public void Serialize_ObjectWithDateTimeOffsetProperty_SerializesCorrectly() { var dto = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.FromHours(2)); var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", DateTimeOffsetValue = dto }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"DateTimeOffsetValue\":\"2024-06-15")); } [TestMethod] public void Serialize_ObjectWithTimeSpanProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", TimeSpanValue = new TimeSpan(2, 30, 45) }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"TimeSpanValue\":\"02:30:45\"")); } [TestMethod] public void Serialize_ObjectWithNullProperties_SkipsNullProperties() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", NullString = null, NullObject = null, Counts = null }; var json = AcJsonSerializer.Serialize(obj); Assert.IsFalse(json.Contains("\"NullString\"")); Assert.IsFalse(json.Contains("\"NullObject\"")); Assert.IsFalse(json.Contains("\"Counts\"")); } [TestMethod] public void Serialize_ObjectWithUIntProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", UIntValue = 4000000000 }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"UIntValue\":4000000000")); } [TestMethod] public void Serialize_ObjectWithULongProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", ULongValue = 18000000000000000000 }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"ULongValue\":18000000000000000000")); } [TestMethod] public void Serialize_ObjectWithSByteProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", SByteValue = -100 }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"SByteValue\":-100")); } [TestMethod] public void Serialize_ObjectWithCharProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", CharValue = 'X' }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"CharValue\":\"X\"")); } [TestMethod] public void Serialize_ObjectWithUShortProperty_SerializesCorrectly() { var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", UShortValue = 60000 }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"UShortValue\":60000")); } [TestMethod] public void Serialize_ArrayWithNullItems_SerializesNullCorrectly() { var obj = new ObjectWithNullItems { Id = 1, MixedItems = [1, null, "text", null, 3] }; var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("[1,null,\"text\",null,3]")); } [TestMethod] public void Serialize_DictionaryDirect_SerializesAsObject() { var dict = new Dictionary { { "name", "Test" }, { "value", 42 } }; var json = AcJsonSerializer.Serialize(dict); Assert.IsTrue(json.StartsWith("{")); Assert.IsTrue(json.Contains("\"name\":\"Test\"")); Assert.IsTrue(json.Contains("\"value\":42")); } [TestMethod] public void Serialize_ObjectWithGuidProperty_SerializesCorrectly() { var guid = Guid.NewGuid(); var obj = new ExtendedPrimitiveTestClass { Id = 1, Name = "Test", Tag = new SharedTag { Id = 1, Name = "Tag" } }; // Using existing Tag property with Guid in SharedTag's CreatedAt var json = AcJsonSerializer.Serialize(obj); Assert.IsTrue(json.Contains("\"Tag\":{")); Assert.IsTrue(json.Contains("\"Name\":\"Tag\"")); } #endregion #region AcJsonDeserializer Extended Tests [TestMethod] public void Deserialize_GenericString_DirectPath() { var json = "\"Hello World\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual("Hello World", result); } [TestMethod] public void Deserialize_GenericInt_DirectPath() { var json = "42"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(42, result); } [TestMethod] public void Deserialize_GenericBool_DirectPath() { var json = "true"; var result = AcJsonDeserializer.Deserialize(json); Assert.IsTrue(result); } [TestMethod] public void Deserialize_GenericDouble_DirectPath() { var json = "3.14159"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(3.14159, result, 0.00001); } [TestMethod] public void Deserialize_GenericGuid_DirectPath() { var guid = Guid.NewGuid(); var json = $"\"{guid}\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(guid, result); } [TestMethod] public void Deserialize_GenericDateTime_DirectPath() { var dt = new DateTime(2024, 12, 25, 12, 30, 45, DateTimeKind.Utc); var json = $"\"{dt:O}\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(dt, result); } [TestMethod] public void Deserialize_GenericEnum_DirectPath() { var json = "2"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(TestStatus.Processing, result); } [TestMethod] public void Deserialize_GenericDecimal_DirectPath() { var json = "123.456"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(123.456m, result); } [TestMethod] public void Deserialize_GenericFloat_DirectPath() { var json = "3.14"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(3.14f, result, 0.001f); } [TestMethod] public void Deserialize_GenericLong_DirectPath() { var json = "9223372036854775807"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(9223372036854775807L, result); } [TestMethod] public void Deserialize_GenericByte_DirectPath() { var json = "255"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual((byte)255, result); } [TestMethod] public void Deserialize_GenericShort_DirectPath() { var json = "32767"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual((short)32767, result); } [TestMethod] public void Deserialize_GenericUShort_DirectPath() { var json = "65535"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual((ushort)65535, result); } [TestMethod] public void Deserialize_GenericUInt_DirectPath() { var json = "4294967295"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(4294967295U, result); } [TestMethod] public void Deserialize_GenericULong_DirectPath() { var json = "18446744073709551615"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(18446744073709551615UL, result); } [TestMethod] public void Deserialize_GenericSByte_DirectPath() { var json = "-128"; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual((sbyte)-128, result); } [TestMethod] public void Deserialize_GenericChar_DirectPath() { var json = "\"A\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual('A', result); } [TestMethod] public void Deserialize_GenericDateTimeOffset_DirectPath() { var dto = new DateTimeOffset(2024, 12, 25, 12, 30, 45, TimeSpan.FromHours(2)); var json = $"\"{dto:O}\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(dto, result); } [TestMethod] public void Deserialize_GenericTimeSpan_DirectPath() { var ts = new TimeSpan(1, 2, 3, 4, 5); var json = $"\"{ts:c}\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(ts, result); } [TestMethod] public void Deserialize_GenericEnumFromString_DirectPath() { var json = "\"Processing\""; var result = AcJsonDeserializer.Deserialize(json); Assert.AreEqual(TestStatus.Processing, result); } [TestMethod] public void Populate_Array_PopulatesList() { var json = "[1, 2, 3]"; var list = new List(); AcJsonDeserializer.Populate(json, list); Assert.AreEqual(3, list.Count); Assert.AreEqual(1, list[0]); Assert.AreEqual(3, list[2]); } [TestMethod] public void Populate_ObjectToObject_PopulatesProperties() { var json = "{\"Name\": \"Updated\", \"Id\": 99}"; var obj = new SharedTag { Id = 1, Name = "Original" }; AcJsonDeserializer.Populate(json, obj, typeof(SharedTag)); Assert.AreEqual(99, obj.Id); Assert.AreEqual("Updated", obj.Name); } [TestMethod] public void Deserialize_NullJson_ReturnsDefault() { var result = AcJsonDeserializer.Deserialize("null"); Assert.IsNull(result); } [TestMethod] public void Deserialize_EmptyJson_ReturnsDefault() { var result = AcJsonDeserializer.Deserialize(""); Assert.IsNull(result); } [TestMethod] public void Deserialize_GenericNullJson_ReturnsDefaultInt() { var result = AcJsonDeserializer.Deserialize("null"); Assert.AreEqual(0, result); } [TestMethod] public void Deserialize_RuntimeType_WithDateTimeOffset() { var dto = new DateTimeOffset(2024, 6, 15, 10, 30, 0, TimeSpan.FromHours(2)); var json = $"\"{dto:O}\""; var result = AcJsonDeserializer.Deserialize(json, typeof(DateTimeOffset)); Assert.AreEqual(dto, result); } [TestMethod] public void Deserialize_RuntimeType_WithTimeSpan() { var ts = new TimeSpan(2, 30, 45); var json = $"\"{ts:c}\""; var result = AcJsonDeserializer.Deserialize(json, typeof(TimeSpan)); Assert.AreEqual(ts, result); } [TestMethod] public void Deserialize_RuntimeType_WithChar() { var json = "\"X\""; var result = AcJsonDeserializer.Deserialize(json, typeof(char)); Assert.AreEqual('X', result); } [TestMethod] public void Deserialize_RuntimeType_WithEnumString() { var json = "\"Active\""; var result = AcJsonDeserializer.Deserialize(json, typeof(TestStatus)); Assert.AreEqual(TestStatus.Active, result); } #endregion #region AcJsonDeserializer Error Handling Tests [TestMethod] public void Deserialize_InvalidJson_ThrowsException() { var invalidJson = "{ this is not valid json }"; try { AcJsonDeserializer.Deserialize(invalidJson); Assert.Fail("Expected AcJsonDeserializationException"); } catch (AcJsonDeserializationException) { // Expected } } [TestMethod] public void Deserialize_DoubleQuotedJson_ThrowsException() { // This is what double-serialized JSON looks like: a JSON string containing escaped JSON var doubleQuotedJson = "\"{\\\"Id\\\":1,\\\"Name\\\":\\\"Test\\\"}\""; try { AcJsonDeserializer.Deserialize(doubleQuotedJson); Assert.Fail("Expected AcJsonDeserializationException for double-serialized JSON"); } catch (AcJsonDeserializationException ex) { Assert.IsTrue(ex.Message.Contains("double-serialized")); } } [TestMethod] public void Deserialize_ArrayToObject_ThrowsException() { // Trying to deserialize an array JSON to a single object var arrayJson = "[{\"Id\":1},{\"Id\":2}]"; try { AcJsonDeserializer.Deserialize(arrayJson); Assert.Fail("Expected AcJsonDeserializationException"); } catch (AcJsonDeserializationException ex) { Assert.IsTrue(ex.Message.Contains("array") || ex.Message.Contains("collection")); } } [TestMethod] public void Deserialize_ObjectToArray_ThrowsException() { // Trying to deserialize an object JSON to a collection var objectJson = "{\"Id\":1,\"ProductName\":\"Test\"}"; try { AcJsonDeserializer.Deserialize>(objectJson); Assert.Fail("Expected AcJsonDeserializationException"); } catch (AcJsonDeserializationException ex) { Assert.IsTrue(ex.Message.Contains("object") || ex.Message.Contains("collection")); } } [TestMethod] public void Populate_NullTarget_ThrowsArgumentNullException() { var json = "{\"Id\":1}"; TestOrderItem? target = null; try { AcJsonDeserializer.Populate(json, target!); Assert.Fail("Expected ArgumentNullException"); } catch (ArgumentNullException) { // Expected } } [TestMethod] public void Populate_InvalidJson_ThrowsException() { var target = new TestOrderItem(); var invalidJson = "{ not valid }"; try { AcJsonDeserializer.Populate(invalidJson, target); Assert.Fail("Expected AcJsonDeserializationException"); } catch (AcJsonDeserializationException) { // Expected } } [TestMethod] public void Populate_ArrayToNonList_ThrowsException() { var target = new TestOrderItem(); var arrayJson = "[1,2,3]"; try { AcJsonDeserializer.Populate(arrayJson, target); Assert.Fail("Expected AcJsonDeserializationException"); } catch (AcJsonDeserializationException) { // Expected } } #endregion #region Edge Case Tests [TestMethod] public void Deserialize_SpecialCharactersInStrings_HandledCorrectly() { var json = "{\"Id\":1,\"ProductName\":\"Test \\\"quoted\\\" and \\\\backslash\"}"; var result = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(result); Assert.AreEqual("Test \"quoted\" and \\backslash", result.ProductName); } [TestMethod] public void Deserialize_UnicodeCharacters_HandledCorrectly() { var json = "{\"Id\":1,\"ProductName\":\"中文日本語한국어🎉\"}"; var result = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(result); Assert.AreEqual("中文日本語한국어🎉", result.ProductName); } [TestMethod] public void Deserialize_LargeNumbers_HandledCorrectly() { var json = "{\"Id\":999999999,\"ProductName\":\"Big\",\"Quantity\":2147483647}"; var result = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(result); Assert.AreEqual(999999999, result.Id); Assert.AreEqual(int.MaxValue, result.Quantity); } [TestMethod] public void Serialize_ThenDeserialize_RoundTripPreservesData() { var original = new TestOrderItem { Id = 42, ProductName = "Test with \"quotes\" and \\backslash", Quantity = 100, UnitPrice = 99.99m, Status = TestStatus.Processing }; var json = original.ToJson(); var restored = AcJsonDeserializer.Deserialize(json); Assert.IsNotNull(restored); Assert.AreEqual(original.Id, restored.Id); Assert.AreEqual(original.ProductName, restored.ProductName); Assert.AreEqual(original.Quantity, restored.Quantity); Assert.AreEqual(original.UnitPrice, restored.UnitPrice); Assert.AreEqual(original.Status, restored.Status); } #endregion #region Task-like JSON Wrapper Tests [TestMethod] public void Deserialize_TaskWrappedJson_DirectDeserialization_OnlyGetsRootProperties() { // This JSON represents a serialized Task - the actual data is in "Result" // This happens when someone forgets to await an async method before serializing var taskWrappedJson = "{\"Result\":{\"Id\":1,\"ProductName\":\"Processed: TestProduct\",\"Quantity\":10,\"UnitPrice\":20,\"TotalPrice\":200},\"Id\":1,\"Status\":5,\"IsCompleted\":true,\"IsCompletedSuccessfully\":true}"; // Direct deserialization to TestOrderItem only gets root-level properties var result = AcJsonDeserializer.Deserialize(taskWrappedJson); Assert.IsNotNull(result); // Id=1 is at root level and matches Assert.AreEqual(1, result.Id); // These values are inside "Result" object, not at root - they remain default Assert.AreEqual(0, result.Quantity, "Quantity should be 0 because it's inside Result, not at root"); Assert.AreEqual(0m, result.UnitPrice, "UnitPrice should be 0 because it's inside Result, not at root"); } [TestMethod] public void Deserialize_TaskWrappedJson_UseWrapperClass_ExtractsCorrectly() { // This JSON represents a serialized Task - the actual data is in "Result" var taskWrappedJson = "{\"Result\":{\"Id\":1,\"ProductName\":\"Processed: TestProduct\",\"Quantity\":10,\"UnitPrice\":20,\"TotalPrice\":200},\"Id\":1,\"Status\":5,\"IsCompleted\":true,\"IsCompletedSuccessfully\":true}"; // Proper approach: deserialize to a wrapper type and extract Result var wrapper = AcJsonDeserializer.Deserialize>(taskWrappedJson); Assert.IsNotNull(wrapper); Assert.IsNotNull(wrapper.Result); Assert.AreEqual(1, wrapper.Result.Id); Assert.AreEqual("Processed: TestProduct", wrapper.Result.ProductName); Assert.AreEqual(10, wrapper.Result.Quantity); Assert.AreEqual(20m, wrapper.Result.UnitPrice); Assert.IsTrue(wrapper.IsCompleted); Assert.IsTrue(wrapper.IsCompletedSuccessfully); Assert.AreEqual(5, wrapper.Status); } /// /// Wrapper class to deserialize Task-like JSON structures. /// This is what you get when you accidentally serialize a Task object instead of awaiting it. /// private class TaskResultWrapper { public T? Result { get; set; } public int Id { get; set; } public int Status { get; set; } public bool IsCompleted { get; set; } public bool IsCompletedSuccessfully { get; set; } } #endregion }