using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using ZB.MOM.WW.CBDD.Bson;
using ZB.MOM.WW.CBDD.Bson.IO;

namespace ZB.MOM.WW.CBDD.Tests;

public class BsonDocumentAndBufferWriterTests
{
    /// <summary>
    ///     Verifies BSON document creation and typed retrieval roundtrip.
    /// </summary>
    [Fact]
    public void BsonDocument_Create_And_TryGet_RoundTrip()
    {
        var keyMap = new ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
        var reverseMap = new ConcurrentDictionary<ushort, string>();

        RegisterKey(keyMap, reverseMap, 1, "name");
        RegisterKey(keyMap, reverseMap, 2, "age");
        RegisterKey(keyMap, reverseMap, 3, "_id");

        var expectedId = ObjectId.NewObjectId();

        var doc = BsonDocument.Create(keyMap, b =>
        {
            b.AddString("name", "Alice");
            b.AddInt32("age", 32);
            b.AddObjectId("_id", expectedId);
        });

        var wrapped = new BsonDocument(doc.RawData.ToArray(), reverseMap);

        wrapped.TryGetString("name", out string? name).ShouldBeTrue();
        name.ShouldBe("Alice");

        wrapped.TryGetInt32("age", out int age).ShouldBeTrue();
        age.ShouldBe(32);

        wrapped.TryGetObjectId("_id", out var id).ShouldBeTrue();
        id.ShouldBe(expectedId);

        var reader = wrapped.GetReader();
        reader.ReadDocumentSize().ShouldBeGreaterThan(0);
    }

    /// <summary>
    ///     Verifies typed getters return false for missing fields and type mismatches.
    /// </summary>
    [Fact]
    public void BsonDocument_TryGet_Should_Return_False_For_Missing_Or_Wrong_Type()
    {
        var keyMap = new ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
        var reverseMap = new ConcurrentDictionary<ushort, string>();

        RegisterKey(keyMap, reverseMap, 1, "name");
        RegisterKey(keyMap, reverseMap, 2, "age");

        var doc = BsonDocument.Create(keyMap, b =>
        {
            b.AddString("name", "Bob");
            b.AddInt32("age", 28);
        });

        var wrapped = new BsonDocument(doc.RawData.ToArray(), reverseMap);

        wrapped.TryGetInt32("name", out _).ShouldBeFalse();
        wrapped.TryGetString("missing", out _).ShouldBeFalse();
        wrapped.TryGetObjectId("age", out _).ShouldBeFalse();
    }

    /// <summary>
    ///     Verifies the BSON document builder grows its internal buffer for large documents.
    /// </summary>
    [Fact]
    public void BsonDocumentBuilder_Should_Grow_Buffer_When_Document_Is_Large()
    {
        var keyMap = new ConcurrentDictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
        var reverseMap = new ConcurrentDictionary<ushort, string>();

        for (ushort i = 1; i <= 180; i++)
        {
            var key = $"k{i}";
            RegisterKey(keyMap, reverseMap, i, key);
        }

        var builder = new BsonDocumentBuilder(keyMap);
        for (var i = 1; i <= 180; i++) builder.AddInt32($"k{i}", i);

        var doc = builder.Build();
        doc.Size.ShouldBeGreaterThan(1024);

        var wrapped = new BsonDocument(doc.RawData.ToArray(), reverseMap);
        wrapped.TryGetInt32("k180", out int value).ShouldBeTrue();
        value.ShouldBe(180);
    }

    /// <summary>
    ///     Verifies BSON buffer writer emits expected nested document and array layout.
    /// </summary>
    [Fact]
    public void BsonBufferWriter_Should_Write_Nested_Document_And_Array()
    {
        var output = new ArrayBufferWriter<byte>();
        var writer = new BsonBufferWriter(output);

        int rootSizePos = writer.BeginDocument();

        int childSizePos = writer.BeginDocument("child");
        writer.WriteString("name", "nested");
        writer.WriteBoolean("active", true);
        writer.EndDocument(childSizePos);
        int childEnd = writer.Position;

        int arraySizePos = writer.BeginArray("nums");
        writer.WriteInt32("0", 1);
        writer.WriteInt32("1", 2);
        writer.EndArray(arraySizePos);
        int arrayEnd = writer.Position;

        writer.EndDocument(rootSizePos);
        int rootEnd = writer.Position;

        byte[] bytes = output.WrittenSpan.ToArray();
        PatchDocumentSize(bytes, childSizePos, childEnd);
        PatchDocumentSize(bytes, arraySizePos, arrayEnd);
        PatchDocumentSize(bytes, rootSizePos, rootEnd);

        var reader = new BsonSpanReader(bytes, new ConcurrentDictionary<ushort, string>());

        reader.ReadDocumentSize().ShouldBe(bytes.Length);

        reader.ReadBsonType().ShouldBe(BsonType.Document);
        reader.ReadCString().ShouldBe("child");
        reader.ReadDocumentSize().ShouldBeGreaterThan(8);

        reader.ReadBsonType().ShouldBe(BsonType.String);
        reader.ReadCString().ShouldBe("name");
        reader.ReadString().ShouldBe("nested");

        reader.ReadBsonType().ShouldBe(BsonType.Boolean);
        reader.ReadCString().ShouldBe("active");
        reader.ReadBoolean().ShouldBeTrue();
        reader.ReadBsonType().ShouldBe(BsonType.EndOfDocument);

        reader.ReadBsonType().ShouldBe(BsonType.Array);
        reader.ReadCString().ShouldBe("nums");
        reader.ReadDocumentSize().ShouldBeGreaterThan(8);

        reader.ReadBsonType().ShouldBe(BsonType.Int32);
        reader.ReadCString().ShouldBe("0");
        reader.ReadInt32().ShouldBe(1);

        reader.ReadBsonType().ShouldBe(BsonType.Int32);
        reader.ReadCString().ShouldBe("1");
        reader.ReadInt32().ShouldBe(2);
        reader.ReadBsonType().ShouldBe(BsonType.EndOfDocument);

        reader.ReadBsonType().ShouldBe(BsonType.EndOfDocument);
    }

    /// <summary>
    ///     Verifies single-byte and C-string span reads operate correctly.
    /// </summary>
    [Fact]
    public void BsonSpanReader_ReadByte_And_ReadCStringSpan_Should_Work()
    {
        var singleByteReader = new BsonSpanReader(new byte[] { 0x2A }, new ConcurrentDictionary<ushort, string>());
        singleByteReader.ReadByte().ShouldBe((byte)0x2A);

        byte[] cstring = Encoding.UTF8.GetBytes("hello\0");
        var cstringReader = new BsonSpanReader(cstring, new ConcurrentDictionary<ushort, string>());
        var destination = new char[16];
        int written = cstringReader.ReadCString(destination);

        new string(destination, 0, written).ShouldBe("hello");
    }

    private static void RegisterKey(
        ConcurrentDictionary<string, ushort> keyMap,
        ConcurrentDictionary<ushort, string> reverseMap,
        ushort id,
        string key)
    {
        keyMap[key] = id;
        reverseMap[id] = key;
    }

    private static void PatchDocumentSize(byte[] output, int sizePosition, int endPosition)
    {
        BinaryPrimitives.WriteInt32LittleEndian(output.AsSpan(sizePosition, 4), endPosition - sizePosition);
    }
}