using ZB.MOM.WW.CBDD.Bson;
using Xunit;
using System.Collections.Concurrent;

namespace ZB.MOM.WW.CBDD.Tests;

public class BsonSpanReaderWriterTests
{
    private readonly ConcurrentDictionary<string, ushort> _keyMap = new(StringComparer.OrdinalIgnoreCase);
    private readonly ConcurrentDictionary<ushort, string> _keys = new();

    public BsonSpanReaderWriterTests()
    {
        ushort id = 1;
        string[] initialKeys = ["name", "age", "active", "_id", "val", "dec", "timestamp", "int32", "int64", "double", "data", "child", "value", "0", "1"];
        foreach (var key in initialKeys)
        {
            _keyMap[key] = id;
            _keys[id] = key;
            id++;
        }
    }

    [Fact]
    public void WriteAndRead_SimpleDocument()
    {
        Span<byte> buffer = stackalloc byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        var sizePos = writer.BeginDocument();
        writer.WriteString("name", "John");
        writer.WriteInt32("age", 30);
        writer.WriteBoolean("active", true);
        writer.EndDocument(sizePos);

        var documentBytes = buffer[..writer.Position];

        var reader = new BsonSpanReader(documentBytes, _keys);
        var size = reader.ReadDocumentSize();

        size.ShouldBe(writer.Position);

        var type1 = reader.ReadBsonType();
        var name1 = reader.ReadElementHeader();
        var value1 = reader.ReadString();

        type1.ShouldBe(BsonType.String);
        name1.ShouldBe("name");
        value1.ShouldBe("John");

        var type2 = reader.ReadBsonType();
        var name2 = reader.ReadElementHeader();
        var value2 = reader.ReadInt32();

        type2.ShouldBe(BsonType.Int32);
        name2.ShouldBe("age");
        value2.ShouldBe(30);

        var type3 = reader.ReadBsonType();
        var name3 = reader.ReadElementHeader();
        var value3 = reader.ReadBoolean();

        type3.ShouldBe(BsonType.Boolean);
        name3.ShouldBe("active");
        value3.ShouldBeTrue();
    }

    [Fact]
    public void WriteAndRead_ObjectId()
    {
        Span<byte> buffer = stackalloc byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        var oid = ObjectId.NewObjectId();

        var sizePos = writer.BeginDocument();
        writer.WriteObjectId("_id", oid);
        writer.EndDocument(sizePos);

        var documentBytes = buffer[..writer.Position];
        var reader = new BsonSpanReader(documentBytes, _keys);

        reader.ReadDocumentSize();
        var type = reader.ReadBsonType();
        var name = reader.ReadElementHeader();
        var readOid = reader.ReadObjectId();

        type.ShouldBe(BsonType.ObjectId);
        name.ShouldBe("_id");
        readOid.ShouldBe(oid);
    }

    [Fact]
    public void ReadWrite_Double()
    {
        var buffer = new byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        writer.WriteDouble("val", 123.456);

        var reader = new BsonSpanReader(buffer, _keys);
        var type = reader.ReadBsonType();
        var name = reader.ReadElementHeader();
        var val = reader.ReadDouble();

        type.ShouldBe(BsonType.Double);
        name.ShouldBe("val");
        val.ShouldBe(123.456);
    }

    [Fact]
    public void ReadWrite_Decimal128_RoundTrip()
    {
        var buffer = new byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        decimal original = 123456.789m;
        writer.WriteDecimal128("dec", original);

        var reader = new BsonSpanReader(buffer, _keys);
        var type = reader.ReadBsonType();
        var name = reader.ReadElementHeader();
        var val = reader.ReadDecimal128();

        type.ShouldBe(BsonType.Decimal128);
        name.ShouldBe("dec");
        val.ShouldBe(original);
    }

