#!/usr/bin/env python3
"""Drive the real Linux AppImage settings restart; run under headless-desktop.
Arguments: AppImage, agent-browser binary, output parent, side, commit, initial-network (on/off).
The output directory is new for each run. No real user database is read or copied.
"""
import datetime, json, os, pathlib, signal, subprocess, sys, time, urllib.request
image, browser, parent, side, commit, initial = sys.argv[1:]
run = pathlib.Path(parent) / (side + '-' + initial + '-' + datetime.datetime.now().strftime('%H%M%S'))
run.mkdir(parents=True)
env = dict(os.environ, XDG_CONFIG_HOME=str(run/'config'), XDG_DATA_HOME=str(run/'data'),
 T3CODE_HOME=str(run/'t3'), T3CODE_DEV_SEED='0', T3CODE_TELEMETRY_ENABLED='false', T3CODE_DISABLE_AUTO_UPDATE='true')
(run/'t3/userdata').mkdir(parents=True)
if initial != 'off':
 settings={'serverExposureMode':'network-accessible'}
 if initial == 'ts-on': settings['tailscaleServeEnabled']=True
 (run/'t3/userdata/desktop-settings.json').write_text(json.dumps(settings))
port = 9361 if side == 'before' else 9362
session = 'proof15681-' + side + '-' + initial
log = (run/'private-app.log').open('w')
app = subprocess.Popen([image,'--no-sandbox','--disable-gpu',f'--remote-debugging-port={port}'],env=env,stdout=log,stderr=log)
video = None
report = {'side':side,'commit':commit,'initialNetwork':initial,'environment':'Ubuntu 24.04 x86-64, Xvfb 1920x1080, Electron 44.10.1, light theme, AppImage --no-sandbox --disable-gpu', 'observations':[]}
def ab(*args, timeout=65):
 p=subprocess.run([browser,'--session',session,*args],capture_output=True,text=True,timeout=timeout)
 if p.returncode: raise RuntimeError(p.stderr.strip() or p.stdout.strip())
 return p.stdout

def endpoint():
 try:
  with urllib.request.urlopen(f'http://127.0.0.1:{port}/json',timeout=1) as r: return json.load(r)
 except Exception: return []

def mainpid():
 # Chromium exposes its main PID through the browser process's system info.
 # The process owning the app's private debugging port is also observable via ss.
 p=subprocess.run(['ss','-H','-ltnp',f'sport = :{port}'],capture_output=True,text=True)
 import re
 for pid in re.findall(r'pid=(\d+)',p.stdout):
  try:
   exe=os.readlink('/proc/'+pid+'/exe')
   cmd=pathlib.Path('/proc/'+pid+'/cmdline').read_bytes()
   if exe.endswith('/t3code') and b'--type=' not in cmd: return int(pid),exe
  except OSError: pass
 return None,None

def stable(name):
 previous=None
 for _ in range(20):
  p=subprocess.run(['import','-window','root','png:-'],capture_output=True,check=True).stdout
  if p==previous:
   (run/(name+'.png')).write_bytes(p); return
  previous=p; time.sleep(.5)
 raise RuntimeError('Screenshot did not settle')
try:
 deadline=time.monotonic()+60
 while not endpoint():
  if time.monotonic()>deadline: raise RuntimeError('AppImage did not expose a window')
  time.sleep(.25)
 ab('connect',str(port))
 ab('wait','--text','Welcome to T3 Code')
 # Decline importing any local provider history after discovery finishes.
 ab('wait','--fn',"!!document.querySelector('[role=checkbox],input[type=checkbox]')")
 ab('find','role','checkbox','click')
 ab('find','role','button','click','--name','Start','--exact')
 ab('find','role','button','click','--name','Settings','--exact')
 ab('find','role','treeitem','click','--name','Sharing & access','--exact')
 ab('wait','--fn',"(() => { const s = document.querySelector('[aria-label=\"Enable network access\"]'); return s && s.getAttribute('aria-disabled') !== 'true'; })()")
 if initial=='ts-on':
  ab('wait','--fn',"document.querySelector('[aria-label=\"Enable Tailscale HTTPS\"]')?.getAttribute('aria-checked') === 'true'")
 before_pid,before_exe=mainpid()
 report['outgoingMount']=pathlib.Path(before_exe).parent.name if before_exe else None
 report['observations'].append('Started with Network access '+initial)
 video=subprocess.Popen(['ffmpeg','-hide_banner','-loglevel','error','-f','x11grab','-video_size','1920x1080','-framerate','15','-i',os.environ['DISPLAY'],'-c:v','libx264','-preset','ultrafast','-crf','23','-pix_fmt','yuv420p',str(run/'raw.mp4')],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=(run/'private-video.log').open('w'))
 time.sleep(2)
 if initial.startswith('ts-'):
  ab('wait','--fn',"!!document.querySelector('[aria-label=\"Enable Tailscale HTTPS\"]')")
  ab('find','role','switch','click','--name','Enable Tailscale HTTPS','--exact')
  time.sleep(2)
  ab('find','role','button','click','--name','Restart and disable' if initial=='ts-on' else 'Enable','--exact')
 else:
  ab('find','role','switch','click','--name','Enable network access','--exact')
  time.sleep(2)
  ab('find','role','button','click','--name','Restart and '+('disable' if initial=='on' else 'enable'),'--exact')
 report['observations'].append('Accepted restart in Settings > Sharing & access')
 time.sleep(15)
 new_pid,new_exe=mainpid()
 report['replacementHasWindow']=bool(endpoint())
 if endpoint():
  ab('connect',str(port))
  ab('find','role','button','click','--name','Settings','--exact')
  ab('find','role','treeitem','click','--name','Sharing & access','--exact')
  ab('wait','--fn',"(() => { const s = document.querySelector('[aria-label=\"Enable network access\"]'); return s && s.getAttribute('aria-disabled') !== 'true'; })()")
  if initial=='ts-off':
   ab('wait','--fn',"document.querySelector('[aria-label=\"Enable Tailscale HTTPS\"]')?.getAttribute('aria-checked') === 'true'")
  if initial.startswith('ts-'):
   ab('wait','--fn',"!!document.querySelector('[aria-label=\"Enable Tailscale HTTPS\"]')")
   report['tailscaleSwitch']=ab('get','attr','[aria-label=\"Enable Tailscale HTTPS\"]','aria-checked').strip()
  report['networkSwitch']=ab('get','attr','[aria-label="Enable network access"]','aria-checked').strip()
  report['replacementMount']=pathlib.Path(new_exe).parent.name
  report['freshMount']=before_exe!=new_exe
  report['outgoingMountRemoved']=not pathlib.Path(before_exe).parent.exists()
  report['observations'].append('Usable replacement; Settings opens and reflects saved Network access')
 else:
  report['observations'].append('No replacement window or debugging endpoint after 15 seconds')
 report['savedSettings']=json.loads((run/'t3/userdata/desktop-settings.json').read_text())
 stable('result')
 time.sleep(3)
finally:
 if video:
  video.communicate(b'q',timeout=20)
 # Quit through the desktop shortcut, including its default hold duration.
 subprocess.run(['xdotool','keydown','ctrl+q'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
 time.sleep(2)
 subprocess.run(['xdotool','keyup','q','ctrl'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
 try: app.wait(timeout=10)
 except subprocess.TimeoutExpired: app.terminate(); app.wait(timeout=10)
 subprocess.run([browser,'--session',session,'close'],capture_output=True)
 (run/'observations.json').write_text(json.dumps(report,indent=2)+'\n')
 print(str(run),flush=True)
