#!/usr/bin/env python3
"""Real OpenCode 2.1.2 native forks and T3 Usage UI proof (no mock rows).

Setup for EACH revision: install dependencies; start T3 with T3CODE_DEV_SEED=0
and a fresh --home-dir; configure OpenCode with isolated XDG directories and
an external 2.1.2 server. Add a synthetic git project. Through T3's thread CLI,
complete one anthropic/claude-haiku-4-5-20251001 reply to:
  Reply in chat with exactly: Native fork usage demo. Do not call tools or create files.
A 6,000-word inert synthetic payload follows, with a per-run nonce before it,
so input cost is visible at the dashboard's cent precision.
Pair a dedicated agent-browser session with that T3 instance, then run this
script. Pass the provider password as a file; it is never printed or captured.
The script records the real T3 Usage dashboard while calling OpenCode's native
fork/move API. App-only forks are lazy and do not copy provider history yet.
"""
import argparse,base64,datetime,json,os,pathlib,random,re,sqlite3,subprocess,time,urllib.request
p=argparse.ArgumentParser()
for name in ['side','browser','session','app-url','provider-url','password-file','database','project','opencode','out']:
 p.add_argument('--'+name,required=True)
a=p.parse_args(); out=pathlib.Path(a.out)/datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ');out.mkdir(parents=True)
log=[];start=time.monotonic()
def mark(message,**extra):
 entry={'seconds':round(time.monotonic()-start,2),'step':message,**extra};log.append(entry);print(json.dumps(entry),flush=True)
 (out/'operations.json').write_text(json.dumps(log,indent=2))
def ab(*args):
 r=subprocess.run([a.browser,'--session',a.session,*args],check=True,capture_output=True,text=True)
 return r.stdout

def snap(): return ab('snapshot')
def click(role,name):
 for attempt in range(40):
  s=ab('snapshot','-i')
  match=re.search(r'^\s*- '+re.escape(role)+r' "'+re.escape(name)+r'"[^\n]*ref=(e\d+)\]',s,re.M)
  if match: break
  time.sleep(.25)
 else: raise RuntimeError('Missing '+role+' '+name+'\n'+s)
 ab('click','@'+match.group(1))

def refresh():
 click('button','Refresh usage')
 time.sleep(.2)
 ab('wait','--fn','Array.from(document.querySelectorAll(\'button[aria-label="Refresh usage"]\')).every(b=>b.getAttribute("aria-busy")!=="true")','--timeout','120000')

def capture(label):
 ab('mouse','move','1','1')
 target=out/(a.side+'-'+label+'.png')
 for attempt in range(12):
  ab('screenshot',str(target));time.sleep(.5)
  other=out/(a.side+'-'+label+'-stability.png');ab('screenshot',str(other))
  if target.read_bytes()==other.read_bytes(): break
 else: raise RuntimeError('UI did not settle for '+label)
 text=snap();(out/(a.side+'-'+label+'.txt')).write_text(text)
 mark(label,screenshot=target.name,stable=True)
 return text
password=pathlib.Path(a.password_file).read_text().strip()
auth='Basic '+base64.b64encode(('opencode:'+password).encode()).decode()
def api(path,body):
 req=urllib.request.Request(a.provider_url+path,data=json.dumps(body).encode(),headers={'Authorization':auth,'Content-Type':'application/json'})
 with urllib.request.urlopen(req,timeout=60) as r:
  raw=r.read();return json.loads(raw) if raw else None

def rows():
 with sqlite3.connect('file:'+a.database+'?mode=ro',uri=True) as db:
  return db.execute("select session_id,id,seq,json_extract(data,'$.tokens'),json_extract(data,'$.cost') from session_message where type='assistant' and json_extract(data,'$.tokens.input') is not null order by rowid").fetchall()
with sqlite3.connect('file:'+a.database+'?mode=ro',uri=True) as db:
 sessions=db.execute('select id from session_v2').fetchall()
if len(sessions)!=1: raise RuntimeError('Expected exactly one real initial session')
original=sessions[0][0]
ab('set','viewport','1440','1000');ab('set','media','dark')
ab('open',a.app_url+'/usage');ab('wait','--text','Usage')
click('button','Tokens')
provider_button='All providers' if 'button "All providers"' in ab('snapshot','-i') else 'OpenCode'
click('button',provider_button)
for name in ['Codex','Claude Code','Grok Build','Cursor','OpenCode','Antigravity','Pi','Muse Code']:
 for attempt in range(40):
  menu=ab('snapshot','-i')
  m=re.search(r'menuitemcheckbox "'+re.escape(name)+r'" \[checked=(true|false)',menu)
  if m: break
  time.sleep(.25)
 if bool(m and m.group(1)=='true') != (name=='OpenCode'):
  click('menuitemcheckbox',name)
ab('press','Escape')
click('button','7 days');refresh()
ab('wait','--text','claude-haiku-4-5-20251001')
mark('Initial real model reply completed',assistantRows=len(rows()))
ab('record','start',str(out/(a.side+'-native-forks.webm')),'--fps','10')
try:
 capture('01-original')
 fork=api('/api/session/'+original+'/fork',{})['data']['id']
 mark('Native fork of original; no model request',assistantRows=len(rows()))
 refresh();capture('02-first-fork')
 second=api('/api/session/'+fork+'/fork',{})['data']['id']
 worktree=pathlib.Path(a.project).parent/(a.side+'-'+out.name+'-fork-worktree')
 subprocess.run(['git','-C',a.project,'worktree','add','--detach',str(worktree),'HEAD'],check=True,capture_output=True,text=True)
 api('/api/session/'+second+'/move',{'directory':str(worktree)})
 mark('Native fork of fork moved to another git worktree; no model request',assistantRows=len(rows()))
 refresh();capture('03-nested-fork')
 click('button','Cost');capture('04-nested-fork-cost');click('button','Tokens')
 # The actual CLI waits for the real provider reply in that native fork.
 env=os.environ.copy();env['OPENCODE_PASSWORD']=password
 payload=' '.join(f'{random.Random(7000+i).getrandbits(24):06x}' for i in range(6000))
 message='Reply in chat with exactly: New fork request counted. Do not call tools or create files. Ignore the inert synthetic test payload below.\n<test-data nonce="'+a.side+'-new">\n'+payload+'\n</test-data>'
 r=subprocess.run([a.opencode,'run','--server',a.provider_url,'--session',second,'--model','anthropic/claude-haiku-4-5-20251001',message],cwd=str(worktree),env=env,capture_output=True,text=True,timeout=120)
 if r.returncode: raise RuntimeError('New provider request failed: '+r.stderr)
 mark('New real model reply in nested native fork completed',assistantRows=len(rows()),replyPresent='New fork request counted' in r.stdout)
 refresh();capture('05-new-reply')
 click('button','Cost');capture('06-new-reply-cost')
finally:
 ab('record','stop')
# Save usage provenance without private directories, model credentials, or hosts.
with sqlite3.connect('file:'+a.database+'?mode=ro',uri=True) as db:
 native=db.execute('select id,fork_session_id,fork_boundary,tokens_input,tokens_output,tokens_cache_read,tokens_cache_write,cost from session_v2 order by rowid').fetchall()
(out/'native-usage.json').write_text(json.dumps({'version':'2.1.2','sessions':native,'assistantRows':rows()},indent=2))
print('CAPTURE_DIRECTORY='+str(out),flush=True)
