{$mode objfpc}{$H+}

/// <summary>
/// test_MQTTPackets — FPCUnit tests for uCICMQTTProtocol pure functions.
///
/// Tests cover variable-length integer encoding/decoding, MQTT UTF-8 string
/// encoding, and all packet-builder functions for both MQTT 3.1.1 and 5.0.
/// No network or I/O is required.
/// </summary>
unit test_MQTTPackets;

interface

uses
  Classes, SysUtils,
  fpcunit, testregistry,
  uCICMQTTProtocol;

type
  TMQTTPacketTests = class(TTestCase)
  private
    // Compares two TBytes arrays and fails with a descriptive message if they differ.
    procedure AssertBytesEqual(const AExpected, AActual: TBytes;
      const AContext: String = '');
    // Returns a hex dump of a TBytes array, e.g. '$10 $82 $00'.
    function BytesToHex(const AData: TBytes): String;
  published
    // --- VarInt encoding ---
    procedure TestEncodeVarInt_Zero;
    procedure TestEncodeVarInt_127;
    procedure TestEncodeVarInt_128;
    procedure TestEncodeVarInt_16383;
    procedure TestEncodeVarInt_Max;

    // --- VarInt decoding ---
    procedure TestDecodeVarInt_RoundTrip;
    procedure TestDecodeVarInt_SingleByte;
    procedure TestDecodeVarInt_TwoBytes;
    procedure TestDecodeVarInt_Truncated;
    procedure TestDecodeVarInt_AdvancesOffset;

    // --- MQTT string encoding ---
    procedure TestEncodeMQTTString_Simple;
    procedure TestEncodeMQTTString_Empty;
    procedure TestEncodeMQTTString_Length;

    // --- WrapWithFixedHeader ---
    procedure TestWrapWithFixedHeader_Connect;
    procedure TestWrapWithFixedHeader_PingReq;

    // --- CONNECT packet ---
    procedure TestBuildConnectPacket_MQTT5;
    procedure TestBuildConnectPacket_MQTT311;
    procedure TestBuildConnectPacket_WithAuth;
    procedure TestBuildConnectPacket_SessionExpiry;

    // --- PUBLISH packet ---
    procedure TestBuildPublishPacket_QoS0;
    procedure TestBuildPublishPacket_QoS1;
    procedure TestBuildPublishPacket_Retain;
    procedure TestBuildPublishPacket_MQTT5HasProperties;

    // --- SUBSCRIBE packet ---
    procedure TestBuildSubscribePacket;
    procedure TestBuildSubscribePacket_MQTT5HasProperties;

    // --- PINGREQ ---
    procedure TestBuildPingReqPacket;

    // --- DISCONNECT ---
    procedure TestBuildDisconnectPacket_MQTT5;
    procedure TestBuildDisconnectPacket_MQTT311;
  end;

implementation

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

function TMQTTPacketTests.BytesToHex(const AData: TBytes): String;
var
  I: Integer;
begin
  Result := '';
  for I := 0 to High(AData) do
  begin
    if I > 0 then Result := Result + ' ';
    Result := Result + Format('$%2.2X', [AData[I]]);
  end;
  if Result = '' then Result := '(empty)';
end;

procedure TMQTTPacketTests.AssertBytesEqual(const AExpected, AActual: TBytes;
  const AContext: String = '');
var
  I:   Integer;
  Ctx: String;
begin
  if AContext <> '' then
    Ctx := ' [' + AContext + ']'
  else
    Ctx := '';

  AssertEquals('Length mismatch' + Ctx +
    ': expected ' + IntToStr(Length(AExpected)) +
    ' bytes (' + BytesToHex(AExpected) + ')' +
    ', got ' + IntToStr(Length(AActual)) +
    ' bytes (' + BytesToHex(AActual) + ')',
    Length(AExpected), Length(AActual));

  for I := 0 to High(AExpected) do
    AssertEquals(Format('Byte[%d]%s expected $%2.2X got $%2.2X',
      [I, Ctx, AExpected[I], AActual[I]]),
      Integer(AExpected[I]), Integer(AActual[I]));
end;

// ---------------------------------------------------------------------------
// VarInt encoding
// ---------------------------------------------------------------------------

// MQTT spec Table 1-1: 0 encodes as a single byte $00.
procedure TMQTTPacketTests.TestEncodeVarInt_Zero;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeVarInt(0);
  SetLength(Exp, 1);
  Exp[0] := $00;
  AssertBytesEqual(Exp, Got, 'EncodeVarInt(0)');
