using AyCode.Core.Extensions; using AyCode.Core.Serializers.Binaries; using AyCode.Core.Tests.TestModels; namespace AyCode.Core.Tests.serialization; [TestClass] public class AcBinarySerializerTests { #region Basic Serialization Tests [TestMethod] public void Serialize_Null_ReturnsSingleNullByte() { var result = AcBinarySerializer.Serialize(null); Assert.AreEqual(1, result.Length); Assert.AreEqual((byte)0, result[0]); // BinaryTypeCode.Null = 0 } [TestMethod] public void Serialize_Int32_RoundTrip() { var value = 12345; var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_Int64_RoundTrip() { var value = 123456789012345L; var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_Double_RoundTrip() { var value = 3.14159265358979; var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_String_RoundTrip() { var value = "Hello, Binary World!"; var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_Boolean_RoundTrip() { var trueResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(true)); var falseResult = AcBinaryDeserializer.Deserialize(AcBinarySerializer.Serialize(false)); Assert.IsTrue(trueResult); Assert.IsFalse(falseResult); } [TestMethod] public void Serialize_DateTime_RoundTrip() { var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] [DataRow(DateTimeKind.Unspecified)] [DataRow(DateTimeKind.Utc)] [DataRow(DateTimeKind.Local)] public void Serialize_DateTime_PreservesKind(DateTimeKind kind) { var value = new DateTime(2024, 12, 25, 10, 30, 45, kind); var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value.Ticks, result.Ticks); Assert.AreEqual(value.Kind, result.Kind); } [TestMethod] public void Serialize_Guid_RoundTrip() { var value = Guid.NewGuid(); var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_Decimal_RoundTrip() { var value = 123456.789012m; var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_TimeSpan_RoundTrip() { var value = TimeSpan.FromHours(2.5); var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); Assert.AreEqual(value, result); } [TestMethod] public void Serialize_DateTimeOffset_RoundTrip() { var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2)); var binary = AcBinarySerializer.Serialize(value); var result = AcBinaryDeserializer.Deserialize(binary); // Compare UTC ticks and offset separately since we store UTC ticks Assert.AreEqual(value.UtcTicks, result.UtcTicks); Assert.AreEqual(value.Offset, result.Offset); } #endregion #region Object Serialization Tests [TestMethod] public void Serialize_SimpleObject_RoundTrip() { var obj = new TestSimpleClass { Id = 42, Name = "Test Object", Value = 3.14, IsActive = true }; var binary = obj.ToBinary(); var result = binary.BinaryTo(); Assert.IsNotNull(result); Assert.AreEqual(obj.Id, result.Id); Assert.AreEqual(obj.Name, result.Name); Assert.AreEqual(obj.Value, result.Value); Assert.AreEqual(obj.IsActive, result.IsActive); } [TestMethod] public void Serialize_NestedObject_RoundTrip() { var obj = new TestNestedClass { Id = 1, Name = "Parent", Child = new TestSimpleClass { Id = 2, Name = "Child", Value = 2.5, IsActive = true } }; var binary = obj.ToBinary(); var result = binary.BinaryTo(); Assert.IsNotNull(result); Assert.AreEqual(obj.Id, result.Id); Assert.AreEqual(obj.Name, result.Name); Assert.IsNotNull(result.Child); Assert.AreEqual(obj.Child.Id, result.Child.Id); Assert.AreEqual(obj.Child.Name, result.Child.Name); } [TestMethod] public void Serialize_List_RoundTrip() { var list = new List { 1, 2, 3, 4, 5 }; var binary = list.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); CollectionAssert.AreEqual(list, result); } [TestMethod] public void Serialize_ObjectWithList_RoundTrip() { var obj = new TestClassWithList { Id = 1, Items = new List { "Item1", "Item2", "Item3" } }; var binary = obj.ToBinary(); var result = binary.BinaryTo(); Assert.IsNotNull(result); Assert.AreEqual(obj.Id, result.Id); Assert.IsNotNull(result.Items); CollectionAssert.AreEqual(obj.Items, result.Items); } [TestMethod] public void Serialize_Dictionary_RoundTrip() { var dict = new Dictionary { ["one"] = 1, ["two"] = 2, ["three"] = 3 }; var binary = dict.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(dict.Count, result.Count); foreach (var kvp in dict) { Assert.IsTrue(result.ContainsKey(kvp.Key)); Assert.AreEqual(kvp.Value, result[kvp.Key]); } } #endregion #region Populate Tests [TestMethod] public void Populate_UpdatesExistingObject() { var target = new TestSimpleClass { Id = 0, Name = "Original" }; var source = new TestSimpleClass { Id = 42, Name = "Updated", Value = 3.14 }; var binary = source.ToBinary(); binary.BinaryTo(target); Assert.AreEqual(42, target.Id); Assert.AreEqual("Updated", target.Name); Assert.AreEqual(3.14, target.Value); } [TestMethod] public void PopulateMerge_MergesNestedObjects() { var target = new TestNestedClass { Id = 1, Name = "Original", Child = new TestSimpleClass { Id = 10, Name = "OriginalChild", Value = 1.0 } }; var source = new TestNestedClass { Id = 2, Name = "Updated", Child = new TestSimpleClass { Id = 20, Name = "UpdatedChild", Value = 2.0 } }; var binary = source.ToBinary(); binary.BinaryToMerge(target); Assert.AreEqual(2, target.Id); Assert.AreEqual("Updated", target.Name); Assert.IsNotNull(target.Child); // Child object should be merged, not replaced Assert.AreEqual(20, target.Child.Id); Assert.AreEqual("UpdatedChild", target.Child.Name); } #endregion #region String Interning Tests [TestMethod] public void Serialize_RepeatedStrings_UsesInterning() { var obj = new TestClassWithRepeatedStrings { Field1 = "Repeated", Field2 = "Repeated", Field3 = "Repeated", Field4 = "Unique" }; var binaryWithInterning = AcBinarySerializer.Serialize(obj, AcBinarySerializerOptions.Default); var binaryWithoutInterning = AcBinarySerializer.Serialize(obj, new AcBinarySerializerOptions { UseStringInterning = false }); // With interning should be smaller Assert.IsTrue(binaryWithInterning.Length < binaryWithoutInterning.Length, $"With interning: {binaryWithInterning.Length}, Without: {binaryWithoutInterning.Length}"); // Both should deserialize correctly var result1 = AcBinaryDeserializer.Deserialize(binaryWithInterning); var result2 = AcBinaryDeserializer.Deserialize(binaryWithoutInterning); Assert.AreEqual(obj.Field1, result1!.Field1); Assert.AreEqual(obj.Field1, result2!.Field1); } /// /// REGRESSION TEST: Comprehensive string interning edge cases. /// /// Production bug pattern: "Invalid interned string index: X. Interned strings count: Y" /// /// Root causes identified: /// 1. Property names not being registered in intern table during deserialization /// 2. String values with same length but different content /// 3. Nested objects creating complex interning order /// 4. Collections of objects with repeated property names /// [TestMethod] public void StringInterning_PropertyNames_MustBeRegisteredDuringDeserialization() { // This test verifies that property names (>= 4 chars) are properly // registered in the intern table during deserialization. // The serializer registers them via WriteString, so deserializer must too. var items = Enumerable.Range(0, 10).Select(i => new TestClassWithLongPropertyNames { FirstProperty = $"Value1_{i}", SecondProperty = $"Value2_{i}", ThirdProperty = $"Value3_{i}", FourthProperty = $"Value4_{i}" }).ToList(); var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(10, result.Count); for (int i = 0; i < 10; i++) { Assert.AreEqual($"Value1_{i}", result[i].FirstProperty); Assert.AreEqual($"Value2_{i}", result[i].SecondProperty); } } [TestMethod] public void StringInterning_MixedShortAndLongStrings_HandledCorrectly() { // Short strings (< 4 chars) are NOT interned // Long strings (>= 4 chars) ARE interned // This creates different traversal patterns var items = Enumerable.Range(0, 20).Select(i => new TestClassWithMixedStrings { Id = i, ShortName = $"A{i % 3}", // 2-3 chars, NOT interned LongName = $"LongName_{i % 5}", // > 4 chars, interned Description = $"Description_value_{i % 7}", // > 4 chars, interned Tag = i % 2 == 0 ? "AB" : "XY" // 2 chars, NOT interned }).ToList(); var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(20, result.Count); for (int i = 0; i < 20; i++) { Assert.AreEqual(i, result[i].Id); Assert.AreEqual($"A{i % 3}", result[i].ShortName); Assert.AreEqual($"LongName_{i % 5}", result[i].LongName); Assert.AreEqual($"Description_value_{i % 7}", result[i].Description); } } [TestMethod] public void StringInterning_DeeplyNestedObjects_MaintainsCorrectOrder() { // Complex nested structure where property names and values // are interleaved in a specific order var root = new TestNestedStructure { RootName = "RootObject", Level1Items = Enumerable.Range(0, 5).Select(i => new TestLevel1 { Level1Name = $"Level1_{i}", Level2Items = Enumerable.Range(0, 3).Select(j => new TestLevel2 { Level2Name = $"Level2_{i}_{j}", Value = $"Value_{i * 3 + j}" }).ToList() }).ToList() }; var binary = root.ToBinary(); var result = binary.BinaryTo(); Assert.IsNotNull(result); Assert.AreEqual("RootObject", result.RootName); Assert.AreEqual(5, result.Level1Items.Count); for (int i = 0; i < 5; i++) { Assert.AreEqual($"Level1_{i}", result.Level1Items[i].Level1Name); Assert.AreEqual(3, result.Level1Items[i].Level2Items.Count); for (int j = 0; j < 3; j++) { Assert.AreEqual($"Level2_{i}_{j}", result.Level1Items[i].Level2Items[j].Level2Name); } } } [TestMethod] public void StringInterning_RepeatedPropertyValuesAcrossObjects_UsesReferences() { // When the same string value appears multiple times, // the serializer writes StringInterned reference instead of the full string. // The deserializer must look up the correct index. var items = Enumerable.Range(0, 50).Select(i => new TestClassWithRepeatedValues { Id = i, Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed", Category = i % 5 == 0 ? "CategoryA" : i % 5 == 1 ? "CategoryB" : "CategoryC", Priority = i % 2 == 0 ? "High" : "Low_Priority_Value" }).ToList(); var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(50, result.Count); for (int i = 0; i < 50; i++) { Assert.AreEqual(i, result[i].Id); var expectedStatus = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed"; Assert.AreEqual(expectedStatus, result[i].Status, $"Status mismatch at index {i}"); } } [TestMethod] public void StringInterning_ManyUniqueStringsFollowedByRepeats_CorrectIndexLookup() { // First create many unique strings (all get registered) // Then repeat some of them (use StringInterned references) // This tests the index calculation var items = new List(); // First 30 items with unique names (all registered as new) for (int i = 0; i < 30; i++) { items.Add(new TestClassWithNameValue { Name = $"UniqueName_{i:D4}", Value = $"UniqueValue_{i:D4}" }); } // Next 20 items reuse names from first batch (should use StringInterned) for (int i = 0; i < 20; i++) { items.Add(new TestClassWithNameValue { Name = $"UniqueName_{i % 10:D4}", // Reuse first 10 names Value = $"UniqueValue_{(i + 10) % 30:D4}" // Reuse different values }); } var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(50, result.Count); // Verify first batch for (int i = 0; i < 30; i++) { Assert.AreEqual($"UniqueName_{i:D4}", result[i].Name, $"Name mismatch at index {i}"); Assert.AreEqual($"UniqueValue_{i:D4}", result[i].Value, $"Value mismatch at index {i}"); } // Verify second batch (reused strings) for (int i = 0; i < 20; i++) { Assert.AreEqual($"UniqueName_{i % 10:D4}", result[30 + i].Name, $"Name mismatch at index {30 + i}"); } } [TestMethod] public void StringInterning_EmptyAndNullStrings_DoNotAffectInternTable() { // Empty strings use StringEmpty type code // Null strings use Null type code // Neither should affect intern table indices var items = new List(); for (int i = 0; i < 25; i++) { items.Add(new TestClassWithNullableStrings { Id = i, RequiredName = $"Required_{i:D3}", OptionalName = i % 3 == 0 ? null : i % 3 == 1 ? "" : $"Optional_{i:D3}", Description = i % 2 == 0 ? $"Description_{i % 5:D3}" : null }); } var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result); Assert.AreEqual(25, result.Count); for (int i = 0; i < 25; i++) { Assert.AreEqual(i, result[i].Id); Assert.AreEqual($"Required_{i:D3}", result[i].RequiredName); if (i % 3 == 0) { Assert.IsNull(result[i].OptionalName, $"OptionalName at index {i} should be null"); } else if (i % 3 == 1) { // Empty string may deserialize as either "" or null depending on implementation Assert.IsTrue(string.IsNullOrEmpty(result[i].OptionalName), $"OptionalName at index {i} should be null or empty, got: '{result[i].OptionalName}'"); } else { Assert.AreEqual($"Optional_{i:D3}", result[i].OptionalName, $"OptionalName at index {i} mismatch"); } } } [TestMethod] public void StringInterning_ProductionLikeCustomerDto_RoundTrip() { // Simulate the CustomerDto structure that causes production issues // Key characteristics: // - Many string properties (FirstName, LastName, Email, Company, etc.) // - GenericAttributes list with repeated Key values // - List of items with common status/category values var customers = Enumerable.Range(0, 25).Select(i => new TestCustomerLikeDto { Id = i, FirstName = $"FirstName_{i % 10}", // 10 unique values LastName = $"LastName_{i % 8}", // 8 unique values Email = $"user{i}@example.com", // All unique Company = $"Company_{i % 5}", // 5 unique values Department = i % 3 == 0 ? "Sales" : i % 3 == 1 ? "Engineering" : "Marketing", Role = i % 4 == 0 ? "Admin" : i % 4 == 1 ? "User" : i % 4 == 2 ? "Manager" : "Guest", Status = i % 2 == 0 ? "Active" : "Inactive", Attributes = new List { new() { Key = "DateOfReceipt", Value = $"{i % 28 + 1}/24/2025 00:27:00" }, new() { Key = "Priority", Value = (i % 5).ToString() }, new() { Key = "CustomField1", Value = $"CustomValue_{i % 7}" }, new() { Key = "CustomField2", Value = i % 2 == 0 ? "Yes" : "No_Value" } } }).ToList(); var binary = customers.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result, "Result should not be null - deserialization failed"); Assert.AreEqual(25, result.Count); for (int i = 0; i < 25; i++) { Assert.AreEqual(i, result[i].Id, $"Id mismatch at index {i}"); Assert.AreEqual($"FirstName_{i % 10}", result[i].FirstName, $"FirstName mismatch at index {i}"); Assert.AreEqual($"LastName_{i % 8}", result[i].LastName, $"LastName mismatch at index {i}"); Assert.AreEqual($"user{i}@example.com", result[i].Email, $"Email mismatch at index {i}"); Assert.AreEqual(4, result[i].Attributes.Count, $"Attributes count mismatch at index {i}"); Assert.AreEqual("DateOfReceipt", result[i].Attributes[0].Key); Assert.AreEqual("Priority", result[i].Attributes[1].Key); } } [TestMethod] public void StringInterning_LargeListWithHighStringReuse_HandlesCorrectly() { // Large dataset (100+ items) with high string reuse ratio // This is the scenario that triggers production bugs const int itemCount = 150; var items = Enumerable.Range(0, itemCount).Select(i => new TestHighReuseDto { Id = i, // Property names are reused 150 times (once per object) CategoryCode = $"CAT_{i % 10:D2}", // 10 unique values, 15x reuse each StatusCode = $"STATUS_{i % 5:D2}", // 5 unique values, 30x reuse each TypeCode = $"TYPE_{i % 3:D2}", // 3 unique values, 50x reuse each PriorityCode = i % 2 == 0 ? "PRIORITY_HIGH" : "PRIORITY_LOW", // 2 values, 75x each UniqueField = $"UNIQUE_{i:D4}" // All unique, no reuse }).ToList(); var binary = items.ToBinary(); var result = binary.BinaryTo>(); Assert.IsNotNull(result, "Result should not be null"); Assert.AreEqual(itemCount, result.Count, $"Expected {itemCount} items"); // Verify every item for (int i = 0; i < itemCount; i++) { Assert.AreEqual(i, result[i].Id, $"Id mismatch at {i}"); Assert.AreEqual($"CAT_{i % 10:D2}", result[i].CategoryCode, $"CategoryCode mismatch at {i}"); Assert.AreEqual($"STATUS_{i % 5:D2}", result[i].StatusCode, $"StatusCode mismatch at {i}"); Assert.AreEqual($"TYPE_{i % 3:D2}", result[i].TypeCode, $"TypeCode mismatch at {i}"); Assert.AreEqual($"UNIQUE_{i:D4}", result[i].UniqueField, $"UniqueField mismatch at {i}"); } } #endregion #region Test Models private class TestSimpleClass { public int Id { get; set; } public string Name { get; set; } = ""; public double Value { get; set; } public bool IsActive { get; set; } } private class TestNestedClass { public int Id { get; set; } public string Name { get; set; } = ""; public TestSimpleClass? Child { get; set; } } private class TestClassWithList { public int Id { get; set; } public List Items { get; set; } = new(); } private class TestClassWithRepeatedStrings { public string Field1 { get; set; } = ""; public string Field2 { get; set; } = ""; public string Field3 { get; set; } = ""; public string Field4 { get; set; } = ""; } // New test models for string interning edge cases private class TestClassWithLongPropertyNames { public string FirstProperty { get; set; } = ""; public string SecondProperty { get; set; } = ""; public string ThirdProperty { get; set; } = ""; public string FourthProperty { get; set; } = ""; } private class TestClassWithMixedStrings { public int Id { get; set; } public string ShortName { get; set; } = ""; // < 4 chars public string LongName { get; set; } = ""; // >= 4 chars public string Description { get; set; } = ""; // >= 4 chars public string Tag { get; set; } = ""; // < 4 chars } private class TestNestedStructure { public string RootName { get; set; } = ""; public List Level1Items { get; set; } = new(); } private class TestLevel1 { public string Level1Name { get; set; } = ""; public List Level2Items { get; set; } = new(); } private class TestLevel2 { public string Level2Name { get; set; } = ""; public string Value { get; set; } = ""; } private class TestClassWithRepeatedValues { public int Id { get; set; } public string Status { get; set; } = ""; public string Category { get; set; } = ""; public string Priority { get; set; } = ""; } private class TestClassWithNameValue { public string Name { get; set; } = ""; public string Value { get; set; } = ""; } private class TestClassWithNullableStrings { public int Id { get; set; } public string RequiredName { get; set; } = ""; public string? OptionalName { get; set; } public string? Description { get; set; } } private class TestCustomerLikeDto { public int Id { get; set; } public string FirstName { get; set; } = ""; public string LastName { get; set; } = ""; public string Email { get; set; } = ""; public string Company { get; set; } = ""; public string Department { get; set; } = ""; public string Role { get; set; } = ""; public string Status { get; set; } = ""; public List Attributes { get; set; } = new(); } private class TestGenericAttribute { public string Key { get; set; } = ""; public string Value { get; set; } = ""; } private class TestHighReuseDto { public int Id { get; set; } public string CategoryCode { get; set; } = ""; public string StatusCode { get; set; } = ""; public string TypeCode { get; set; } = ""; public string PriorityCode { get; set; } = ""; public string UniqueField { get; set; } = ""; } #endregion #region Benchmark Order Tests [TestMethod] public void Serialize_BenchmarkOrder_RoundTrip() { // This is the exact same data that causes stack overflow in benchmarks var order = TestDataFactory.CreateBenchmarkOrder( itemCount: 3, palletsPerItem: 2, measurementsPerPallet: 2, pointsPerMeasurement: 5); // Should not throw stack overflow var binary = AcBinarySerializer.Serialize(order); Assert.IsTrue(binary.Length > 0, "Binary data should not be empty"); var result = AcBinaryDeserializer.Deserialize(binary); Assert.IsNotNull(result); Assert.AreEqual(order.Id, result.Id); Assert.AreEqual(order.OrderNumber, result.OrderNumber); Assert.AreEqual(order.Items.Count, result.Items.Count); } [TestMethod] public void Serialize_BenchmarkOrder_SmallData_RoundTrip() { // Smaller test to isolate the issue var order = TestDataFactory.CreateBenchmarkOrder( itemCount: 1, palletsPerItem: 1, measurementsPerPallet: 1, pointsPerMeasurement: 1); var binary = AcBinarySerializer.Serialize(order); Assert.IsTrue(binary.Length > 0); var result = AcBinaryDeserializer.Deserialize(binary); Assert.IsNotNull(result); Assert.AreEqual(order.Id, result.Id); } #endregion }