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;

/// <summary>
/// Focused repro tests for <c>MaxDepthBehavior.Truncate</c> on cyclic graphs.
///
/// Tracks <c>BINARY_ISSUES.md#accore-bin-i-t7k3</c>: SGen path produces wire-misalignment
/// (<c>DECIMAL_DRIFT</c> 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 <c>MaxDepth=5</c>,
/// single cycle property. Step through `WriteObjectFullMarkerIId` to compare runtime
/// vs SGen call sequences at the truncation boundary.
/// </summary>
[TestClass]
public class AcBinarySerializerMaxDepthTruncateTests
{
    private const int MaxDepthForTest = 5;

    /// <summary>
    /// Builds the minimal cyclic graph used by most tests below.
    /// Cycle: <c>order → Items[0] → ParentOrder → order → …</c>.
    /// Only primitive properties on the leaf entities so the body is short and the wire is easy to diff.
    /// </summary>
    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;
    }

    /// <summary>
    /// Builds the cyclic graph PLUS sets the polymorphic <c>Parent</c> property (declared <c>object?</c>)
    /// to a non-IId concrete instance — mirroring the failing SameInstance test's setup for None mode.
    /// This routes through <see cref="WriteValueNonPrimitiveWithWrapperPoly"/> →
    /// <see cref="WriteObjectPolymorphic"/> on every cycle level (Parent is written at every TestOrder body).
    /// </summary>
    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;
    }

    /// <summary>
    /// Diagnostic helper: dump the wire bytes as hex for visual comparison.
    /// Useful in the debugger to spot the runtime-vs-SGen wire diff.
    /// </summary>
    private static void DumpWire(string label, byte[] wire)
    {
        Console.WriteLine($"=== {label} | {wire.Length} bytes ===");
        Console.WriteLine(BitConverter.ToString(wire));
        Console.WriteLine();
    }

    /// <summary>
    /// CONTROL TEST — runtime path with Truncate. Should pass.
    /// If this fails, the bug is broader than the SGen path; fix here first.
    /// </summary>
    [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<TestOrder_Circ_Ref>();

        // 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".
    }

    /// <summary>
    /// BUG REPRO — SGen path with Truncate. Currently fails with <c>DECIMAL_DRIFT</c>
    /// on round-trip. Same input as the runtime control above; only <c>UseGeneratedCode</c> differs.
    /// </summary>
    /// <remarks>
    /// Step through with the VS debugger:
    /// 1. Break in <c>WriteObjectFullMarkerIId</c> for both runs (runtime test above + this).
    /// 2. Compare <c>_position</c>, <c>_recursionDepth</c>, 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:
    ///    - <c>TryEnterRecursion</c> inc/dec balance on the SGen-emit code path
    ///    - <c>WriteObjectFullMarkerIId</c> ref-handling branches (2nd-occurrence ExitRecursion undo)
    ///    - SGen-emitted property-loop ordering vs runtime <c>WritePropertiesMarkerless</c>
    /// </remarks>
    [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<TestOrder_Circ_Ref>();

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

    /// <summary>
    /// 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.
    /// </summary>
    [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<TestOrder_Circ_Ref>();

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

    /// <summary>
    /// CONTROL — runtime + polymorphic Parent. Should pass.
    /// Compared to <see cref="Sgen_None_Truncate_PolymorphicCycle_RoundTrips"/> below: same data, different code path.
    /// </summary>
    [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<TestOrder_Circ_Ref>();

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

    /// <summary>
    /// REPRO — SGen + polymorphic Parent inside a cycle. This is the failing case in the
    /// original SameInstance test for (useSgen=true, useMeta=false) None mode.
    /// </summary>
    /// <remarks>
    /// The cycle (Items[0].ParentOrder = order) makes <c>TestOrder.WriteProperties</c> recurse.
    /// At each cycle level, the body writes its <c>Parent</c> property polymorphically via
    /// <c>WriteValueNonPrimitiveWithWrapperPoly</c> → <c>WriteObjectPolymorphic</c>. When the
    /// cycle reaches <c>MaxDepth</c>, the SGen path produces wire bytes that the SGen reader
    /// later mis-interprets (<c>DECIMAL_DRIFT</c> 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:
    /// - <see cref="AcBinarySerializer.WriteObjectPolymorphic"/> Truncate path (Null written)
    /// - <see cref="AcBinarySerializer.BinarySerializationContext{TOutput}.TryEnterRecursion"/> inc/dec balance
    /// - The polymorphic-prefix wire bytes (FixObj-slot vs ObjectWithTypeName) immediately before/after the truncate
    /// </remarks>
    [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<TestOrder_Circ_Ref>();

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

    /// <summary>
    /// 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.
    /// </summary>
    [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<TestOrder_Circ_Ref>();

        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".
    }
}