    [Fact]
    public void WriteAndRead_DateTime()
    {
        Span<byte> buffer = stackalloc byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        var now = DateTime.UtcNow;
        // Round to milliseconds as BSON only stores millisecond precision
        var expectedTime = new DateTime(now.Year, now.Month, now.Day,
            now.Hour, now.Minute, now.Second, now.Millisecond, DateTimeKind.Utc);

        var sizePos = writer.BeginDocument();
        writer.WriteDateTime("timestamp", expectedTime);
        writer.EndDocument(sizePos);

        var documentBytes = buffer[..writer.Position];
        var reader = new BsonSpanReader(documentBytes, _keys);

        reader.ReadDocumentSize();
        var type = reader.ReadBsonType();
        var name = reader.ReadElementHeader();
        var readTime = reader.ReadDateTime();

        type.ShouldBe(BsonType.DateTime);
        name.ShouldBe("timestamp");
        readTime.ShouldBe(expectedTime);
    }

    [Fact]
    public void WriteAndRead_NumericTypes()
    {
        Span<byte> buffer = stackalloc byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        var sizePos = writer.BeginDocument();
        writer.WriteInt32("int32", int.MaxValue);
        writer.WriteInt64("int64", long.MaxValue);
        writer.WriteDouble("double", 3.14159);
        writer.EndDocument(sizePos);

        var documentBytes = buffer[..writer.Position];
        var reader = new BsonSpanReader(documentBytes, _keys);

        reader.ReadDocumentSize();

        reader.ReadBsonType();
        reader.ReadElementHeader();
        reader.ReadInt32().ShouldBe(int.MaxValue);

        reader.ReadBsonType();
        reader.ReadElementHeader();
        reader.ReadInt64().ShouldBe(long.MaxValue);

        reader.ReadBsonType();
        reader.ReadElementHeader();
        Math.Round(reader.ReadDouble(), 5).ShouldBe(Math.Round(3.14159, 5));
    }

    [Fact]
    public void WriteAndRead_Binary()
    {
        Span<byte> buffer = stackalloc byte[256];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        byte[] testData = [1, 2, 3, 4, 5];

        var sizePos = writer.BeginDocument();
        writer.WriteBinary("data", testData);
        writer.EndDocument(sizePos);

        var documentBytes = buffer[..writer.Position];
        var reader = new BsonSpanReader(documentBytes, _keys);

        reader.ReadDocumentSize();
        var type = reader.ReadBsonType();
        var name = reader.ReadElementHeader();
        var readData = reader.ReadBinary(out var subtype);

        type.ShouldBe(BsonType.Binary);
        name.ShouldBe("data");
        subtype.ShouldBe((byte)0);
        testData.AsSpan().SequenceEqual(readData).ShouldBeTrue();
    }

    [Fact]
    public void WriteAndRead_NestedDocument()
    {
        Span<byte> buffer = stackalloc byte[512];
        var writer = new BsonSpanWriter(buffer, _keyMap);

        var rootSizePos = writer.BeginDocument();
        writer.WriteString("name", "Parent");

        var childSizePos = writer.BeginDocument("child");
        writer.WriteString("name", "Child");
        writer.WriteInt32("value", 42);
        writer.EndDocument(childSizePos);

        writer.EndDocument(rootSizePos);

        var documentBytes = buffer[..writer.Position];
        var reader = new BsonSpanReader(documentBytes, _keys);
        var rootSize = reader.ReadDocumentSize();

        rootSize.ShouldBe(writer.Position);

        reader.ReadBsonType(); // String
        reader.ReadElementHeader().ShouldBe("name");
        reader.ReadString().ShouldBe("Parent");

        reader.ReadBsonType(); // Document
        reader.ReadElementHeader().ShouldBe("child");

        reader.ReadDocumentSize();
        reader.ReadBsonType(); // String
        reader.ReadElementHeader().ShouldBe("name");
        reader.ReadString().ShouldBe("Child");

        reader.ReadBsonType(); // Int32
        reader.ReadElementHeader().ShouldBe("value");
        reader.ReadInt32().ShouldBe(42);
    }
}
