#!/usr/bin/env bun
/**
 * Generate cross-platform test vectors from the TypeScript implementation.
 *
 * The output JSON files are loaded by BOTH the TypeScript and Kotlin test
 * suites. Any divergence between platforms fails CI immediately.
 *
 * Schema: every file is `{ "version": <int>, "vectors": [...] }`.
 * Bump `VECTOR_FILE_VERSION` whenever the vector schema (NOT just values)
 * changes, so downstream consumers can fail loudly on mismatch.
 *
 * Usage: bun run scripts/generate-vectors.ts
 */
import { writeFileSync } from 'fs';
import { join } from 'path';
import { SubtleCryptoProvider } from '../packages/shade-crypto-web/src/index.js';
import { computeFingerprint } from '../packages/shade-core/src/fingerprint.js';
import { kdfChainKey, kdfRootKey, deriveInitialRootKey } from '../packages/shade-core/src/keys.js';
import { encodeEnvelope, encodeStreamChunk, decodeStreamChunk } from '../packages/shade-proto/src/index.js';
import type { StreamChunkWire } from '../packages/shade-proto/src/index.js';
import type { ShadeEnvelope, RatchetMessage, PreKeyMessage } from '../packages/shade-core/src/index.js';
import {
  deriveStreamKey,
  deriveLaneKey,
  buildChunkNonce,
  buildChunkAad,
  aesGcmEncryptWithNonce,
} from '../packages/shade-streams/src/index.js';
import {
  deriveBlobSlotId,
  deriveBlobKey,
  deriveBlobSigningSeed,
} from '../packages/shade-storage-encrypted/src/crypto.js';
import { ed25519PublicKeyFromSeed } from '../packages/shade-crypto-web/src/index.js';
import {
  canonicalApprovalSigningBytes,
  signProxyApproval,
  buildApprovalRequest,
  type ApprovalRequestFrame,
} from '../packages/shade-sdk/src/index.js';

const VECTOR_FILE_VERSION = 2;

const crypto = new SubtleCryptoProvider();
const OUT_DIR = join(import.meta.dir, '..', 'test-vectors');

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

function fromHex(str: string): Uint8Array {
  const bytes = new Uint8Array(str.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(str.substring(i * 2, i * 2 + 2), 16);
  }
  return bytes;
}

interface Vector {
  description: string;
  [key: string]: unknown;
}

// AES-GCM with caller-supplied nonce. The CryptoProvider interface picks a
// random nonce internally, so vector generation goes around it via SubtleCrypto
// directly — same primitive `@shade/streams` already uses.
async function aesGcmEncryptDeterministic(
  key: Uint8Array,
  nonce: Uint8Array,
  plaintext: Uint8Array,
  aad: Uint8Array,
): Promise<Uint8Array> {
  const subtle = globalThis.crypto.subtle;
  const aesKey = await subtle.importKey(
    'raw',
    key as unknown as ArrayBuffer,
    'AES-GCM',
    false,
    ['encrypt'],
  );
  const out = await subtle.encrypt(
    { name: 'AES-GCM', iv: nonce as unknown as ArrayBuffer, additionalData: aad as unknown as ArrayBuffer },
    aesKey,
    plaintext as unknown as ArrayBuffer,
  );
  return new Uint8Array(out);
}

// Mirror of `encodeHeader` in @shade/core/ratchet.ts — kept inline to avoid
// exporting an internal symbol just for tests.
function encodeRatchetHeader(
  dhPublicKey: Uint8Array,
  previousCounter: number,
  counter: number,
): Uint8Array {
  const buf = new Uint8Array(40);
  buf.set(dhPublicKey, 0);
  const view = new DataView(buf.buffer);
  view.setUint32(32, previousCounter, false);
  view.setUint32(36, counter, false);
  return buf;
}

// ─── HKDF vectors ───────────────────────────────────────────
async function generateHkdfVectors(): Promise<Vector[]> {
  const cases = [
    { ikm: '01'.repeat(32), salt: '02'.repeat(32), info: 'test', length: 32 },
    { ikm: 'ab'.repeat(32), salt: '00'.repeat(32), info: 'ShadeRootRatchet', length: 64 },
    { ikm: 'cd'.repeat(32), salt: '00'.repeat(32), info: 'ShadeX3DH', length: 32 },
  ];

  const vectors: Vector[] = [];
  for (const c of cases) {
    const out = await crypto.hkdf(
      fromHex(c.ikm),
      fromHex(c.salt),
      new TextEncoder().encode(c.info),
      c.length,
    );
    vectors.push({
      description: `HKDF-SHA256 with ikm=${c.ikm.slice(0, 8)}... info="${c.info}"`,
      ikm: c.ikm,
      salt: c.salt,
      info: c.info,
      length: c.length,
      output: hex(out),
    });
  }
  return vectors;
}

