using AyCode.Core.Extensions;
using static AyCode.Core.Tests.Serialization.AcSerializerModels;
namespace AyCode.Core.Tests.Serialization;
///
/// Tests for navigation property serialization issues.
///
/// CRITICAL BUG REPRODUCTION:
/// When a navigation property (like StockTakingItem.Product) is populated,
/// the serializer writes properties of the navigation target (Product),
/// but these property names were NOT registered in the metadata header!
///
/// The bug pattern:
/// 1. RegisterMetadataForType walks List<StockTakingItem> and registers StockTakingItem properties
/// 2. StockTakingItem has a "Product" property of type Product - this property NAME is registered
/// 3. BUT Product's own properties (Name, Description, Price, CategoryId) are NOT registered!
/// 4. When Product is NOT NULL at runtime, WriteObject writes Product's property indices
/// 5. GetPropertyNameIndex returns NEW indices that weren't in the header!
/// 6. Deserializer reads property indices that don't exist in its table ? crash/type mismatch
///
[TestClass]
public class AcBinarySerializerNavigationPropertyTests
{
///
/// CRITICAL REGRESSION TEST: Navigation properties causing metadata mismatch.
/// This is the EXACT production scenario:
/// - StockTakingItem.Product is populated by the database query
/// - Product's properties are serialized with wrong indices
/// - Deserializer fails with type mismatch
///
[TestMethod]
public void Deserialize_NavigationPropertyPopulated_MetadataIncludesNestedType()
{
var parent = new ParentWithNavigatingItems
{
Id = 1,
Name = "Parent",
Creator = 6, // The exact value from production error
Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
Modified = DateTime.UtcNow,
Items = new List
{
new()
{
Id = 10,
ParentId = 1,
ProductId = 100,
IsMeasured = true,
Quantity = 50,
Created = DateTime.UtcNow.AddHours(-1),
Modified = DateTime.UtcNow,
// Navigation property IS populated - this is the key!
Product = new ProductEntity
{
Id = 100,
Name = "TestProduct",
Description = "Product description with long text",
Price = 99.99,
CategoryId = 5,
Created = DateTime.UtcNow.AddDays(-30)
}
}
}
};
var binary = parent.ToBinary();
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual(6, result.Creator, "Creator should be 6");
Assert.AreEqual(parent.Created.Ticks, result.Created.Ticks,
$"Created mismatch. Expected: {parent.Created}, Got: {result.Created}");
Assert.IsNotNull(result.Items);
Assert.AreEqual(1, result.Items.Count);
var item = result.Items[0];
Assert.AreEqual(10, item.Id);
Assert.AreEqual(100, item.ProductId);
// Navigation property should be deserialized correctly
Assert.IsNotNull(item.Product, "Product navigation property should not be null");
Assert.AreEqual(100, item.Product.Id);
Assert.AreEqual("TestProduct", item.Product.Name);
Assert.AreEqual(5, item.Product.CategoryId);
}
///
/// Test with multiple items, some with Product populated, some without.
/// This creates a mixed scenario where some items have navigation properties.
///
[TestMethod]
public void Deserialize_MixedNavigationProperties_AllItemsCorrect()
{
var parent = new ParentWithNavigatingItems
{
Id = 1,
Name = "Parent",
Creator = 6,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Items = Enumerable.Range(1, 5).Select(i => new ItemWithNavigationProperty
{
Id = i * 10,
ParentId = 1,
ProductId = 100 + i,
IsMeasured = i % 2 == 0,
Quantity = i * 10,
Created = DateTime.UtcNow.AddHours(-i),
Modified = DateTime.UtcNow,
// Only populate Product for even items
Product = i % 2 == 0 ? new ProductEntity
{
Id = 100 + i,
Name = $"Product_{i}",
Description = $"Description for product {i}",
Price = i * 10.5,
CategoryId = i % 3,
Created = DateTime.UtcNow.AddDays(-i)
} : null
}).ToList()
};
var binary = parent.ToBinary();
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual(6, result.Creator);
Assert.IsNotNull(result.Items);
Assert.AreEqual(5, result.Items.Count);
for (int i = 1; i <= 5; i++)
{
var item = result.Items[i - 1];
Assert.AreEqual(i * 10, item.Id, $"Item {i} Id mismatch");
if (i % 2 == 0)
{
Assert.IsNotNull(item.Product, $"Item {i} should have Product");
Assert.AreEqual($"Product_{i}", item.Product.Name);
}
else
{
Assert.IsNull(item.Product, $"Item {i} should not have Product");
}
}
}
///
/// Test with list of parents, each with items with navigation properties.
/// This is the exact production scenario - multiple StockTaking entities
/// each with StockTakingItems that have Product navigation properties.
///
[TestMethod]
public void Deserialize_ListOfParentsWithNavigationProperties_AllCorrect()
{
var parents = Enumerable.Range(1, 3).Select(p => new ParentWithNavigatingItems
{
Id = p,
Name = $"Parent_{p}",
Creator = p,
Created = DateTime.UtcNow.AddDays(-p),
Modified = DateTime.UtcNow,
Items = Enumerable.Range(1, 2).Select(i => new ItemWithNavigationProperty
{
Id = p * 100 + i,
ParentId = p,
ProductId = 1000 + i,
IsMeasured = true,
Quantity = 10 * i,
Created = DateTime.UtcNow.AddHours(-i),
Modified = DateTime.UtcNow,
Product = new ProductEntity
{
Id = 1000 + i,
Name = $"Product_{p}_{i}",
Description = $"Description {p}_{i}",
Price = (p * 10) + (i * 1.5),
CategoryId = i % 3,
Created = DateTime.UtcNow.AddDays(-10)
}
}).ToList()
}).ToList();
var binary = parents.ToBinary();
var result = binary.BinaryTo>();
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Count);
for (int p = 0; p < 3; p++)
{
var parent = result[p];
Assert.AreEqual(p + 1, parent.Id, $"Parent[{p}].Id mismatch");
Assert.AreEqual(p + 1, parent.Creator, $"Parent[{p}].Creator mismatch");
Assert.IsNotNull(parent.Items);
Assert.AreEqual(2, parent.Items.Count);
for (int i = 0; i < 2; i++)
{
var item = parent.Items[i];
Assert.IsNotNull(item.Product, $"Parent[{p}].Items[{i}].Product should not be null");
Assert.AreEqual($"Product_{p + 1}_{i + 1}", item.Product.Name);
}
}
}
///
/// Test deeply nested navigation properties.
/// Product has a Category, Category has a Parent, etc.
///
[TestMethod]
public void Deserialize_DeeplyNestedNavigationProperties_AllCorrect()
{
// This tests that the serializer correctly handles navigation properties
// even when they are deeply nested (Product -> Category -> Parent)
var parent = new ParentWithNavigatingItems
{
Id = 1,
Name = "Parent",
Creator = 6,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Items = new List
{
new()
{
Id = 10,
ParentId = 1,
ProductId = 100,
IsMeasured = true,
Quantity = 50,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Product = new ProductEntity
{
Id = 100,
Name = "ProductWithDetails",
Description = "Very long description that should be interned",
Price = 123.45,
CategoryId = 10,
Created = DateTime.UtcNow.AddMonths(-6)
}
},
new()
{
Id = 20,
ParentId = 1,
ProductId = 200,
IsMeasured = false,
Quantity = 25,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Product = new ProductEntity
{
Id = 200,
Name = "AnotherProduct",
Description = "Another description",
Price = 67.89,
CategoryId = 20,
Created = DateTime.UtcNow.AddMonths(-3)
}
}
}
};
var binary = parent.ToBinary();
// Log binary size for debugging
Console.WriteLine($"Binary size: {binary.Length} bytes");
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual(6, result.Creator);
Assert.IsNotNull(result.Items);
Assert.AreEqual(2, result.Items.Count);
// First item
Assert.AreEqual(10, result.Items[0].Id);
Assert.IsNotNull(result.Items[0].Product);
Assert.AreEqual("ProductWithDetails", result.Items[0].Product.Name);
Assert.AreEqual(123.45, result.Items[0].Product.Price);
// Second item
Assert.AreEqual(20, result.Items[1].Id);
Assert.IsNotNull(result.Items[1].Product);
Assert.AreEqual("AnotherProduct", result.Items[1].Product.Name);
Assert.AreEqual(67.89, result.Items[1].Product.Price);
}
///
/// Test with same Product instance referenced multiple times.
/// This tests the reference handling with navigation properties.
///
[TestMethod]
public void Deserialize_SharedNavigationProperty_ReferencesPreserved()
{
// Create a shared Product that is referenced by multiple items
var sharedProduct = new ProductEntity
{
Id = 999,
Name = "SharedProduct",
Description = "This product is shared across items",
Price = 50.00,
CategoryId = 1,
Created = DateTime.UtcNow.AddYears(-1)
};
var parent = new ParentWithNavigatingItems
{
Id = 1,
Name = "Parent",
Creator = 6,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Items = new List
{
new()
{
Id = 10,
ParentId = 1,
ProductId = 999,
IsMeasured = true,
Quantity = 50,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Product = sharedProduct // Same reference
},
new()
{
Id = 20,
ParentId = 1,
ProductId = 999,
IsMeasured = false,
Quantity = 75,
Created = DateTime.UtcNow,
Modified = DateTime.UtcNow,
Product = sharedProduct // Same reference
}
}
};
var binary = parent.ToBinary();
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.IsNotNull(result.Items);
Assert.AreEqual(2, result.Items.Count);
// Both items should have the same Product values
Assert.IsNotNull(result.Items[0].Product);
Assert.IsNotNull(result.Items[1].Product);
Assert.AreEqual(999, result.Items[0].Product.Id);
Assert.AreEqual(999, result.Items[1].Product.Id);
Assert.AreEqual("SharedProduct", result.Items[0].Product.Name);
Assert.AreEqual("SharedProduct", result.Items[1].Product.Name);
// With reference handling, they should be the same instance
// (This depends on UseReferenceHandling being enabled)
Console.WriteLine($"Same instance: {ReferenceEquals(result.Items[0].Product, result.Items[1].Product)}");
}
}