// =============================================================================
// library.test.ts — bun test suite for MonoDisplay library
//
// run:  bun test
// =============================================================================

import { test, expect, describe } from "bun:test";
import {
  BinaryReader,
  MonoDisplayParser,
  MonoDisplayFile,
  ElementType,
  SectionType,
  buildBinBuffer,
  type Image2DElement,
  type MonoFormatElementsAlways,
  type MonoFormatImage2D,
} from "../src/index";

// ---------------------------------------------------------------------------
// Low-level buffer helpers
// ---------------------------------------------------------------------------

// Real magic: 0xAF 0x7E 0x2B 0x63
const MAGIC_BYTES = new Uint8Array([0xAF, 0x7E, 0x2B, 0x63]);

function concat(...parts: (Uint8Array | ArrayBuffer)[]): ArrayBuffer {
  const arrays = parts.map((p: Uint8Array | ArrayBuffer) =>
    p instanceof ArrayBuffer ? new Uint8Array(p) : p
  );
  const total = arrays.reduce((s, a) => s + a.byteLength, 0);
  const out = new Uint8Array(total);
  let off = 0;
  for (const a of arrays) { out.set(a, off); off += a.byteLength; }
  return out.buffer;
}

function u8(n: number)    { return new Uint8Array([n]); }
function u16le(n: number) { const b = new Uint8Array(2); new DataView(b.buffer).setUint16(0, n, true); return b; }
function u24le(n: number) { return new Uint8Array([n & 0xFF, (n >> 8) & 0xFF, (n >> 16) & 0xFF]); }
function u32le(n: number) { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; }

// ---------------------------------------------------------------------------
// BinaryReader
// ---------------------------------------------------------------------------

describe("BinaryReader", () => {
  test("readByte", () => {
    const r = new BinaryReader(new Uint8Array([0xAB]).buffer);
    expect(r.readByte()).toBe(0xAB);
    expect(r.remaining).toBe(0);
  });

  test("readUint16 LE", () => {
    const r = new BinaryReader(new Uint8Array([0x34, 0x12]).buffer);
    expect(r.readUint16()).toBe(0x1234);
  });

  test("readShort alias", () => {
    const r = new BinaryReader(new Uint8Array([0x01, 0x00]).buffer);
    expect(r.readShort()).toBe(1);
  });

  test("readUint24 LE", () => {
    // 0x010203 LE → bytes [0x03, 0x02, 0x01]
    const r = new BinaryReader(new Uint8Array([0x03, 0x02, 0x01]).buffer);
    expect(r.readUint24()).toBe(0x010203);
  });

  test("readUint32 LE", () => {
    const r = new BinaryReader(new Uint8Array([0x78, 0x56, 0x34, 0x12]).buffer);
    expect(r.readUint32()).toBe(0x12345678);
  });

  test("readUint64 LE", () => {
    const bytes = new Uint8Array(8);
    new DataView(bytes.buffer).setBigUint64(0, 0x0102030405060708n, true);
    const r = new BinaryReader(bytes.buffer);
    expect(r.readUint64()).toBe(0x0102030405060708n);
  });

  test("offset advances through mixed reads", () => {
    const r = new BinaryReader(new Uint8Array([1, 2, 3, 4, 5, 6]).buffer);
    r.readByte();    expect(r.offset).toBe(1);
    r.readUint16();  expect(r.offset).toBe(3);
    expect(() => r.readUint32()).toThrow(RangeError); // only 3 remain
  });

  test("RangeError on exhaustion", () => {
    const r = new BinaryReader(new Uint8Array([0x01]).buffer);
    r.readByte();
    expect(() => r.readByte()).toThrow(RangeError);
  });

  test("seek", () => {
    const r = new BinaryReader(new Uint8Array([10, 20, 30]).buffer);
    r.seek(2);
    expect(r.readByte()).toBe(30);
  });

  test("seek out of bounds throws", () => {
    const r = new BinaryReader(new Uint8Array([1, 2]).buffer);
    expect(() => r.seek(5)).toThrow(RangeError);
  });

  test("readUtf8 ASCII", () => {
    const enc = new TextEncoder().encode("hello");
    const r = new BinaryReader(enc.buffer);
    expect(r.readUtf8(5)).toBe("hello");
  });

  test("readUtf8 multi-byte codepoints", () => {
    const str = "日本語🚀";
    const enc = new TextEncoder().encode(str);
    const r = new BinaryReader(enc.buffer);
    expect(r.readUtf8(enc.byteLength)).toBe(str);
  });
});

