#!/usr/bin/env python3
"""Manual real-app proof. Run under headless-desktop, with a packaged executable.
Usage: drive.py EXECUTABLE SIDE COMMIT OUTPUT_DIRECTORY INSPECTOR_PORT
Requires strace, node, xterm, ImageMagick, ffmpeg. No source changes or mocks.
The inspector only positions the real window, reads isPackaged, and calls app.quit().
Raw app/strace logs stay private. Published reports use symbolic path names.
"""
import os,sys,pathlib,subprocess,time,json,hashlib,re,datetime,signal
exe,side,sha,out,port=sys.argv[1:]
out=pathlib.Path(out).resolve();out.mkdir(parents=True,exist_ok=False)
root=out.parent
for name in ['home','config','data','state']: (out/name).mkdir()
env={k:v for k,v in os.environ.items() if not k.startswith(('T3CODE_', 'VITE_', 'CODEX_', 'OPENAI_', 'ANTHROPIC_')) and k != 'NODE_OPTIONS'}
for name in ['APPIMAGE','APPDIR','KDE_FULL_SESSION','KDE_SESSION_VERSION','DESKTOP_SESSION','ELECTRON_RUN_AS_NODE','T3CODE_DESKTOP_DEV','VITE_DEV_SERVER_URL','T3CODE_DESKTOP_LAUNCHER_PATH']:
    env.pop(name,None)
env.update(HOME=str(out/'home'),XDG_CONFIG_HOME=str(out/'config'),XDG_DATA_HOME=str(out/'data'),T3CODE_HOME=str(out/'state'),T3CODE_DEV_SEED='0',XDG_CURRENT_DESKTOP='generic',DE='generic',DO_NOT_TRACK='1',PATH='/usr/bin:/bin',T3CODE_PORT=str(int(port)+100))
paths={'desktop entry':out/'data/applications/com.t3tools.T3Code.desktop','persistent icon':out/'data/icons/com.t3tools.T3Code.desktop.png','MIME cache':out/'data/applications/mimeinfo.cache','defaults':out/'config/mimeapps.list'}
summary=[];results=[]
def complete_syscalls(raw):
    pending={}; lines=[]
    for line in raw.splitlines():
        pid=line.split()[0]
        if '<unfinished ...>' in line:
            pending[pid]=line.split('<unfinished ...>')[0]
        elif '<... ' in line and ' resumed>' in line and pid in pending:
            lines.append(pending.pop(pid)+line.split(' resumed>',1)[1])
        else: lines.append(line)
    return lines