// ─── KDF chain vectors ─────────────────────────────────────
async function generateKdfChainVectors(): Promise<Vector[]> {
  const rootKey = new Uint8Array(32).fill(0x11);
  const dhOutput = new Uint8Array(32).fill(0x22);
  const rootResult = await kdfRootKey(crypto, rootKey, dhOutput);

  const chainKey = new Uint8Array(32).fill(0x33);
  const chainResult = await kdfChainKey(crypto, chainKey);

  return [
    {
      description: 'Root key ratchet: kdfRootKey',
      rootKey: hex(rootKey),
      dhOutput: hex(dhOutput),
      newRootKey: hex(rootResult.newRootKey),
      chainKey: hex(rootResult.chainKey),
    },
    {
      description: 'Chain key ratchet: kdfChainKey',
      chainKey: hex(chainKey),
      newChainKey: hex(chainResult.newChainKey),
      messageKey: hex(chainResult.messageKey),
    },
  ];
}

// ─── X3DH initial root key ─────────────────────────────────
async function generateX3DHVectors(): Promise<Vector[]> {
  const secrets = [
    new Uint8Array(32).fill(0xaa),
    new Uint8Array(32).fill(0xbb),
    new Uint8Array(32).fill(0xcc),
  ];
  const rootKey3 = await deriveInitialRootKey(crypto, secrets);

  const secrets4 = [...secrets, new Uint8Array(32).fill(0xdd)];
  const rootKey4 = await deriveInitialRootKey(crypto, secrets4);

  return [
    {
      description: 'X3DH initial root key with 3 DH outputs (no one-time prekey)',
      secrets: secrets.map(hex),
      rootKey: hex(rootKey3),
    },
    {
      description: 'X3DH initial root key with 4 DH outputs (with one-time prekey)',
      secrets: secrets4.map(hex),
      rootKey: hex(rootKey4),
    },
  ];
}

// ─── Fingerprint vectors ───────────────────────────────────
async function generateFingerprintVectors(): Promise<Vector[]> {
  const cases = [
    { sig: '01'.repeat(32), dh: '02'.repeat(32) },
    { sig: 'ab'.repeat(32), dh: 'cd'.repeat(32) },
  ];

  const vectors: Vector[] = [];
  for (const c of cases) {
    const fp = await computeFingerprint(crypto, fromHex(c.sig), fromHex(c.dh));
    vectors.push({
      description: `Fingerprint for signing=${c.sig.slice(0, 8)}... dh=${c.dh.slice(0, 8)}...`,
      signingKey: c.sig,
      dhKey: c.dh,
      fingerprint: fp,
    });
  }
  return vectors;
}

