import { describe, expect, test } from 'bun:test';
import {
  DOMAIN_BUNDLE,
  computeBundleHash,
  encodeLeafData,
  leafHash,
  nodeHash,
  OP_REGISTER,
} from '../src/hashes.js';
import { sha256Sync } from '../src/sha256.js';

describe('RFC 6962 hash primitives', () => {
  test('leafHash applies 0x00 prefix', () => {
    const data = new Uint8Array([1, 2, 3]);
    const expected = sha256Sync(new Uint8Array([0x00, 1, 2, 3]));
    expect(leafHash(data)).toEqual(expected);
  });

  test('nodeHash applies 0x01 prefix', () => {
    const left = new Uint8Array(32).fill(0xaa);
    const right = new Uint8Array(32).fill(0xbb);
    const concat = new Uint8Array(1 + 32 + 32);
    concat[0] = 0x01;
    concat.set(left, 1);
    concat.set(right, 33);
    const expected = sha256Sync(concat);
    expect(nodeHash(left, right)).toEqual(expected);
  });

  test('leafHash and nodeHash never collide', () => {
    // Same content but different domain → different hash
    const x = new Uint8Array(32).fill(0x42);
    const lh = leafHash(x);
    const concat = new Uint8Array(64).fill(0x42);
    const nh = nodeHash(concat.slice(0, 32), concat.slice(32));
    expect(Buffer.from(lh).toString('hex')).not.toBe(Buffer.from(nh).toString('hex'));
  });
});

describe('encodeLeafData', () => {
  test('encodes timestamp + operation + address + bundleHash', () => {
    const buf = encodeLeafData(
      0x010203040506,
      OP_REGISTER,
      'alice',
      new Uint8Array([0xde, 0xad, 0xbe, 0xef]),
    );
    // 8 + 1 + 2 + 5 + 2 + 4 = 22
    expect(buf.length).toBe(22);
    // last 4 bytes = bundleHash
    expect(buf[buf.length - 4]).toBe(0xde);
    expect(buf[buf.length - 1]).toBe(0xef);
  });

  test('rejects address > 65535 bytes', () => {
    const huge = 'a'.repeat(0x10000);
    expect(() => encodeLeafData(0, OP_REGISTER, huge, new Uint8Array(0))).toThrow();
  });
});

describe('computeBundleHash', () => {
  test('deterministic over equal input', () => {
    const sk = new Uint8Array(32).fill(0x11);
    const dk = new Uint8Array(32).fill(0x22);
    const pk = new Uint8Array(32).fill(0x33);
    const sig = new Uint8Array(64).fill(0x44);
    const a = computeBundleHash({
      identitySigningKey: sk,
      identityDHKey: dk,
      signedPreKey: { keyId: 7, publicKey: pk, signature: sig },
    });
    const b = computeBundleHash({
      identitySigningKey: sk,
      identityDHKey: dk,
      signedPreKey: { keyId: 7, publicKey: pk, signature: sig },
    });
    expect(a).toEqual(b);
  });

  test('changing keyId changes hash', () => {
    const sk = new Uint8Array(32).fill(0x11);
    const dk = new Uint8Array(32).fill(0x22);
    const pk = new Uint8Array(32).fill(0x33);
    const sig = new Uint8Array(64).fill(0x44);
    const a = computeBundleHash({
      identitySigningKey: sk,
      identityDHKey: dk,
      signedPreKey: { keyId: 1, publicKey: pk, signature: sig },
    });
    const b = computeBundleHash({
      identitySigningKey: sk,
      identityDHKey: dk,
      signedPreKey: { keyId: 2, publicKey: pk, signature: sig },
    });
    expect(a).not.toEqual(b);
  });

  test('rejects wrong-length keys', () => {
    expect(() =>
      computeBundleHash({
        identitySigningKey: new Uint8Array(31),
        identityDHKey: new Uint8Array(32),
        signedPreKey: {
          keyId: 0,
          publicKey: new Uint8Array(32),
          signature: new Uint8Array(64),
        },
      }),
    ).toThrow();
  });

  test('uses domain prefix 0x01', () => {
    const sk = new Uint8Array(32);
    const dk = new Uint8Array(32);
    const pk = new Uint8Array(32);
    const sig = new Uint8Array(64);
    const expected = sha256Sync(
      Buffer.concat([Buffer.from([DOMAIN_BUNDLE]), sk, dk, Buffer.alloc(4), pk, sig]),
    );
    const got = computeBundleHash({
      identitySigningKey: sk,
      identityDHKey: dk,
      signedPreKey: { keyId: 0, publicKey: pk, signature: sig },
    });
    expect(Buffer.from(got).toString('hex')).toBe(Buffer.from(expected).toString('hex'));
  });
});