end;

// 127 is the largest single-byte value: $7F.
procedure TMQTTPacketTests.TestEncodeVarInt_127;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeVarInt(127);
  SetLength(Exp, 1);
  Exp[0] := $7F;
  AssertBytesEqual(Exp, Got, 'EncodeVarInt(127)');
end;

// 128 requires two bytes: $80 $01.
procedure TMQTTPacketTests.TestEncodeVarInt_128;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeVarInt(128);
  SetLength(Exp, 2);
  Exp[0] := $80;
  Exp[1] := $01;
  AssertBytesEqual(Exp, Got, 'EncodeVarInt(128)');
end;

// 16383 is the largest two-byte value: $FF $7F.
procedure TMQTTPacketTests.TestEncodeVarInt_16383;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeVarInt(16383);
  SetLength(Exp, 2);
  Exp[0] := $FF;
  Exp[1] := $7F;
  AssertBytesEqual(Exp, Got, 'EncodeVarInt(16383)');
end;

// 268435455 is the MQTT maximum (4 bytes): $FF $FF $FF $7F.
procedure TMQTTPacketTests.TestEncodeVarInt_Max;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeVarInt(268435455);
  SetLength(Exp, 4);
  Exp[0] := $FF;
  Exp[1] := $FF;
  Exp[2] := $FF;
  Exp[3] := $7F;
  AssertBytesEqual(Exp, Got, 'EncodeVarInt(268435455)');
end;

// ---------------------------------------------------------------------------
// VarInt decoding
// ---------------------------------------------------------------------------

// Encoding and then decoding a set of representative values must recover
// the originals.
procedure TMQTTPacketTests.TestDecodeVarInt_RoundTrip;
const
  TestValues: array[0..5] of Cardinal = (0, 1, 127, 128, 16383, 268435455);
var
  I:       Integer;
  Encoded: TBytes;
  Decoded: Cardinal;
  Offset:  Integer;
begin
  for I := Low(TestValues) to High(TestValues) do
  begin
    Encoded := EncodeVarInt(TestValues[I]);
    Offset  := 0;
    AssertTrue(Format('DecodeVarInt(%u) returned False', [TestValues[I]]),
      DecodeVarInt(Encoded, Offset, Decoded));
    AssertEquals(Format('DecodeVarInt(%u) round-trip mismatch', [TestValues[I]]),
      Int64(TestValues[I]), Int64(Decoded));
    AssertEquals(Format('DecodeVarInt(%u) offset not at end', [TestValues[I]]),
      Length(Encoded), Offset);
  end;
end;

// A single-byte value such as 42 decodes to exactly 42.
procedure TMQTTPacketTests.TestDecodeVarInt_SingleByte;
var
  Data:    TBytes;
  Val:     Cardinal;
  Offset:  Integer;
begin
  SetLength(Data, 1);
  Data[0] := 42;
  Offset  := 0;
  AssertTrue('DecodeVarInt single byte', DecodeVarInt(Data, Offset, Val));
  AssertEquals('Value', 42, Integer(Val));
  AssertEquals('Offset after', 1, Offset);
end;

// $80 $01 decodes to 128.
procedure TMQTTPacketTests.TestDecodeVarInt_TwoBytes;
var
  Data:   TBytes;
  Val:    Cardinal;
  Offset: Integer;
begin
  SetLength(Data, 2);
  Data[0] := $80;
  Data[1] := $01;
  Offset  := 0;
  AssertTrue('DecodeVarInt two bytes', DecodeVarInt(Data, Offset, Val));
  AssertEquals('Value', 128, Integer(Val));
  AssertEquals('Offset after', 2, Offset);
end;

// Truncated data (continuation bit set but no next byte) must return False.
procedure TMQTTPacketTests.TestDecodeVarInt_Truncated;
var
  Data:   TBytes;
  Val:    Cardinal;
  Offset: Integer;
begin
  SetLength(Data, 1);
  Data[0] := $80; // continuation bit set, but no following byte
  Offset  := 0;
  AssertFalse('DecodeVarInt truncated should return False',
    DecodeVarInt(Data, Offset, Val));
end;

// DecodeVarInt must advance AOffset past all consumed bytes and leave any
// trailing bytes untouched.
procedure TMQTTPacketTests.TestDecodeVarInt_AdvancesOffset;
var
  Data:   TBytes;
  Val:    Cardinal;
  Offset: Integer;
