// Reference: golang/nats-server/server/filestore.go
// Go wire format: filestore.go:6720-6724 (writeMsgRecordLocked)
// Go decode: filestore.go:8180-8250 (msgFromBufEx)
// Go size calc: filestore.go:8770-8777 (fileStoreMsgSizeRaw)
//
// These tests verify the .NET MessageRecord binary encoder/decoder that matches
// the Go message block record format:
//   [1:flags][varint:subj_len][N:subject][varint:hdr_len][M:headers][varint:payload_len][P:payload][8:sequence_LE][8:checksum]

using NATS.Server.JetStream.Storage;
using System.Text;

namespace NATS.Server.JetStream.Tests.JetStream.Storage;

public sealed class MessageRecordTests
{
    // Go: writeMsgRecordLocked / msgFromBufEx — basic round-trip
    [Fact]
    public void RoundTrip_SimpleMessage()
    {
        var record = new MessageRecord
        {
            Sequence = 42,
            Subject = "foo.bar",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = Encoding.UTF8.GetBytes("hello world"),
            Timestamp = 1_700_000_000_000_000_000L,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);
        var decoded = MessageRecord.Decode(encoded);

        decoded.Sequence.ShouldBe(record.Sequence);
        decoded.Subject.ShouldBe(record.Subject);
        decoded.Headers.Length.ShouldBe(0);
        decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("hello world"));
        decoded.Timestamp.ShouldBe(record.Timestamp);
        decoded.Deleted.ShouldBe(false);
    }

    // Go: writeMsgRecordLocked with headers — hdr_len(4) hdr present in record
    [Fact]
    public void RoundTrip_WithHeaders()
    {
        var headers = "NATS/1.0\r\nX-Test: value\r\n\r\n"u8.ToArray();
        var record = new MessageRecord
        {
            Sequence = 99,
            Subject = "test.headers",
            Headers = headers,
            Payload = Encoding.UTF8.GetBytes("payload with headers"),
            Timestamp = 1_700_000_000_000_000_000L,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);
        var decoded = MessageRecord.Decode(encoded);

        decoded.Sequence.ShouldBe(99UL);
        decoded.Subject.ShouldBe("test.headers");
        decoded.Headers.ToArray().ShouldBe(headers);
        decoded.Payload.ToArray().ShouldBe(Encoding.UTF8.GetBytes("payload with headers"));
        decoded.Timestamp.ShouldBe(record.Timestamp);
    }

    // Verify that the last 8 bytes of the encoded record contain a nonzero XxHash64 checksum.
    [Fact]
    public void Encode_SetsChecksumInTrailer()
    {
        var record = new MessageRecord
        {
            Sequence = 1,
            Subject = "checksum.test",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = Encoding.UTF8.GetBytes("data"),
            Timestamp = 0,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);

        // Last 8 bytes are the checksum — should be nonzero for any non-trivial message.
        var checksumBytes = encoded.AsSpan(encoded.Length - 8);
        var checksum = BitConverter.ToUInt64(checksumBytes);
        checksum.ShouldNotBe(0UL);
    }

    // Flip a byte in the encoded data and verify decode throws InvalidDataException.
    [Fact]
    public void Decode_DetectsCorruptChecksum()
    {
        var record = new MessageRecord
        {
            Sequence = 7,
            Subject = "corrupt",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = Encoding.UTF8.GetBytes("will be corrupted"),
            Timestamp = 0,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);

        // Flip a byte in the payload area (not the checksum itself).
        var corrupted = encoded.ToArray();
        corrupted[corrupted.Length / 2] ^= 0xFF;

        Should.Throw<InvalidDataException>(() => MessageRecord.Decode(corrupted));
    }

    // Go: varint encoding matches protobuf convention — high-bit continuation.
    [Theory]
    [InlineData(0UL)]
    [InlineData(1UL)]
    [InlineData(127UL)]
    [InlineData(128UL)]
    [InlineData(16383UL)]
    [InlineData(16384UL)]
    public void Varint_RoundTrip(ulong value)
    {
        Span<byte> buf = stackalloc byte[10];
        var written = MessageRecord.WriteVarint(buf, value);
        written.ShouldBeGreaterThan(0);

        var (decoded, bytesRead) = MessageRecord.ReadVarint(buf);
        decoded.ShouldBe(value);
        bytesRead.ShouldBe(written);
    }

    // Go: ebit (1 << 63) marks deleted/erased messages in the sequence field.
    [Fact]
    public void RoundTrip_DeletedFlag()
    {
        var record = new MessageRecord
        {
            Sequence = 100,
            Subject = "deleted.msg",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = ReadOnlyMemory<byte>.Empty,
            Timestamp = 0,
            Deleted = true,
        };

        var encoded = MessageRecord.Encode(record);
        var decoded = MessageRecord.Decode(encoded);

        decoded.Deleted.ShouldBe(true);
        decoded.Sequence.ShouldBe(100UL);
        decoded.Subject.ShouldBe("deleted.msg");
    }

    // Edge case: empty payload should encode and decode cleanly.
    [Fact]
    public void RoundTrip_EmptyPayload()
    {
        var record = new MessageRecord
        {
            Sequence = 1,
            Subject = "empty",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = ReadOnlyMemory<byte>.Empty,
            Timestamp = 0,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);
        var decoded = MessageRecord.Decode(encoded);

        decoded.Subject.ShouldBe("empty");
        decoded.Payload.Length.ShouldBe(0);
        decoded.Headers.Length.ShouldBe(0);
    }

    // Verify 64KB payload works (large payload stress test).
    [Fact]
    public void RoundTrip_LargePayload()
    {
        var payload = new byte[64 * 1024];
        Random.Shared.NextBytes(payload);

        var record = new MessageRecord
        {
            Sequence = 999_999,
            Subject = "large.payload.test",
            Headers = ReadOnlyMemory<byte>.Empty,
            Payload = payload,
            Timestamp = long.MaxValue,
            Deleted = false,
        };

        var encoded = MessageRecord.Encode(record);
        var decoded = MessageRecord.Decode(encoded);

        decoded.Sequence.ShouldBe(999_999UL);
        decoded.Subject.ShouldBe("large.payload.test");
        decoded.Payload.ToArray().ShouldBe(payload);
        decoded.Timestamp.ShouldBe(long.MaxValue);
    }
}