// ---------------------------------------------------------------------------
// MonoDisplayParser — header validation
// ---------------------------------------------------------------------------

describe("MonoDisplayParser — header", () => {
  const parser = new MonoDisplayParser();

  test("rejects wrong magic", () => {
    const bad = new Uint8Array(16).fill(0);
    bad[0] = 0xDE; bad[1] = 0xAD; bad[2] = 0xBE; bad[3] = 0xEF;
    expect(() => parser.parse(bad.buffer)).toThrow(/magic/i);
  });

  test("rejects version 0 (illegal)", () => {
    const buf = concat(MAGIC_BYTES, u32le(0), u16le(0), u16le(0));
    expect(() => parser.parse(buf)).toThrow(/version/i);
  });

  test("rejects version > 1 (reserved)", () => {
    const buf = concat(MAGIC_BYTES, u32le(2), u16le(0), u16le(0));
    expect(() => parser.parse(buf)).toThrow(/version/i);
  });
});

// ---------------------------------------------------------------------------
// MonoDisplayParser — Image2D round-trip
// ---------------------------------------------------------------------------

describe("MonoDisplayParser — Image2D round-trip", () => {
  const parser = new MonoDisplayParser();

  /**
   * Build a minimal valid file buffer containing one ElementsAlways section
   * with one Image2D element.
   *
   * File header (12 bytes):
   *   magic u32le + version u32le + numSections u16le + reserved u16le
   *
   * Section header (4 bytes):
   *   type u8 + size u24le (INCLUDES 4-byte header)
   *
   * ElementsAlways section data:
   *   flags u16le + numElements u16le
   *
   * Image2D element (12 fixed bytes + packed pixel data):
   *   type u16le + xOffset u16le + yOffset u16le + width u16le + height u16le + reserved u16le
   *   + packed 1bpp pixel data + padding to 4-byte boundary
   */
  function makeImage2DFile(
    width: number,
    height: number,
    pixels: Uint8Array,
    xOffset: number = 0,
    yOffset: number = 0,
  ): ArrayBuffer {
    // Pack pixels to 1bpp
    const total = width * height;
    const packedLen = (total + 7) >> 3;
    const packed = new Uint8Array(packedLen);
    for (let i = 0; i < total; i++) {
      if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7));
    }
    // Padding to 4-byte boundary for element (12 fixed bytes + packed data)
    const elementDataLen = 12 + packedLen;
    const padLen = (4 - (elementDataLen & 3)) & 3;
    const padding = new Uint8Array(padLen);

    // Image2D element bytes
    const element = new Uint8Array(concat(
      u16le(1),          // type = Image2D
      u16le(xOffset),
      u16le(yOffset),
      u16le(width),
      u16le(height),
      u16le(0),          // reserved
      packed,
      padding,
    ));

    // Section data: flags + numElements + element
    const sectionData = new Uint8Array(concat(
      u16le(0),          // flags = 0
      u16le(1),          // numElements = 1
      element,
    ));

    // Section header: type u8 + size u24le (4 header bytes + data)
    const sectionSize = 4 + sectionData.byteLength;
    const sectionHeader = new Uint8Array(concat(
      u8(1),             // SectionType.ElementsAlways = 1
      u24le(sectionSize),
    ));

    // File header
    const fileHeader = new Uint8Array(concat(
      MAGIC_BYTES,
      u32le(1),          // version = 1
      u16le(1),          // numSections = 1
      u16le(0),          // reserved
    ));

    return concat(fileHeader, sectionHeader, sectionData);
  }

  test("sections[0].sectionType === SectionType.ElementsAlways", () => {
    const pixels = new Uint8Array([1, 0, 0, 1]);
    const buf = makeImage2DFile(2, 2, pixels);
    const result = parser.parse(buf);
    expect(result.sections.length).toBe(1);
    expect(result.sections[0]!.sectionType).toBe(SectionType.ElementsAlways);
  });

  test("elements[0].type === ElementType.Image2D", () => {
    const pixels = new Uint8Array([1, 0, 0, 1]);
    const buf = makeImage2DFile(2, 2, pixels);
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    expect(section.elements.length).toBe(1);
    expect(section.elements[0]!.type).toBe(ElementType.Image2D);
  });

  test("correct dimensions after parse", () => {
    const pixels = new Uint8Array([0, 1, 1, 0]);
    const buf = makeImage2DFile(2, 2, pixels);
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const el = section.elements[0] as MonoFormatImage2D;
    expect(el.image.width).toBe(2);
    expect(el.image.height).toBe(2);
  });

  test("correct pixel data after round-trip", () => {
    // pixels: [1, 0, 0, 1] → top-left and bottom-right are on
    const pixels = new Uint8Array([1, 0, 0, 1]);
    const buf = makeImage2DFile(2, 2, pixels);
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const el = section.elements[0] as MonoFormatImage2D;
    expect(Array.from(el.image.pixels)).toEqual([1, 0, 0, 1]);
  });

  test("xOffset and yOffset are preserved", () => {
    const pixels = new Uint8Array([1, 1, 1, 1]);
    const buf = makeImage2DFile(2, 2, pixels, 5, 3);
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const el = section.elements[0] as MonoFormatImage2D;
    expect(el.xOffset).toBe(5);
    expect(el.yOffset).toBe(3);
  });
});