begin
  // $7F = 127 (1 byte), followed by sentinel $FF
  SetLength(Data, 2);
  Data[0] := $7F;
  Data[1] := $FF;
  Offset  := 0;
  AssertTrue('DecodeVarInt advances offset', DecodeVarInt(Data, Offset, Val));
  AssertEquals('Value', 127, Integer(Val));
  AssertEquals('Offset', 1, Offset); // $FF not consumed
end;

// ---------------------------------------------------------------------------
// MQTT string encoding
// ---------------------------------------------------------------------------

// EncodeMQTTString('hello') = [$00, $05, 'h', 'e', 'l', 'l', 'o'].
procedure TMQTTPacketTests.TestEncodeMQTTString_Simple;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeMQTTString('hello');
  SetLength(Exp, 7);
  Exp[0] := $00;
  Exp[1] := $05;
  Exp[2] := Ord('h');
  Exp[3] := Ord('e');
  Exp[4] := Ord('l');
  Exp[5] := Ord('l');
  Exp[6] := Ord('o');
  AssertBytesEqual(Exp, Got, 'EncodeMQTTString(hello)');
end;

// EncodeMQTTString('') = [$00, $00].
procedure TMQTTPacketTests.TestEncodeMQTTString_Empty;
var
  Got: TBytes;
  Exp: TBytes;
begin
  Got := EncodeMQTTString('');
  SetLength(Exp, 2);
  Exp[0] := $00;
  Exp[1] := $00;
  AssertBytesEqual(Exp, Got, 'EncodeMQTTString(empty)');
end;

// Length of encoded result must always be 2 + len(UTF-8 bytes).
procedure TMQTTPacketTests.TestEncodeMQTTString_Length;
var
  S:   String;
  Got: TBytes;
begin
  S   := 'test/topic';
  Got := EncodeMQTTString(S);
  // ASCII-only: UTF-8 byte count equals character count
  AssertEquals('Encoded length', 2 + Length(S), Length(Got));
  AssertEquals('High byte of length prefix', 0, Integer(Got[0]));
  AssertEquals('Low byte of length prefix',  Length(S), Integer(Got[1]));
end;

// ---------------------------------------------------------------------------
// WrapWithFixedHeader
// ---------------------------------------------------------------------------

// WrapWithFixedHeader(MQTT_CONNECT=1, 0, []) should produce [$10, $00].
procedure TMQTTPacketTests.TestWrapWithFixedHeader_Connect;
var
  Content: TBytes;
  Got:     TBytes;
  Exp:     TBytes;
begin
  SetLength(Content, 0);
  Got := WrapWithFixedHeader(MQTT_CONNECT, 0, Content);
  // (1 shl 4) or 0 = $10; remaining length 0 = $00
  SetLength(Exp, 2);
  Exp[0] := $10;
  Exp[1] := $00;
  AssertBytesEqual(Exp, Got, 'WrapWithFixedHeader CONNECT empty');
end;

// WrapWithFixedHeader(MQTT_PINGREQ=12, 0, []) = [$C0, $00].
procedure TMQTTPacketTests.TestWrapWithFixedHeader_PingReq;
var
  Content: TBytes;
  Got:     TBytes;
  Exp:     TBytes;
begin
  SetLength(Content, 0);
  Got := WrapWithFixedHeader(MQTT_PINGREQ, 0, Content);
  // (12 shl 4) or 0 = $C0; remaining length 0 = $00
  SetLength(Exp, 2);
  Exp[0] := $C0;
  Exp[1] := $00;
  AssertBytesEqual(Exp, Got, 'WrapWithFixedHeader PINGREQ');
end;

// ---------------------------------------------------------------------------
// CONNECT packet
// ---------------------------------------------------------------------------

// MQTT 5.0 CONNECT: first byte $10, protocol name 'MQTT', level 5.
// Packet layout (no auth, CleanStart=True, KeepAlive=60, ClientID='test'):
//   [$10] [remlen] [$00,$04,'M','Q','T','T'] [$05] [flags] [ka_hi] [ka_lo]
//   [prop_len=0] [$00,$04,'t','e','s','t']
procedure TMQTTPacketTests.TestBuildConnectPacket_MQTT5;
var
  Pkt: TBytes;
