using System.Diagnostics;
using AyCode.Core.Extensions;
using AyCode.Core.Serializers;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Binaries;
using AyCode.Core.Serializers.Jsons;
using AyCode.Core.Tests.TestModels;
using MessagePack;
using MessagePack.Resolvers;

namespace AyCode.Core.Tests.Serialization;

[TestClass]
public class QuickBenchmark
{
    private static readonly MessagePackSerializerOptions MsgPackOptions =
        ContractlessStandardResolver.Options.WithCompression(MessagePackCompression.None);

    private const int DefaultIterations = 10;

    #region Helper Methods

    private static void PrintBanner(string title)
    {
        Console.WriteLine();
        Console.WriteLine(new string('=', 78));
        Console.WriteLine(title);
        Console.WriteLine(new string('=', 78));
    }

    private static void PrintTableHeader(string title)
    {
        PrintBanner(title);
        Console.WriteLine($"{"Metric",-25} | {"AcBinary",14} | {"MessagePack",14} | {"Ratio",14}");
        Console.WriteLine(new string('-', 78));
    }

    private static void PrintTableRow(string metric, double acBinary, double msgPack, string unit = "ms")
    {
        if (msgPack > 0)
        {
            var ratio = acBinary / msgPack;
            var ratioStr = ratio < 1 ? $"{ratio:F2}x faster" : $"{ratio:F2}x slower";
            Console.WriteLine($"{metric,-25} | {acBinary,10:F2} {unit,-2} | {msgPack,10:F2} {unit,-2} | {ratioStr,14}");
        }
        else
        {
            Console.WriteLine($"{metric,-25} | {acBinary,10:F2} {unit,-2} | {"N/A",12} | {"(unique)",14}");
        }
    }

    private static void PrintTableRowSize(string metric, int acBinary, int msgPack)
    {
        var ratio = msgPack == 0 ? 0 : 100.0 * acBinary / msgPack;
        Console.WriteLine($"{metric,-25} | {acBinary,14:N0} | {msgPack,14:N0} | {ratio,12:F1}%");
    }

    private static void PrintTableFooter()
    {
        Console.WriteLine(new string('-', 78));
    }

    private static void PrintSummary(int acBinarySize, int msgPackSize, double acSerMs, double msgSerMs, double acDeserMs, double msgDeserMs)
    {
        PrintBanner("SUMMARY");

        var sizeAdvantage = msgPackSize == 0 ? 0 : 100.0 - (100.0 * acBinarySize / msgPackSize);
        if (sizeAdvantage > 0)
            Console.WriteLine($"[OK] Size: AcBinary is {sizeAdvantage:F1}% smaller ({msgPackSize - acBinarySize:N0} bytes saved)");
        else
            Console.WriteLine($"[WARN] Size: AcBinary is {-sizeAdvantage:F1}% larger");

        var serRatio = msgSerMs == 0 ? 0 : acSerMs / msgSerMs;
        if (serRatio > 0 && serRatio < 1)
            Console.WriteLine($"[OK] Serialize: AcBinary is {1 / serRatio:F2}x faster");
        else if (serRatio > 0)
            Console.WriteLine($"[WARN] Serialize: AcBinary is {serRatio:F2}x slower");

        var deserRatio = msgDeserMs == 0 ? 0 : acDeserMs / msgDeserMs;
        if (deserRatio > 0 && deserRatio < 1)
            Console.WriteLine($"[OK] Deserialize: AcBinary is {1 / deserRatio:F2}x faster");
        else if (deserRatio > 0)
            Console.WriteLine($"[WARN] Deserialize: AcBinary is {deserRatio:F2}x slower");
    }

    private static TestOrder_All_True CreatePopulateTarget(TestOrder_All_True source)
    {
        var target = new TestOrder_All_True { Id = source.Id };
        foreach (var item in source.Items)
        {
            target.Items.Add(new TestOrderItem_All_True { Id = item.Id });
        }
        return target;
    }

    #endregion

    #region Basic Benchmarks

