using System;
using System.Collections.Generic;
using System.Linq;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Tests.TestModels;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AyCode.Core.Tests.Serialization;
///
/// Tests for IId-based reference handling in Binary 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 output uses ObjectRef (not redundant full objects)
/// - Deserialized result maintains reference identity
///
[TestClass]
public class AcBinarySerializerIIdReferenceTests
{
#region Helper Methods
///
/// Counts occurrences of ObjectRef in binary data.
/// Uses BinaryTypeCode.ObjectRef constant to stay in sync with format changes.
///
private static int CountObjectRefs(byte[] binary, bool writeBinaryToConsole = true)
{
if (writeBinaryToConsole) WriteBinaryToConsole(binary);
var count = 0;
for (var i = 0; i < binary.Length; i++)
{
if (binary[i] == BinaryTypeCode.ObjectRef)
count++;
}
return count;
}
private static void WriteBinaryToConsole(byte[] binary)
{
Console.WriteLine();
Console.WriteLine(BitConverter.ToString(binary));
Console.WriteLine();
}
///
/// Counts occurrences of a string in binary data (UTF8).
///
private static int CountStringOccurrences(byte[] binary, string searchString)
{
var searchBytes = System.Text.Encoding.UTF8.GetBytes(searchString);
var count = 0;
for (var i = 0; i <= binary.Length - searchBytes.Length; i++)
{
var match = true;
for (var j = 0; j < searchBytes.Length; j++)
{
if (binary[i + j] != searchBytes[j])
{
match = false;
break;
}
}
if (match) count++;
}
return count;
}
#endregion
#region Scenario 1: Same Instance (Object Identity)
///
/// SCENARIO 1: Same instance referenced multiple times.
/// Tests all ReferenceHandling modes: None, OnlyId, All
///
[TestMethod]
[DataRow(true, true)]
[DataRow(false, true)]
[DataRow(true, false)]
[DataRow(false, false)]
public void SameInstance_SerializeAndDeserialize(bool useSgen, bool useMeta)
{
var modes = new[]
{
ReferenceHandlingMode.None,
ReferenceHandlingMode.OnlyId,
ReferenceHandlingMode.All
};
foreach (var mode in modes)
{
// Arrange: SAME instance used multiple times
var userPreferences = new UserPreferences_All_True();
var sharedTag = new SharedTag_All_True { Id = 1, Name = "ImportantTag", Color = "#FF0000" };
var sharedUser = new SharedUser_All_True { Id = 1, Preferences = userPreferences };
var order = new TestOrder_Circ_Ref
{
Id = 1,
OrderNumber = "ORD-001",
PrimaryTag = sharedTag,
Owner = sharedUser,
Items =
[
new TestOrderItem_Circ_Ref { Id = 1, ProductName = "Product-A", Tag = sharedTag, Assignee = sharedUser },
new TestOrderItem_Circ_Ref { Id = 2, ProductName = "Product-B", Tag = sharedTag, Assignee = new SharedUser_All_True { Id = 2, Preferences = userPreferences }},
new TestOrderItem_Circ_Ref { Id = 3, ProductName = "Product-C", Tag = sharedTag, Assignee = new SharedUser_All_True { Id = 3, Preferences = userPreferences } }
]
};
//order.Parent = order.Items[1];
if (mode != ReferenceHandlingMode.None) order.Parent = order.Items[1];
else order.Parent = userPreferences;
order.Items[1].ParentOrder = order;
var options = new AcBinarySerializerOptions
{
ReferenceHandling = mode,
UseGeneratedCode = useSgen,
UseMetadata = useMeta,
MaxDepth = 10,
// None mode has no ref tracking → the cycle (Items[1].ParentOrder = order) is unprotected.
// Use Truncate so the recursion silently bottoms out with Null at the depth limit instead of throwing.
MaxDepthBehavior = mode == ReferenceHandlingMode.None
? MaxDepthBehavior.Truncate
: MaxDepthBehavior.Throw
};
Console.WriteLine($"\n========== ReferenceHandling: {options.ReferenceHandling}, UseSgen: {options.UseGeneratedCode}, UseMeta: {options.UseMetadata} ==========");
// Act
var binary = AcBinarySerializer.Serialize(order, options);
if (mode == ReferenceHandlingMode.None) WriteBinaryToConsole(binary);
var result = binary.BinaryTo(); // Options from header
var objectRefCount = CountObjectRefs(binary, false);
Console.WriteLine($"Binary size: {binary.Length} bytes");
Console.WriteLine($"ObjectRef count: {objectRefCount}");
Assert.IsNotNull(result, $"[{mode}] Deserialized result is null");
//Assert.IsNotNull(result.Parent);
Assert.IsNotNull(result.Owner);
// Assert based on mode
switch (mode)
{
case ReferenceHandlingMode.None:
// Truncate semantic: cycle bottoms out with Null at MaxDepth=10 → serialize succeeds, deserialize
// produces a partial graph where deep cyclic references read as null. Data integrity at root +
// first few levels still holds (verified below after the switch). CountObjectRefs raw byte scan
// is unreliable in None mode — byte 65 (ObjectRef) == ASCII 'A', so "Product-A" produces false
// positives. Skip count assertion; rely on data integrity checks instead.
break;
case ReferenceHandlingMode.OnlyId:
// sharedTag (Id=1) 4x → 3 ObjectRefs, sharedUser (Id=1) 2x → 1 ObjectRef = 4 total
Assert.IsTrue(objectRefCount >= 4, $"[{mode}] Expected at least 4 ObjectRefs, found {objectRefCount}");
// IId types should have reference identity
Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, $"[{mode}] Tag reference identity failed");
Assert.AreSame(result.Owner, result.Items[0].Assignee, $"[{mode}] User reference identity failed");
Assert.AreSame(result, result.Items[1].ParentOrder);
Assert.AreSame(result.Parent, result.Items[1]);
break;
case ReferenceHandlingMode.All:
// IId types + Non-IId (UserPreferences_All_True) should have ObjectRefs
Assert.IsTrue(objectRefCount >= 4, $"[{mode}] Expected at least 4 ObjectRefs, found {objectRefCount}");
Assert.AreSame(result.PrimaryTag, result.Items[0].Tag, $"[{mode}] Tag reference identity failed");
Assert.AreSame(result.Owner, result.Items[0].Assignee, $"[{mode}] User reference identity failed");
Assert.AreSame(result, result.Items[1].ParentOrder);
Assert.AreSame(result.Parent, result.Items[1]);
// Non-IId should also have reference identity in All mode
Assert.AreSame(result.Owner.Preferences, result.Items[0].Assignee.Preferences, $"[{mode}] UserPreferences_All_True reference identity failed - Non-IId should work in All mode!");
break;
}
// Data integrity - always check
Assert.IsNotNull(result.PrimaryTag, $"[{mode}] PrimaryTag is null");
Assert.AreEqual(1, result.PrimaryTag.Id, $"[{mode}] PrimaryTag.Id incorrect");
Assert.AreEqual("ImportantTag", result.PrimaryTag.Name, $"[{mode}] PrimaryTag.Name incorrect");
Assert.AreEqual(3, result.Items.Count, $"[{mode}] Items count incorrect");
Console.WriteLine($"[{mode}] PASSED ✓");
}
}
#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:
/// - ObjectRef should be used in binary
/// - 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 sharedTag = new SharedTag_All_True { Id = 55, Name = "ImportantTag_55", Color = "#FF0000" };
var order = new TestOrder_All_True
{
Id = 1,
OrderNumber = "ORD-001",
// All three types have Id=1 - tests (Type, Id) keying, not just Id
PrimaryTag = new SharedTag_All_True { Id = 1, Name = "Tag_Id1", Color = "#FF0000" },
Owner = new SharedUser_All_True { Id = 1, Username = "User_Id1", Email = "user1@test.com" },
Category = new SharedCategory_All_True { Id = 1, Name = "Category_Id1", SortOrder = 10 },
Items =
[
new TestOrderItem_All_True
{
Id = 1,
ProductName = "Product-A",
Tag = new SharedTag_All_True { Id = 1, Name = "Tag_Id1", Color = "#FF0000" },
Assignee = new SharedUser_All_True { Id = 1, Username = "User_Id1", Email = "user1@test.com" }
},
new TestOrderItem_All_True
{
Id = 2,
ProductName = "Product-B",
Tag = new SharedTag_All_True { Id = 1, Name = "Tag_Id1", Color = "#FF0000" },
Assignee = new SharedUser_All_True { Id = 1, Username = "User_Id1", Email = "user1@test.com" }
},
new TestOrderItem_All_True
{
Id = 3,
ProductName = "Product-C",
Tag = new SharedTag_All_True { Id = 1, Name = "Tag_Id1", Color = "#FF0000" },
Assignee = new SharedUser_All_True { Id = 1, Username = "User_Id1", Email = "user1@test.com" }
}
]
};
// Act
var binary = order.ToBinary();
var result = binary.BinaryTo();
// Assert 1: Check if ObjectRef is used (IId-based deduplication active)
var objectRefCount = CountObjectRefs(binary);
Console.WriteLine($"\nBinary size: {binary.Length} bytes (WithRef)");
Console.WriteLine($"ObjectRef count: {objectRefCount}");
// 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_All_True.Id=1)");
Assert.AreSame(result.PrimaryTag, result.Items[1].Tag,
"CRITICAL: Item[1].Tag should be same reference as PrimaryTag (same SharedTag_All_True.Id=1)");
Assert.AreSame(result.PrimaryTag, result.Items[2].Tag,
"CRITICAL: Item[2].Tag should be same reference as PrimaryTag (same SharedTag_All_True.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