begin
  Pkt := BuildConnectPacket(MQTT_PROTOCOL_LEVEL_50, True, 60, 'test');

  AssertTrue('Packet must not be empty', Length(Pkt) > 2);
  // Fixed-header first byte
  AssertEquals('CONNECT fixed header byte', $10, Integer(Pkt[0]));

  // Locate the protocol-name section at offset 2 (after fixed-header byte + 1-byte remlen).
  // Protocol name is length-prefixed: $00 $04 'M' 'Q' 'T' 'T'
  AssertEquals('Proto len hi',  $00, Integer(Pkt[2]));
  AssertEquals('Proto len lo',  $04, Integer(Pkt[3]));
  AssertEquals('Proto[0] M',    Ord('M'), Integer(Pkt[4]));
  AssertEquals('Proto[1] Q',    Ord('Q'), Integer(Pkt[5]));
  AssertEquals('Proto[2] T',    Ord('T'), Integer(Pkt[6]));
  AssertEquals('Proto[3] T',    Ord('T'), Integer(Pkt[7]));

  // Protocol level byte
  AssertEquals('Protocol level 5', 5, Integer(Pkt[8]));

  // Connect flags: CleanStart=1 => bit 1 set; no username/password => $02
  AssertEquals('Connect flags (CleanStart)', $02, Integer(Pkt[9]));
end;

// MQTT 3.1.1 CONNECT: protocol level byte must be 4, no properties section.
procedure TMQTTPacketTests.TestBuildConnectPacket_MQTT311;
var
  Pkt: TBytes;
begin
  Pkt := BuildConnectPacket(MQTT_PROTOCOL_LEVEL_311, True, 30, 'client1');

  AssertTrue('Packet must not be empty', Length(Pkt) > 2);
  AssertEquals('CONNECT fixed header byte', $10, Integer(Pkt[0]));

  // Protocol level at offset 8 (same layout as MQTT 5 up to this point)
  AssertEquals('Protocol level 4 (MQTT 3.1.1)', 4, Integer(Pkt[8]));

  // For MQTT 3.1.1 there is no properties section.  After the connect flags
  // and keep-alive word (offsets 9, 10, 11) comes the payload ClientID string
  // directly.  The ClientID length field starts at offset 12.
  AssertEquals('ClientID len hi (3.1.1)', $00, Integer(Pkt[12]));
  AssertEquals('ClientID len lo (3.1.1)', 7,   Integer(Pkt[13])); // 'client1' = 7 bytes
end;

// Username+Password flags: bits 7+6 of the connect-flags byte.
procedure TMQTTPacketTests.TestBuildConnectPacket_WithAuth;
var
  Pkt:        TBytes;
  ConnFlags:  Byte;
begin
  Pkt := BuildConnectPacket(
    MQTT_PROTOCOL_LEVEL_311, True, 60, 'myClient',
    'alice', 's3cr3t');

  AssertTrue('Packet not empty', Length(Pkt) > 9);
  ConnFlags := Pkt[9];

  // Bit 7: Username present
  AssertTrue('Username flag (bit 7) set', (ConnFlags and $80) <> 0);
  // Bit 6: Password present
  AssertTrue('Password flag (bit 6) set', (ConnFlags and $40) <> 0);
  // Bit 1: CleanStart
  AssertTrue('CleanStart flag (bit 1) set', (ConnFlags and $02) <> 0);
end;

// Session Expiry Interval property only appears in MQTT 5.0 packets and
// causes the properties section to be longer than a single $00 byte.
procedure TMQTTPacketTests.TestBuildConnectPacket_SessionExpiry;
var
  PktNoExpiry:   TBytes;
  PktWithExpiry: TBytes;
begin
  PktNoExpiry   := BuildConnectPacket(MQTT_PROTOCOL_LEVEL_50,
    True, 60, 'c', '', '', 0);
  PktWithExpiry := BuildConnectPacket(MQTT_PROTOCOL_LEVEL_50,
    True, 60, 'c', '', '', 300);

  // Packet with a session expiry interval must be longer (property added).
  AssertTrue('Packet with session expiry must be longer than without',
    Length(PktWithExpiry) > Length(PktNoExpiry));
end;

// ---------------------------------------------------------------------------
// PUBLISH packet
// ---------------------------------------------------------------------------

// QoS 0 PUBLISH: fixed-header = $30 (MQTT_PUBLISH nibble 3, no flags),
// no packet identifier in the variable header.
procedure TMQTTPacketTests.TestBuildPublishPacket_QoS0;
var
  Payload: TBytes;
  Pkt:     TBytes;
