// Functional test of the 3 libraries against the RFC 8291 §5 receiver keys.
// We can DECRYPT their output with the UA private key -> proves correctness.
function b64uDec(s:string){s=s.replace(/-/g,'+').replace(/_/g,'/');while(s.length%4)s+='=';return Uint8Array.from(atob(s),c=>c.charCodeAt(0));}
function b64uEnc(u:Uint8Array){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;}
const enc=new TextEncoder(), dec=new TextDecoder();
const AUTH="BTBZMqHH6r4Tts7J_aSIgg";
const UA_PRIV=b64uDec("q1dXpw3UpT5VOmu_cf_v6ih07Aems3njxI-JWgLcM94");
const UA_PUB_B64="BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4";
const UA_PUB=b64uDec(UA_PUB_B64);
const MSG="hello from insuro";

async function decryptBody(body: Uint8Array): Promise<string> {
  const salt=body.slice(0,16);
  const idlen=body[20];
  const asPub=body.slice(21,21+idlen);
  const ct=body.slice(21+idlen);
  const uaPriv=await crypto.subtle.importKey('jwk',{kty:'EC',crv:'P-256',d:b64uEnc(UA_PRIV),x:b64uEnc(UA_PUB.slice(1,33)),y:b64uEnc(UA_PUB.slice(33,65)),ext:true,key_ops:['deriveBits']},{name:'ECDH',namedCurve:'P-256'},true,['deriveBits']);
  const asPubK=await crypto.subtle.importKey('raw',asPub,{name:'ECDH',namedCurve:'P-256'},false,[]);
  const ecdh=new Uint8Array(await crypto.subtle.deriveBits({name:'ECDH',public:asPubK},uaPriv,256));
  const hkdf=async(s:Uint8Array,ikm:Uint8Array,info:Uint8Array,l:number)=>new Uint8Array(await crypto.subtle.deriveBits({name:'HKDF',hash:'SHA-256',salt:s,info},await crypto.subtle.importKey('raw',ikm,'HKDF',false,['deriveBits']),l*8));
  const IKM=await hkdf(b64uDec(AUTH),ecdh,cat(enc.encode("WebPush: info"),new Uint8Array([0]),UA_PUB,asPub),32);
  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);
  const k=await crypto.subtle.importKey('raw',CEK,'AES-GCM',false,['decrypt']);
  const pt=new Uint8Array(await crypto.subtle.decrypt({name:'AES-GCM',iv:NONCE,tagLength:128},k,ct));
  // strip padding delimiter 0x02
  let end=pt.length-1; while(end>=0 && pt[end]===0) end--;
  if(pt[end]!==0x02) throw new Error("bad padding delimiter 0x"+pt[end].toString(16));
  return dec.decode(pt.slice(0,end));
}

const sub={endpoint:"https://fcm.googleapis.com/fcm/send/FAKE_TOKEN_ONLY_FOR_HEADER_BUILD",keys:{p256dh:UA_PUB_B64,auth:AUTH}};

// ---- web-push (esm.sh) ----
try {
  const wp=(await import("https://esm.sh/web-push@3.6.7")).default;
  const vk=wp.generateVAPIDKeys();
  wp.setVapidDetails("mailto:a@b.c", vk.publicKey, vk.privateKey);
  const rd=wp.generateRequestDetails(sub, MSG, {contentEncoding:'aes128gcm'});
  const body=new Uint8Array(rd.body);
  console.log("[web-push] headers:", JSON.stringify(rd.headers));
  console.log("[web-push] bodyLen:", body.length, "decrypted:", JSON.stringify(await decryptBody(body)));
} catch(e){ console.log("[web-push] FAIL:", (e as Error).name, (e as Error).message); }

// ---- @block65/webcrypto-web-push ----
try {
  const m=await import("npm:@block65/webcrypto-web-push@1.0.2");
  console.log("[block65] buildPushPayload sig test...");
  const kp=await crypto.subtle.generateKey({name:'ECDSA',namedCurve:'P-256'},true,['sign','verify']);
  const jwk=await crypto.subtle.exportKey('jwk',kp.privateKey);
  const pub=b64uEnc(new Uint8Array(await crypto.subtle.exportKey('raw',kp.publicKey)));
  const r=await m.buildPushPayload({data:MSG,options:{ttl:60}} as never,{endpoint:sub.endpoint,keys:sub.keys} as never,{subject:"mailto:a@b.c",publicKey:pub,privateKey:jwk.d} as never);
  console.log("[block65] headers:", JSON.stringify(r.headers));
  const body=new Uint8Array(r.body as ArrayBuffer);
  console.log("[block65] bodyLen:", body.length, "decrypted:", JSON.stringify(await decryptBody(body)));
} catch(e){ console.log("[block65] FAIL:", (e as Error).name, (e as Error).message); }

// ---- jsr:@negrel/webpush ----
try {
  const w=await import("jsr:@negrel/webpush@0.5.0");
  const vapid=await w.generateVapidKeys({extractable:true});
  const server=await w.ApplicationServer.new({contactInformation:"mailto:a@b.c", vapidKeys:vapid});
  const subr=server.subscribe(sub as never);
  // intercept fetch to capture request
  const origFetch=globalThis.fetch;
  let captured:{h:Record<string,string>,b:Uint8Array}|null=null;
  globalThis.fetch=(async (input:RequestInfo|URL, init?:RequestInit)=>{
    const h:Record<string,string>={};
    new Headers(init?.headers).forEach((v,k)=>h[k]=v);
    captured={h, b:new Uint8Array(init!.body as ArrayBuffer)};
    return new Response(null,{status:201});
  }) as typeof fetch;
  try { await subr.pushTextMessage(MSG,{}); } finally { globalThis.fetch=origFetch; }
  console.log("[negrel] headers:", JSON.stringify(captured!.h));
  console.log("[negrel] bodyLen:", captured!.b.length, "decrypted:", JSON.stringify(await decryptBody(captured!.b)));
} catch(e){ console.log("[negrel] FAIL:", (e as Error).name, (e as Error).message, (e as Error).stack?.split('\n').slice(0,4).join(' | ')); }
