using AyCode.Core.Extensions; using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.Serialization; /// /// Tests for IId-based reference handling in JSON serializer. /// Two scenarios: /// 1. Same instance referenced multiple times (object identity) /// 2. Different instances with same IId.Id (IId-based deduplication) /// /// Tests verify BOTH: /// - Serialized JSON uses $ref (not redundant full objects) /// - Deserialized result maintains reference identity /// [TestClass] public class AcJsonSerializerIIdReferenceTests { #region Helper Methods /// /// Counts occurrences of a string in JSON. /// private static int CountOccurrences(string json, string searchString) { var count = 0; var index = 0; while ((index = json.IndexOf(searchString, index, StringComparison.Ordinal)) != -1) { count++; index += searchString.Length; } return count; } #endregion #region Scenario 1: Same Instance (Object Identity) /// /// SCENARIO 1: Same instance referenced multiple times. /// JSON should contain $ref for 2nd, 3rd, 4th occurrence. /// Validates: $ref present + data integrity after deserialize + reference identity /// [TestMethod] public void SameInstance_Json_SerializeAndDeserialize() { // Arrange: SAME instance used 4 times var sharedTag = new SharedTag { Id = 1, Name = "ImportantTag", Color = "#FF0000" }; var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", PrimaryTag = sharedTag, Items = [ new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = sharedTag }, new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = sharedTag }, new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = sharedTag } ] }; // Act var json = order.ToJson(); Console.WriteLine(json); var result = json.JsonTo(); // Assert 1: JSON contains $ref markers (reference handling is active) var refCount = CountOccurrences(json, "{\"$ref\":\"1\"}"); Console.WriteLine($"JSON length: {json.Length} chars"); Console.WriteLine($"$ref count: {refCount}"); Assert.IsTrue(refCount >= 3, $"Expected at least 3 $ref entries for shared tag, found {refCount}. " + "Reference handling may not be active!"); // Assert 2: Data integrity - ALL tags are present and have correct data Assert.IsNotNull(result, "Deserialized result is null"); Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, "PrimaryTag.Name incorrect"); Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); Assert.IsNotNull(result.Items, "Items is null"); Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); for (var i = 0; i < 3; i++) { Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); Assert.AreEqual("ImportantTag", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect"); } // Assert 3: Reference identity - all should be same object reference Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, "Item[0].Tag should be same reference as PrimaryTag"); Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, "Item[1].Tag should be same reference as PrimaryTag"); Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, "Item[2].Tag should be same reference as PrimaryTag"); } #endregion #region Scenario 2: Different Instances with Same IId (IId-Based Deduplication) /// /// SCENARIO 2: DIFFERENT instances with SAME IId.Id value. /// CRITICAL test - if IId-based deduplication works: /// - $ref should be used in JSON /// - Data should be complete after deserialize /// - References should be identical (AreSame) /// - Different TYPES with same int Id should NOT be confused! /// [TestMethod] public void DifferentInstances_SameIId_SerializeAndDeserialize() { // Arrange: DIFFERENT instances but SAME IId.Id // CRITICAL: Multiple DIFFERENT TYPES all have Id=1 - must not be confused! var order = new TestOrder { Id = 1, OrderNumber = "ORD-001", // All three types have Id=1 - tests (Type, Id) keying, not just Id PrimaryTag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, Owner = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" }, Category = new SharedCategory { Id = 1, Name = "Category_Id1", SortOrder = 10 }, Items = [ new TestOrderItem { Id = 1, ProductName = "Product-A", Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } }, new TestOrderItem { Id = 2, ProductName = "Product-B", Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } }, new TestOrderItem { Id = 3, ProductName = "Product-C", Tag = new SharedTag { Id = 1, Name = "Tag_Id1", Color = "#FF0000" }, Assignee = new SharedUser { Id = 1, Username = "User_Id1", Email = "user1@test.com" } } ] }; // Act var json = order.ToJson(); var result = json.JsonTo(); // Assert 1: Check if $ref is used (IId-based deduplication active) var refCount = CountOccurrences(json, "\"$ref\""); Console.WriteLine($"JSON length: {json.Length} chars"); Console.WriteLine($"$ref count: {refCount}"); Console.WriteLine("JSON (first 2000 chars):"); Console.WriteLine(json.Length > 2000 ? json[..2000] + "..." : json); // 4 Tags, 4 Users - each should have 3 $refs = 6 total minimum Assert.IsTrue(refCount >= 6, $"CRITICAL: Expected at least 6 $ref entries (3 per type for Tag and User), found {refCount}. " + "IId-based reference deduplication is NOT working!"); // Assert 2: Data integrity - ALL data present and correct Assert.IsNotNull(result, "Deserialized result is null"); // Tag data Assert.IsNotNull(result.PrimaryTag, "PrimaryTag is null - data lost!"); Assert.AreEqual(1, result.PrimaryTag.Id, "PrimaryTag.Id incorrect"); Assert.AreEqual("Tag_Id1", result.PrimaryTag.Name, "PrimaryTag.Name incorrect - might be confused with User!"); Assert.AreEqual("#FF0000", result.PrimaryTag.Color, "PrimaryTag.Color incorrect"); // User data - MUST NOT be confused with Tag (both have Id=1) Assert.IsNotNull(result.Owner, "Owner is null - data lost!"); Assert.AreEqual(1, result.Owner.Id, "Owner.Id incorrect"); Assert.AreEqual("User_Id1", result.Owner.Username, "Owner.Username incorrect - might be confused with Tag!"); Assert.AreEqual("user1@test.com", result.Owner.Email, "Owner.Email incorrect"); // Category data - MUST NOT be confused with Tag or User (all have Id=1) Assert.IsNotNull(result.Category, "Category is null - data lost!"); Assert.AreEqual(1, result.Category.Id, "Category.Id incorrect"); Assert.AreEqual("Category_Id1", result.Category.Name, "Category.Name incorrect - might be confused with Tag!"); Assert.AreEqual(10, result.Category.SortOrder, "Category.SortOrder incorrect"); Assert.IsNotNull(result.Items, "Items is null"); Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); for (var i = 0; i < 3; i++) { // Tag in items Assert.IsNotNull(result.Items[i].Tag, $"Items[{i}].Tag is null - data lost!"); Assert.AreEqual(1, result.Items[i].Tag!.Id, $"Items[{i}].Tag.Id incorrect"); Assert.AreEqual("Tag_Id1", result.Items[i].Tag.Name, $"Items[{i}].Tag.Name incorrect - confused with User?"); // User in items - MUST NOT be confused with Tag Assert.IsNotNull(result.Items[i].Assignee, $"Items[{i}].Assignee is null - data lost!"); Assert.AreEqual(1, result.Items[i].Assignee!.Id, $"Items[{i}].Assignee.Id incorrect"); Assert.AreEqual("User_Id1", result.Items[i].Assignee.Username, $"Items[{i}].Assignee.Username incorrect - confused with Tag?"); } // Assert 3: Reference identity - same TYPE with same Id should be same reference // Tags with Id=1 should all be same reference Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, "CRITICAL: Item[0].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); Assert.AreSame(result.PrimaryTag, result.Items[1].Tag, "CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); Assert.AreSame(result.PrimaryTag, result.Items[2].Tag, "CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag.Id=1)"); // Users with Id=1 should all be same reference Assert.AreSame(result.Owner, result.Items[0].Assignee, "CRITICAL: Item[0].Assignee should be same reference as Owner (same SharedUser.Id=1)"); Assert.AreSame(result.Owner, result.Items[1].Assignee, "CRITICAL: Item[1].Assignee should be same reference as Owner (same SharedUser.Id=1)"); Assert.AreSame(result.Owner, result.Items[2].Assignee, "CRITICAL: Item[2].Assignee should be same reference as Owner (same SharedUser.Id=1)"); // Assert 4: Different TYPES with same Id should NOT be same reference! Assert.AreNotSame(result.PrimaryTag, result.Owner, "CRITICAL BUG: Tag and User are same reference! Types with same int Id were confused!"); Assert.AreNotSame(result.PrimaryTag, result.Category, "CRITICAL BUG: Tag and Category are same reference! Types with same int Id were confused!"); Assert.AreNotSame(result.Owner, result.Category, "CRITICAL BUG: User and Category are same reference! Types with same int Id were confused!"); } /// /// Size comparison: Same IId should result in smaller JSON + verify data integrity. /// [TestMethod] public void DifferentInstances_SameIId_SmallerJsonWithDataIntegrity() { // Arrange: 10 different instances with SAME IId var orderWithSameIId = new TestOrder { Id = 1, OrderNumber = "SAME-IID", Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem { Id = i, ProductName = $"Product-{i}", Assignee = new SharedUser { Id = 1, Username = "shared_user_name", Email = "shared@test.com" } }).ToList() }; // Arrange: 10 different instances with DIFFERENT IIds var orderWithDifferentIIds = new TestOrder { Id = 1, OrderNumber = "DIFF-IID", Items = Enumerable.Range(1, 10).Select(i => new TestOrderItem { Id = i, ProductName = $"Product-{i}", Assignee = new SharedUser { Id = i * 100, Username = "unique_user_name", Email = "unique@test.com" } }).ToList() }; // Act var sameIIdJson = orderWithSameIId.ToJson(); var diffIIdJson = orderWithDifferentIIds.ToJson(); var sameIIdResult = sameIIdJson.JsonTo(); var diffIIdResult = diffIIdJson.JsonTo(); // Assert 1: Size comparison Console.WriteLine($"Same IId JSON size: {sameIIdJson.Length} chars"); Console.WriteLine($"Different IId JSON size: {diffIIdJson.Length} chars"); Console.WriteLine($"Size difference: {diffIIdJson.Length - sameIIdJson.Length} chars"); Assert.IsTrue(sameIIdJson.Length < diffIIdJson.Length, $"Same IId ({sameIIdJson.Length}) should be smaller than different IIds ({diffIIdJson.Length}). " + "IId-based deduplication NOT working!"); // Assert 2: Data integrity for sameIId result Assert.IsNotNull(sameIIdResult, "sameIIdResult is null"); Assert.IsNotNull(sameIIdResult.Items, "sameIIdResult.Items is null"); Assert.AreEqual(10, sameIIdResult.Items.Count, "sameIIdResult should have 10 items"); for (var i = 0; i < 10; i++) { Assert.IsNotNull(sameIIdResult.Items[i].Assignee, $"sameIIdResult.Items[{i}].Assignee is null - data lost!"); Assert.AreEqual(1, sameIIdResult.Items[i].Assignee!.Id, $"sameIIdResult.Items[{i}].Assignee.Id should be 1"); Assert.AreEqual("shared_user_name", sameIIdResult.Items[i].Assignee.Username, $"sameIIdResult.Items[{i}].Assignee.Username incorrect"); } // Assert 3: Reference identity for sameIId var firstAssignee = sameIIdResult.Items[0].Assignee; for (var i = 1; i < 10; i++) { Assert.AreSame(firstAssignee, sameIIdResult.Items[i].Assignee, $"CRITICAL: Items[{i}].Assignee should be same reference as Items[0].Assignee (same IId.Id=1)"); } } #endregion #region Guid-Based IId Tests /// /// Guid IId with same instance - validates $ref + data integrity + reference identity. /// [TestMethod] public void GuidIId_SameInstance_SerializeAndDeserialize() { // Arrange: SAME instance var sharedGuid = Guid.NewGuid(); var sharedItem = new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }; var order = new TestGuidOrder { Id = Guid.NewGuid(), Code = "GUID-001", Count = 3, Items = [sharedItem, sharedItem, sharedItem] }; // Act var json = order.ToJson(); Console.WriteLine(json); var result = json.JsonTo(); // Assert 1: $ref should be present var refCount = CountOccurrences(json, "{\"$ref\":\"1\"}"); Console.WriteLine($"JSON length: {json.Length} chars"); Console.WriteLine($"$ref count: {refCount}"); Assert.IsTrue(refCount >= 2, $"Expected at least 2 $refs, found {refCount}"); // Assert 2: Data integrity - all items present and correct Assert.IsNotNull(result, "Result is null"); Assert.IsNotNull(result.Items, "Items is null"); Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); for (var i = 0; i < 3; i++) { Assert.IsNotNull(result.Items[i], $"Items[{i}] is null"); Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); } // Assert 3: Reference identity Assert.AreSame(result.Items[0], result.Items[1], "Items[0] and Items[1] should be same reference"); Assert.AreSame(result.Items[1], result.Items[2], "Items[1] and Items[2] should be same reference"); } /// /// CRITICAL: Guid IId with different instances but same Id - tests IId-based deduplication. /// [TestMethod] public void GuidIId_DifferentInstances_SameId_SerializeAndDeserialize() { // Arrange: DIFFERENT instances but SAME Guid var sharedGuid = Guid.NewGuid(); var order = new TestGuidOrder { Id = Guid.NewGuid(), Code = "GUID-001", Count = 3, Items = [ new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" }, new TestGuidItem { Id = sharedGuid, Name = "SharedGuidItem" } ] }; // Act var json = order.ToJson(); var result = json.JsonTo(); // Assert 1: $ref should be present if IId-based dedup works var refCount = CountOccurrences(json, "\"$ref\""); Console.WriteLine($"JSON length: {json.Length} chars"); Console.WriteLine($"$ref count: {refCount}"); Console.WriteLine("JSON:"); Console.WriteLine(json); Assert.IsTrue(refCount >= 2, $"CRITICAL: Expected at least 2 $refs for same Guid IId, found {refCount}. " + "Guid-based IId deduplication NOT working!"); // Assert 2: Data integrity - all items present and correct Assert.IsNotNull(result, "Result is null"); Assert.IsNotNull(result.Items, "Items is null"); Assert.AreEqual(3, result.Items.Count, "Items count incorrect"); for (var i = 0; i < 3; i++) { Assert.IsNotNull(result.Items[i], $"Items[{i}] is null - data lost!"); Assert.AreEqual(sharedGuid, result.Items[i].Id, $"Items[{i}].Id incorrect"); Assert.AreEqual("SharedGuidItem", result.Items[i].Name, $"Items[{i}].Name incorrect"); } // Assert 3: Reference identity - if IId-based, should be same reference Assert.AreSame(result.Items[0], result.Items[1], "CRITICAL: Items[0] and Items[1] should be same reference (same Guid)"); Assert.AreSame(result.Items[1], result.Items[2], "CRITICAL: Items[1] and Items[2] should be same reference (same Guid)"); } #endregion #region Data Integrity Tests [TestMethod] public void SharedCategory_DataIntegrity() { var categories = new List { new() { Id = 1, Name = "Category1", SortOrder = 1, IsDefault = true }, new() { Id = 2, Name = "Category2", SortOrder = 2, ParentCategoryId = 1 }, new() { Id = 3, Name = "Category3", SortOrder = 3, ParentCategoryId = 1 } }; var json = categories.ToJson(); var result = json.JsonTo>(); Assert.IsNotNull(result); Assert.AreEqual(3, result.Count); Assert.AreEqual(1, result[0].Id); Assert.AreEqual("Category1", result[0].Name); Assert.IsTrue(result[0].IsDefault); Assert.AreEqual(2, result[1].Id); Assert.AreEqual(1, result[1].ParentCategoryId); } #endregion }