// ─── Wire format vectors ───────────────────────────────────
async function generateWireFormatVectors(): Promise<Vector[]> {
  const ratchetMsg: RatchetMessage = {
    dhPublicKey: new Uint8Array(32).fill(0x11),
    previousCounter: 42,
    counter: 7,
    ciphertext: new Uint8Array(16).fill(0x22),
    nonce: new Uint8Array(12).fill(0x33),
  };
  const envelopeRatchet: ShadeEnvelope = {
    type: 'ratchet',
    content: ratchetMsg,
    timestamp: 0,
    senderAddress: '',
  };
  const bytesRatchet = encodeEnvelope(envelopeRatchet);

  const innerRatchet: RatchetMessage = {
    dhPublicKey: new Uint8Array(32).fill(0x44),
    previousCounter: 0,
    counter: 0,
    ciphertext: new Uint8Array(8).fill(0x55),
    nonce: new Uint8Array(12).fill(0x66),
  };
  const preKeyMsgWithOTPK: PreKeyMessage = {
    registrationId: 0x12345678,
    preKeyId: 99,
    signedPreKeyId: 1,
    ephemeralKey: new Uint8Array(32).fill(0x77),
    identityDHKey: new Uint8Array(32).fill(0x88),
    message: innerRatchet,
  };
  const envelopePreKey: ShadeEnvelope = {
    type: 'prekey',
    content: preKeyMsgWithOTPK,
    timestamp: 0,
    senderAddress: '',
  };
  const bytesPreKey = encodeEnvelope(envelopePreKey);

  const preKeyMsgNoOTPK: PreKeyMessage = {
    registrationId: 1,
    preKeyId: undefined,
    signedPreKeyId: 1,
    ephemeralKey: new Uint8Array(32).fill(0x99),
    identityDHKey: new Uint8Array(32).fill(0xaa),
    message: innerRatchet,
  };
  const bytesPreKeyNoOTPK = encodeEnvelope({
    type: 'prekey',
    content: preKeyMsgNoOTPK,
    timestamp: 0,
    senderAddress: '',
  });

  return [
    {
      description: 'Wire format: RatchetMessage encoding (wire VERSION 0x02 — u32 length-prefixed)',
      kind: 'ratchet',
      message: {
        dhPublicKey: hex(ratchetMsg.dhPublicKey),
        previousCounter: ratchetMsg.previousCounter,
        counter: ratchetMsg.counter,
        ciphertext: hex(ratchetMsg.ciphertext),
        nonce: hex(ratchetMsg.nonce),
      },
      encoded: hex(bytesRatchet),
    },
    {
      description: 'Wire format: PreKeyMessage with one-time prekey (wire 0x02 type 0x01)',
      kind: 'prekey',
      message: {
        registrationId: preKeyMsgWithOTPK.registrationId,
        preKeyId: preKeyMsgWithOTPK.preKeyId ?? null,
        signedPreKeyId: preKeyMsgWithOTPK.signedPreKeyId,
        ephemeralKey: hex(preKeyMsgWithOTPK.ephemeralKey),
        identityDHKey: hex(preKeyMsgWithOTPK.identityDHKey),
        inner: {
          dhPublicKey: hex(innerRatchet.dhPublicKey),
          previousCounter: innerRatchet.previousCounter,
          counter: innerRatchet.counter,
          ciphertext: hex(innerRatchet.ciphertext),
          nonce: hex(innerRatchet.nonce),
        },
      },
      encoded: hex(bytesPreKey),
    },
    {
      description: 'Wire format: PreKeyMessage without one-time prekey (preKeyId=null encoded as 0xFFFFFFFF)',
      kind: 'prekey',
      message: {
        registrationId: preKeyMsgNoOTPK.registrationId,
        preKeyId: null,
        signedPreKeyId: preKeyMsgNoOTPK.signedPreKeyId,
        ephemeralKey: hex(preKeyMsgNoOTPK.ephemeralKey),
        identityDHKey: hex(preKeyMsgNoOTPK.identityDHKey),
        inner: {
          dhPublicKey: hex(innerRatchet.dhPublicKey),
          previousCounter: innerRatchet.previousCounter,
          counter: innerRatchet.counter,
          ciphertext: hex(innerRatchet.ciphertext),
          nonce: hex(innerRatchet.nonce),
        },
      },
      encoded: hex(bytesPreKeyNoOTPK),
    },
  ];
}

