import { describe, it, expect } from "vitest";
import {
  KISS_FEND,
  KISS_FESC,
  KISS_TFEND,
  KISS_TFESC,
  encodeKISSFrame,
  decodeKISSFrame,
  kissFrameReader,
  kissFrameWriter,
  type KISSFrame,
} from "../src/kiss";

const u8 = (arr: number[]) => Uint8Array.from(arr);

describe("kiss.ts", () => {
  it("encodeKISSFrame produces proper FEND wrappers and decodes back", () => {
    const data = u8([1, 2, 3]);
    const buf = encodeKISSFrame(data);
    expect(buf[0]).toBe(KISS_FEND);
    expect(buf[buf.length - 1]).toBe(KISS_FEND);
    const frame = decodeKISSFrame(buf);
    expect(frame).not.toBeNull();
    expect(frame!.port).toBe(0);
    expect(frame!.command).toBe(0);
    expect(Array.from(frame!.data)).toEqual(Array.from(data));
  });

  it("encodes and decodes escape sequences correctly", () => {
    const data = u8([0x01, KISS_FEND, 0x02, KISS_FESC, 0x03]);
    const encoded = encodeKISSFrame(data, 1, 2);
    // command byte should encode port=1, command=2
    expect(encoded[1]).toBe(((1 & 0x0f) << 4) | (2 & 0x0f));
    // ensure escaped bytes present
    const arr = Array.from(encoded);
    // look for FESC TFEND and FESC TFESC sequences
    const hasFescTfend = arr.includes(KISS_FESC) && arr.includes(KISS_TFEND);
    const hasFescTfesc = arr.includes(KISS_FESC) && arr.includes(KISS_TFESC);
    expect(hasFescTfend).toBeTruthy();
    expect(hasFescTfesc).toBeTruthy();

    const decoded = decodeKISSFrame(encoded)!;
    expect(decoded.port).toBe(1);
    expect(decoded.command).toBe(2);
    expect(Array.from(decoded.data)).toEqual(Array.from(data));
  });

  it("decodeKISSFrame returns null for invalid frames", () => {
    // missing FEND start
    expect(decodeKISSFrame(u8([0x00, 0x00, 0x00]))).toBeNull();

    // invalid escape sequence
    const bad = u8([KISS_FEND, 0x00, KISS_FESC, 0x00, KISS_FEND]);
    expect(decodeKISSFrame(bad)).toBeNull();
  });

  it("parses port and command from the command byte", () => {
    const port = 3;
    const command = 4;
    const data = u8([9, 8]);
    const cmd = ((port & 0x0f) << 4) | (command & 0x0f);
    const buf = u8([KISS_FEND, cmd, ...Array.from(data), KISS_FEND]);
    const f = decodeKISSFrame(buf)!;
    expect(f.port).toBe(port);
    expect(f.command).toBe(command);
    expect(Array.from(f.data)).toEqual(Array.from(data));
  });

  it("kissFrameReader yields frames assembled across chunks and ignores incomplete frames", async () => {
    // create two frames and split them across chunks
    const a = encodeKISSFrame(u8([1, 2, 3]));
    const b = encodeKISSFrame(u8([4, 5]));

    // craft an async iterable that yields multiple chunks, splitting frames
    async function* source() {
      // first chunk: first half of a
      yield u8(Array.from(a.slice(0, 2)));
      // second chunk: rest of a plus start of b
      yield u8(Array.from(a.slice(2)).concat(Array.from(b.slice(0, 1))));
      // third chunk: rest of b
      yield u8(Array.from(b.slice(1)));
    }

    const results: KISSFrame[] = [];
    for await (const fr of kissFrameReader(source())) {
      results.push(fr);
    }
    expect(results.length).toBe(2);
    expect(Array.from(results[0].data)).toEqual([1, 2, 3]);
    expect(Array.from(results[1].data)).toEqual([4, 5]);

    // incomplete frame (no trailing FEND) should not yield
    async function* sourceIncomplete() {
      const partial = Array.from(a.slice(0, a.length - 1));
      yield u8(partial);
    }
    const got: KISSFrame[] = [];
    for await (const fr of kissFrameReader(sourceIncomplete())) got.push(fr);
    expect(got.length).toBe(0);
  });

  it("kissFrameWriter calls the sink with the encoded buffer", async () => {
    const received: Uint8Array[] = [];
    const sink = async (data: Uint8Array) => {
      received.push(data);
    };
    const frame: KISSFrame = { port: 2, command: 1, data: u8([7, 8, 9]) };
    await kissFrameWriter(sink, frame);
    expect(received.length).toBe(1);
    const expected = encodeKISSFrame(frame.data, frame.port, frame.command);
    expect(Array.from(received[0])).toEqual(Array.from(expected));
  });
});
