import { describe, test, expect } from 'bun:test';
import { StreamingSha256, sha256Once } from '../src/index.js';

function hex(bytes: Uint8Array): string {
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}

async function subtleHashHex(data: Uint8Array): Promise<string> {
  const buf = await globalThis.crypto.subtle.digest('SHA-256', data as unknown as ArrayBuffer);
  return hex(new Uint8Array(buf));
}

describe('StreamingSha256', () => {
  test('digest of empty input matches the well-known SHA-256 zero hash', () => {
    const h = new StreamingSha256().digest();
    expect(hex(h)).toBe('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
  });

  test('matches one-shot sha256 over the same bytes', () => {
    const data = new TextEncoder().encode('the quick brown fox');
    const streaming = new StreamingSha256().update(data).digest();
    expect(hex(streaming)).toBe(hex(sha256Once(data)));
  });

  test('matches SubtleCrypto digest over the same bytes', async () => {
    const data = new TextEncoder().encode('cross-impl parity check');
    const streaming = new StreamingSha256().update(data).digest();
    expect(hex(streaming)).toBe(await subtleHashHex(data));
  });

  test('chunked updates produce identical digest to a single update', () => {
    const buf = new Uint8Array(4096);
    for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff;
    const a = new StreamingSha256().update(buf).digest();
    const b = new StreamingSha256();
    for (let off = 0; off < buf.length; off += 137) {
      b.update(buf.slice(off, Math.min(off + 137, buf.length)));
    }
    expect(hex(a)).toBe(hex(b.digest()));
  });

  test('handles multi-megabyte inputs (memory-bounded streaming)', () => {
    const chunk = new Uint8Array(1024 * 1024);
    for (let i = 0; i < chunk.length; i++) chunk[i] = (i * 31) & 0xff;
    const h = new StreamingSha256();
    for (let i = 0; i < 4; i++) h.update(chunk);
    const digest = h.digest();
    expect(digest.length).toBe(32);
  });

  test('throws on update after digest()', () => {
    const h = new StreamingSha256();
    h.digest();
    expect(() => h.update(new Uint8Array([1]))).toThrow();
  });

  test('isFinalized reflects digest()', () => {
    const h = new StreamingSha256();
    expect(h.isFinalized).toBe(false);
    h.digest();
    expect(h.isFinalized).toBe(true);
  });

  test('skips no-op empty updates', () => {
    const h = new StreamingSha256();
    h.update(new Uint8Array(0));
    h.update(new Uint8Array(0));
    expect(hex(h.digest())).toBe(
      'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
    );
  });
});
