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}"); } }