// ─── Ratchet step vectors ──────────────────────────────────
//
// A ratchet "encrypt step" is fully deterministic given (rootKey, dhSendPriv,
// dhRemotePub, plaintext, fixed nonce, counters). The vector records every
// intermediate derivation so each implementation can verify byte-parity at
// every layer (kdfRootKey → kdfChainKey → header AAD → AES-GCM ciphertext) and
// also verify decrypt(ciphertext, nonce, aad, messageKey) === plaintext.
async function generateRatchetStepVectors(): Promise<Vector[]> {
  // Deterministic inputs
  const rootKey = new Uint8Array(32).fill(0xa1);
  const dhSendPriv = new Uint8Array(32).fill(0xb2);
  const dhSendPub = new Uint8Array(32).fill(0xb3); // not used in derivation, only AAD
  const dhRemotePub = new Uint8Array(32).fill(0xc4);
  const plaintext = new TextEncoder().encode('Shade ratchet roundtrip vector v1');
  const fixedNonce = new Uint8Array(12).fill(0x5e);
  const previousCounter = 2;
  const counter = 0;

  // Step 1: DH between local send priv and remote pub
  const dhOutput = await crypto.x25519(dhSendPriv, dhRemotePub);

  // Step 2: kdfRootKey to advance root + get chain key
  const { newRootKey, chainKey } = await kdfRootKey(crypto, rootKey, dhOutput);

  // Step 3: kdfChainKey to derive messageKey
  const { newChainKey, messageKey } = await kdfChainKey(crypto, chainKey);

  // Step 4: Header AAD bytes
  const aad = encodeRatchetHeader(dhSendPub, previousCounter, counter);

  // Step 5: AES-GCM with deterministic nonce
  const ciphertext = await aesGcmEncryptDeterministic(messageKey, fixedNonce, plaintext, aad);

  return [
    {
      description: 'Ratchet step: deterministic encrypt (kdfRootKey + kdfChainKey + AES-GCM with fixed nonce)',
      inputs: {
        rootKey: hex(rootKey),
        dhSendPrivateKey: hex(dhSendPriv),
        dhSendPublicKey: hex(dhSendPub),
        dhRemotePublicKey: hex(dhRemotePub),
        previousCounter,
        counter,
        plaintext: hex(plaintext),
        nonce: hex(fixedNonce),
      },
      derived: {
        dhOutput: hex(dhOutput),
        newRootKey: hex(newRootKey),
        chainKey: hex(chainKey),
        newChainKey: hex(newChainKey),
        messageKey: hex(messageKey),
        aad: hex(aad),
      },
      ciphertext: hex(ciphertext),
    },
  ];
}

// ─── Streams 0x11 vectors ──────────────────────────────────
//
// Covers the @shade/streams primitives that V3.5 §3 (M-Cross 3) requires
// Kotlin to mirror byte-for-byte: HKDF labels with embedded NULs, u32-be
// laneId encoding inside the lane-key info, deterministic (laneId, seq)
// chunk nonces, the 29-byte chunk AAD, end-to-end chunk encrypt/decrypt,
// and the wire 0x11 envelope encode/decode.
async function generateStreamsVectors(): Promise<Vector[]> {
  const streamSecret = new Uint8Array(32).fill(0xa1);
  const streamId = new Uint8Array(16).fill(0xb2);

  const streamKey = await deriveStreamKey(crypto, streamSecret, streamId);

  const laneIdsForKeys = [0, 1, 2, 0xffff_ffff];
  const laneKeyVectors: Array<{ laneId: number; laneKey: string }> = [];
  for (const laneId of laneIdsForKeys) {
    const laneKey = await deriveLaneKey(crypto, streamKey, streamId, laneId);
    laneKeyVectors.push({ laneId, laneKey: hex(laneKey) });
  }

  const noncePairs: Array<{ laneId: number; seq: bigint }> = [
    { laneId: 0, seq: 0n },
    { laneId: 0, seq: 1n },
    { laneId: 1, seq: 0n },
    { laneId: 0xffff_ffff, seq: 0xffff_ffff_ffff_fffen },
  ];
  const nonceVectors = noncePairs.map((p) => ({
    laneId: p.laneId,
    seq: p.seq.toString(),
    nonce: hex(buildChunkNonce(p.laneId, p.seq)),
  }));

  const aadCases: Array<{ laneId: number; seq: bigint; isLast: boolean }> = [
    { laneId: 0, seq: 0n, isLast: false },
    { laneId: 1, seq: 7n, isLast: true },
    { laneId: 0xffff_ffff, seq: 0xffff_ffff_ffff_fffen, isLast: false },
  ];
  const aadVectors = aadCases.map((c) => ({
    laneId: c.laneId,
    seq: c.seq.toString(),
    isLast: c.isLast,
    aad: hex(buildChunkAad(streamId, c.laneId, c.seq, c.isLast)),
  }));

  // End-to-end chunk encrypt with lane 0, seq 0, isLast=true
  const laneId = 0;
  const seq = 0n;
  const isLast = true;
  const laneKey = await deriveLaneKey(crypto, streamKey, streamId, laneId);
  const nonce = buildChunkNonce(laneId, seq);
  const aad = buildChunkAad(streamId, laneId, seq, isLast);
  const plaintext = new TextEncoder().encode('Shade streams 0x11 chunk vector');
  const ciphertext = await aesGcmEncryptWithNonce(laneKey, nonce, plaintext, aad);

  // Wire 0x11 envelope (extra-aad field = 0 bytes per current spec)
  const wire: StreamChunkWire = {
    streamId,
    laneId,
    seq,
    isLast,
    nonce,
    aad: new Uint8Array(0),
    ciphertext,
  };
  const wireBytes = encodeStreamChunk(wire);
  // Sanity: roundtrip-decode locally so the recorded bytes are always parseable
  const decoded = decodeStreamChunk(wireBytes);
  if (hex(decoded.ciphertext) !== hex(ciphertext)) {
    throw new Error('streams wire 0x11 roundtrip diverged in generator');
  }

  return [
    {
      description: 'deriveStreamKey: HKDF(streamSecret, salt=streamId, info="shade-stream/v1\\0master")',
      streamSecret: hex(streamSecret),
      streamId: hex(streamId),
      streamKey: hex(streamKey),
    },
    {
      description: 'deriveLaneKey: HKDF(streamKey, salt=streamId, info="shade-stream/v1\\0lane\\0" || u32_be(laneId))',
      streamKey: hex(streamKey),
      streamId: hex(streamId),
      lanes: laneKeyVectors,
    },
    {
      description: 'buildChunkNonce(laneId, seq): u32_be(laneId) || u64_be(seq)',
      nonces: nonceVectors,
    },
    {
      description: 'buildChunkAad(streamId, laneId, seq, isLast): streamId(16) || u32_be(laneId) || u64_be(seq) || u8(isLast)',
      streamId: hex(streamId),
      cases: aadVectors,
    },
    {
      description: 'End-to-end chunk encrypt: AES-256-GCM(laneKey, nonce, plaintext, aad)',
      laneId,
      seq: seq.toString(),
      isLast,
      laneKey: hex(laneKey),
      nonce: hex(nonce),
      aad: hex(aad),
      plaintext: hex(plaintext),
      ciphertext: hex(ciphertext),
    },
    {
      description: 'Wire 0x11 stream-chunk envelope encode/decode',
      streamId: hex(streamId),
      laneId,
      seq: seq.toString(),
      isLast,
      nonce: hex(nonce),
      extraAad: hex(new Uint8Array(0)),
      ciphertext: hex(ciphertext),
      encoded: hex(wireBytes),
    },
  ];
}