    [TestMethod]
    public void RunQuickBenchmark()
    {
        // Warmup
        var order = TestDataFactory.CreateBenchmarkOrder(2, 2, 2, 3);
        for (int i = 0; i < 10; i++)
        {
            var bytes = order.ToBinary();
            var result = bytes.BinaryTo<TestOrder_All_True>();
        }

        // Measure serialize
        const int iterations = DefaultIterations;
        var sw = Stopwatch.StartNew();
        byte[] serialized = null!;
        for (int i = 0; i < iterations; i++)
        {
            serialized = order.ToBinary();
        }
        sw.Stop();
        var serializeMs = sw.Elapsed.TotalMilliseconds;

        // Measure deserialize
        sw.Restart();
        TestOrder_All_True? deserialized = null;
        for (int i = 0; i < iterations; i++)
        {
            deserialized = serialized.BinaryTo<TestOrder_All_True>();
        }
        sw.Stop();
        var deserializeMs = sw.Elapsed.TotalMilliseconds;

        // JSON comparison
        var jsonOptions = AcJsonSerializerOptions.Default;
        sw.Restart();
        string json = null!;
        for (int i = 0; i < iterations; i++)
        {
            json = order.ToJson(jsonOptions);
        }
        sw.Stop();
        var jsonSerializeMs = sw.Elapsed.TotalMilliseconds;

        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            var _ = json.JsonTo<TestOrder_All_True>(jsonOptions);
        }
        sw.Stop();
        var jsonDeserializeMs = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine($"=== Quick Benchmark ({iterations} iterations) ===");
        Console.WriteLine($"Binary size: {serialized.Length} bytes");
        Console.WriteLine($"JSON size:   {json.Length} chars ({System.Text.Encoding.UTF8.GetByteCount(json)} bytes)");
        Console.WriteLine();
        Console.WriteLine($"Binary Serialize:   {serializeMs:F2}ms ({serializeMs / iterations:F4}ms/op)");
        Console.WriteLine($"Binary Deserialize: {deserializeMs:F2}ms ({deserializeMs / iterations:F4}ms/op)");
        Console.WriteLine($"JSON Serialize:     {jsonSerializeMs:F2}ms ({jsonSerializeMs / iterations:F4}ms/op)");
        Console.WriteLine($"JSON Deserialize:   {jsonDeserializeMs:F2}ms ({jsonDeserializeMs / iterations:F4}ms/op)");
        Console.WriteLine();
        Console.WriteLine($"Binary vs JSON Serialize:   {serializeMs / jsonSerializeMs:F2}x");
        Console.WriteLine($"Binary vs JSON Deserialize: {deserializeMs / jsonDeserializeMs:F2}x");
        Console.WriteLine($"Size ratio: {100.0 * serialized.Length / System.Text.Encoding.UTF8.GetByteCount(json):F1}%");

