import { describe, test, expect } from 'bun:test';
import {
  canonicalRpcBytes,
  canonicalJsonStringify,
  hashArgs,
  bytesToHex,
  bytesToBase64,
  base64ToBytes,
} from '../../src/index.js';

describe('canonicalJsonStringify', () => {
  test('sorts object keys', () => {
    expect(canonicalJsonStringify({ b: 1, a: 2 })).toBe('{"a":2,"b":1}');
  });

  test('skips undefined values', () => {
    expect(canonicalJsonStringify({ a: 1, b: undefined, c: 2 })).toBe('{"a":1,"c":2}');
  });

  test('preserves array order', () => {
    expect(canonicalJsonStringify([3, 1, 2])).toBe('[3,1,2]');
  });

  test('recursive sorting', () => {
    const a = canonicalJsonStringify({ outer: { y: 1, x: 2 } });
    const b = canonicalJsonStringify({ outer: { x: 2, y: 1 } });
    expect(a).toBe(b);
    expect(a).toBe('{"outer":{"x":2,"y":1}}');
  });

  test('handles primitives', () => {
    expect(canonicalJsonStringify(null)).toBe('null');
    expect(canonicalJsonStringify(true)).toBe('true');
    expect(canonicalJsonStringify(42)).toBe('42');
    expect(canonicalJsonStringify('hi')).toBe('"hi"');
  });

  test('different argument orders hash identically', () => {
    const h1 = hashArgs({ b: 1, a: 2 });
    const h2 = hashArgs({ a: 2, b: 1 });
    expect(bytesToHex(h1)).toBe(bytesToHex(h2));
  });

  test('different values hash differently', () => {
    expect(bytesToHex(hashArgs({ a: 1 }))).not.toBe(bytesToHex(hashArgs({ a: 2 })));
  });
});

describe('canonicalRpcBytes', () => {
  test('deterministic for same input (known vector)', () => {
    const args = {
      address: 'alice',
      signedAt: 1730000000000,
      kind: 'shade.fs.list/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      argsHash: new Uint8Array(32).fill(0xab),
    };
    const a = canonicalRpcBytes(args);
    const b = canonicalRpcBytes(args);
    expect(a).toEqual(b);
  });

  test('changes when address differs', () => {
    const base = {
      signedAt: 1,
      kind: 'shade.fs.list/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      argsHash: new Uint8Array(32),
    };
    const a = canonicalRpcBytes({ ...base, address: 'alice' });
    const b = canonicalRpcBytes({ ...base, address: 'bob' });
    expect(a).not.toEqual(b);
  });

  test('changes when signedAt differs', () => {
    const base = {
      address: 'alice',
      kind: 'shade.fs.list/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      argsHash: new Uint8Array(32),
    };
    expect(canonicalRpcBytes({ ...base, signedAt: 1 })).not.toEqual(
      canonicalRpcBytes({ ...base, signedAt: 2 }),
    );
  });

  test('changes when kind differs', () => {
    const base = {
      address: 'alice',
      signedAt: 1,
      id: 'AbCdEfGhIjKlMnOpQrStUv',
      argsHash: new Uint8Array(32),
    };
    expect(canonicalRpcBytes({ ...base, kind: 'shade.fs.list/v1' })).not.toEqual(
      canonicalRpcBytes({ ...base, kind: 'shade.fs.stat/v1' }),
    );
  });

  test('changes when argsHash differs', () => {
    const base = {
      address: 'alice',
      signedAt: 1,
      kind: 'shade.fs.list/v1',
      id: 'AbCdEfGhIjKlMnOpQrStUv',
    };
    expect(
      canonicalRpcBytes({ ...base, argsHash: new Uint8Array(32) }),
    ).not.toEqual(
      canonicalRpcBytes({ ...base, argsHash: new Uint8Array(32).fill(1) }),
    );
  });
});

describe('hashArgs', () => {
  test('produces 32-byte digest', () => {
    expect(hashArgs({ a: 1 }).length).toBe(32);
  });

  test('null input is fine', () => {
    expect(hashArgs(null).length).toBe(32);
  });
});

describe('bytesToHex / base64', () => {
  test('hex roundtrip', () => {
    const b = new Uint8Array([0x01, 0xab, 0xff, 0x00]);
    expect(bytesToHex(b)).toBe('01abff00');
  });

  test('base64 roundtrip', () => {
    const b = new Uint8Array([1, 2, 3, 4]);
    const enc = bytesToBase64(b);
    expect(base64ToBytes(enc)).toEqual(b);
  });
});