// ─── Backup format vectors ─────────────────────────────────
//
// Backup v1 derives an AES-256-GCM key from `(passphrase, salt)` via
// HKDF-SHA256 with info `"ShadeBackupKey"`, then encrypts the payload.
// The vector pins the HKDF output and an end-to-end encrypt/decrypt for
// a known plaintext + fixed nonce.
async function generateBackupVectors(): Promise<Vector[]> {
  const passphrase = 'correct-horse-battery-staple';
  const salt = new Uint8Array(32).fill(0xa5);
  const info = new TextEncoder().encode('ShadeBackupKey');
  const backupKey = await crypto.hkdf(
    new TextEncoder().encode(passphrase),
    salt,
    info,
    32,
  );
  const plaintext = new TextEncoder().encode(
    JSON.stringify({ version: 1, identity: null, sessions: [] }),
  );
  const fixedNonce = new Uint8Array(12).fill(0xc7);
  const ciphertext = await aesGcmEncryptDeterministic(
    backupKey,
    fixedNonce,
    plaintext,
    new Uint8Array(0),
  );
  return [
    {
      description: 'Backup v1: HKDF(passphrase_utf8, salt, info="ShadeBackupKey", 32) -> backupKey',
      passphrase,
      salt: hex(salt),
      info: 'ShadeBackupKey',
      backupKey: hex(backupKey),
    },
    {
      description: 'Backup v1: AES-256-GCM(backupKey, plaintext, no AAD) with deterministic nonce',
      backupKey: hex(backupKey),
      nonce: hex(fixedNonce),
      plaintext: hex(plaintext),
      plaintextUtf8: new TextDecoder().decode(plaintext),
      ciphertext: hex(ciphertext),
    },
  ];
}