def snapshot():
    return {name: {'mtime_ns':p.stat().st_mtime_ns,'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'bytes':p.stat().st_size} if p.exists() else None for name,p in paths.items()}
def inspect(expression):
    r=subprocess.run(['node',str(root/'inspect.mjs'),port,expression],capture_output=True,text=True,timeout=15)
    if r.returncode: raise RuntimeError(r.stderr)
    return json.loads(r.stdout)
def dashboard(status):
    text=['LINUX DESKTOP REGISTRATION - REAL APP',f'{side.upper()}  {sha[:10]}','Packaged Electron 44.10.1 / Linux x86_64','Xvfb 1920x1080; app 1060x940; light theme','Fresh HOME, XDG config/data, T3 state','Generic xdg-mime backend; real system helpers','No provider turns; no copied user data','',status,'']+summary
    (out/'dashboard.txt').write_text('\n'.join(text)+'\n')
def stable_capture(name):
    # The fixed top-right mask removes the environment hostname only.
    # Raw originals stay local; registration evidence in the left panel is untouched.
    for attempt in range(12):
        a=out/f'{name}-raw-a.png';b=out/f'{name}-raw-b.png'
        subprocess.run(['import','-window','root',str(a)],check=True)
        time.sleep(.5)
        subprocess.run(['import','-window','root',str(b)],check=True)
        same=subprocess.run(['compare','-metric','AE',str(a),str(b),'null:'],capture_output=True,text=True)
        if same.returncode==0:
            subprocess.run(['convert',str(b),'-fill','#fafafa','-draw','rectangle 990,110 1840,350',str(out/f'{name}.png')],check=True)
            return
    raise RuntimeError('Screen did not become stable')
dashboard('Starting capture...')
# Display only the report, never shell prompts or private log paths.
term=subprocess.Popen(['xterm','-title','Registration evidence','-geometry','79x53+0+0','-fa','DejaVu Sans Mono','-fs','12','-bg','#101820','-fg','#e9f3f9','-e','python3','-c',
    'import pathlib,time,sys; p=pathlib.Path(sys.argv[1]); last=""\nwhile True:\n t=p.read_text()\n if t!=last: print("\033[2J\033[H"+t,end="",flush=True);last=t\n time.sleep(.2)',str(out/'dashboard.txt')],env=env)
video=subprocess.Popen(['ffmpeg','-y','-loglevel','error','-f','x11grab','-framerate','8','-video_size','1920x1080','-i',os.environ['DISPLAY'],'-vf','drawbox=x=990:y=110:w=850:h=240:color=0xfafafa:t=fill','-c:v','libx264','-preset','ultrafast','-crf','24','-pix_fmt','yuv420p',str(out/f'{side}.mp4')],stdout=subprocess.DEVNULL,stderr=open(out/'video-private.log','w'))
try:
    for index,stage in enumerate(['initial launch','unchanged relaunch','missing MIME cache','stale MIME cache','changed desktop entry','changed default','damaged persistent icon','unchanged after repair'],1):
        if stage=='missing MIME cache': paths['MIME cache'].unlink()
        elif stage=='stale MIME cache': os.utime(paths['MIME cache'],ns=(1_000_000_000,1_000_000_000))
        elif stage=='changed desktop entry': paths['desktop entry'].write_text('[Desktop Entry]\nType=Application\nName=Stale registration\nExec=/obsolete/t3code %U\n')
        elif stage=='changed default': paths['defaults'].write_text('[Default Applications]\nx-scheme-handler/t3code=other.desktop;\n')
        elif stage=='damaged persistent icon': paths['persistent icon'].write_bytes(b'damaged icon')
        before=snapshot()
        dashboard(f'Launch {index}/8: {stage} ...')
        log=out/f'{index}-app-private.log';trace=out/f'{index}-strace-private.log'
        stream=open(log,'w')
        proc=subprocess.Popen(['strace','-D','-f','-tt','-s','512','-e','trace=%file,%process','-o',str(trace),exe,'--no-sandbox','--disable-gpu',f'--inspect=127.0.0.1:{port}'],env=env,stdout=stream,stderr=subprocess.STDOUT,start_new_session=True)
        try:
            deadline=time.monotonic()+60
            while time.monotonic()<deadline:
                if proc.poll() is not None: raise RuntimeError(f'app exited early: {proc.returncode}')
                if 'registered URL scheme handler' in log.read_text(): break
                time.sleep(.25)
            else: raise RuntimeError('Registration never completed')
            info=inspect('(()=>{const e=process.mainModule.require("electron");const w=e.BrowserWindow.getAllWindows()[0];w.setBounds({x:850,y:20,width:1060,height:940});return {isPackaged:e.app.isPackaged,version:e.app.getVersion()}})()')
            deadline=time.monotonic()+100
            while time.monotonic()<deadline:
                ready=inspect('process.mainModule.require("electron").BrowserWindow.getAllWindows()[0]?.webContents.executeJavaScript('+json.dumps("document.body.innerText.includes('Start without an agent')")+')')
                if ready: break
                time.sleep(1)
            else: raise RuntimeError('Renderer never reached welcome screen')
            time.sleep(3)
            after=snapshot()
            raw=trace.read_text()
            helpers={cmd:len([line for line in complete_syscalls(raw) if re.search(r'execve\("[^\"]*/'+cmd+r'"',line) and '= 0' in line]) for cmd in ['update-desktop-database','xdg-mime']}
            writes={name:[line for line in complete_syscalls(raw) if str(p) in line and ('O_TRUNC' in line or 'copy_file_range(' in line)] for name,p in paths.items()}
            delta={name: 'created' if before[name] is None else ('UNCHANGED' if before[name]['mtime_ns']==after[name]['mtime_ns'] else 'REWRITTEN') for name in paths}
            row={'launch':index,'stage':stage,'runtime':info,'before':before,'after':after,'mtime_result':delta,'helpers':helpers,'write_open_count':{k:len(v) for k,v in writes.items()}}
            results.append(row)
            summary += [f'{index}. {stage}',f'   entry: {delta["desktop entry"]}; icon: {delta["persistent icon"]}',f'   cache: {delta["MIME cache"]}',f'   helpers: database={helpers["update-desktop-database"]}, xdg-mime={helpers["xdg-mime"]}']
            dashboard(f'Launch {index}/8: registration complete; packaged=true')
            time.sleep(2)
            if index in [2,3,8]: stable_capture(f'{side}-{index}')
            inspect('process.mainModule.require("electron").app.quit()')
            proc.wait(timeout=60)
            row['exitCode']=proc.returncode
            if proc.returncode !=0: raise RuntimeError(f'Nonzero app exit {proc.returncode}')
            time.sleep(1)
        finally:
            if proc.poll() is None:
                # Ask only the app spawned by this run to quit normally.
                try: inspect('process.mainModule.require("electron").app.quit()'); proc.wait(timeout=20)
                except Exception: proc.terminate()
            stream.close()
    (out/f'{side}-report.json').write_text(json.dumps({'side':side,'commit':sha,'environment':'Linux x86_64; packaged Electron; generic XDG backend; fresh isolated state; Xvfb 1920x1080; light theme','launches':results},indent=2))
    (out/f'{side}-transcript.txt').write_text((out/'dashboard.txt').read_text()+'\nMtime values are nanoseconds since epoch; SHA-256 and exact values are in the report.\nNormal exit: app.quit() through Electron inspector; each exit code was 0.\nHostname-only privacy mask in captures; raw registration measurements are unchanged.\n')
finally:
    video.send_signal(signal.SIGINT);video.wait(timeout=20)
    term.terminate();term.wait(timeout=5)
