using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using System.Reflection;
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
namespace AyCode.Core.Tests.Serialization;
///
/// Diagnostic tests to help debug serialization issues.
///
[TestClass]
public class AcBinarySerializerDiagnosticTests
{
///
/// Diagnostic test to understand the exact binary structure.
/// This test outputs the binary bytes to help debug production issues.
///
[TestMethod]
public void Diagnostic_StockTaking_BinaryStructure()
{
var stockTaking = new TestStockTakingWithInheritance
{
Id = 1,
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
IsClosed = false,
Creator = 6,
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
StockTakingItems = null
};
var binary = stockTaking.ToBinary();
var hexDump = string.Join(" ", binary.Select(b => b.ToString("X2")));
Console.WriteLine($"Binary length: {binary.Length}");
Console.WriteLine($"Binary hex: {hexDump}");
for (int i = 0; i < binary.Length; i++)
{
if (binary[i] == 214)
{
Console.WriteLine($"Found 0xD6 at position {i}");
}
}
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(6, result.Creator);
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks);
}
///
/// Test with nested list to ensure proper stream positioning.
///
[TestMethod]
public void Diagnostic_StockTaking_WithNestedItems()
{
var stockTaking = new TestStockTakingWithInheritance
{
Id = 1,
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
IsClosed = false,
Creator = 6,
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
StockTakingItems = new List
{
new()
{
Id = 10,
StockTakingId = 1,
ProductId = 100,
IsMeasured = true,
OriginalStockQuantity = 50,
MeasuredStockQuantity = 48,
Created = new DateTime(2025, 1, 24, 14, 0, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 14, 30, 0, DateTimeKind.Utc),
StockTakingItemPallets = null
}
}
};
var binary = stockTaking.ToBinary();
Console.WriteLine($"Binary length with 1 item: {binary.Length}");
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(6, result.Creator, "Creator should be 6");
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
Assert.IsNotNull(result.StockTakingItems);
Assert.AreEqual(1, result.StockTakingItems.Count);
}
///
/// CRITICAL TEST: Verify property order is consistent.
/// This test checks that the reflection-based property order matches
/// what's expected for serialization/deserialization.
///
[TestMethod]
public void Diagnostic_PropertyOrder_InheritanceHierarchy()
{
var type = typeof(SimStockTaking);
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
Console.WriteLine($"Properties of {type.Name} (count: {props.Length}):");
for (int i = 0; i < props.Length; i++)
{
var prop = props[i];
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name} (declared in: {prop.DeclaringType?.Name})");
}
// The exact order may vary by platform!
// Log it so we can compare server vs client
Assert.IsTrue(props.Length >= 7, "Should have at least 7 properties");
// Check that all expected properties exist
var propNames = props.Select(p => p.Name).ToHashSet();
Assert.IsTrue(propNames.Contains("Id"), "Should have Id");
Assert.IsTrue(propNames.Contains("StartDateTime"), "Should have StartDateTime");
Assert.IsTrue(propNames.Contains("IsClosed"), "Should have IsClosed");
Assert.IsTrue(propNames.Contains("Creator"), "Should have Creator");
Assert.IsTrue(propNames.Contains("Created"), "Should have Created");
Assert.IsTrue(propNames.Contains("Modified"), "Should have Modified");
Assert.IsTrue(propNames.Contains("StockTakingItems"), "Should have StockTakingItems");
}
///
/// CRITICAL REGRESSION TEST: Simulates exact production hierarchy.
/// StockTaking : MgStockTaking<StockTakingItem> : MgEntityBase : BaseEntity
///
[TestMethod]
public void Diagnostic_SimStockTaking_RoundTrip()
{
var stockTaking = new SimStockTaking
{
Id = 1,
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
IsClosed = false,
Creator = 6, // The exact value from production error
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
StockTakingItems = null // loadRelations = false means no items
};
var binary = stockTaking.ToBinary();
// Log the property names in the header
Console.WriteLine($"Binary length: {binary.Length}");
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id, "Id should be 1");
Assert.AreEqual(6, result.Creator, "Creator should be 6 - this is where the bug occurs!");
Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks,
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
Assert.IsFalse(result.IsClosed);
}
///
/// Test List of SimStockTaking - exact production scenario.
///
[TestMethod]
public void Diagnostic_ListOfSimStockTaking_RoundTrip()
{
var stockTakings = Enumerable.Range(1, 3).Select(i => new SimStockTaking
{
Id = i,
StartDateTime = DateTime.UtcNow.AddDays(-i),
IsClosed = i % 2 == 0,
Creator = i,
Created = DateTime.UtcNow.AddDays(-i),
Modified = DateTime.UtcNow,
StockTakingItems = null
}).ToList();
var binary = stockTakings.ToBinary();
var result = binary.BinaryTo>();
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
for (int i = 0; i < 3; i++)
{
var original = stockTakings[i];
var deserialized = result[i];
Assert.AreEqual(original.Id, deserialized.Id, $"[{i}] Id mismatch");
Assert.AreEqual(original.Creator, deserialized.Creator, $"[{i}] Creator mismatch");
Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks,
$"[{i}] Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
}
}
///
/// Diagnostic: Check what PropertyType the reflection returns for generic type parameter.
///
[TestMethod]
public void Diagnostic_GenericProperty_ReflectionType()
{
var parentType = typeof(ConcreteParent);
var itemsProp = parentType.GetProperty("Items");
Assert.IsNotNull(itemsProp);
var propType = itemsProp.PropertyType;
Console.WriteLine($"PropertyType: {propType}");
Console.WriteLine($"PropertyType.FullName: {propType.FullName}");
Console.WriteLine($"IsGenericType: {propType.IsGenericType}");
if (propType.IsGenericType)
{
var args = propType.GetGenericArguments();
Console.WriteLine($"GenericArguments.Length: {args.Length}");
foreach (var arg in args)
{
Console.WriteLine($" GenericArgument: {arg.FullName}");
}
}
Assert.IsTrue(propType.IsGenericType);
var elementType = propType.GetGenericArguments()[0];
Assert.AreEqual(typeof(GenericItemImpl), elementType,
"Element type should be GenericItemImpl, not IGenericItem");
}
///
/// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
/// This test verifies what happens when:
/// 1. Metadata header registers ALL properties including StockTakingItems
/// 2. Body SKIPS StockTakingItems because it's null
/// 3. Deserializer reads the body and must correctly map indices
///
[TestMethod]
public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
{
var stockTaking = new SimStockTaking
{
Id = 1,
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
IsClosed = false,
Creator = 6, // The exact value from production error (becomes TinyInt 0xD6)
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
StockTakingItems = null // THIS IS THE KEY - loadRelations = false
};
var binary = stockTaking.ToBinary();
// Log the binary structure
Console.WriteLine($"Binary length: {binary.Length}");
// Parse the header manually to understand structure
var pos = 0;
var version = binary[pos++];
Console.WriteLine($"Version: {version}");
var marker = binary[pos++];
Console.WriteLine($"Marker: 0x{marker:X2}");
// Read property count from metadata header
if ((marker & 0x10) != 0) // HasMetadata flag
{
var propCount = binary[pos++];
Console.WriteLine($"\n=== METADATA HEADER ===");
Console.WriteLine($"Property count in header: {propCount}");
for (int i = 0; i < propCount; i++)
{
var strLen = binary[pos++];
var propName = System.Text.Encoding.UTF8.GetString(binary, pos, strLen);
pos += strLen;
Console.WriteLine($" Header property [{i}]: '{propName}'");
}
}
Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");
// The body should start with Object marker (0x19)
var bodyStart = pos;
var objectMarker = binary[pos++];
Console.WriteLine($"Object marker: 0x{objectMarker:X2} (expected 0x19 for Object)");
// Read ref ID (if reference handling is enabled)
// VarInt: if top bit is set, continue reading
var refIdByte = binary[pos];
int refId;
if ((refIdByte & 0x80) == 0)
{
refId = refIdByte;
pos++;
}
else
{
// Multi-byte VarInt - simplified parsing
refId = -1;
pos += 2; // Skip for now
}
Console.WriteLine($"RefId: {refId}");
// Read property count in body
var bodyPropCount = binary[pos++];
Console.WriteLine($"Property count in body: {bodyPropCount}");
Console.WriteLine($"\n=== BODY PROPERTIES ===");
for (int i = 0; i < bodyPropCount && pos < binary.Length; i++)
{
var propIndex = binary[pos++];
Console.WriteLine($" Body property [{i}]: index={propIndex}, next bytes: 0x{binary[pos]:X2} 0x{(pos + 1 < binary.Length ? binary[pos + 1] : 0):X2}");
// Skip the value (simplified - just log)
var valueType = binary[pos];
if (valueType == 0x14) // DateTime
{
Console.WriteLine($" -> DateTime (9 bytes)");
pos += 10; // type + 9 bytes
}
else if (valueType >= 0xD0 && valueType <= 0xE7) // TinyInt
{
var tinyValue = valueType - 0xD0;
Console.WriteLine($" -> TinyInt value: {tinyValue}");
pos += 1;
}
else if (valueType == 0x03) // False
{
Console.WriteLine($" -> Boolean: false");
pos += 1;
}
else if (valueType == 0x02) // True
{
Console.WriteLine($" -> Boolean: true");
pos += 1;
}
else
{
Console.WriteLine($" -> Unknown type: 0x{valueType:X2}");
break;
}
}
// Find where 0xD6 (Creator = 6) appears in the body
Console.WriteLine($"\n=== 0xD6 OCCURRENCES ===");
for (int i = bodyStart; i < binary.Length; i++)
{
if (binary[i] == 0xD6)
{
Console.WriteLine($"Found 0xD6 (TinyInt 6 = Creator value) at position {i}");
}
}
// Deserialize and verify
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id, "Id should be 1");
Assert.AreEqual(6, result.Creator,
$"Creator should be 6. Got: {result.Creator}. " +
$"If this fails with a very large number, it means DateTime bytes were interpreted as int!");
Assert.AreEqual(stockTaking.Created, result.Created,
$"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}. " +
$"If Created has wrong value, deserializer read wrong bytes!");
Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
Assert.IsFalse(result.IsClosed);
Assert.IsNull(result.StockTakingItems, "StockTakingItems should remain null");
}
///
/// Test to verify property order consistency between serializer and deserializer.
///
[TestMethod]
public void Diagnostic_VerifyPropertyOrderConsistency()
{
// Get serializer's property order
var serializerType = typeof(AcBinarySerializer);
var metadataCacheField = serializerType.GetField("TypeMetadataCache",
BindingFlags.NonPublic | BindingFlags.Static);
// Clear cache to force fresh metadata creation
// (This helps ensure we're testing the actual order)
var type = typeof(SimStockTaking);
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
.ToArray();
Console.WriteLine($"Properties of {type.Name} (reflection order):");
for (int i = 0; i < props.Length; i++)
{
var prop = props[i];
Console.WriteLine($" [{i}] {prop.Name} : {prop.PropertyType.Name}");
}
// Verify Creator comes BEFORE Created in the reflection order
var creatorIndex = Array.FindIndex(props, p => p.Name == "Creator");
var createdIndex = Array.FindIndex(props, p => p.Name == "Created");
var stockTakingItemsIndex = Array.FindIndex(props, p => p.Name == "StockTakingItems");
Console.WriteLine($"\nKey indices:");
Console.WriteLine($" StockTakingItems: {stockTakingItemsIndex}");
Console.WriteLine($" Creator: {creatorIndex}");
Console.WriteLine($" Created: {createdIndex}");
// The bug scenario: if StockTakingItems is skipped during serialization,
// but the deserializer still expects it at the original index position,
// then Creator (index 3) would be read when expecting StockTakingItems (index 2)
// and Created (index 4) would be read when expecting Creator (index 3)
Assert.IsTrue(stockTakingItemsIndex >= 0, "StockTakingItems should exist");
Assert.IsTrue(creatorIndex >= 0, "Creator should exist");
Assert.IsTrue(createdIndex >= 0, "Created should exist");
// In the class definition order:
// StockTakingItems comes BEFORE Creator and Created
Assert.IsTrue(stockTakingItemsIndex < creatorIndex,
"StockTakingItems should come before Creator");
Assert.IsTrue(creatorIndex < createdIndex,
"Creator should come before Created");
}
///
/// Test multiple StockTakings with null StockTakingItems - exact production scenario.
///
[TestMethod]
public void Diagnostic_MultipleStockTakings_NullItems()
{
var stockTakings = new List
{
new()
{
Id = 1,
StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
IsClosed = false,
Creator = 6,
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
StockTakingItems = null
},
new()
{
Id = 2,
StartDateTime = new DateTime(2025, 1, 23, 9, 0, 0, DateTimeKind.Utc),
IsClosed = true,
Creator = 12,
Created = new DateTime(2025, 1, 23, 14, 0, 0, DateTimeKind.Utc),
Modified = new DateTime(2025, 1, 23, 15, 30, 0, DateTimeKind.Utc),
StockTakingItems = null
}
};
var binary = stockTakings.ToBinary();
Console.WriteLine($"Binary length for 2 StockTakings: {binary.Length}");
var result = binary.BinaryTo>();
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Count);
// First item
Assert.AreEqual(1, result[0].Id);
Assert.AreEqual(6, result[0].Creator, "First item Creator should be 6");
Assert.AreEqual(stockTakings[0].Created, result[0].Created,
$"First item Created mismatch. Expected: {stockTakings[0].Created}, Got: {result[0].Created}");
// Second item
Assert.AreEqual(2, result[1].Id);
Assert.AreEqual(12, result[1].Creator, "Second item Creator should be 12");
Assert.AreEqual(stockTakings[1].Created, result[1].Created,
$"Second item Created mismatch. Expected: {stockTakings[1].Created}, Got: {result[1].Created}");
}
}