// ─── Group sender-keys vectors ─────────────────────────────
//
// Sender-key step pins three things:
//   1. The 12-byte sender header AAD (`u16_be(gLen) || g || u16_be(sLen) || s || u32_be(iter)`)
//   2. The chain-key advance (kdfChainKey) producing (newChainKey, messageKey)
//   3. AES-256-GCM encrypt with deterministic nonce + Ed25519 signature
//      over `aad || ciphertext`. Ed25519 is deterministic so the signature
//      bytes are byte-parity-checkable cross-platform.
async function generateGroupVectors(): Promise<Vector[]> {
  // Static Ed25519 keypair (RFC 8032 §7.1 test vector 1)
  const signingPrivateKey = fromHex(
    '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60',
  );
  const signingPublicKey = fromHex(
    'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a',
  );

  const groupId = 'group:42';
  const senderAddress = 'alice@example.com';
  const iteration = 5;
  const chainKey = new Uint8Array(32).fill(0x9b);

  const enc = new TextEncoder();
  const gBytes = enc.encode(groupId);
  const sBytes = enc.encode(senderAddress);
  const aad = new Uint8Array(2 + gBytes.length + 2 + sBytes.length + 4);
  const aadView = new DataView(aad.buffer);
  let off = 0;
  aadView.setUint16(off, gBytes.length, false); off += 2;
  aad.set(gBytes, off); off += gBytes.length;
  aadView.setUint16(off, sBytes.length, false); off += 2;
  aad.set(sBytes, off); off += sBytes.length;
  aadView.setUint32(off, iteration, false);

  const { newChainKey, messageKey } = await kdfChainKey(crypto, chainKey);
  const fixedNonce = new Uint8Array(12).fill(0x7d);
  const plaintext = enc.encode('hello group');
  const ciphertext = await aesGcmEncryptDeterministic(messageKey, fixedNonce, plaintext, aad);

  const signed = new Uint8Array(aad.length + ciphertext.length);
  signed.set(aad, 0);
  signed.set(ciphertext, aad.length);
  const signature = await crypto.sign(signingPrivateKey, signed);
  // Sanity: verify with the matching public key in the generator
  const ok = await crypto.verify(signingPublicKey, signed, signature);
  if (!ok) throw new Error('group sender-key signature verify failed in generator');

  return [
    {
      description: 'Sender header AAD: u16_be(gLen) || g || u16_be(sLen) || s || u32_be(iter)',
      groupId,
      senderAddress,
      iteration,
      aad: hex(aad),
    },
    {
      description: 'Sender-key step: kdfChainKey + deterministic AES-GCM + Ed25519 sign(aad || ct)',
      chainKey: hex(chainKey),
      groupId,
      senderAddress,
      iteration,
      plaintext: hex(plaintext),
      nonce: hex(fixedNonce),
      signingPrivateKey: hex(signingPrivateKey),
      signingPublicKey: hex(signingPublicKey),
      newChainKey: hex(newChainKey),
      messageKey: hex(messageKey),
      aad: hex(aad),
      ciphertext: hex(ciphertext),
      signature: hex(signature),
    },
  ];
}

// ─── Storage-encryption HKDF parity ────────────────────────
//
// `test-vectors/storage-encryption.json` already exists (V3.2). It pins
// scrypt params + HKDF info templates + AAD templates. The Kotlin port
// will need scrypt (likely via Bouncy Castle) before the full file can
// be consumed; for now this generator emits a sub-vector covering only
// the HKDF-storage-key + HKDF-field-key + deterministic-nonce derivations
// — those Tink already supports. Bumps the `_ts_subset_version`.
async function generateStorageEncryptionSubset(): Promise<Vector[]> {
  const masterKey = new Uint8Array(32);
  for (let i = 0; i < 32; i++) masterKey[i] = i + 1;

  const storageInfo = new TextEncoder().encode('shade-storage-v1');
  const storageKey = await crypto.hkdf(masterKey, new Uint8Array(0), storageInfo, 32);

  const fieldCases = [
    { table: 'sessions', column: 'session' },
    { table: 'identity', column: 'identity' },
    { table: 'trusted_identities', column: 'trusted_identity' },
  ];
  const fieldKeys: Array<{ table: string; column: string; fieldKey: string }> = [];
  for (const c of fieldCases) {
    const info = new TextEncoder().encode(`shade-field-v1:${c.table}:${c.column}`);
    const k = await crypto.hkdf(storageKey, new Uint8Array(0), info, 32);
    fieldKeys.push({ table: c.table, column: c.column, fieldKey: hex(k) });
  }

  const rowKey = new Uint8Array(32).fill(0xcd);
  const nonceCases = [
    { table: 'sessions', pk: 'alice' },
    { table: 'sessions', pk: 'bob' },
    { table: 'identity', pk: '1' },
  ];
  const nonces: Array<{ table: string; pk: string; nonce: string }> = [];
  for (const c of nonceCases) {
    const info = new TextEncoder().encode(`shade-row-nonce-v1:${c.table}:${c.pk}`);
    const n = await crypto.hkdf(rowKey, new Uint8Array(0), info, 12);
    nonces.push({ table: c.table, pk: c.pk, nonce: hex(n) });
  }

  return [
    {
      description: 'Storage HKDF: storageKey = HKDF(masterKey, salt=0, info="shade-storage-v1", 32)',
      masterKey: hex(masterKey),
      storageKey: hex(storageKey),
    },
    {
      description: 'Storage HKDF: fieldKey = HKDF(storageKey, salt=0, info="shade-field-v1:{table}:{column}", 32)',
      storageKey: hex(storageKey),
      fields: fieldKeys,
    },
    {
      description: 'Storage HKDF: rowNonce = HKDF(rowKey, salt=0, info="shade-row-nonce-v1:{table}:{pk}", 12)',
      rowKey: hex(rowKey),
      nonces,
    },
  ];
}