        Assert.IsNotNull(deserialized);
        Assert.AreEqual(order.Id, deserialized.Id);
    }

    [TestMethod]
    public void RunStringInterningBenchmark()
    {
        // Create data with repeated strings
        var items = Enumerable.Range(0, 100).Select(i => new TestClassWithRepeatedValues
        {
            Id = i,
            Status = i % 3 == 0 ? "Pending" : i % 3 == 1 ? "Processing" : "Completed",
            Category = $"Category_{i % 5}",
            Priority = i % 2 == 0 ? "High_Priority" : "Low_Priority"
        }).ToList();

        // Warmup
        for (int i = 0; i < 10; i++)
        {
            var bytes = items.ToBinary();
            var result = bytes.BinaryTo<List<TestClassWithRepeatedValues>>();
        }

        const int iterations = DefaultIterations;

        // With interning (default)
        var sw = Stopwatch.StartNew();
        byte[] withInterning = null!;
        for (int i = 0; i < iterations; i++)
        {
            withInterning = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
        }
        sw.Stop();
        var withInterningMs = sw.Elapsed.TotalMilliseconds;

        // Without interning
        var noInternOptions = new AcBinarySerializerOptions { UseStringInterning = StringInterningMode.None };
        sw.Restart();
        byte[] withoutInterning = null!;
        for (int i = 0; i < iterations; i++)
        {
            withoutInterning = AcBinarySerializer.Serialize(items, noInternOptions);
        }
        sw.Stop();
        var withoutInterningMs = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine($"=== String Interning Benchmark ({iterations} iterations) ===");
        Console.WriteLine($"With interning:    {withInterning.Length} bytes, {withInterningMs:F2}ms");
        Console.WriteLine($"Without interning: {withoutInterning.Length} bytes, {withoutInterningMs:F2}ms");
        Console.WriteLine($"Size savings:      {withoutInterning.Length - withInterning.Length} bytes ({100.0 * (withoutInterning.Length - withInterning.Length) / withoutInterning.Length:F1}%)");
        Console.WriteLine($"Speed ratio:       {withInterningMs / withoutInterningMs:F2}x");

        // Verify both deserialize correctly
        var result1 = withInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
        var result2 = withoutInterning.BinaryTo<List<TestClassWithRepeatedValues>>();
        Assert.AreEqual(100, result1!.Count);
        Assert.AreEqual(100, result2!.Count);
    }

    #endregion

    #region MessagePack Comparison

    [TestMethod]
    public void RunMessagePackComparison()
    {
        // Create test data
        var order = TestDataFactory.CreateBenchmarkOrder(3, 3, 3, 4);

        // Warmup
        for (int i = 0; i < 20; i++)
        {
            var binBytes = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
            var binResult = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(binBytes);
            var msgBytes = MessagePackSerializer.Serialize(order, MsgPackOptions);
            var msgResult = MessagePackSerializer.Deserialize<TestOrder_All_True>(msgBytes, MsgPackOptions);
        }

        const int iterations = DefaultIterations;

        // === AcBinary Serialize ===
        var sw = Stopwatch.StartNew();
        byte[] acBinaryData = null!;
        for (int i = 0; i < iterations; i++)
        {
            acBinaryData = AcBinarySerializer.Serialize(order, AcBinarySerializerOptions.Default);
        }
        sw.Stop();
        var acBinarySerMs = sw.Elapsed.TotalMilliseconds;

        // === MessagePack Serialize ===
        sw.Restart();
        byte[] msgPackData = null!;
        for (int i = 0; i < iterations; i++)
        {
            msgPackData = MessagePackSerializer.Serialize(order, MsgPackOptions);
        }
        sw.Stop();
        var msgPackSerMs = sw.Elapsed.TotalMilliseconds;

        // === AcBinary Deserialize ===
        sw.Restart();
        TestOrder_All_True? acBinaryResult = null;
        for (int i = 0; i < iterations; i++)
        {
            acBinaryResult = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(acBinaryData);
        }
        sw.Stop();
        var acBinaryDeserMs = sw.Elapsed.TotalMilliseconds;

        // === MessagePack Deserialize ===
        sw.Restart();
        TestOrder_All_True? msgPackResult = null;
        for (int i = 0; i < iterations; i++)
        {
            msgPackResult = MessagePackSerializer.Deserialize<TestOrder_All_True>(msgPackData, MsgPackOptions);
        }
        sw.Stop();
        var msgPackDeserMs = sw.Elapsed.TotalMilliseconds;

        // Print results
        Console.WriteLine($"=== AcBinary vs MessagePack Benchmark ({iterations} iterations) ===");
        Console.WriteLine();
        Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
        Console.WriteLine(new string('-', 60));
        Console.WriteLine($"{"Size (bytes)",-25} {acBinaryData.Length,12:N0} {msgPackData.Length,12:N0} {100.0 * acBinaryData.Length / msgPackData.Length,9:F1}%");
        Console.WriteLine($"{"Serialize (ms)",-25} {acBinarySerMs,12:F2} {msgPackSerMs,12:F2} {acBinarySerMs / msgPackSerMs,9:F2}x");
        Console.WriteLine($"{"Deserialize (ms)",-25} {acBinaryDeserMs,12:F2} {msgPackDeserMs,12:F2} {acBinaryDeserMs / msgPackDeserMs,9:F2}x");
        Console.WriteLine($"{"Round-trip (ms)",-25} {acBinarySerMs + acBinaryDeserMs,12:F2} {msgPackSerMs + msgPackDeserMs,12:F2} {(acBinarySerMs + acBinaryDeserMs) / (msgPackSerMs + msgPackDeserMs),9:F2}x");
        Console.WriteLine();

        var sizeDiff = msgPackData.Length - acBinaryData.Length;
        if (sizeDiff > 0)
            Console.WriteLine($"[OK] AcBinary {sizeDiff:N0} bytes smaller ({100.0 * sizeDiff / msgPackData.Length:F1}% savings)");
        else
            Console.WriteLine($"[WARN] AcBinary {-sizeDiff:N0} bytes larger");

        Assert.IsNotNull(acBinaryResult);
        Assert.IsNotNull(msgPackResult);
        Assert.AreEqual(order.Id, acBinaryResult.Id);
    }

    [TestMethod]
    public void RunStringInterningVsMessagePack()
    {
        // Create data with many repeated strings (worst case for MessagePack, best for interning)
        var items = Enumerable.Range(0, 200).Select(i => new TestClassWithRepeatedValues
        {
            Id = i,
            Status = i % 3 == 0 ? "PendingStatus" : i % 3 == 1 ? "ProcessingStatus" : "CompletedStatus",
            Category = $"Category_{i % 5}",
            Priority = i % 2 == 0 ? "HighPriority" : "LowPriority"
        }).ToList();

        // Warmup
        for (int i = 0; i < 20; i++)
        {
            var b1 = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
            var r1 = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(b1);
            var b2 = MessagePackSerializer.Serialize(items, MsgPackOptions);
            var r2 = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(b2, MsgPackOptions);
        }

        const int iterations = DefaultIterations;

        // AcBinary with interning
        var sw = Stopwatch.StartNew();
        byte[] acWithIntern = null!;
        for (int i = 0; i < iterations; i++)
        {
            acWithIntern = AcBinarySerializer.Serialize(items, AcBinarySerializerOptions.Default);
        }
        var acSerMs = sw.Elapsed.TotalMilliseconds;

        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            var _ = AcBinaryDeserializer.Deserialize<List<TestClassWithRepeatedValues>>(acWithIntern);
        }
        var acDeserMs = sw.Elapsed.TotalMilliseconds;

        // MessagePack
        sw.Restart();
        byte[] msgPack = null!;
        for (int i = 0; i < iterations; i++)
        {
            msgPack = MessagePackSerializer.Serialize(items, MsgPackOptions);
        }
        var msgSerMs = sw.Elapsed.TotalMilliseconds;

        sw.Restart();
        for (int i = 0; i < iterations; i++)
        {
            var _ = MessagePackSerializer.Deserialize<List<TestClassWithRepeatedValues>>(msgPack, MsgPackOptions);
        }
        var msgDeserMs = sw.Elapsed.TotalMilliseconds;

        Console.WriteLine($"=== String Interning Advantage ({iterations} iterations, 200 items with repeated strings) ===");
        Console.WriteLine();
        Console.WriteLine($"{"Metric",-25} {"AcBinary",12} {"MessagePack",12} {"Ratio",10}");
        Console.WriteLine(new string('-', 60));
        Console.WriteLine($"{"Size (bytes)",-25} {acWithIntern.Length,12:N0} {msgPack.Length,12:N0} {100.0 * acWithIntern.Length / msgPack.Length,9:F1}%");
        Console.WriteLine($"{"Serialize (ms)",-25} {acSerMs,12:F2} {msgSerMs,12:F2} {acSerMs / msgSerMs,9:F2}x");
        Console.WriteLine($"{"Deserialize (ms)",-25} {acDeserMs,12:F2} {msgDeserMs,12:F2} {acDeserMs / msgDeserMs,9:F2}x");
        Console.WriteLine();

        var sizeSaving = msgPack.Length - acWithIntern.Length;
        Console.WriteLine($"[OK] String interning saves {sizeSaving:N0} bytes ({100.0 * sizeSaving / msgPack.Length:F1}%)");

        Assert.IsTrue(acWithIntern.Length < msgPack.Length, "AcBinary with interning should be smaller");
    }

    #endregion

    #region Full Comparison (WithRef, NoRef, Populate, Merge)

