import { describe, test, expect } from 'bun:test';
import {
  toB64url,
  fromB64url,
  crc32,
  encodeMeta,
  encodeData,
  decodeFrame,
  chunkFile,
  reassemble,
  newSessionId,
} from './protocol';

// The wire format is about to be reimplemented in Swift and Kotlin, so these tests are the spec: any
// other implementation should be able to reproduce them exactly.

const bytes = (...n: number[]) => new Uint8Array(n);

describe('base64url', () => {
  test('round-trips every byte value', () => {
    const all = new Uint8Array(256);
    for (let i = 0; i < 256; i++) all[i] = i;
    expect(Array.from(fromB64url(toB64url(all)))).toEqual(Array.from(all));
  });

  // The three lengths that exercise each padding case.
  test.each([[bytes(1)], [bytes(1, 2)], [bytes(1, 2, 3)]])('round-trips length %#', (input) => {
    expect(Array.from(fromB64url(toB64url(input)))).toEqual(Array.from(input));
  });

  test('emits no character that collides with the separator', () => {
    const all = new Uint8Array(256).map((_, i) => i);
    const encoded = toB64url(all);
    expect(encoded).not.toContain(':');
    expect(encoded).not.toContain('/');
    expect(encoded).not.toContain('+');
    expect(encoded).not.toContain('=');
  });
});

describe('crc32', () => {
  // The standard check value: CRC-32 of "123456789" is 0xCBF43926 in every stock implementation.
  test('matches the reference value for "123456789"', () => {
    expect(crc32(new TextEncoder().encode('123456789'))).toBe('cbf43926');
  });

  test('is 8 hex characters even when it has leading zeros', () => {
    expect(crc32(new Uint8Array(0))).toHaveLength(8);
  });
});

describe('frames', () => {
  test('meta round-trips, including a name with unicode', () => {
    const encoded = encodeMeta({
      sid: 'a1b2c3d4',
      total: 42,
      size: 24_000,
      crc32: 'cbf43926',
      name: 'relatório ✅.pdf',
      mime: 'application/pdf',
    });
    const f = decodeFrame(encoded);
    expect(f?.kind).toBe('meta');
    if (f?.kind !== 'meta') throw new Error('wrong kind');
    expect(f).toEqual({
      kind: 'meta',
      sid: 'a1b2c3d4',
      total: 42,
      size: 24_000,
      crc32: 'cbf43926',
      name: 'relatório ✅.pdf',
      mime: 'application/pdf',
    });
  });

  test('data round-trips exact bytes', () => {
    const payload = bytes(0, 255, 58, 47, 43, 61, 10, 13);
    const f = decodeFrame(encodeData('a1b2c3d4', 7, payload));
    expect(f?.kind).toBe('data');
    if (f?.kind !== 'data') throw new Error('wrong kind');
    expect(f.seq).toBe(7);
    expect(Array.from(f.bytes)).toEqual(Array.from(payload));
  });

  // A camera sees every QR code in view, not just ours.
  test('ignores foreign QR content', () => {
    expect(decodeFrame('https://example.com')).toBeNull();
    expect(decodeFrame('QRF2:D:x:0:AAAA')).toBeNull();
    expect(decodeFrame('')).toBeNull();
  });

  test('survives a mime type containing a colon', () => {
    const f = decodeFrame(encodeMeta({ sid: 's', total: 1, size: 1, crc32: 'x', name: 'a', mime: 'x:y' }));
    expect(f?.kind === 'meta' && f.mime).toBe('x:y');
  });
});

describe('chunk and reassemble', () => {
  const file = new Uint8Array(2500).map((_, i) => i % 256);

  test('chunks cover the file exactly and rebuild it', () => {
    const chunks = chunkFile(file, 600);
    expect(chunks).toHaveLength(5); // 600*4 + 100
    expect(chunks[4]!.length).toBe(100);

    const map = new Map(chunks.map((c, i) => [i, c]));
    const rebuilt = reassemble(map, chunks.length)!;
    expect(Array.from(rebuilt)).toEqual(Array.from(file));
    expect(crc32(rebuilt)).toBe(crc32(file));
  });

  // The carousel's whole premise: frames arrive in any order, over several passes.
  test('order does not matter', () => {
    const chunks = chunkFile(file, 600);
    const shuffled = new Map([...chunks.entries()].reverse().map(([i, c]) => [i, c]));
    expect(Array.from(reassemble(shuffled, chunks.length)!)).toEqual(Array.from(file));
  });

  test('returns null while any frame is still missing', () => {
    const chunks = chunkFile(file, 600);
    const map = new Map(chunks.map((c, i) => [i, c]));
    map.delete(2);
    expect(reassemble(map, chunks.length)).toBeNull();
  });

  test('handles a file smaller than one chunk', () => {
    const small = bytes(1, 2, 3);
    const chunks = chunkFile(small, 600);
    expect(chunks).toHaveLength(1);
    expect(Array.from(reassemble(new Map([[0, chunks[0]!]]), 1)!)).toEqual([1, 2, 3]);
  });
});

describe('session ids', () => {
  test('are 8 hex characters and differ between transfers', () => {
    const a = newSessionId();
    expect(a).toMatch(/^[0-9a-f]{8}$/);
    expect(a).not.toBe(newSessionId());
  });
});
