import assert from 'node:assert/strict';
import {DatabaseSync} from 'node:sqlite';
import {readFileSync,readdirSync,writeFileSync} from 'node:fs';
import {env} from '../db/node-env';
import type {Database} from '../db/database';
import {seal} from '../lib/secrets';
import {invoiceXml,queueInvoice,processInvoice,defaultInvoiceConfig,type InvoicePayload} from '../lib/invoicing';
import {billingProfileSchema} from '../lib/invoice-profile';
import {invoiceForCheckout,invoiceForPaidSubscription} from '../lib/invoice-events';
import {GET as adminGet,POST as adminPost} from '../app/api/admin/invoicing/route';
import {GET as profileGet,POST as profilePost} from '../app/api/billing-profile/route';
import {POST as billingPost} from '../app/api/billing/route';
import {makeSession} from '../lib/auth';
const sql=new DatabaseSync(':memory:');for(const f of readdirSync('drizzle').filter(f=>f.endsWith('.sql')).sort())sql.exec(readFileSync('drizzle/'+f,'utf8').replaceAll('--> statement-breakpoint',''));
type Params=(string|number|null)[];
const db:Database={kind:'d1',async first<T>(s:string,p:Params=[]){return (sql.prepare(s).get(...p)||null) as T|null},async all<T>(s:string,p:Params=[]){return sql.prepare(s).all(...p) as T[]},async run(s,p=[]){return Number(sql.prepare(s).run(...p).changes)}};
Object.assign(env,{ADMIN_USER_ID:'admin',APP_ORIGIN:'http://127.0.0.1:5180',BILLING_ENCRYPTION_KEY:'1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',DB:{prepare(s:string){return {bind(...p:Params){return {first:()=>db.first(s,p),all:async()=>({results:await db.all(s,p)}),run:async()=>({meta:{changes:await db.run(s,p)}})}}}}}});
const buyer={kind:'individual' as const,name:'Teszt & <Név>',postalCode:'1111',city:'Budapest',address:'Minta utca 1.',taxNumber:'',email:'buyer@example.test',electronicInvoice:true as const};
const payload:InvoicePayload={buyer,gross:349000,paidDate:'2026-09-29',description:'Projekthely',prefix:'TEST',replyTo:'sender@example.test'};
assert.throws(()=>billingProfileSchema.parse({...buyer,kind:'company'}));assert.throws(()=>billingProfileSchema.parse({...buyer,electronicInvoice:false}));assert.throws(()=>billingProfileSchema.parse({...buyer,postalCode:'ABC'}));
const xml=invoiceXml('SC-test-1',payload,'FAKE_TEST');assert.ok(xml.includes('Teszt &amp; &lt;Név&gt;'));assert.ok(xml.includes('<afakulcs>AAM</afakulcs>'));assert.ok(xml.includes('<bruttoErtek>3490.00</bruttoErtek>'));assert.ok(xml.includes('<sendEmail>true</sendEmail>'));assert.ok(!xml.includes('<adoszam>'));assert.ok(invoiceXml('company',{...payload,buyer:{...buyer,kind:'company',taxNumber:'12345678-1-12'}},'FAKE').includes('<adoalany>1</adoalany>'));writeFileSync('.sites-runtime/invoice-example.xml',xml);
const cfg={...defaultInvoiceConfig,enabled:true,duplicateProtection:true,testKey:'FAKE_TEST',liveKey:'FAKE_LIVE'};
await db.run('INSERT INTO invoice_settings VALUES (?,?,?,?)',['szamlazz',await seal(cfg),1,'now']);
const documents=new Map<string,string>();let creates=0,timeout=false,reject=false;const requests:string[]=[];
const miss='<xmlszamlavalasz><sikeres>false</sikeres><hibakod>7</hibakod></xmlszamlavalasz>';
let paid=true,invoiceId='in_first',wrongCustomer=false;
globalThis.fetch=async (input,init)=>{
 const url=String(input);
 if(url.startsWith('https://api.stripe.com/')){
  if(url.includes('/subscriptions/'))return Response.json({id:'sub_test',livemode:true,customer:'cus_ours',metadata:{order_id:'sub-order',user_id:'buyer'},latest_invoice:invoiceId});
  if(url.includes('/invoices/'))return Response.json({id:invoiceId,livemode:true,status:paid?'paid':'open',currency:'huf',amount_paid:249000,billing_reason:'subscription_cycle',customer:wrongCustomer?'cus_other':'cus_ours',status_transitions:{paid_at:1780000000},parent:{subscription_details:{subscription:'sub_test'}},lines:{data:[{parent:{subscription_item_details:{subscription:'sub_test'}},period:{start:1780000000,end:1782592000}}]}});
  if(url.includes('/payment_intents/'))return Response.json({id:'pi_local',status:'succeeded',amount_received:349000,currency:'huf',latest_charge:{id:'ch_local',paid:true,captured:true,created:1780000000}});
  throw Error('Unexpected Stripe test request');
 }
 assert.equal(url,'https://www.szamlazz.hu/szamla/');const form=init?.body as FormData;const action=form.has('action-szamla_agent')?'action-szamla_agent':'action-szamla_agent_xml';const body=await (form.get(action) as Blob).text();const id=body.match(/<rendelesSzam>(.*?)<\/rendelesSzam>/)![1];requests.push(body);
 assert.ok(body.includes(id.includes('-live-')?'FAKE_LIVE':'FAKE_TEST'));
 if(action==='action-szamla_agent_xml')return new Response(documents.has(id)?`<szamla><alap><szamlaszam>${documents.get(id)}</szamlaszam></alap></szamla>`:miss);
 creates++;if(reject)return new Response('<xmlszamlavalasz><sikeres>false</sikeres><hibakod>3</hibakod><hibauzenet>secret</hibauzenet></xmlszamlavalasz>');documents.set(id,'TEST-'+creates);if(timeout)throw Error('connection lost');return new Response(`<xmlszamlavalasz><sikeres>true</sikeres><szamlaszam>TEST-${creates}</szamlaszam></xmlszamlavalasz>`);
};
await queueInvoice(db,'SC-test-first','buyer','test',payload);await queueInvoice(db,'SC-test-first','buyer','test',{...payload,gross:999});assert.equal(creates,1,'duplicate event never creates twice');
await Promise.allSettled([queueInvoice(db,'SC-test-race','buyer','test',payload),queueInvoice(db,'SC-test-race','buyer','test',payload)]);assert.equal(creates,2,'database lease serializes concurrent work');
timeout=true;await assert.rejects(()=>queueInvoice(db,'SC-test-timeout','buyer','test',payload));assert.equal((await db.first<{status:string}>('SELECT status FROM invoice_jobs WHERE id = ?',['SC-test-timeout']))?.status,'uncertain');timeout=false;await processInvoice(db,'SC-test-timeout');assert.equal(creates,3,'lost success reconciles without recreation');
await db.run('UPDATE invoice_jobs SET status = ?, number = NULL WHERE id = ?',['uncertain','SC-test-timeout']);documents.delete('SC-test-timeout');await assert.rejects(()=>processInvoice(db,'SC-test-timeout'));assert.equal(creates,3,'unknown outcome with no record fails closed');
reject=true;await assert.rejects(()=>queueInvoice(db,'SC-test-rejected','buyer','test',payload));assert.equal((await db.first<{status:string}>('SELECT status FROM invoice_jobs WHERE id = ?',['SC-test-rejected']))?.status,'failed');reject=false;await processInvoice(db,'SC-test-rejected');assert.equal(creates,5,'confirmed rejection can retry');
await db.run('UPDATE invoice_settings SET data = ?',[await seal({...cfg,testKey:''})]);await queueInvoice(db,'SC-test-no-key','buyer','test',payload);assert.equal(creates,5,'test never uses live credential');await db.run('UPDATE invoice_settings SET data = ?',[await seal(cfg)]);
await db.run('INSERT INTO billing_settings VALUES (?,?,?,?)',['stripe',await seal({enabled:true,mode:'live',test:{secretKey:'sk_test_fake',webhookSecret:'test'},live:{secretKey:'sk_live_fake',webhookSecret:'live'}}),1,'now']);
for(const [id,kind,amount] of [['project-order','project',349000],['sub-order','subscription',249000]] as const){await db.run('INSERT INTO billing_orders (kind,id,user_id,amount,currency,mode,status,session_id,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)',[kind,id,'buyer',amount,'huf','live','paid',id==='project-order'?'cs_project':'cs_sub',1,1]);await db.run('INSERT INTO order_billing VALUES (?,?)',[id,JSON.stringify(buyer)])}
const session={id:'cs_project',livemode:true,client_reference_id:'buyer',metadata:{order_id:'project-order',user_id:'buyer'},mode:'payment',payment_status:'paid',amount_total:349000,currency:'huf',payment_intent:'pi_local'};
await invoiceForCheckout(db,session as never,'live');const afterProject=creates;await invoiceForCheckout(db,session as never,'live');assert.equal(creates,afterProject);
paid=false;await invoiceForPaidSubscription(db,invoiceId,'live');assert.equal(creates,afterProject,'unpaid renewal never invoices');paid=true;await invoiceForPaidSubscription(db,invoiceId,'live');await invoiceForPaidSubscription(db,invoiceId,'live');assert.equal(creates,afterProject+1);invoiceId='in_renewal';await invoiceForPaidSubscription(db,invoiceId,'live');assert.equal(creates,afterProject+2,'each paid renewal gets its own invoice');wrongCustomer=true;await assert.rejects(()=>invoiceForPaidSubscription(db,'in_wrong','live'));assert.equal(creates,afterProject+2);
for(const id of ['admin','buyer','other'])await db.run('INSERT INTO users (id,email,name,password_hash,created_at) VALUES (?,?,?,?,?)',[id,id+'@example.test',id,'unused',1]);
const headers=async(id:string)=>new Headers({host:'127.0.0.1:5180',origin:'http://127.0.0.1:5180',cookie:(await makeSession(db,id,new Headers({host:'127.0.0.1:5180'}),0)).split(';')[0],'Content-Type':'application/json'});
const adminH=await headers('admin'),buyerH=await headers('buyer'),otherH=await headers('other');const base='http://127.0.0.1:5180';
assert.equal((await adminGet(new Request(base+'/api/admin/invoicing',{headers:buyerH}))).status,403);const adminText=await (await adminGet(new Request(base+'/api/admin/invoicing',{headers:adminH}))).text();assert.ok(!adminText.includes('FAKE_TEST'));assert.ok(!adminText.includes('FAKE_LIVE'));
assert.equal((await profilePost(new Request(base+'/api/billing-profile',{method:'POST',headers:buyerH,body:JSON.stringify(buyer)}))).status,200);assert.equal((await (await profileGet(new Request(base+'/api/billing-profile',{headers:otherH}))).json() as {invoices:unknown[]}).invoices.length,0);
const wrongOrigin=new Headers(adminH);wrongOrigin.set('origin','https://evil.test');assert.equal((await adminPost(new Request(base+'/api/admin/invoicing',{method:'POST',headers:wrongOrigin,body:'{}'}))).status,403);
const buyRequest=(h:Headers)=>new Request(base+'/api/billing',{method:'POST',headers:h,body:JSON.stringify({action:'checkout',requestId:crypto.randomUUID(),kind:'project'})});
assert.match(await (await billingPost(buyRequest(otherH))).text(),/számlázási adataidat/);
await db.run('UPDATE invoice_settings SET data = ?',[await seal({...cfg,liveKey:''})]);assert.match(await (await billingPost(buyRequest(buyerH))).text(),/számlázás még nincs beállítva/);await db.run('UPDATE invoice_settings SET data = ?',[await seal(cfg)]);
const saveConfig=(revision:number)=>new Request(base+'/api/admin/invoicing',{method:'POST',headers:adminH,body:JSON.stringify({...cfg,testKey:'',liveKey:'',revision})});
assert.equal((await adminPost(saveConfig(1))).status,200);assert.equal((await adminPost(saveConfig(1))).status,409,'stale admin configuration cannot overwrite');
const saved=JSON.parse(await (await adminGet(new Request(base+'/api/admin/invoicing',{headers:adminH}))).text());assert.equal(saved.testKey,true);assert.equal(saved.liveKey,true,'blank key inputs preserve credentials');
assert.equal((await adminPost(new Request(base+'/api/admin/invoicing',{method:'POST',headers:adminH,body:JSON.stringify({action:'retry',id:'missing'})}))).status,400);
console.log('PASS: AAM XML, buyer validation, escaped text, email request, separate keys, duplicate/concurrent jobs, timeout reconciliation, rejection retry, unpaid and renewed subscriptions, owner isolation, admin masking and CSRF. No external invoice or email was created.');
