using System;
using System.Collections.Generic;
using AcDream.Core.Net.Messages;
using Xunit;
namespace AcDream.Core.Net.Tests.Messages;
///
/// Phase I.6: codec round-trip tests for all
/// three payload variants and the UTF-16LE Turbine string codec.
///
///
/// Golden fixtures from holtburger
/// references/holtburger/crates/holtburger-protocol/src/messages/chat/turbine.rs
/// lines 555-639 — generated by ACE's
/// SyntheticProtocolTests.GenerateTurbineChatFixtures.
///
///
public sealed class TurbineChatTests
{
// ──────────────────────────────────────────────────────────────
// Round-trip fixtures (parse, then re-serialise, then parse again)
// ──────────────────────────────────────────────────────────────
[Fact]
public void TryParse_EventSendToRoom_GoldenFixture()
{
byte[] fixture = HexDecode(
"DEF700005E000000010000000100000001000000" +
"B5000B0001000000B5000B00000000003E000000" +
"020000000541006C006900630065000B68006500" +
"6C006C006F00200077006F0072006C0064000C00" +
"0000010000500000000002000000");
var parsed = TurbineChat.TryParse(fixture);
Assert.NotNull(parsed);
Assert.Equal(TurbineChat.BlobType.EventBinary, parsed!.Value.BlobType);
Assert.Equal(TurbineChat.DispatchType.SendToRoomByName, parsed.Value.DispatchType);
Assert.Equal(1u, parsed.Value.TargetType);
Assert.Equal(0x000B00B5u, parsed.Value.TargetId);
Assert.Equal(1u, parsed.Value.TransportType);
Assert.Equal(0x000B00B5u, parsed.Value.TransportId);
Assert.Equal(0u, parsed.Value.Cookie);
var ev = Assert.IsType(parsed.Value.Body);
Assert.Equal(2u, ev.RoomId); // TurbineChatChannel.General
Assert.Equal("Alice", ev.SenderName);
Assert.Equal("hello world", ev.Message);
Assert.Equal(0x0Cu, ev.ExtraDataSize);
Assert.Equal(0x50000001u, ev.SenderId);
Assert.Equal(0, ev.HResult);
Assert.Equal(2u, ev.ChatType); // TurbineChatType.General
}
[Fact]
public void Build_EventSendToRoom_RoundTripsThroughTryParse()
{
byte[] built = TurbineChat.Build(
blobType: TurbineChat.BlobType.EventBinary,
dispatchType: TurbineChat.DispatchType.SendToRoomByName,
targetType: 1u,
targetId: 0x000B00B5u,
transportType: 1u,
transportId: 0x000B00B5u,
cookie: 0u,
payload: new TurbineChat.Payload.EventSendToRoom(
RoomId: 2u,
SenderName: "Alice",
Message: "hello world",
ExtraDataSize: 0x0Cu,
SenderId: 0x50000001u,
HResult: 0,
ChatType: 2u));
var parsed = TurbineChat.TryParse(built);
Assert.NotNull(parsed);
var ev = Assert.IsType(parsed!.Value.Body);
Assert.Equal("Alice", ev.SenderName);
Assert.Equal("hello world", ev.Message);
}
[Fact]
public void TryParse_Response_GoldenFixture()
{
byte[] fixture = HexDecode(
"DEF7000038000000050000000100000001000000" +
"B5000B0001000000B5000B00000000001800000007000000" +
"020000000200000000000000");
var parsed = TurbineChat.TryParse(fixture);
Assert.NotNull(parsed);
Assert.Equal(TurbineChat.BlobType.ResponseBinary, parsed!.Value.BlobType);
Assert.Equal(TurbineChat.DispatchType.SendToRoomByName, parsed.Value.DispatchType);
var resp = Assert.IsType(parsed.Value.Body);
Assert.Equal(7u, resp.ContextId);
Assert.Equal(2u, resp.ResponseId);
Assert.Equal(2u, resp.MethodId);
Assert.Equal(0, resp.HResult);
}
[Fact]
public void Build_Response_RoundTripsThroughTryParse()
{
byte[] built = TurbineChat.Build(
blobType: TurbineChat.BlobType.ResponseBinary,
dispatchType: TurbineChat.DispatchType.SendToRoomByName,
targetType: 1u, targetId: 0x000B00B5u,
transportType: 1u, transportId: 0x000B00B5u,
cookie: 0u,
payload: new TurbineChat.Payload.Response(
ContextId: 7u, ResponseId: 2u, MethodId: 2u, HResult: 0));
var parsed = TurbineChat.TryParse(built);
Assert.NotNull(parsed);
var resp = Assert.IsType(parsed!.Value.Body);
Assert.Equal(7u, resp.ContextId);
}
[Fact]
public void TryParse_RequestSendToRoomById_GoldenFixture()
{
byte[] fixture = HexDecode(
"DEF700005D000000030000000200000001000000" +
"00000000000000000000000000000000" +
"3D000000" +
"07000000020000000200000002000000" +
"0A7400720061006400650020007300700061006D00" +
"0C000000010000500000000003000000");
var parsed = TurbineChat.TryParse(fixture);
Assert.NotNull(parsed);
Assert.Equal(TurbineChat.BlobType.RequestBinary, parsed!.Value.BlobType);
Assert.Equal(TurbineChat.DispatchType.SendToRoomById, parsed.Value.DispatchType);
var req = Assert.IsType(parsed.Value.Body);
Assert.Equal(7u, req.ContextId);
Assert.Equal(2u, req.RoomId); // TurbineChatChannel.General
Assert.Equal("trade spam", req.Message);
Assert.Equal(0x0Cu, req.ExtraDataSize);
Assert.Equal(0x50000001u, req.SenderId);
Assert.Equal(0, req.HResult);
Assert.Equal(3u, req.ChatType); // TurbineChatType.Trade
}
[Fact]
public void Build_RequestSendToRoomById_RoundTripsThroughTryParse()
{
byte[] built = TurbineChat.Build(
blobType: TurbineChat.BlobType.RequestBinary,
dispatchType: TurbineChat.DispatchType.SendToRoomById,
targetType: 1u, targetId: 0u,
transportType: 0u, transportId: 0u,
cookie: 0u,
payload: new TurbineChat.Payload.RequestSendToRoomById(
ContextId: 7u,
RoomId: 2u,
Message: "trade spam",
ExtraDataSize: 0x0Cu,
SenderId: 0x50000001u,
HResult: 0,
ChatType: 3u));
var parsed = TurbineChat.TryParse(built);
Assert.NotNull(parsed);
var req = Assert.IsType(parsed!.Value.Body);
Assert.Equal("trade spam", req.Message);
Assert.Equal(2u, req.RoomId);
}
[Fact]
public void TryParse_RequestRejectsNonAceRequestIds()
{
// Per turbine.rs:642-651: ACE's RequestSendToRoomById is the only
// (response_id=2, method_id=2) pair. Other ids are rejected.
byte[] fixture = HexDecode(
"DEF700005D000000030000000200000001000000" +
"00000000000000000000000000000000" +
"3D000000" +
"07000000020000000200000002000000" +
"0A7400720061006400650020007300700061006D00" +
"0C000000010000500000000003000000");
// Mutate response_id (offset 4 + 36 + 4 = 44) to 3 — must reject.
BitConverter.GetBytes(3u).CopyTo(fixture, 44);
Assert.Null(TurbineChat.TryParse(fixture));
}
// ──────────────────────────────────────────────────────────────
// UTF-16LE Turbine string codec
// ──────────────────────────────────────────────────────────────
[Fact]
public void TurbineString_ShortPrefix_RoundTrips()
{
var buf = new List();
TurbineChat.WriteTurbineString(buf, "Hello");
// 1-byte prefix, then 5 UTF-16LE code units = 5 + 1 = 11 bytes.
Assert.Equal(11, buf.Count);
Assert.Equal(5, buf[0]); // length prefix
Assert.Equal(0, 0x80 & buf[0]); // high bit clear
byte[] data = buf.ToArray();
int pos = 0;
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
Assert.Equal("Hello", roundTrip);
Assert.Equal(data.Length, pos);
}
[Fact]
public void TurbineString_LongPrefix_RoundTrips()
{
// 200 chars > 0x80 → 2-byte prefix form.
string s = new string('a', 200);
var buf = new List();
TurbineChat.WriteTurbineString(buf, s);
// Prefix: 2 bytes; high bit of first byte set.
Assert.NotEqual(0, buf[0] & 0x80);
// Decoded length = ((b0 & 0x7F) << 8) | b1
int decodedLen = ((buf[0] & 0x7F) << 8) | buf[1];
Assert.Equal(200, decodedLen);
Assert.Equal(2 + 200 * 2, buf.Count);
byte[] data = buf.ToArray();
int pos = 0;
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
Assert.Equal(s, roundTrip);
Assert.Equal(data.Length, pos);
}
[Fact]
public void TurbineString_NonAscii_RoundTripsAsUtf16LE()
{
// 'Café' has 4 UTF-16 code units, last with 0x00E9 — verifies
// we're using UTF-16LE rather than CP1252 like the rest of the
// chat layer.
const string s = "Café";
var buf = new List();
TurbineChat.WriteTurbineString(buf, s);
// Bytes: prefix(1) + UTF-16LE C-a-f-é = 1 + 8 = 9 bytes.
Assert.Equal(9, buf.Count);
Assert.Equal(4, buf[0]);
// 'é' = 0xE9 0x00 in UTF-16LE.
Assert.Equal(0xE9, buf[1 + 6]);
Assert.Equal(0x00, buf[1 + 7]);
byte[] data = buf.ToArray();
int pos = 0;
string roundTrip = TurbineChat.ReadTurbineString(data, ref pos);
Assert.Equal(s, roundTrip);
}
[Fact]
public void TurbineString_EmptyString_RoundTrips()
{
var buf = new List();
TurbineChat.WriteTurbineString(buf, "");
Assert.Single(buf);
Assert.Equal(0, buf[0]);
byte[] data = buf.ToArray();
int pos = 0;
Assert.Equal("", TurbineChat.ReadTurbineString(data, ref pos));
Assert.Equal(1, pos);
}
// ──────────────────────────────────────────────────────────────
// Helpers
// ──────────────────────────────────────────────────────────────
private static byte[] HexDecode(string hex)
{
if ((hex.Length & 1) != 0) throw new ArgumentException("hex length must be even");
byte[] result = new byte[hex.Length / 2];
for (int i = 0; i < result.Length; i++)
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
return result;
}
}