begin
  SetLength(Payload, 0);
  Pkt := BuildPublishPacket('home/temp', Payload, 0, 0,
    MQTT_PROTOCOL_LEVEL_311);

  AssertTrue('PUBLISH QoS0 not empty', Length(Pkt) > 2);
  // (MQTT_PUBLISH=3 shl 4) or (QoS0=0 shl 1) or retain=0 = $30
  AssertEquals('PUBLISH QoS0 fixed header', $30, Integer(Pkt[0]));
end;

// QoS 1 PUBLISH: fixed-header = $32 (bit 1 of flags set for QoS 1),
// packet identifier present after the topic string.
procedure TMQTTPacketTests.TestBuildPublishPacket_QoS1;
var
  Payload:    TBytes;
  Pkt:        TBytes;
  TopicLen:   Integer;
  PktIdOffset: Integer;
begin
  Payload    := TEncoding.UTF8.GetBytes('hello');
  Pkt        := BuildPublishPacket('test/q1', Payload, 1, $0102,
    MQTT_PROTOCOL_LEVEL_311);

  AssertTrue('PUBLISH QoS1 not empty', Length(Pkt) > 2);
  // (3 shl 4) or (1 shl 1) = $32
  AssertEquals('PUBLISH QoS1 fixed header', $32, Integer(Pkt[0]));

  // After fixed header byte + 1-byte remaining-length varint, the variable
  // header starts with the topic string.  Topic is 'test/q1' = 7 bytes,
  // so topic field occupies bytes [2..10] (2 len bytes + 7 data bytes).
  // Packet identifier follows immediately at offset 11.
  TopicLen    := 7; // 'test/q1'
  PktIdOffset := 2 + 2 + TopicLen;   // fixed_hdr + remlen_varint + topic
  AssertTrue('Packet ID offset in bounds', PktIdOffset + 1 < Length(Pkt));
  AssertEquals('Packet ID hi byte', $01, Integer(Pkt[PktIdOffset]));
  AssertEquals('Packet ID lo byte', $02, Integer(Pkt[PktIdOffset + 1]));
end;

// RETAIN flag sets bit 0 of the fixed-header flags nibble.
procedure TMQTTPacketTests.TestBuildPublishPacket_Retain;
var
  Payload: TBytes;
  Pkt:     TBytes;
begin
  SetLength(Payload, 0);
  Pkt := BuildPublishPacket('retained/topic', Payload, 0, 0,
    MQTT_PROTOCOL_LEVEL_311, {ARetain=}True);

  // (3 shl 4) or 0 (QoS 0) or $01 (RETAIN) = $31
  AssertEquals('PUBLISH RETAIN fixed header', $31, Integer(Pkt[0]));
end;

// MQTT 5.0 PUBLISH must include a zero-length properties varint ($00) in
// the variable header after the topic (and optional packet ID).  This makes
// the packet longer than the equivalent MQTT 3.1.1 packet.
procedure TMQTTPacketTests.TestBuildPublishPacket_MQTT5HasProperties;
var
  Payload:   TBytes;
  Pkt311:    TBytes;
  Pkt50:     TBytes;
begin
  SetLength(Payload, 3);
  Payload[0] := Ord('a');
  Payload[1] := Ord('b');
  Payload[2] := Ord('c');

  Pkt311 := BuildPublishPacket('t', Payload, 0, 0, MQTT_PROTOCOL_LEVEL_311);
  Pkt50  := BuildPublishPacket('t', Payload, 0, 0, MQTT_PROTOCOL_LEVEL_50);

  // MQTT 5.0 adds 1 byte (the $00 properties-length varint) vs 3.1.1
  AssertEquals('MQTT 5.0 PUBLISH is 1 byte longer than 3.1.1',
    Length(Pkt311) + 1, Length(Pkt50));
end;

// ---------------------------------------------------------------------------
// SUBSCRIBE packet
// ---------------------------------------------------------------------------

// SUBSCRIBE fixed-header flags must be $02 (MQTT spec §3.8.1).
// First byte = (MQTT_SUBSCRIBE nibble 8 shl 4) or $02 = $82.
procedure TMQTTPacketTests.TestBuildSubscribePacket;
var
  Pkt:        TBytes;
  TopicLen:   Integer;
  OptionsOff: Integer;
