import { describe, test, expect } from 'bun:test';
import { SubtleCryptoProvider } from '@shade/crypto-web';
import {
  aesGcmEncryptWithNonce,
  aesGcmDecryptWithNonce,
  buildChunkNonce,
  buildChunkAad,
  deriveStreamKey,
  deriveLaneKey,
  StreamDecryptionError,
} from '../src/index.js';

const crypto = new SubtleCryptoProvider();

async function laneKey(): Promise<{ key: Uint8Array; streamId: Uint8Array }> {
  const secret = new Uint8Array(32).fill(0x42);
  const streamId = new Uint8Array(16).fill(0x99);
  const sk = await deriveStreamKey(crypto, secret, streamId);
  const lk = await deriveLaneKey(crypto, sk, streamId, 0);
  return { key: lk, streamId };
}

describe('aesGcmEncryptWithNonce / aesGcmDecryptWithNonce', () => {
  test('encrypt → decrypt roundtrip', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, false);
    const plaintext = new TextEncoder().encode('hello shade streams');

    const ct = await aesGcmEncryptWithNonce(key, nonce, plaintext, aad);
    const pt = await aesGcmDecryptWithNonce(key, nonce, ct, aad);
    expect(new TextDecoder().decode(pt)).toBe('hello shade streams');
  });

  test('produces ciphertext length = plaintext + 16-byte tag', async () => {
    const { key, streamId } = await laneKey();
    const plaintext = new Uint8Array(1024);
    const ct = await aesGcmEncryptWithNonce(
      key,
      buildChunkNonce(0, 0),
      plaintext,
      buildChunkAad(streamId, 0, 0, false),
    );
    expect(ct.length).toBe(1024 + 16);
  });

  test('handles empty plaintext', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, true);
    const ct = await aesGcmEncryptWithNonce(key, nonce, new Uint8Array(0), aad);
    expect(ct.length).toBe(16); // tag only
    const pt = await aesGcmDecryptWithNonce(key, nonce, ct, aad);
    expect(pt.length).toBe(0);
  });

  test('handles 1 MiB plaintext (default chunk size)', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, false);
    const plaintext = crypto.randomBytes(1024 * 1024);
    const ct = await aesGcmEncryptWithNonce(key, nonce, plaintext, aad);
    const pt = await aesGcmDecryptWithNonce(key, nonce, ct, aad);
    expect(pt).toEqual(plaintext);
  });

  test('different nonces with same key produce different ciphertexts', async () => {
    const { key, streamId } = await laneKey();
    const aad = buildChunkAad(streamId, 0, 0, false);
    const plaintext = new TextEncoder().encode('same plaintext');
    const ct1 = await aesGcmEncryptWithNonce(key, buildChunkNonce(0, 0), plaintext, aad);
    const ct2 = await aesGcmEncryptWithNonce(key, buildChunkNonce(0, 1), plaintext, aad);
    expect(ct1).not.toEqual(ct2);
  });

  test('tampered ciphertext byte → StreamDecryptionError', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, false);
    const ct = await aesGcmEncryptWithNonce(key, nonce, new TextEncoder().encode('hi'), aad);
    ct[0] ^= 0x01;
    await expect(aesGcmDecryptWithNonce(key, nonce, ct, aad)).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('tampered tag byte → StreamDecryptionError', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, false);
    const ct = await aesGcmEncryptWithNonce(key, nonce, new TextEncoder().encode('hi'), aad);
    ct[ct.length - 1] ^= 0x80;
    await expect(aesGcmDecryptWithNonce(key, nonce, ct, aad)).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('wrong AAD → StreamDecryptionError', async () => {
    const { key, streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aadEnc = buildChunkAad(streamId, 0, 0, false);
    const aadDec = buildChunkAad(streamId, 0, 0, true); // isLast flipped
    const ct = await aesGcmEncryptWithNonce(key, nonce, new TextEncoder().encode('hi'), aadEnc);
    await expect(aesGcmDecryptWithNonce(key, nonce, ct, aadDec)).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('wrong nonce → StreamDecryptionError', async () => {
    const { key, streamId } = await laneKey();
    const aad = buildChunkAad(streamId, 0, 0, false);
    const ct = await aesGcmEncryptWithNonce(
      key,
      buildChunkNonce(0, 0),
      new TextEncoder().encode('hi'),
      aad,
    );
    await expect(
      aesGcmDecryptWithNonce(key, buildChunkNonce(0, 1), ct, aad),
    ).rejects.toThrow(StreamDecryptionError);
  });

  test('wrong key → StreamDecryptionError', async () => {
    const { streamId } = await laneKey();
    const nonce = buildChunkNonce(0, 0);
    const aad = buildChunkAad(streamId, 0, 0, false);
    const k1 = new Uint8Array(32).fill(1);
    const k2 = new Uint8Array(32).fill(2);
    const ct = await aesGcmEncryptWithNonce(k1, nonce, new TextEncoder().encode('hi'), aad);
    await expect(aesGcmDecryptWithNonce(k2, nonce, ct, aad)).rejects.toThrow(
      StreamDecryptionError,
    );
  });

  test('rejects non-12-byte nonce', async () => {
    const { key, streamId } = await laneKey();
    const aad = buildChunkAad(streamId, 0, 0, false);
    await expect(
      aesGcmEncryptWithNonce(key, new Uint8Array(11), new Uint8Array(0), aad),
    ).rejects.toThrow();
    await expect(
      aesGcmDecryptWithNonce(key, new Uint8Array(13), new Uint8Array(16), aad),
    ).rejects.toThrow();
  });
});
