// RFC 8291 §5 test-vector reproduction using ONLY crypto.subtle
const enc = new TextEncoder();
function b64uDec(s: string): Uint8Array {
  s = s.replace(/-/g,'+').replace(/_/g,'/'); while (s.length%4) s+='=';
  return Uint8Array.from(atob(s), c=>c.charCodeAt(0));
}
function b64uEnc(u: Uint8Array): string {
  return btoa(String.fromCharCode(...u)).replace(/\+/g,'-').replace(/\//g,'_').replace(/=+$/,'');
}
function cat(...a: Uint8Array[]) { const n=a.reduce((s,x)=>s+x.length,0); const o=new Uint8Array(n); let i=0; for(const x of a){o.set(x,i);i+=x.length;} return o; }
function hex(u: Uint8Array){return [...u].map(x=>x.toString(16).padStart(2,'0')).join('');}

// --- RFC 8291 §5 fixtures
const AUTH   = b64uDec("BTBZMqHH6r4Tts7J_aSIgg");                        // 16
const UA_PRIV= b64uDec("q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94");
const UA_PUB = b64uDec("BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4");
const AS_PRIV= b64uDec("yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw");
const AS_PUB = b64uDec("BP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A8");
const PLAINTEXT = "When I grow up, I want to be a watermelon";
const EXPECTED_BODY_B64U =
  "DGv6ra1nlYgDCS1FRnbzlwAAEABBBP4z9KsN6nGRTbVYI_c7VJSPQTBtkgcy27mlmlMoZIIgDll6e3vCYLocInmYWAmS6TlzAC8wEqKK6PBru3jl7A_yl95bQpu6cVPTpK4Mqgkf1CXztLVBSt2Ks3oZwbuwXPXLWyouBWLVWGNWQexSgSxsj_Qulcy4a-fN";
console.log("AUTH len", AUTH.length, "UA_PUB len", UA_PUB.length, "AS_PUB len", AS_PUB.length);

// import raw P-256 private key via JWK (WebCrypto cannot import 'raw' private)
async function importEcdhPriv(d: Uint8Array, pub: Uint8Array): Promise<CryptoKey> {
  return crypto.subtle.importKey('jwk', {
    kty:'EC', crv:'P-256',
    d: b64uEnc(d), x: b64uEnc(pub.slice(1,33)), y: b64uEnc(pub.slice(33,65)),
    ext:true, key_ops:['deriveBits'],
  }, {name:'ECDH',namedCurve:'P-256'}, true, ['deriveBits']);
}
const asPrivKey = await importEcdhPriv(AS_PRIV, AS_PUB);
const uaPubKey  = await crypto.subtle.importKey('raw', UA_PUB, {name:'ECDH',namedCurve:'P-256'}, false, []);

// 1. ECDH shared secret = ECDH(as_private, ua_public)
const ecdhSecret = new Uint8Array(await crypto.subtle.deriveBits({name:'ECDH', public: uaPubKey}, asPrivKey, 256));
console.log("ecdh_secret =", b64uEnc(ecdhSecret), " (hex", hex(ecdhSecret), ")");

// 2. PRK_key = HMAC(auth_secret, ecdh_secret); IKM = HMAC(PRK_key, key_info||0x01)
async function hkdf(salt: Uint8Array, ikm: Uint8Array, info: Uint8Array, len: number): Promise<Uint8Array> {
  const k = await crypto.subtle.importKey('raw', ikm, 'HKDF', false, ['deriveBits']);
  return new Uint8Array(await crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt,info}, k, len*8));
}
const keyInfo = cat(enc.encode("WebPush: info"), new Uint8Array([0]), UA_PUB, AS_PUB);
console.log("key_info len =", keyInfo.length, "(expect 13+1+65+65=144)");
const IKM = await hkdf(AUTH, ecdhSecret, keyInfo, 32);
console.log("IKM =", b64uEnc(IKM));

// 3. salt from expected body header (first 16 bytes)
const expected = b64uDec(EXPECTED_BODY_B64U);
const salt = expected.slice(0,16);
const rs = new DataView(expected.buffer, expected.byteOffset+16, 4).getUint32(0,false);
const idlen = expected[20];
const keyid = expected.slice(21, 21+idlen);
console.log("HEADER salt=", b64uEnc(salt), "rs=", rs, "idlen=", idlen, "keyid==AS_PUB:", hex(keyid)===hex(AS_PUB));

// 4. CEK / NONCE
const CEK   = await hkdf(salt, IKM, cat(enc.encode("Content-Encoding: aes128gcm"), new Uint8Array([0])), 16);
const NONCE = await hkdf(salt, IKM, cat(enc.encode("Content-Encoding: nonce"),    new Uint8Array([0])), 12);
console.log("CEK   =", b64uEnc(CEK));
console.log("NONCE =", b64uEnc(NONCE));

// 5. encrypt plaintext || 0x02
const aesKey = await crypto.subtle.importKey('raw', CEK, 'AES-GCM', false, ['encrypt','decrypt']);
const padded = cat(enc.encode(PLAINTEXT), new Uint8Array([0x02]));
const ct = new Uint8Array(await crypto.subtle.encrypt({name:'AES-GCM', iv: NONCE, tagLength:128}, aesKey, padded));
console.log("ciphertext len =", ct.length, "(plaintext", PLAINTEXT.length, "+1 delim +16 tag =", PLAINTEXT.length+17, ")");

// 6. assemble body
const rsBuf = new Uint8Array(4); new DataView(rsBuf.buffer).setUint32(0, rs, false);
const body = cat(salt, rsBuf, new Uint8Array([AS_PUB.length]), AS_PUB, ct);
const got = b64uEnc(body);
console.log("\n--- RESULT ---");
console.log("expected len", expected.length, "got len", body.length);
console.log("MATCH:", got === EXPECTED_BODY_B64U);
if (got !== EXPECTED_BODY_B64U) { console.log("expected:", EXPECTED_BODY_B64U); console.log("got     :", got); }
