import fs from 'node:fs';import path from 'node:path';import net from 'node:net';import assert from 'node:assert/strict';import {spawn} from 'node:child_process'; const tag=process.argv[2];assert(/^[a-f0-9]{12}$/.test(tag??''));const run=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')),root=path.join(run.root,'private/receipt-composition-'+tag),s=JSON.parse(fs.readFileSync(path.join(root,'state.json'))),source=JSON.parse(fs.readFileSync(path.join(root,'receipt-ui-source.json'))),n=JSON.parse(fs.readFileSync(path.join(root,'native-fixture.json'))),out=path.join(root,'receipt-ui-http.json');assert(s.continuity&&source.passed&&!fs.existsSync(out));assert.equal(s.env.DB_DATABASE,'budget_core_sgf_'+tag);assert.equal(s.env.DB_PORT,'55439');const checks=[],ok=(v,name)=>{assert(v,name);checks.push(name);fs.writeFileSync(path.join(root,'receipt-ui-http-progress.json'),JSON.stringify(checks,null,2));}; const port=await new Promise((resolve,reject)=>{const server=net.createServer();server.once('error',reject);server.listen(0,'127.0.0.1',()=>{const port=server.address().port;server.close(()=>resolve(port));});});const log=fs.openSync(path.join(root,'receipt-ui-http-server.log'),'a'),server=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:'+port,'-t',path.join(s.app,'public'),path.resolve('scripts/receipt-composition-http.php')],{cwd:s.app,env:s.env,windowsHide:true,stdio:['ignore',log,log]}),base='http://127.0.0.1:'+port,originalFetch=globalThis.fetch,cookies=new Map; const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));async function until(test,label){const end=Date.now()+30000;while(Date.now(){for(const cookie of response.headers.getSetCookie()){const pair=cookie.split(';')[0],at=pair.indexOf('=');cookies.set(pair.slice(0,at),pair.slice(at+1));}};async function raw(url,options={}){const headers=new Headers(options.headers);if(cookies.size)headers.set('Cookie',[...cookies].map(([k,v])=>k+'='+v).join('; '));const response=await originalFetch(base+url,{...options,headers});absorb(response);return response;} class Element{ constructor(tag,text){this.tag=tag;this.textContent=text??'';this.children=[];this.dataset={};this.value='';this.checked=false;this.disabled=false;this.isConnected=true;this.parent=null;} append(...nodes){for(const node of nodes){node.parent=this;node.isConnected=true;this.children.push(node);}} prepend(...nodes){for(const node of nodes){node.parent=this;node.isConnected=true;}this.children.unshift(...nodes);} replaceChildren(...nodes){for(const node of this.children){node.parent=null;node.isConnected=false;}this.children=[];this.append(...nodes);} setAttribute(key,value){this[key]=value;} dispatchEvent(event){let current=this;do{current['on'+event.type]?.(event);current=current.parent;}while(event.bubbles&¤t);return true;} } globalThis.Option=class extends Element{constructor(text,value){super('option',text);this.value=value;}};const elements=new Map,walk=node=>[node,...node.children.flatMap(walk)],find=(parent,name)=>walk(parent).find(e=>e.name===name),calls=[];let allowed=new Set,lostResponse=false,receiptId=null; globalThis.document={createElement:tag=>new Element(tag),querySelector:selector=>{const menu=/^nav.*a\[href="\/purchasing\/([a-z]+)"\]$/.exec(selector);if(menu)return allowed.has(menu[1])?new Element('a'):null;if(!elements.has(selector))elements.set(selector,new Element(selector));return elements.get(selector);}}; try{ await until(()=>server.pid&&server.exitCode===null,'PHP start');let login;for(let i=0;i<80;i++){try{login=await raw('/qa/enter/validator');break;}catch{await delay(50);}}assert(login?.ok);const session=await login.json(),headers={Accept:'application/json','X-SGF-Context':session.context,'X-CSRF-TOKEN':session.csrf};const screen=await raw('/purchasing/receipts',{headers}),html=await screen.text();assert.equal(screen.status,200);allowed=new Set([...html.matchAll(/href="\/purchasing\/([a-z]+)"/g)].map(m=>m[1]));const uiRoot=document.querySelector('#receipts');uiRoot.dataset={context:session.context,csrf:session.csrf,focusEnabled:'true',continuityEnabled:'true'};globalThis.location={search:'?order='+source.order.id}; globalThis.fetch=async(url,options={})=>{assert(url.startsWith('/api/purchasing-receipts'));const response=await raw(url,options),data=await response.clone().json(),call={url,method:options.method??'GET',status:response.status,data,...(options.body?{input:JSON.parse(options.body)}:{})};calls.push(call);if(call.method==='POST'&&url.endsWith('/confirm')&&response.status===201){receiptId=data.receipt.id;if(lostResponse){lostResponse=false;throw Error('Resposta QA perdida depois do commit.');}}return response;}; await import('../public/receipt-workspace.mjs?live='+tag);const detail=document.querySelector('#detail');await until(()=>find(detail,'chosen'),'receipt form');ok(calls.every(v=>v.method==='GET'),'opening real receipt form performs no preview or confirmation');ok(!calls.some(v=>v.url.includes('/catalogue')),'unchecked line does not load article catalogue');ok(!walk(detail).some(e=>e.href?.startsWith('/purchasing/invoices')),'warehouse menu does not advertise inaccessible financial continuation'); const form=walk(detail).find(e=>e.tag==='form'),chosen=find(form,'chosen');chosen.checked=true;chosen.onchange();await until(()=>calls.some(v=>v.url.includes('/catalogue')),'native catalogue response');assert.equal(calls.at(-1).status,200,JSON.stringify(calls.at(-1).data));assert(calls.at(-1).data.data.some(v=>v.id===n.stock),'Fixture stock not in catalogue: '+JSON.stringify(calls.at(-1).data));const choice=find(form,'stock_item_id');ok(calls.some(v=>v.url.includes('/lines/')&&v.url.includes('/catalogue')),'checking line activates actual per-line native catalogue without type error');ok(choice.value==='','catalogue browsing chooses no article automatically'); const catalogue=calls.filter(v=>v.url.includes('/catalogue')).at(-1).data,item=catalogue.data.find(v=>v.id===n.stock);choice.value=String(n.stock);choice.onchange();ok(choice.value===String(n.stock),'explicit catalogue choice survives model publication');const search=find(form,'stock_search');search.value=item.code;search.oninput();ok(choice.value==='','editing article search invalidates previous choice');const findButton=walk(form).find(e=>e.tag==='button'&&e.textContent==='Pesquisar artigos'),beforeSearch=calls.length;findButton.onclick();await until(()=>calls.length>beforeSearch&&choice.children.some(v=>String(v.value)===String(n.stock)),'literal article search');choice.value=String(n.stock);choice.onchange();ok(new URL('http://qa'+calls.filter(v=>v.url.includes('/catalogue')).at(-1).url).searchParams.get('search')===item.code,'search control sends actual literal code to catalogue endpoint'); for(const [name,value]of Object.entries({delivery_note:'QA-UI-DELIVERY',received_on:source.received_on,warehouse_id:String(n.warehouse),received_quantity:'1.000',accepted_quantity:'1.000',quality_status:'compliant'})){const input=find(form,name);assert(input,name);input.value=value;}form.dispatchEvent(new Event('input',{bubbles:true}));await form.onsubmit({preventDefault(){}});const confirm=walk(form).find(e=>e.tag==='button'&&e.textContent==='Confirmar esta recepção'),preview=calls.filter(v=>v.url.endsWith('/preview')).at(-1);ok(preview?.status===200&&!confirm.disabled,'real UI prepares valid native receipt preview');ok(preview.input.items[0].stock_item_id===n.stock&&/^[a-f0-9]{64}$/.test(preview.input.item_revisions[String(n.stock)]),'preview transmits explicit article and current selection fingerprint');ok(!calls.some(v=>v.url.endsWith('/confirm')),'preview does not submit confirmation automatically'); form.dispatchEvent(new Event('input',{bubbles:true}));ok(confirm.disabled,'editing form invalidates ready confirmation');await form.onsubmit({preventDefault(){}});assert(!confirm.disabled);lostResponse=true;await confirm.onclick();ok(receiptId!==null&&!confirm.disabled&&document.querySelector('#message').textContent.includes('repita'),'lost response after real commit preserves same preview for explicit recovery');await confirm.onclick();const statuses=calls.filter(v=>v.url.endsWith('/confirm')).map(v=>v.status);ok(JSON.stringify(statuses)==='[201,200]','second UI confirmation recovers committed result without another receipt');await until(()=>calls.filter(v=>v.url.split('?')[0]==='/api/purchasing-receipts/orders/'+source.order.id).length>=2,'refreshed receipt state'); const state=calls.filter(v=>v.url.split('?')[0]==='/api/purchasing-receipts/orders/'+source.order.id).at(-1).data;ok(state.progress.state==='complete'&&state.history.total===1,'same UI refresh shows exactly one completed receipt');const response=await globalThis.fetch('/api/purchasing-receipts/history/'+receiptId,{headers}),actual=await response.json();ok(actual.receipt.items[0].stock_item_id===n.stock&&actual.movements.length===1&&actual.movements[0].quantity_milli===1000,'native read verifies chosen article and one physical movement of1UN');ok(calls.filter(v=>v.method==='POST').every(v=>/\/(preview|confirm)$/.test(v.url)),'UI flow invokes only existing receipt commands'); fs.writeFileSync(out,JSON.stringify({passed:true,checks,dom_simulation:true,real_http:true,source_workflow_setup_only:true,receipt_id:receiptId,receipt_count:state.history.total,movements:actual.movements.length,quantity_milli:actual.movements[0].quantity_milli,concurrency:false,lost_response_statuses:statuses,visual_review:false,deployment_capacity_validated:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,receipt_count:1,quantity_milli:1000})); }finally{if(!fs.existsSync(out))fs.writeFileSync(path.join(root,'receipt-ui-http-diagnostic.json'),JSON.stringify({calls,status:document.querySelector('#message').textContent},null,2));globalThis.fetch=originalFetch;server.kill();await new Promise(resolve=>{if(server.exitCode!==null)resolve();else{server.once('exit',resolve);setTimeout(resolve,5000);}});fs.closeSync(log);}