// ---------------------------------------------------------------------------
// MonoDisplayFile
// ---------------------------------------------------------------------------

describe("MonoDisplayFile", () => {
  const parser = new MonoDisplayParser();

  test("toBuffer() produces parseable output for Image2D element", () => {
    const pixels = new Uint8Array([1, 0, 1, 0]);
    const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
    const file = new MonoDisplayFile({ elements_always: [el] });
    const buf = file.toBuffer();
    const result = parser.parse(buf);
    expect(result.sections.length).toBeGreaterThan(0);
    const section = result.sections[0] as MonoFormatElementsAlways;
    expect(section.sectionType).toBe(SectionType.ElementsAlways);
    expect(section.elements.length).toBe(1);
    const parsed = section.elements[0] as MonoFormatImage2D;
    expect(parsed.type).toBe(ElementType.Image2D);
    expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 1, 0]);
  });

  test("xOffset/yOffset are round-tripped", () => {
    const pixels = new Uint8Array([1, 1, 1, 1]);
    const el: Image2DElement = {
      type: ElementType.Image2D,
      pixels,
      width: 2,
      height: 2,
      xOffset: 10,
      yOffset: 7,
    };
    const file = new MonoDisplayFile({ elements_always: [el] });
    const buf = file.toBuffer();
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const parsed = section.elements[0] as MonoFormatImage2D;
    expect(parsed.xOffset).toBe(10);
    expect(parsed.yOffset).toBe(7);
  });

  test("flags.drawFront default is applied", () => {
    const pixels = new Uint8Array([0]);
    const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 1, height: 1 };
    const file = new MonoDisplayFile({ elements_always: [el] });
    const buf = file.toBuffer();
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    // Default flags should have drawFront = true (flags byte bit 0 = 1)
    expect(section.flags.drawFront).toBe(true);
  });

  test("empty elements_always array is valid and produces parseable output", () => {
    const file = new MonoDisplayFile({ elements_always: [] });
    const buf = file.toBuffer();
    const result = parser.parse(buf);
    expect(result.sections.length).toBeGreaterThan(0);
    const section = result.sections[0] as MonoFormatElementsAlways;
    expect(section.sectionType).toBe(SectionType.ElementsAlways);
    expect(section.elements.length).toBe(0);
  });
});

// ---------------------------------------------------------------------------
// buildBinBuffer
// ---------------------------------------------------------------------------

describe("buildBinBuffer", () => {
  const parser = new MonoDisplayParser();

  test("builds a parseable Uint8Array from Image2DElement", () => {
    const pixels = new Uint8Array([0, 1, 1, 0]);
    const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 2, height: 2 };
    const buf = buildBinBuffer(el);
    expect(buf).toBeInstanceOf(Uint8Array);
    const result = parser.parse(buf);
    expect(result.sections.length).toBeGreaterThan(0);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const parsed = section.elements[0] as MonoFormatImage2D;
    expect(Array.from(parsed.image.pixels)).toEqual([0, 1, 1, 0]);
  });

  test("pixel data round-trips correctly", () => {
    const pixels = new Uint8Array([1, 0, 0, 0, 0, 0, 0, 1]); // 8 pixels, corners on
    const el: Image2DElement = { type: ElementType.Image2D, pixels, width: 8, height: 1 };
    const buf = buildBinBuffer(el);
    const result = parser.parse(buf);
    const section = result.sections[0] as MonoFormatElementsAlways;
    const parsed = section.elements[0] as MonoFormatImage2D;
    expect(parsed.image.width).toBe(8);
    expect(parsed.image.height).toBe(1);
    expect(Array.from(parsed.image.pixels)).toEqual([1, 0, 0, 0, 0, 0, 0, 1]);
  });
});
