using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Binaries;
using System.Reflection;
using static AyCode.Core.Tests.TestModels.AcSerializerModels;

namespace AyCode.Core.Tests.Serialization;

/// <summary>
/// Diagnostic tests to help debug serialization issues.
/// </summary>
[TestClass]
public class AcBinarySerializerDiagnosticTests
{
    /// <summary>
    /// Diagnostic test to understand the exact binary structure.
    /// This test outputs the binary bytes to help debug production issues.
    /// </summary>
    [TestMethod]
    public void Diagnostic_StockTaking_BinaryStructure()
    {
        var stockTaking = new TestStockTakingWithInheritance
        {
            Id = 1,
            StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
            IsClosed = false,
            Creator = 6,
            Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
            Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
            StockTakingItems = null
        };

        var binary = stockTaking.ToBinary();
        
        var hexDump = string.Join(" ", binary.Select(b => b.ToString("X2")));
        Console.WriteLine($"Binary length: {binary.Length}");
        Console.WriteLine($"Binary hex: {hexDump}");
        
        for (int i = 0; i < binary.Length; i++)
        {
            if (binary[i] == 214)
            {
                Console.WriteLine($"Found 0xD6 at position {i}");
            }
        }

        var result = binary.BinaryTo<TestStockTakingWithInheritance>();
        
        Assert.IsNotNull(result);
        Assert.AreEqual(6, result.Creator);
        Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks);
    }

    /// <summary>
    /// Test with nested list to ensure proper stream positioning.
    /// </summary>
    [TestMethod]
    public void Diagnostic_StockTaking_WithNestedItems()
    {
        var stockTaking = new TestStockTakingWithInheritance
        {
            Id = 1,
            StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
            IsClosed = false,
            Creator = 6,
            Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
            Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
            StockTakingItems = new List<TestStockTakingItemWithInheritance>
            {
                new()
                {
                    Id = 10,
                    StockTakingId = 1,
                    ProductId = 100,
                    IsMeasured = true,
                    OriginalStockQuantity = 50,
                    MeasuredStockQuantity = 48,
                    Created = new DateTime(2025, 1, 24, 14, 0, 0, DateTimeKind.Utc),
                    Modified = new DateTime(2025, 1, 24, 14, 30, 0, DateTimeKind.Utc),
                    StockTakingItemPallets = null
                }
            }
        };

        var binary = stockTaking.ToBinary();
        Console.WriteLine($"Binary length with 1 item: {binary.Length}");

        var result = binary.BinaryTo<TestStockTakingWithInheritance>();
        
        Assert.IsNotNull(result);
        Assert.AreEqual(6, result.Creator, "Creator should be 6");
        Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks, 
            $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
        Assert.IsNotNull(result.StockTakingItems);
        Assert.AreEqual(1, result.StockTakingItems.Count);
    }

    /// <summary>
    /// CRITICAL TEST: Verify property order is consistent.
    /// This test checks that the reflection-based property order matches
    /// what's expected for serialization/deserialization.
    /// </summary>
    [TestMethod]
    public void Diagnostic_PropertyOrder_InheritanceHierarchy()
    {
        var type = typeof(SimStockTaking);
        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
        
        Console.WriteLine($"Properties of {type.Name} (count: {props.Length}):");
        for (int i = 0; i < props.Length; i++)
        {
            var prop = props[i];
            Console.WriteLine($"  [{i}] {prop.Name} : {prop.PropertyType.Name} (declared in: {prop.DeclaringType?.Name})");
        }
        
        // The exact order may vary by platform!
        // Log it so we can compare server vs client
        Assert.IsTrue(props.Length >= 7, "Should have at least 7 properties");
        
        // Check that all expected properties exist
        var propNames = props.Select(p => p.Name).ToHashSet();
        Assert.IsTrue(propNames.Contains("Id"), "Should have Id");
        Assert.IsTrue(propNames.Contains("StartDateTime"), "Should have StartDateTime");
        Assert.IsTrue(propNames.Contains("IsClosed"), "Should have IsClosed");
        Assert.IsTrue(propNames.Contains("Creator"), "Should have Creator");
        Assert.IsTrue(propNames.Contains("Created"), "Should have Created");
        Assert.IsTrue(propNames.Contains("Modified"), "Should have Modified");
        Assert.IsTrue(propNames.Contains("StockTakingItems"), "Should have StockTakingItems");
    }

    /// <summary>
    /// CRITICAL REGRESSION TEST: Simulates exact production hierarchy.
    /// StockTaking : MgStockTaking&lt;StockTakingItem&gt; : MgEntityBase : BaseEntity
    /// </summary>
    [TestMethod]
    public void Diagnostic_SimStockTaking_RoundTrip()
    {
        var stockTaking = new SimStockTaking
        {
            Id = 1,
            StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
            IsClosed = false,
            Creator = 6, // The exact value from production error
            Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
            Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
            StockTakingItems = null // loadRelations = false means no items
        };

        var binary = stockTaking.ToBinary();
        
        // Log the property names in the header
        Console.WriteLine($"Binary length: {binary.Length}");
        
        var result = binary.BinaryTo<SimStockTaking>();
        
        Assert.IsNotNull(result);
        Assert.AreEqual(1, result.Id, "Id should be 1");
        Assert.AreEqual(6, result.Creator, "Creator should be 6 - this is where the bug occurs!");
        Assert.AreEqual(stockTaking.Created.Ticks, result.Created.Ticks, 
            $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}");
        Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
        Assert.IsFalse(result.IsClosed);
    }

    /// <summary>
    /// Test List of SimStockTaking - exact production scenario.
    /// </summary>
    [TestMethod]
    public void Diagnostic_ListOfSimStockTaking_RoundTrip()
    {
        var stockTakings = Enumerable.Range(1, 3).Select(i => new SimStockTaking
        {
            Id = i,
            StartDateTime = DateTime.UtcNow.AddDays(-i),
            IsClosed = i % 2 == 0,
            Creator = i,
            Created = DateTime.UtcNow.AddDays(-i),
            Modified = DateTime.UtcNow,
            StockTakingItems = null
        }).ToList();

        var binary = stockTakings.ToBinary();
        var result = binary.BinaryTo<List<SimStockTaking>>();

        Assert.IsNotNull(result);
        Assert.AreEqual(3, result.Count);

        for (int i = 0; i < 3; i++)
        {
            var original = stockTakings[i];
            var deserialized = result[i];

            Assert.AreEqual(original.Id, deserialized.Id, $"[{i}] Id mismatch");
            Assert.AreEqual(original.Creator, deserialized.Creator, $"[{i}] Creator mismatch");
            Assert.AreEqual(original.Created.Ticks, deserialized.Created.Ticks, 
                $"[{i}] Created mismatch. Expected: {original.Created}, Got: {deserialized.Created}");
        }
    }

    /// <summary>
    /// Diagnostic: Check what PropertyType the reflection returns for generic type parameter.
    /// </summary>
    [TestMethod]
    public void Diagnostic_GenericProperty_ReflectionType()
    {
        var parentType = typeof(ConcreteParent);
        var itemsProp = parentType.GetProperty("Items");
        
        Assert.IsNotNull(itemsProp);
        
        var propType = itemsProp.PropertyType;
        Console.WriteLine($"PropertyType: {propType}");
        Console.WriteLine($"PropertyType.FullName: {propType.FullName}");
        Console.WriteLine($"IsGenericType: {propType.IsGenericType}");
        
        if (propType.IsGenericType)
        {
            var args = propType.GetGenericArguments();
            Console.WriteLine($"GenericArguments.Length: {args.Length}");
            foreach (var arg in args)
            {
                Console.WriteLine($"  GenericArgument: {arg.FullName}");
            }
        }
        
        Assert.IsTrue(propType.IsGenericType);
        var elementType = propType.GetGenericArguments()[0];
        Assert.AreEqual(typeof(GenericItemImpl), elementType, 
            "Element type should be GenericItemImpl, not IGenericItem");
    }

    /// <summary>
    /// CRITICAL BUG REPRODUCTION: StockTakingItems is null (loadRelations = false)
    /// This test verifies that property-index-based serialization correctly handles null properties.
    /// </summary>
    [TestMethod]
    public void Diagnostic_NullStockTakingItems_VerifyPropertyIndices()
    {
        var stockTaking = new SimStockTaking
        {
            Id = 1,
            StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
            IsClosed = false,
            Creator = 6, // The exact value from production error (becomes TinyInt 0xD6)
            Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
            Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
            StockTakingItems = null // THIS IS THE KEY - loadRelations = false
        };

        var binary = stockTaking.ToBinary();

        // Log the binary structure
        Console.WriteLine($"Binary length: {binary.Length}");
        Console.WriteLine($"Binary hex: {string.Join(" ", binary.Select(b => b.ToString("X2")))}");

        // === HEADER PARSING (using BinaryTypeCode constants) ===
        var pos = 0;
        var version = binary[pos++];
        Console.WriteLine($"Version: {version}");

        var headerFlags = binary[pos++];
        Console.WriteLine($"Header flags: 0x{headerFlags:X2}");

        bool hasMetadata = (headerFlags & BinaryTypeCode.HeaderFlag_Metadata) != 0;
        bool hasRefOnlyId = (headerFlags & BinaryTypeCode.HeaderFlag_RefHandling_OnlyId) != 0;
        bool hasRefAll = (headerFlags & BinaryTypeCode.HeaderFlag_RefHandling_All) != 0;
        bool hasCacheCount = (headerFlags & BinaryTypeCode.HeaderFlag_HasCacheCount) != 0;
        Console.WriteLine($"  Metadata={hasMetadata}, RefOnlyId={hasRefOnlyId}, RefAll={hasRefAll}, HasCacheCount={hasCacheCount}");

        if (hasCacheCount)
        {
            var ccByte = binary[pos];
            int cacheCount = (ccByte & 0x80) == 0 ? ccByte : (ccByte & 0x7F) | (binary[pos + 1] << 7);
            pos += (ccByte & 0x80) == 0 ? 1 : 2;
            Console.WriteLine($"Cache count: {cacheCount}");
        }

        Console.WriteLine($"\n=== BODY (starts at position {pos}) ===");

        // Read the object marker — can be FixObj slot (0..SlotCount-1) or explicit marker
        var objectMarker = binary[pos++];
        bool isFixObj = objectMarker < BinaryTypeCode.SlotCount;
        Console.WriteLine($"Object marker: 0x{objectMarker:X2} (FixObj={isFixObj}, " +
            $"Object=0x{BinaryTypeCode.Object:X2}, ObjectRefFirst=0x{BinaryTypeCode.ObjectRefFirst:X2}, " +
            $"ObjectWithMetadata=0x{BinaryTypeCode.ObjectWithMetadata:X2})");

        Assert.IsTrue(
            isFixObj
            || objectMarker == BinaryTypeCode.Object
            || objectMarker == BinaryTypeCode.ObjectWithMetadata
            || objectMarker == BinaryTypeCode.ObjectRefFirst
            || objectMarker == BinaryTypeCode.ObjectWithMetadataRefFirst,
            $"Expected an object marker, got 0x{objectMarker:X2}");

        // If ObjectWithMetadata, skip inline metadata
        if (objectMarker is BinaryTypeCode.ObjectWithMetadata or BinaryTypeCode.ObjectWithMetadataRefFirst)
        {
            var propNameHash = BitConverter.ToInt32(binary, pos);
            pos += 4;
            Console.WriteLine($"PropNameHash: 0x{propNameHash:X8}");

            var pcByte = binary[pos];
            int inlinePropCount = (pcByte & 0x80) == 0 ? pcByte : (pcByte & 0x7F) | (binary[pos + 1] << 7);
            pos += (pcByte & 0x80) == 0 ? 1 : 2;
            Console.WriteLine($"Inline metadata propCount: {inlinePropCount}");

            for (int h = 0; h < inlinePropCount; h++)
            {
                var hash = BitConverter.ToInt32(binary, pos);
                Console.WriteLine($"  Property hash [{h}]: 0x{hash:X8}");
                pos += 4;
            }
        }

        // If RefFirst marker, read VarUInt cache index
        if (objectMarker is BinaryTypeCode.ObjectRefFirst or BinaryTypeCode.ObjectWithMetadataRefFirst)
        {
            var rByte = binary[pos];
            int refCacheIndex = (rByte & 0x80) == 0 ? rByte : (rByte & 0x7F) | (binary[pos + 1] << 7);
            pos += (rByte & 0x80) == 0 ? 1 : 2;
            Console.WriteLine($"RefCacheIndex: {refCacheIndex}");
        }

        // Markerless format: properties are written in order, no property count header
        Console.WriteLine($"\n=== BODY PROPERTIES (remaining {binary.Length - pos} bytes) ===");
        int propIdx = 0;
        while (pos < binary.Length)
        {
            var b = binary[pos];
            if (b == BinaryTypeCode.DateTime)
            {
                Console.WriteLine($"  Property [{propIdx}]: DateTime (1+8 bytes)");
                pos += 9; // marker + 8 bytes ticks
            }
            else if (BinaryTypeCode.IsTinyInt(b))
            {
                Console.WriteLine($"  Property [{propIdx}]: TinyInt value={BinaryTypeCode.DecodeTinyInt(b)} (0x{b:X2})");
                pos += 1;
            }
            else if (b == BinaryTypeCode.False)
            {
                Console.WriteLine($"  Property [{propIdx}]: Boolean: false");
                pos += 1;
            }
            else if (b == BinaryTypeCode.True)
            {
                Console.WriteLine($"  Property [{propIdx}]: Boolean: true");
                pos += 1;
            }
            else if (b == BinaryTypeCode.Null)
            {
                Console.WriteLine($"  Property [{propIdx}]: Null");
                pos += 1;
            }
            else if (b == BinaryTypeCode.PropertySkip)
            {
                Console.WriteLine($"  Property [{propIdx}]: PropertySkip (default/null)");
                pos += 1;
            }
            else
            {
                Console.WriteLine($"  Property [{propIdx}]: Unknown type: 0x{b:X2}");
                break;
            }
            propIdx++;
        }
        
        // Deserialize and verify
        var result = binary.BinaryTo<SimStockTaking>();
        
        Assert.IsNotNull(result);
        Assert.AreEqual(1, result.Id, "Id should be 1");
        Assert.AreEqual(6, result.Creator, 
            $"Creator should be 6. Got: {result.Creator}. " +
            $"If this fails with a very large number, it means DateTime bytes were interpreted as int!");
        Assert.AreEqual(stockTaking.Created, result.Created, 
            $"Created mismatch. Expected: {stockTaking.Created}, Got: {result.Created}. " +
            $"If Created has wrong value, deserializer read wrong bytes!");
        Assert.AreEqual(stockTaking.StartDateTime, result.StartDateTime);
        Assert.IsFalse(result.IsClosed);
        Assert.IsNull(result.StockTakingItems, "StockTakingItems should remain null");
    }

    /// <summary>
    /// Test to verify property order consistency between serializer and deserializer.
    /// </summary>
    [TestMethod]
    public void Diagnostic_VerifyPropertyOrderConsistency()
    {
        // Get serializer's property order
        var serializerType = typeof(AcBinarySerializer);
        var metadataCacheField = serializerType.GetField("TypeMetadataCache", 
            BindingFlags.NonPublic | BindingFlags.Static);
        
        // Clear cache to force fresh metadata creation
        // (This helps ensure we're testing the actual order)
        
        var type = typeof(SimStockTaking);
        var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .Where(p => p.CanRead && p.GetIndexParameters().Length == 0)
            .ToArray();
        
        Console.WriteLine($"Properties of {type.Name} (reflection order):");
        for (int i = 0; i < props.Length; i++)
        {
            var prop = props[i];
            Console.WriteLine($"  [{i}] {prop.Name} : {prop.PropertyType.Name}");
        }
        
        // Verify Creator comes BEFORE Created in the reflection order
        var creatorIndex = Array.FindIndex(props, p => p.Name == "Creator");
        var createdIndex = Array.FindIndex(props, p => p.Name == "Created");
        var stockTakingItemsIndex = Array.FindIndex(props, p => p.Name == "StockTakingItems");
        
        Console.WriteLine($"\nKey indices:");
        Console.WriteLine($"  StockTakingItems: {stockTakingItemsIndex}");
        Console.WriteLine($"  Creator: {creatorIndex}");
        Console.WriteLine($"  Created: {createdIndex}");
        
        // The bug scenario: if StockTakingItems is skipped during serialization,
        // but the deserializer still expects it at the original index position,
        // then Creator (index 3) would be read when expecting StockTakingItems (index 2)
        // and Created (index 4) would be read when expecting Creator (index 3)
        
        Assert.IsTrue(stockTakingItemsIndex >= 0, "StockTakingItems should exist");
        Assert.IsTrue(creatorIndex >= 0, "Creator should exist");
        Assert.IsTrue(createdIndex >= 0, "Created should exist");
        
        // In the class definition order:
        // StockTakingItems comes BEFORE Creator and Created
        Assert.IsTrue(stockTakingItemsIndex < creatorIndex, 
            "StockTakingItems should come before Creator");
        Assert.IsTrue(creatorIndex < createdIndex, 
            "Creator should come before Created");
    }

    /// <summary>
    /// Test multiple StockTakings with null StockTakingItems - exact production scenario.
    /// </summary>
    [TestMethod]
    public void Diagnostic_MultipleStockTakings_NullItems()
    {
        var stockTakings = new List<SimStockTaking>
        {
            new()
            {
                Id = 1,
                StartDateTime = new DateTime(2025, 1, 24, 10, 0, 0, DateTimeKind.Utc),
                IsClosed = false,
                Creator = 6,
                Created = new DateTime(2025, 1, 24, 15, 25, 0, DateTimeKind.Utc),
                Modified = new DateTime(2025, 1, 24, 16, 0, 0, DateTimeKind.Utc),
                StockTakingItems = null
            },
            new()
            {
                Id = 2,
                StartDateTime = new DateTime(2025, 1, 23, 9, 0, 0, DateTimeKind.Utc),
                IsClosed = true,
                Creator = 12,
                Created = new DateTime(2025, 1, 23, 14, 0, 0, DateTimeKind.Utc),
                Modified = new DateTime(2025, 1, 23, 15, 30, 0, DateTimeKind.Utc),
                StockTakingItems = null
            }
        };

        var binary = stockTakings.ToBinary();
        Console.WriteLine($"Binary length for 2 StockTakings: {binary.Length}");

        var result = binary.BinaryTo<List<SimStockTaking>>();

        Assert.IsNotNull(result);
        Assert.AreEqual(2, result.Count);

        // First item
        Assert.AreEqual(1, result[0].Id);
        Assert.AreEqual(6, result[0].Creator, "First item Creator should be 6");
        Assert.AreEqual(stockTakings[0].Created, result[0].Created, 
            $"First item Created mismatch. Expected: {stockTakings[0].Created}, Got: {result[0].Created}");

        // Second item
        Assert.AreEqual(2, result[1].Id);
        Assert.AreEqual(12, result[1].Creator, "Second item Creator should be 12");
        Assert.AreEqual(stockTakings[1].Created, result[1].Created,
            $"Second item Created mismatch. Expected: {stockTakings[1].Created}, Got: {result[1].Created}");
    }
}
