'use strict';

const test = require('node:test');
const assert = require('node:assert');
const {
  encodeVarInt,
  parseVarInt,
  encodeString,
  readString,
  buildPacket,
  buildMCFrame,
  parseMCFrame,
  drainMCFrames,
} = require('../src/bot/packets');

test('encodeVarInt: 0', () => {
  assert.deepStrictEqual([...encodeVarInt(0)], [0x00]);
});

test('encodeVarInt: 127', () => {
  assert.deepStrictEqual([...encodeVarInt(127)], [0x7F]);
});

test('encodeVarInt: 128', () => {
  assert.deepStrictEqual([...encodeVarInt(128)], [0x80, 0x01]);
});

test('encodeVarInt: 16383', () => {
  assert.deepStrictEqual([...encodeVarInt(16383)], [0xFF, 0x7F]);
});

test('encodeVarInt: 16384', () => {
  assert.deepStrictEqual([...encodeVarInt(16384)], [0x80, 0x80, 0x01]);
});

test('encodeVarInt: 2^31-1', () => {
  assert.deepStrictEqual([...encodeVarInt(0x7FFFFFFF)], [0xFF, 0xFF, 0xFF, 0xFF, 0x07]);
});

test('parseVarInt: round-trip', () => {
  for (const v of [0, 1, 127, 128, 255, 16383, 16384, 2 ** 31 - 1]) {
    const buf = encodeVarInt(v);
    const r = parseVarInt(buf, 0);
    assert.strictEqual(r.value, v, `round-trip ${v}`);
    assert.strictEqual(r.bytesRead, buf.length, `bytes ${v}`);
  }
});

test('parseVarInt: incomplete returns 0 bytes', () => {
  const buf = Buffer.from([0x80]);
  const r = parseVarInt(buf, 0);
  assert.strictEqual(r.bytesRead, 0);
});

test('encodeString/readString: ASCII', () => {
  const s = 'Hello';
  const buf = encodeString(s);
  const r = readString(buf, 0);
  assert.strictEqual(r.value, s);
  assert.strictEqual(r.bytesRead, buf.length);
});

test('encodeString/readString: UTF-8 multibyte', () => {
  const s = 'héllo★world';
  const buf = encodeString(s);
  const r = readString(buf, 0);
  assert.strictEqual(r.value, s);
  assert.strictEqual(r.bytesRead, buf.length);
});

test('buildPacket: id + data', () => {
  const buf = buildPacket(0x02, encodeString('hi'));
  assert.strictEqual(buf[0], 0x02);
  assert.strictEqual(buf[1], 2);
  assert.strictEqual(buf[2], 0x68);
  assert.strictEqual(buf[3], 0x69);
});

test('buildMCFrame + parseMCFrame round-trip', () => {
  const frame = buildMCFrame(0x1F, Buffer.from([1, 2, 3, 4, 5, 6, 7, 8]));
  const r = parseMCFrame(frame, 0);
  assert.strictEqual(r.complete, true);
  assert.strictEqual(r.packetId, 0x1F);
  assert.deepStrictEqual([...r.data], [1, 2, 3, 4, 5, 6, 7, 8]);
  assert.strictEqual(r.bytesRead, frame.length);
});

test('parseMCFrame: incomplete returns complete=false', () => {
  const frame = buildMCFrame(0x1F, Buffer.from([1, 2, 3]));
  const partial = frame.slice(0, frame.length - 2);
  const r = parseMCFrame(partial, 0);
  assert.strictEqual(r.complete, false);
  assert.strictEqual(r.bytesRead, 0);
});

test('drainMCFrames: consumes multiple', () => {
  const f1 = buildMCFrame(0x1F, Buffer.from([0xAA, 0xBB]));
  const f2 = buildMCFrame(0x23, Buffer.from([0x01]));
  const f3 = buildMCFrame(0x2F, Buffer.alloc(33));
  const combined = Buffer.concat([f1, f2, f3]);
  const seen = [];
  const { consumed, frames } = drainMCFrames(combined, (id, data) => seen.push({ id, data }));
  assert.strictEqual(consumed, combined.length);
  assert.strictEqual(frames, 3);
  assert.strictEqual(seen[0].id, 0x1F);
  assert.strictEqual(seen[1].id, 0x23);
  assert.strictEqual(seen[2].id, 0x2F);
});

test('drainMCFrames: partial at end', () => {
  const f1 = buildMCFrame(0x1F, Buffer.from([0xAA]));
  const f2 = buildMCFrame(0x23, Buffer.from([0x01]));
  const partial = Buffer.concat([f1, f2]).slice(0, f1.length + 2);
  const seen = [];
  const { consumed, frames } = drainMCFrames(partial, (id, data) => seen.push({ id }));
  assert.strictEqual(frames, 1);
  assert.strictEqual(seen[0].id, 0x1F);
  assert.strictEqual(consumed, f1.length);
});
