using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using ZB.MOM.WW.CBDD.Bson;
namespace ZB.MOM.WW.CBDD.Tests;
public class BsonDocumentAndBufferWriterTests
{
///
/// Verifies BSON document creation and typed retrieval roundtrip.
///
[Fact]
public void BsonDocument_Create_And_TryGet_RoundTrip()
{
var keyMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
var reverseMap = new ConcurrentDictionary();
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 var name).ShouldBeTrue();
name.ShouldBe("Alice");
wrapped.TryGetInt32("age", out var age).ShouldBeTrue();
age.ShouldBe(32);
wrapped.TryGetObjectId("_id", out var id).ShouldBeTrue();
id.ShouldBe(expectedId);
var reader = wrapped.GetReader();
reader.ReadDocumentSize().ShouldBeGreaterThan(0);
}
///
/// Verifies typed getters return false for missing fields and type mismatches.
///
[Fact]
public void BsonDocument_TryGet_Should_Return_False_For_Missing_Or_Wrong_Type()
{
var keyMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
var reverseMap = new ConcurrentDictionary();
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();
}
///
/// Verifies the BSON document builder grows its internal buffer for large documents.
///
[Fact]
public void BsonDocumentBuilder_Should_Grow_Buffer_When_Document_Is_Large()
{
var keyMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
var reverseMap = new ConcurrentDictionary();
for (ushort i = 1; i <= 180; i++)
{
var key = $"k{i}";
RegisterKey(keyMap, reverseMap, i, key);
}
var builder = new BsonDocumentBuilder(keyMap);
for (int 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 var value).ShouldBeTrue();
value.ShouldBe(180);
}
///
/// Verifies BSON buffer writer emits expected nested document and array layout.
///
[Fact]
public void BsonBufferWriter_Should_Write_Nested_Document_And_Array()
{
var output = new ArrayBufferWriter();
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;
var bytes = output.WrittenSpan.ToArray();
PatchDocumentSize(bytes, childSizePos, childEnd);
PatchDocumentSize(bytes, arraySizePos, arrayEnd);
PatchDocumentSize(bytes, rootSizePos, rootEnd);
var reader = new BsonSpanReader(bytes, new ConcurrentDictionary());
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);
}
///
/// Verifies single-byte and C-string span reads operate correctly.
///
[Fact]
public void BsonSpanReader_ReadByte_And_ReadCStringSpan_Should_Work()
{
var singleByteReader = new BsonSpanReader(new byte[] { 0x2A }, new ConcurrentDictionary());
singleByteReader.ReadByte().ShouldBe((byte)0x2A);
var cstring = Encoding.UTF8.GetBytes("hello\0");
var cstringReader = new BsonSpanReader(cstring, new ConcurrentDictionary());
var destination = new char[16];
var written = cstringReader.ReadCString(destination);
new string(destination, 0, written).ShouldBe("hello");
}
private static void RegisterKey(
ConcurrentDictionary keyMap,
ConcurrentDictionary 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);
}
}