#if DEBUG
    [TestMethod]
    public void GetAnalyzeStringInternCandidatesLog()
    {
        TestDataFactory.ResetIdCounter();
        var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True");
        var sharedUser = TestDataFactory.CreateUser("shareduser");
        var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);

        var testOrder = TestDataFactory.CreateOrder(
            itemCount: 3,
            palletsPerItem: 3,
            measurementsPerPallet: 3,
            pointsPerMeasurement: 4,
            sharedTag: sharedTag,
            sharedUser: sharedUser,
            sharedMetadata: sharedMeta);

        var options = AcBinarySerializerOptions.Default;
        options.ReferenceHandling = ReferenceHandlingMode.OnlyId;
        var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(testOrder,options);
        
        Assert.IsNotNull(analysisLog);
        Assert.IsGreaterThan(0, analysisLog.Length);

        // Print results sorted by occurrence count
        Console.WriteLine(analysisLog.ToString());
        Console.WriteLine();
    }
#endif

    [TestMethod]
    public void RunFullBenchmarkComparison2()
    {
        // Create test data with shared references
        TestDataFactory.ResetIdCounter();
        var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True");
        var sharedUser = TestDataFactory.CreateUser("shareduser");
        var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);

        var testOrder = TestDataFactory.CreateOrder(
            itemCount: 3,
            palletsPerItem: 3,
            measurementsPerPallet: 3,
            pointsPerMeasurement: 4,
            sharedTag: sharedTag,
            sharedUser: sharedUser,
            sharedMetadata: sharedMeta);

        var singleOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
        singleOptions.UseStringInterning = StringInterningMode.All;

        Console.WriteLine("=== MINIMAL WARMUP TEST ===");
        Console.WriteLine();
        
        // Wait for tiered JIT
        //Console.WriteLine("Waiting 3s for tiered JIT...");
        
        // MINIMAL WARMUP: Just 1 call to populate caches
        Console.WriteLine("Single warmup call (cache only)...");
        for (int i = 0; i < DefaultIterations; i++)
        {
            _ = AcBinarySerializer.Serialize(testOrder, singleOptions);
            _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
        }
        Thread.Sleep(2000);

        Console.WriteLine();
        Console.WriteLine("--- MEASURED TESTS (5x) ---");
        
        var sw = Stopwatch.StartNew();
        var results = new double[5];
        for (int test = 0; test < 5; test++)
        {
            sw.Restart();
            for (int i = 0; i < DefaultIterations; i++)
                _ = AcBinarySerializer.Serialize(testOrder, singleOptions);
            results[test] = sw.Elapsed.TotalMilliseconds;
            Console.WriteLine($"AcBinary Test{test + 1}: {results[test]:F2}ms");
        }
        
        Console.WriteLine();
        var msgResults = new double[5];
        for (int test = 0; test < 5; test++)
        {
            sw.Restart();
            for (int i = 0; i < DefaultIterations; i++)
                _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
            msgResults[test] = sw.Elapsed.TotalMilliseconds;
            Console.WriteLine($"MsgPack Test{test + 1}: {msgResults[test]:F2}ms");
        }
        
        Console.WriteLine();
        Console.WriteLine("--- ANALYSIS ---");
        var acBinaryAvg = results.Average();
        var acBinaryVariance = results.Max() - results.Min();
        var msgPackAvg = msgResults.Average();
        var msgPackVariance = msgResults.Max() - msgResults.Min();
        
        Console.WriteLine($"AcBinary: avg={acBinaryAvg:F2}ms, variance={acBinaryVariance:F2}ms ({100 * acBinaryVariance / acBinaryAvg:F1}%)");
        Console.WriteLine($"MsgPack:  avg={msgPackAvg:F2}ms, variance={msgPackVariance:F2}ms ({100 * msgPackVariance / msgPackAvg:F1}%)");
        
        var isVarianceOk = acBinaryVariance <= acBinaryAvg * 0.2;
        Console.WriteLine(isVarianceOk 
            ? "[OK] Minimal warmup (1 call + 3s delay) is SUFFICIENT!" 
            : "[PROBLEM] Minimal warmup is NOT enough - need more iterations");
    }

    [TestMethod]
    public void RunFullBenchmarkComparison()
    {
        PrintBanner("AcBinary vs MessagePack Full Benchmark");

        // Create test data with shared references
        TestDataFactory.ResetIdCounter();
        var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True");
        var sharedUser = TestDataFactory.CreateUser("shareduser");
        var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);

        var testOrder = TestDataFactory.CreateOrder(
            itemCount: 3,
            palletsPerItem: 3,
            measurementsPerPallet: 3,
            pointsPerMeasurement: 4,
            sharedTag: sharedTag,
            sharedUser: sharedUser,
            sharedMetadata: sharedMeta);

        // Options
        var withRefOptions = AcBinarySerializerOptions.Default;
        withRefOptions.UseMetadata = false;
        //withRefOptions.UseStringInterning = StringInterningMode.None;

        var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;
        noRefOptions.UseMetadata = false;
        //noRefOptions.ReferenceHandling = ReferenceHandlingMode.All;
        //noRefOptions.UseStringInterning = StringInterningMode.None;

        // Pre-serialize
        var acBinaryWithRef = AcBinarySerializer.Serialize(testOrder, withRefOptions);
        var acBinaryNoRef = AcBinarySerializer.Serialize(testOrder, noRefOptions);
        var msgPackData = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);

        Console.WriteLine("withRefOptions:");
        WriteBinaryToConsole(acBinaryWithRef);

        // Warmup - MUST be 1000+ iterations for tiered JIT to complete
        Console.WriteLine($"\nSerialize warming up ({DefaultIterations} iterations each)...");
        for (int i = 0; i < DefaultIterations; i++)
        {
            _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
            _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
            _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);

            Console.WriteLine("acBinaryWithRef");
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(acBinaryWithRef);
            Console.WriteLine("acBinaryNoRef");
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(acBinaryNoRef);
            _ = MessagePackSerializer.Deserialize<TestOrder_All_True>(msgPackData, MsgPackOptions);
        }

        // Wait for tiered JIT background compilation to complete
        Thread.Sleep(2000);

        Console.WriteLine($"Iterations: {DefaultIterations:N0}");

        // Size comparison
        PrintTableHeader("SIZE COMPARISON");
        PrintTableRowSize("AcBinary (WithRef)", acBinaryWithRef.Length, msgPackData.Length);
        PrintTableRowSize("AcBinary (NoRef)", acBinaryNoRef.Length, msgPackData.Length);
        PrintTableRowSize("MessagePack (baseline)", msgPackData.Length, msgPackData.Length);
        PrintTableFooter();

        var sw = Stopwatch.StartNew();

        // === Serialize WithRef ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
        var acWithRefSerMs = sw.Elapsed.TotalMilliseconds;

        // === Serialize NoRef ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
        var acNoRefSerMs = sw.Elapsed.TotalMilliseconds;

        // === MessagePack Serialize ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = MessagePackSerializer.Serialize(testOrder, MsgPackOptions);
        var msgPackSerMs = sw.Elapsed.TotalMilliseconds;

        // === Deserialize WithRef ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(acBinaryWithRef);
        var acWithRefDeserMs = sw.Elapsed.TotalMilliseconds;

        // === Deserialize NoRef ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(acBinaryNoRef);
        var acNoRefDeserMs = sw.Elapsed.TotalMilliseconds;

        // === MessagePack Deserialize ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = MessagePackSerializer.Deserialize<TestOrder_All_True>(msgPackData, MsgPackOptions);
        var msgPackDeserMs = sw.Elapsed.TotalMilliseconds;

        // === Populate (AcBinary only) ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);
            AcBinaryDeserializer.Populate(acBinaryNoRef, target);
        }
        var acPopulateMs = sw.Elapsed.TotalMilliseconds;

        // === PopulateMerge (AcBinary only) ===
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);
            AcBinaryDeserializer.PopulateMerge(acBinaryNoRef, target);
        }
        var acMergeMs = sw.Elapsed.TotalMilliseconds;

        // Print performance table
        PrintTableHeader("PERFORMANCE COMPARISON (lower is better)");
        PrintTableRow("Serialize (WithRef)", acWithRefSerMs, msgPackSerMs);
        PrintTableRow("Serialize (NoRef)", acNoRefSerMs, msgPackSerMs);
        PrintTableRow("Deserialize (WithRef)", acWithRefDeserMs, msgPackDeserMs);
        PrintTableRow("Deserialize (NoRef)", acNoRefDeserMs, msgPackDeserMs);
        PrintTableRow("Populate (NoRef)", acPopulateMs, 0);
        PrintTableRow("Merge (NoRef)", acMergeMs, 0);
        PrintTableRow("Round-trip (WithRef)", acWithRefSerMs + acWithRefDeserMs, msgPackSerMs + msgPackDeserMs);
        PrintTableRow("Round-trip (NoRef)", acNoRefSerMs + acNoRefDeserMs, msgPackSerMs + msgPackDeserMs);
        PrintTableFooter();

        PrintSummary(acBinaryNoRef.Length, msgPackData.Length, acNoRefSerMs, msgPackSerMs, acNoRefDeserMs, msgPackDeserMs);

        // Assertions
        Assert.IsTrue(acBinaryWithRef.Length < msgPackData.Length, "AcBinary WithRef should be smaller than MessagePack");
        Assert.IsTrue(acBinaryNoRef.Length < msgPackData.Length, "AcBinary NoRef should be smaller than MessagePack");
    }

    [TestMethod]
    public void RunWithRefVsNoRefComparison()
    {
        PrintBanner("AcBinary WithRef vs NoRef Comparison");

        // Create test data WITH shared references (to show WithRef advantage)
        TestDataFactory.ResetIdCounter();
        var sharedTag = TestDataFactory.CreateTag("SharedTag_All_True");
        var sharedUser = TestDataFactory.CreateUser("shareduser");
        var sharedMeta = TestDataFactory.CreateMetadata("shared", withChild: true);

        var testOrder = TestDataFactory.CreateOrder(
            itemCount: 5,
            palletsPerItem: 4,
            measurementsPerPallet: 3,
            pointsPerMeasurement: 5,
            sharedTag: sharedTag,
            sharedUser: sharedUser,
            sharedMetadata: sharedMeta);

        var withRefOptions = new AcBinarySerializerOptions();
        var noRefOptions = AcBinarySerializerOptions.WithoutReferenceHandling;

        // Warmup
        Console.WriteLine("Warming up...");
        for (int i = 0; i < 50; i++)
        {
            _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
            _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
        }

        var withRefData = AcBinarySerializer.Serialize(testOrder, withRefOptions);
        var noRefData = AcBinarySerializer.Serialize(testOrder, noRefOptions);

        Console.WriteLine($"Iterations: {DefaultIterations:N0}");

        // Size comparison
        PrintBanner("SIZE COMPARISON (bytes)");
        Console.WriteLine($"WithRef : {withRefData.Length,12:N0} (baseline)");
        var sizeDiff = noRefData.Length - withRefData.Length;
        var ratio = withRefData.Length == 0 ? 0 : 100.0 * noRefData.Length / withRefData.Length;
        Console.WriteLine($"NoRef   : {noRefData.Length,12:N0} (diff {sizeDiff:+#;-#;0}) => {ratio:F1}% of WithRef");

        var sw = Stopwatch.StartNew();

        // Serialize WithRef
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinarySerializer.Serialize(testOrder, withRefOptions);
        var withRefSerMs = sw.Elapsed.TotalMilliseconds;

        // Serialize NoRef
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinarySerializer.Serialize(testOrder, noRefOptions);
        var noRefSerMs = sw.Elapsed.TotalMilliseconds;

        // Deserialize WithRef
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(withRefData);
        var withRefDeserMs = sw.Elapsed.TotalMilliseconds;

        // Deserialize NoRef
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(noRefData);
        var noRefDeserMs = sw.Elapsed.TotalMilliseconds;

        PrintBanner("PERFORMANCE COMPARISON (ms)");
        Console.WriteLine($"Serialize  -> WithRef: {withRefSerMs,8:F2} | NoRef: {noRefSerMs,8:F2}");
        Console.WriteLine($"Deserialize-> WithRef: {withRefDeserMs,8:F2} | NoRef: {noRefDeserMs,8:F2}");
        Console.WriteLine($"Round-trip -> WithRef: {withRefSerMs + withRefDeserMs,8:F2} | NoRef: {noRefSerMs + noRefDeserMs,8:F2}");

        if (withRefData.Length < noRefData.Length)
        {
            Console.WriteLine($"[OK] WithRef saves {noRefData.Length - withRefData.Length:N0} bytes by deduplicating shared references.");
        }
        else
        {
            Console.WriteLine($"[INFO] NoRef saves {withRefData.Length - noRefData.Length:N0} bytes because it skips reference metadata.");
        }

        // Verify correctness
        var resultWithRef = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(withRefData);
        var resultNoRef = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(noRefData);
        Assert.IsNotNull(resultWithRef);
        Assert.IsNotNull(resultNoRef);
        Assert.AreEqual(testOrder.Id, resultWithRef.Id);
        Assert.AreEqual(testOrder.Id, resultNoRef.Id);
    }

    [TestMethod]
    public void RunPopulateAndMergeBenchmark()
    {
        PrintBanner("AcBinary Populate & Merge Benchmark");

        TestDataFactory.ResetIdCounter();
        var testOrder = TestDataFactory.CreateBenchmarkOrder(
            itemCount: 3,
            palletsPerItem: 3,
            measurementsPerPallet: 3,
            pointsPerMeasurement: 4);

        var options = AcBinarySerializerOptions.WithoutReferenceHandling;
        //options.ReferenceHandling = ReferenceHandlingMode.All;
        var binaryData = AcBinarySerializer.Serialize(testOrder, options);

        WriteBinaryToConsole(binaryData);

        Console.WriteLine("Warming up...");
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);

            //Console.WriteLine("Populate");
            AcBinaryDeserializer.Populate(binaryData, target);

            //Console.WriteLine("PopulateMerge");
            AcBinaryDeserializer.PopulateMerge(binaryData, target);
        }

        Console.WriteLine($"Iterations: {DefaultIterations:N0}");
        Console.WriteLine($"Data size: {binaryData.Length:N0} bytes");

        var sw = Stopwatch.StartNew();

        // Deserialize (creates new object)
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
            _ = AcBinaryDeserializer.Deserialize<TestOrder_All_True>(binaryData);
        var deserializeMs = sw.Elapsed.TotalMilliseconds;

        // Populate (reuses existing object)
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);
            AcBinaryDeserializer.Populate(binaryData, target);
        }
        var populateMs = sw.Elapsed.TotalMilliseconds;

        // PopulateMerge (IId-based merge)
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);
            AcBinaryDeserializer.PopulateMerge(binaryData, target);
        }
        var mergeMs = sw.Elapsed.TotalMilliseconds;

        // PopulateMerge with RemoveOrphanedItems
        var mergeWithRemoveOptions = new AcBinarySerializerOptions { RemoveOrphanedItems = true };
        sw.Restart();
        for (int i = 0; i < DefaultIterations; i++)
        {
            var target = CreatePopulateTarget(testOrder);
            AcBinaryDeserializer.PopulateMerge(binaryData, target, mergeWithRemoveOptions);
        }
        var mergeWithRemoveMs = sw.Elapsed.TotalMilliseconds;

        PrintBanner("OPERATION COMPARISON (ms)");
        Console.WriteLine($"Deserialize (new object): {deserializeMs:F2} (baseline)");
        Console.WriteLine($"Populate  (reuse obj)  : {populateMs:F2} ({populateMs / deserializeMs:F2}x of baseline)");
        Console.WriteLine($"PopulateMerge          : {mergeMs:F2} ({mergeMs / deserializeMs:F2}x of baseline)");
        Console.WriteLine($"PopulateMerge + cleanup: {mergeWithRemoveMs:F2} ({mergeWithRemoveMs / deserializeMs:F2}x of baseline)");

        Console.WriteLine("[INFO] Populate/Merge reuse existing objects - ideal for UI data binding scenarios.");

        Assert.IsTrue(true); // Test passed if no exceptions
    }

    #endregion

    private static void WriteBinaryToConsole(byte[] binary)
    {
        Console.WriteLine();
        Console.WriteLine(BitConverter.ToString(binary));
        Console.WriteLine();
    }

    #region Test Models

    [AcBinarySerializable]
    public class TestClassWithRepeatedValues
    {
        public int Id { get; set; }
        public string Status { get; set; } = "";
        public string Category { get; set; } = "";
        public string Priority { get; set; } = "";
    }

    #endregion
}
