#!/usr/bin/env python3
"""Run inside headless-desktop with isolated HOME/XDG directories.
Arguments: extracted-deb-root, evidence-directory. Requires GNOME Shell 46,
xdotool, gdbus, ImageMagick and ffmpeg. Preserves packaged launcher metadata.
"""
import ast, json, os, pathlib, shutil, subprocess as sp, sys, time
root=pathlib.Path(sys.argv[1]).resolve(); out=pathlib.Path(sys.argv[2]).resolve(); out.mkdir(parents=True,exist_ok=True)
def run(*args,**kw):return sp.run(args,check=True,**kw)
def ev(code):
 s=sp.check_output(['gdbus','call','--session','--dest','org.gnome.Shell','--object-path','/org/gnome/Shell','--method','org.gnome.Shell.Eval',code],text=True)
 ok,val=ast.literal_eval(s.replace('(true,','(True,',1).replace('(false,','(False,',1))
 if not ok:raise RuntimeError(val)
 return json.loads(val) if val else None
def capture(label):
 a=out/(label+'.png'); b=out/(label+'-stability.png')
 for attempt in range(10):
  run('import','-window','root','-crop','1600x920+0+0',str(a));time.sleep(.5)
  run('import','-window','root','-crop','1600x920+0+0',str(b))
  if sp.run(['compare','-metric','AE',str(a),str(b),'null:'],stderr=sp.DEVNULL).returncode==0:return
 raise RuntimeError('Capture did not settle')
