using System;
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;
///
/// Focused repro tests for MaxDepthBehavior.Truncate on cyclic graphs.
///
/// Tracks BINARY_ISSUES.md#accore-bin-i-t7k3: SGen path produces wire-misalignment
/// (DECIMAL_DRIFT on round-trip) when Truncate fires inside a cycle. Runtime path
/// works correctly with the same data — that's the control test for diagnosis.
///
/// Designed for interactive debugger sessions: minimal graph, small MaxDepth=5,
/// single cycle property. Step through `WriteObjectFullMarkerIId` to compare runtime
/// vs SGen call sequences at the truncation boundary.
///
[TestClass]
public class AcBinarySerializerMaxDepthTruncateTests
{
private const int MaxDepthForTest = 5;
///
/// Builds the minimal cyclic graph used by most tests below.
/// Cycle: order → Items[0] → ParentOrder → order → ….
/// Only primitive properties on the leaf entities so the body is short and the wire is easy to diff.
///
private static TestOrder_Circ_Ref BuildMinimalCycle()
{
var order = new TestOrder_Circ_Ref
{
Id = 1,
OrderNumber = "TEST-001",
Items =
[
new TestOrderItem_Circ_Ref
{
Id = 10,
ProductName = "Product-A",
Quantity = 5
}
]
};
order.Items[0].ParentOrder = order; // ← closes the cycle
return order;
}
///
/// Builds the cyclic graph PLUS sets the polymorphic Parent property (declared object?)
/// to a non-IId concrete instance — mirroring the failing SameInstance test's setup for None mode.
/// This routes through →
/// on every cycle level (Parent is written at every TestOrder body).
///
private static TestOrder_Circ_Ref BuildCycleWithPolymorphicParent()
{
var order = BuildMinimalCycle();
// Parent is `object?` on TestOrder_Circ_Ref — polymorphic write path.
// UserPreferences_All_True is non-IId, leaf-like (Language, LightTheme strings + scalars), no further refs.
order.Parent = new UserPreferences_All_True { Language = "en-US", Theme = "light" };
return order;
}
///
/// Diagnostic helper: dump the wire bytes as hex for visual comparison.
/// Useful in the debugger to spot the runtime-vs-SGen wire diff.
///
private static void DumpWire(string label, byte[] wire)
{
Console.WriteLine($"=== {label} | {wire.Length} bytes ===");
Console.WriteLine(BitConverter.ToString(wire));
Console.WriteLine();
}
///
/// CONTROL TEST — runtime path with Truncate. Should pass.
/// If this fails, the bug is broader than the SGen path; fix here first.
///
[TestMethod]
public void Runtime_None_Truncate_CyclicGraph_RoundTrips()
{
var order = BuildMinimalCycle();
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = false, // ← runtime path
UseMetadata = false,
MaxDepth = MaxDepthForTest,
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
// Act
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire("Runtime + Truncate", binary);
var result = binary.BinaryTo();
// Assert: serialize+deserialize succeeds; root + first-level data intact;
// ParentOrder is null at the truncation boundary (instead of a full cycle round-trip).
Assert.IsNotNull(result, "Deserialize result should not be null");
Assert.AreEqual(1, result.Id, "root Id");
Assert.AreEqual("TEST-001", result.OrderNumber, "root OrderNumber");
Assert.IsNotNull(result.Items, "Items list should be materialized");
Assert.AreEqual(1, result.Items.Count, "Items count");
Assert.AreEqual(10, result.Items[0].Id, "Item Id");
Assert.AreEqual("Product-A", result.Items[0].ProductName, "Item ProductName");
Assert.AreEqual(5, result.Items[0].Quantity, "Item Quantity");
// ParentOrder may or may not be set depending on where truncation fires —
// the contract is "the deserialize must not throw and root-level data must be intact".
}
///
/// BUG REPRO — SGen path with Truncate. Currently fails with DECIMAL_DRIFT
/// on round-trip. Same input as the runtime control above; only UseGeneratedCode differs.
///
///
/// Step through with the VS debugger:
/// 1. Break in WriteObjectFullMarkerIId for both runs (runtime test above + this).
/// 2. Compare _position, _recursionDepth, and the wire-bytes-just-written at each
/// call-site between the two paths.
/// 3. Identify the byte position where the SGen wire diverges from the runtime wire.
/// 4. Likely culprits to inspect:
/// - TryEnterRecursion inc/dec balance on the SGen-emit code path
/// - WriteObjectFullMarkerIId ref-handling branches (2nd-occurrence ExitRecursion undo)
/// - SGen-emitted property-loop ordering vs runtime WritePropertiesMarkerless
///
[TestMethod]
public void Sgen_None_Truncate_CyclicGraph_RoundTrips()
{
var order = BuildMinimalCycle();
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = true, // ← SGen path (triggers the bug)
UseMetadata = false,
MaxDepth = MaxDepthForTest,
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
// Act
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire("SGen + Truncate", binary);
var result = binary.BinaryTo();
// Assert: same as runtime control. Currently throws DECIMAL_DRIFT.
Assert.IsNotNull(result, "Deserialize result should not be null");
Assert.AreEqual(1, result.Id, "root Id");
Assert.AreEqual("TEST-001", result.OrderNumber, "root OrderNumber");
Assert.IsNotNull(result.Items, "Items list should be materialized");
Assert.AreEqual(1, result.Items.Count, "Items count");
Assert.AreEqual(10, result.Items[0].Id, "Item Id");
Assert.AreEqual("Product-A", result.Items[0].ProductName, "Item ProductName");
Assert.AreEqual(5, result.Items[0].Quantity, "Item Quantity");
}
///
/// SGen + Truncate + useMetadata=true variant. Also currently fails (multi-byte marker variant
/// of the same underlying issue). Useful for the debug session to confirm whether the fix
/// also covers the metadata code path or just the simple Object-marker path.
///
[TestMethod]
public void Sgen_None_Truncate_UseMetadata_CyclicGraph_RoundTrips()
{
var order = BuildMinimalCycle();
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = true,
UseMetadata = true, // ← multi-byte marker (ObjectWithMetadata + inline meta)
MaxDepth = MaxDepthForTest,
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
// Act
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire("SGen + Truncate + useMetadata", binary);
var result = binary.BinaryTo();
// Assert: same root-level integrity expectation.
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("TEST-001", result.OrderNumber);
Assert.IsNotNull(result.Items);
Assert.AreEqual(1, result.Items.Count);
Assert.AreEqual("Product-A", result.Items[0].ProductName);
}
///
/// CONTROL — runtime + polymorphic Parent. Should pass.
/// Compared to below: same data, different code path.
///
[TestMethod]
public void Runtime_None_Truncate_PolymorphicCycle_RoundTrips()
{
var order = BuildCycleWithPolymorphicParent();
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = false,
UseMetadata = false,
MaxDepth = MaxDepthForTest,
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire("Runtime + Truncate + PolymorphicParent", binary);
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("TEST-001", result.OrderNumber);
Assert.IsNotNull(result.Items);
Assert.AreEqual(1, result.Items.Count);
// Root-level Parent should round-trip (depth 1 — well below MaxDepth).
Assert.IsNotNull(result.Parent, "Root order.Parent should round-trip — depth 1 < MaxDepth");
Assert.IsInstanceOfType(result.Parent, typeof(UserPreferences_All_True));
}
///
/// REPRO — SGen + polymorphic Parent inside a cycle. This is the failing case in the
/// original SameInstance test for (useSgen=true, useMeta=false) None mode.
///
///
/// The cycle (Items[0].ParentOrder = order) makes TestOrder.WriteProperties recurse.
/// At each cycle level, the body writes its Parent property polymorphically via
/// WriteValueNonPrimitiveWithWrapperPoly → WriteObjectPolymorphic. When the
/// cycle reaches MaxDepth, the SGen path produces wire bytes that the SGen reader
/// later mis-interprets (DECIMAL_DRIFT on TotalAmount at the deepest unwind frame).
/// Runtime control above with the same data works correctly — diff the two wires to find
/// where SGen diverges.
///
/// Focus debug-watch targets at the truncation boundary:
/// - Truncate path (Null written)
/// - inc/dec balance
/// - The polymorphic-prefix wire bytes (FixObj-slot vs ObjectWithTypeName) immediately before/after the truncate
///
[TestMethod]
public void Sgen_None_Truncate_PolymorphicCycle_RoundTrips()
{
var order = BuildCycleWithPolymorphicParent();
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = true, // ← SGen path (triggers the bug)
UseMetadata = false,
MaxDepth = MaxDepthForTest,
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire("SGen + Truncate + PolymorphicParent", binary);
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Id);
Assert.AreEqual("TEST-001", result.OrderNumber);
Assert.IsNotNull(result.Items);
Assert.AreEqual(1, result.Items.Count);
Assert.IsNotNull(result.Parent, "Root order.Parent should round-trip — depth 1 < MaxDepth");
Assert.IsInstanceOfType(result.Parent, typeof(UserPreferences_All_True));
}
///
/// Non-cyclic shallow case — the primary delta-update use case.
/// Serialize an entity with intentionally truncated nested collections (MaxDepth=1),
/// verify root + first-level scalar properties round-trip while nested complex ones become null.
/// Both runtime and SGen paths should pass this. If SGen fails here too, the bug isn't
/// cycle-specific — it's pure Truncate-emission corruption.
///
[TestMethod]
[DataRow(false, DisplayName = "Runtime")]
[DataRow(true, DisplayName = "SGen")]
public void Sgen_Or_Runtime_None_Truncate_NoCycle_ShallowRoundTrip(bool useSgen)
{
var order = new TestOrder_Circ_Ref
{
Id = 42,
OrderNumber = "DELTA-UPDATE-001",
Items =
[
new TestOrderItem_Circ_Ref { Id = 1, ProductName = "P1" },
new TestOrderItem_Circ_Ref { Id = 2, ProductName = "P2" }
]
// No cycle. Items array elements truncate at MaxDepth=1.
};
var options = new AcBinarySerializerOptions
{
ReferenceHandling = ReferenceHandlingMode.None,
UseGeneratedCode = useSgen,
UseMetadata = false,
MaxDepth = 1, // root + 1 level
MaxDepthBehavior = MaxDepthBehavior.Truncate
};
var binary = AcBinarySerializer.Serialize(order, options);
DumpWire($"NoCycle Truncate ({(useSgen ? "SGen" : "Runtime")})", binary);
var result = binary.BinaryTo();
Assert.IsNotNull(result);
Assert.AreEqual(42, result.Id);
Assert.AreEqual("DELTA-UPDATE-001", result.OrderNumber);
// Items elements are at depth 2 — get truncated to null at MaxDepth=1.
// Result.Items list itself should exist (it's at depth 1), but elements should be null.
// The exact element-or-null result is depth-implementation-dependent — the strict invariant
// is "deserialize doesn't throw and root scalars are intact".
}
}