async function generateBlobVectors(): Promise<Vector[]> {
  // Three (master, app) cases. The first two share a master with
  // different app namespaces, exercising the namespace separation;
  // the third uses a different master entirely.
  const cases: Array<{ masterKey: Uint8Array; app: string }> = [
    { masterKey: new Uint8Array(32).map((_, i) => i + 1), app: 'prism-profile-v1' },
    { masterKey: new Uint8Array(32).map((_, i) => i + 1), app: 'test-namespace' },
    { masterKey: new Uint8Array(32).fill(0xff), app: 'prism-profile-v1' },
  ];

  const kdf = cases.map((c) => {
    const slotId = deriveBlobSlotId(c.masterKey, c.app);
    const blobKey = deriveBlobKey(c.masterKey, c.app);
    const signingSeed = deriveBlobSigningSeed(c.masterKey, c.app);
    const ownerPubkey = ed25519PublicKeyFromSeed(signingSeed);
    return {
      description: `V4.9 blob KDF (master + app="${c.app}")`,
      masterKey: hex(c.masterKey),
      app: c.app,
      slotId: hex(slotId),
      blobKey: hex(blobKey),
      signingSeed: hex(signingSeed),
      ownerPubkey: hex(ownerPubkey),
    };
  });

  // Three deterministic AEAD round-trips: pinned key, pinned nonce,
  // pinned plaintext. The wire form is `nonce || ct||tag`.
  const aeadCases = [
    {
      key: new Uint8Array(32).fill(0xab),
      nonce: new Uint8Array(12).fill(0x01),
      slotIdHex: '00'.repeat(32),
      plaintext: new TextEncoder().encode('hello shade-blob-v1'),
    },
    {
      key: new Uint8Array(32).map((_, i) => i),
      nonce: new Uint8Array(12).map((_, i) => 0xa0 + i),
      slotIdHex: 'ff'.repeat(32),
      plaintext: new TextEncoder().encode(
        '{"version":1,"hosts":[],"clients":[],"trustedApproverFingerprints":[],"updatedAt":1}',
      ),
    },
  ];

  const aead = await Promise.all(
    aeadCases.map(async (c) => {
      const aad = new TextEncoder().encode(`shade-profile-aad-v1:${c.slotIdHex}`);
      const ctTag = await aesGcmEncryptDeterministic(c.key, c.nonce, c.plaintext, aad);
      const wire = new Uint8Array(c.nonce.length + ctTag.length);
      wire.set(c.nonce, 0);
      wire.set(ctTag, c.nonce.length);
      return {
        description: 'V4.9 blob AEAD: nonce || AES-256-GCM(key, plaintext, aad="shade-profile-aad-v1:<slotIdHex>")',
        key: hex(c.key),
        nonce: hex(c.nonce),
        slotIdHex: c.slotIdHex,
        plaintext: hex(c.plaintext),
        wire: hex(wire),
      };
    }),
  );

  return [...kdf, ...aead];
}