applications=pathlib.Path(os.environ['XDG_DATA_HOME'])/'applications';applications.mkdir(parents=True,exist_ok=True)
entry=next((root/'usr/share/applications').glob('*.desktop'))
binary=next((root/'opt').glob('*/t3code'))
launcher=out/'launch.sh';launcher.write_text('#!/bin/sh\nexec "'+str(binary)+'" --no-sandbox --disable-gpu --ozone-platform=wayland "$@"\n');launcher.chmod(0o755)
text=entry.read_text();text='\n'.join('Exec='+str(launcher)+' %U' if x.startswith('Exec=') else x for x in text.splitlines())+'\n'
(applications/entry.name).write_text(text)
icons=root/'usr/share/icons'
if icons.exists():shutil.copytree(icons,pathlib.Path(os.environ['XDG_DATA_HOME'])/'icons',dirs_exist_ok=True)
run('update-desktop-database',str(applications))
run('gsettings','set','org.gnome.desktop.interface','enable-animations','false')
run('gsettings','set','org.gnome.shell','favorite-apps','[]')
shell=sp.Popen(['gnome-shell','--wayland','--nested','--wayland-display','proof-wayland','--sm-disable'],stdout=open(out/'shell.log','w'),stderr=sp.STDOUT)
rec=None;app=None
try:
 time.sleep(12)
 run('xdotool','mousemove','400','300','click','1','key','Escape');time.sleep(1);run('xdotool','key','alt+F2');time.sleep(1);run('xdotool','type','lg');run('xdotool','key','Return');time.sleep(2)
 run('xdotool','type','global.context.unsafe_mode = true');run('xdotool','key','Return');time.sleep(1);run('xdotool','key','Escape');time.sleep(1)
 state=sp.check_output(['gdbus','call','--session','--dest','org.gnome.Mutter.DisplayConfig','--object-path','/org/gnome/Mutter/DisplayConfig','--method','org.gnome.Mutter.DisplayConfig.GetCurrentState'],text=True)
 serial=state.split('uint32 ')[1].split(',')[0]
 run('gdbus','call','--session','--dest','org.gnome.Mutter.DisplayConfig','--object-path','/org/gnome/Mutter/DisplayConfig','--method','org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig',serial,'1',"[(0,0,1.0,0,true,[('LVDS1','1600x920@60.000',{})])]",'{}',stdout=sp.DEVNULL)
 ev("import('resource:///org/gnome/shell/ui/main.js').then(m=>globalThis.proofMain=m); true");time.sleep(1)
 ev('proofMain.overview.hide(); true')
 env=dict(os.environ,WAYLAND_DISPLAY='proof-wayland',XDG_SESSION_TYPE='wayland',XDG_CURRENT_DESKTOP='GNOME',T3CODE_DEV_SEED='0')
 app=sp.Popen([str(launcher)],env=env,stdout=open(out/'app.log','w'),stderr=sp.STDOUT)
 for _ in range(90):
  if ev('global.get_window_actors().some(a=>a.meta_window.get_wm_class()?.includes("T3Code"))'):break
  if app.poll() is not None:raise RuntimeError('Application exited')
  time.sleep(1)
 else:raise RuntimeError('No T3 Code window')
 time.sleep(8)
 ev('global.get_window_actors().forEach(a=>a.meta_window.maximize(3)); true')
 rec=sp.Popen(['ffmpeg','-y','-loglevel','error','-f','x11grab','-video_size','1600x920','-framerate','15','-i',os.environ['DISPLAY'],'-c:v','libx264','-preset','ultrafast','-pix_fmt','yuv420p',str(out/'interaction.mp4')],stdout=sp.DEVNULL,stderr=open(out/'recording.log','w'))
 time.sleep(1)
 ev('proofMain.overview.show(); true');time.sleep(2)
 items=ev('proofMain.overview.dash._box.get_children().filter(x=>x.child?.app).map(x=>({id:x.child.app.get_id(),name:x.child.app.get_name()}))')
 ev('globalThis.proofIcon=proofMain.overview.dash._box.get_children().find(x=>x.child?.app?.get_name().includes("T3 Code")).child; proofIcon.popupMenu(); true');time.sleep(1)
 menu=ev('proofIcon._menu._getMenuItems().map(x=>x.label?.text).filter(Boolean)')
 capture('running-menu')
 windows=ev('global.get_window_actors().map(a=>({title:a.meta_window.get_title(),wmClass:a.meta_window.get_wm_class(),appId:a.meta_window.get_gtk_application_id(),clientType:a.meta_window.get_client_type()}))')
 result={'launcher':entry.name,'startupWMClass':next(x for x in text.splitlines() if x.startswith('StartupWMClass=')),'dash':items,'menu':menu,'windows':windows};(out/'observations.json').write_text(json.dumps(result,indent=2))
 pin=next((x for x in menu if x in ['Pin to Dash','Add to Favorites']),None)
 if pin:
  pos=ev('(()=>{let a=proofIcon._menu._getMenuItems().find(x=>["Pin to Dash","Add to Favorites"].includes(x.label?.text));let [x,y]=a.get_transformed_position();return [Math.round(x+a.width/2),Math.round(y+a.height/2)]})()');run('xdotool','mousemove',str(pos[0]),str(pos[1]),'click','1');time.sleep(1)
  result['favorites']=sp.check_output(['gsettings','get','org.gnome.shell','favorite-apps'],text=True).strip()
  ev('proofIcon.popupMenu(); true');time.sleep(1);pos=ev('(()=>{let a=proofIcon._menu._getMenuItems().find(x=>x.label?.text === "Quit");let [x,y]=a.get_transformed_position();return [Math.round(x+a.width/2),Math.round(y+a.height/2)]})()');run('xdotool','mousemove',str(pos[0]),str(pos[1]),'click','1');app.wait(timeout=30);time.sleep(2)
  ev('proofMain.overview.show(); true');time.sleep(2);capture('pinned-quit')
  ev('proofMain.overview.dash._box.get_children().find(x=>x.child?.app?.get_id() === '+json.dumps(entry.name)+').child.activate(1); true')
  time.sleep(16)
  ev('proofMain.overview.show(); true');time.sleep(2)
  result['relaunchedDash']=ev('proofMain.overview.dash._box.get_children().filter(x=>x.child?.app).map(x=>({id:x.child.app.get_id(),windows:x.child.app.get_n_windows()}))')
  ev('globalThis.proofIcon=proofMain.overview.dash._box.get_children().find(x=>x.child?.app?.get_id() === '+json.dumps(entry.name)+').child; proofIcon.popupMenu(); true');time.sleep(1);capture('pinned-relaunch');ev('proofIcon._menu.close(); true')
  ev('proofMain.overview.hide(); global.get_window_actors().forEach(a=>a.meta_window.delete(global.get_current_time())); true');time.sleep(3)
 (out/'observations.json').write_text(json.dumps(result,indent=2))
 time.sleep(1)
finally:
 if rec:rec.send_signal(2);rec.wait(timeout=15)
 if app and app.poll() is None:app.terminate();app.wait(timeout=30)
 shell.terminate();shell.wait(timeout=30)
