import { describe, test, expect } from 'bun:test';
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { fromBase64, toBase64 } from '@shade/core';
import {
  buildAad, deriveFieldKey, deriveMasterKey, deriveNonce, deriveStorageKey,
} from '../src/crypto/kdf.js';
import { aeadOpen, aeadSeal } from '../src/crypto/aead.js';

const VECTOR_PATH = resolve(__dirname, '../../../test-vectors/storage-encryption.json');

interface Vector {
  kdf: {
    scrypt: { passphrase: string; salt_hex: string; N: number; r: number; p: number; dkLen: number };
    hkdf_storage_key: { master_key_hex: string };
    hkdf_field_key: { storage_key_hex: string; samples: { table: string; column: string }[] };
    deterministic_nonce: { samples: { table: string; pk: string }[] };
  };
  aead: { round_trips: { table: string; column: string; pk: string; plaintext_utf8: string }[] };
}

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

function toHex(bytes: Uint8Array): string {
  return [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');
}

const vec: Vector = JSON.parse(readFileSync(VECTOR_PATH, 'utf-8'));

describe('storage-encryption test vectors', () => {
  test('scrypt → masterKey is stable for the published parameters', async () => {
    const { passphrase, salt_hex, N, r, p, dkLen } = vec.kdf.scrypt;
    const out = await deriveMasterKey(passphrase, fromHex(salt_hex), { N, r, p, dkLen });
    expect(out.length).toBe(dkLen);
    // Pin the result for cross-impl parity.
    expect(toHex(out)).toBe('aee2dc14f3a46c563f8906a9c8777f167c868dc06015a983fdf2dbba078a3597');
  });

  test('HKDF storageKey derivation matches pinned value', () => {
    const master = fromHex(vec.kdf.hkdf_storage_key.master_key_hex);
    const sk = deriveStorageKey(master);
    expect(sk.length).toBe(32);
    expect(toHex(sk)).toBe('059a250e15aa02952ab977f441e217cfcd4a6d9be8b51cf93d001a90eeb6accc');
  });

  test('HKDF fieldKey derivation is deterministic for known (table, column)', () => {
    // Use a fixed storageKey (different from the pinned one above so this
    // test can run independently).
    const sk = new Uint8Array(32).fill(0xAB);
    const fk = deriveFieldKey(sk, 'sessions', 'session');
    expect(fk.length).toBe(32);
    // Pin: any change to the info-string format must update this value
    // *and* the Android implementation in lockstep.
    expect(toHex(fk)).toBe('cbe428b4e8be2d7c4cd707dbac7e02881f2da34ee5b00bdc9bc1ebf2f096087a');
  });

  test('deriveNonce is 12 bytes and stable for known inputs', () => {
    const k = new Uint8Array(32).fill(0xCD);
    const n = deriveNonce(k, 'sessions', 'alice');
    expect(n.length).toBe(12);
    expect(toHex(n)).toBe('f72f291a2d3cd0ba652b60c5');
  });

  test('AAD templates encode (table, column, pk) verbatim', () => {
    const aad = buildAad('sessions', 'session', 'alice');
    expect(new TextDecoder().decode(aad)).toBe('shade-aad-v1|sessions|session|alice');
  });

  test('AEAD round-trip matches advertised wire format', async () => {
    for (const sample of vec.aead.round_trips) {
      const sk = new Uint8Array(32).fill(0x01);
      const fk = deriveFieldKey(sk, sample.table, sample.column);
      const nonce = deriveNonce(fk, sample.table, sample.pk);
      const aad = buildAad(sample.table, sample.column, sample.pk);
      const pt = new TextEncoder().encode(sample.plaintext_utf8);

      const blob = await aeadSeal(fk, nonce, pt, aad);
      // Wire format: first 12 bytes are the nonce.
      expect(blob.subarray(0, 12)).toEqual(nonce);
      // Last 16 bytes are the GCM tag (we don't pin the tag, just length).
      expect(blob.length).toBe(12 + pt.length + 16);

      const opened = await aeadOpen(fk, blob, aad, nonce);
      expect(new TextDecoder().decode(opened)).toBe(sample.plaintext_utf8);
    }
  });

  test('base64 helper round-trip (sanity)', () => {
    const b = new Uint8Array([1, 2, 3, 4, 5]);
    expect(fromBase64(toBase64(b))).toEqual(b);
  });
});