async function generateApprovalVectors(): Promise<Vector[]> {
  // Pinned signing-payload bytes for canonical approval. Length-
  // prefixed UTF-8 with u16 BE lengths — Kotlin/Swift implementations
  // produce byte-identical input by spec.
  const cases = [
    {
      domain: 'shade-link-approve-v1',
      requestId: 'aabbccddeeff00112233445566778899',
      hostFingerprint: '11111 22222 33333 44444',
      requestingDeviceFingerprint: '55555 66666 77777 88888',
      decision: 'approve' as const,
    },
    {
      domain: 'shade-link-approve-v1',
      requestId: 'aabbccddeeff00112233445566778899',
      hostFingerprint: '11111 22222 33333 44444',
      requestingDeviceFingerprint: '55555 66666 77777 88888',
      decision: 'reject' as const,
    },
    {
      domain: 'prism-link-approve-v1',
      requestId: '00000000000000000000000000000000',
      hostFingerprint: 'a',
      requestingDeviceFingerprint: 'b',
      decision: 'approve' as const,
    },
  ];

  const payloads = cases.map((c) => ({
    description: 'V4.10 approval signing payload (length-prefixed u16 BE UTF-8)',
    domain: c.domain,
    requestId: c.requestId,
    hostFingerprint: c.hostFingerprint,
    requestingDeviceFingerprint: c.requestingDeviceFingerprint,
    decision: c.decision,
    signingPayload: hex(canonicalApprovalSigningBytes(c)),
  }));

  // Pinned end-to-end sign + verify: deterministic seed → pubkey →
  // sign(payload) → verify against the pubkey. Lets the Kotlin port
  // assert the exact 64-byte signature without re-running RNG.
  const seed = new Uint8Array(32).map((_, i) => 0x10 + i);
  const pubkey = ed25519PublicKeyFromSeed(seed);
  const fakeReq: ApprovalRequestFrame = {
    kind: 'approvalNeeded',
    requestId: 'cafebabe1234567890abcdef00112233',
    hostAddress: 'device:host',
    hostFingerprint: 'host-fp',
    requestingDevice: { fingerprint: 'req-fp', receivedAt: 1 },
    expiresAt: 9_999_999_999_999,
    domain: 'shade-link-approve-v1',
  };
  const signed = await signProxyApproval({
    request: fakeReq,
    decision: 'approve',
    approverFingerprint: 'approver-fp',
    approverSigningKey: seed,
    crypto,
  });

  const e2e = {
    description: 'V4.10 approval Ed25519 sign/verify (deterministic seed)',
    seed: hex(seed),
    publicKey: hex(pubkey),
    request: {
      requestId: fakeReq.requestId,
      hostFingerprint: fakeReq.hostFingerprint,
      requestingDeviceFingerprint: fakeReq.requestingDevice.fingerprint,
      decision: 'approve',
      domain: fakeReq.domain,
    },
    signingPayload: hex(
      canonicalApprovalSigningBytes({
        domain: fakeReq.domain,
        requestId: fakeReq.requestId,
        hostFingerprint: fakeReq.hostFingerprint,
        requestingDeviceFingerprint: fakeReq.requestingDevice.fingerprint,
        decision: 'approve',
      }),
    ),
    signature: signed.signature,
  };

  // Sanity self-check at generation time so a silently broken sign
  // path can't ship vectors that "verify" themselves.
  void buildApprovalRequest; // imported but unused — keeps the symbol live

  return [...payloads, e2e];
}

async function main() {
  console.log('Generating cross-platform test vectors…');

  const files: Array<[string, { vectors: Vector[] }]> = [
    ['hkdf.json', { vectors: await generateHkdfVectors() }],
    ['kdf-chain.json', { vectors: await generateKdfChainVectors() }],
    ['x3dh.json', { vectors: await generateX3DHVectors() }],
    ['fingerprint.json', { vectors: await generateFingerprintVectors() }],
    ['wire-format.json', { vectors: await generateWireFormatVectors() }],
    ['ratchet-step.json', { vectors: await generateRatchetStepVectors() }],
    ['streams.json', { vectors: await generateStreamsVectors() }],
    ['backup.json', { vectors: await generateBackupVectors() }],
    ['group.json', { vectors: await generateGroupVectors() }],
    ['storage-hkdf.json', { vectors: await generateStorageEncryptionSubset() }],
    ['blob.json', { vectors: await generateBlobVectors() }],
    ['approval.json', { vectors: await generateApprovalVectors() }],
  ];

  for (const [name, data] of files) {
    const path = join(OUT_DIR, name);
    const versioned = { version: VECTOR_FILE_VERSION, ...data };
    writeFileSync(path, JSON.stringify(versioned, null, 2) + '\n');
    console.log(`  ✓ ${name} (v${VECTOR_FILE_VERSION}, ${data.vectors.length} vectors)`);
  }

  console.log('Done.');
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