begin
  Pkt := BuildSubscribePacket('home/#', 1, $0001, MQTT_PROTOCOL_LEVEL_311);

  AssertTrue('SUBSCRIBE not empty', Length(Pkt) > 2);
  AssertEquals('SUBSCRIBE fixed header byte', $82, Integer(Pkt[0]));

  // Variable header for MQTT 3.1.1 (no properties):
  //   [pkt_id_hi] [pkt_id_lo] followed by topic filter + options byte
  // Offsets (all after fixed_hdr byte at [0] and remlen byte at [1]):
  //   [2] = pkt_id_hi, [3] = pkt_id_lo
  AssertEquals('Packet ID hi', $00, Integer(Pkt[2]));
  AssertEquals('Packet ID lo', $01, Integer(Pkt[3]));

  // Topic filter starts at [4]: len_hi len_lo followed by 'home/#' (6 bytes)
  AssertEquals('Topic len hi', $00, Integer(Pkt[4]));
  AssertEquals('Topic len lo', 6,   Integer(Pkt[5]));  // 'home/#' = 6 chars

  // Subscription options byte follows the topic bytes (offset 4+2+6 = 12)
  TopicLen    := 6;
  OptionsOff  := 4 + 2 + TopicLen;
  AssertTrue('Options byte in bounds', OptionsOff < Length(Pkt));
  // QoS 1 in bits 0-1
  AssertEquals('Subscription options QoS=1', 1, Integer(Pkt[OptionsOff] and $03));
end;

// MQTT 5.0 SUBSCRIBE includes a zero-length properties section after the
// packet identifier, making the packet 1 byte longer than the 3.1.1 version.
procedure TMQTTPacketTests.TestBuildSubscribePacket_MQTT5HasProperties;
var
  Pkt311: TBytes;
  Pkt50:  TBytes;
begin
  Pkt311 := BuildSubscribePacket('a/b', 0, 1, MQTT_PROTOCOL_LEVEL_311);
  Pkt50  := BuildSubscribePacket('a/b', 0, 1, MQTT_PROTOCOL_LEVEL_50);

  AssertEquals('MQTT 5.0 SUBSCRIBE is 1 byte longer than 3.1.1',
    Length(Pkt311) + 1, Length(Pkt50));
end;

// ---------------------------------------------------------------------------
// PINGREQ
// ---------------------------------------------------------------------------

// PINGREQ is always exactly [$C0, $00].
procedure TMQTTPacketTests.TestBuildPingReqPacket;
var
  Pkt: TBytes;
  Exp: TBytes;
begin
  Pkt := BuildPingReqPacket;
  SetLength(Exp, 2);
  Exp[0] := $C0;
  Exp[1] := $00;
  AssertBytesEqual(Exp, Pkt, 'PINGREQ');
end;

// ---------------------------------------------------------------------------
// DISCONNECT
// ---------------------------------------------------------------------------

// MQTT 5.0 DISCONNECT: [$E0, $02, reason_code, $00].
// (MQTT_DISCONNECT=14 shl 4) = $E0; remaining-length = 2.
procedure TMQTTPacketTests.TestBuildDisconnectPacket_MQTT5;
var
  Pkt: TBytes;
  Exp: TBytes;
begin
  Pkt := BuildDisconnectPacket($00, MQTT_PROTOCOL_LEVEL_50);
  SetLength(Exp, 4);
  Exp[0] := $E0;  // fixed header
  Exp[1] := $02;  // remaining length = 2
  Exp[2] := $00;  // reason code: normal disconnection
  Exp[3] := $00;  // properties length = 0
  AssertBytesEqual(Exp, Pkt, 'DISCONNECT MQTT5');
end;

// MQTT 3.1.1 DISCONNECT: [$E0, $00] — no variable header.
procedure TMQTTPacketTests.TestBuildDisconnectPacket_MQTT311;
var
  Pkt: TBytes;
  Exp: TBytes;
begin
  Pkt := BuildDisconnectPacket($00, MQTT_PROTOCOL_LEVEL_311);
  SetLength(Exp, 2);
  Exp[0] := $E0;
  Exp[1] := $00;
  AssertBytesEqual(Exp, Pkt, 'DISCONNECT MQTT311');
end;

// ---------------------------------------------------------------------------
// Registration
// ---------------------------------------------------------------------------

initialization
  RegisterTest(TMQTTPacketTests);

end.
