import os,sys,time,json,sqlite3,subprocess,datetime,pathlib,signal
ROOT=pathlib.Path(__file__).resolve().parent
RUN=ROOT/('run-'+datetime.datetime.now(datetime.timezone.utc).strftime('%Y%m%dT%H%M%SZ')); RUN.mkdir()
HOME=pathlib.Path.home()
SIDES=[('before','a2be070f30778bfb0502bd7453e130e897c8d4df',HOME/'wt/factory-issue-36050-evidence-1-base',38001),('after','b27d61095cf5e5949e4902296303c3386b12faa7',HOME/'wt/factory-issue-36050-evidence-1',38002)]
servers=[]; records={}
def emit(s,line):
    r=records[s]; line=str(line).replace(str(RUN),'<scratch>').replace(str(HOME),'<home>')
    r['events'].append([round(time.monotonic()-r['start'],3),line]); print(s+': '+line,flush=True)
    (RUN/(s+'.txt')).write_text('\n'.join(x[1] for x in r['events'])+'\n')
def cli(r,args):
    p=subprocess.run(['node','apps/server/src/bin.ts',*args,'--base-dir',str(r['data'])],cwd=r['cwd'],env=r['env'],capture_output=True,text=True)
    return p

def rows(r):
    with sqlite3.connect('file:'+str(r['db'])+'?mode=ro',uri=True) as c:
        c.row_factory=sqlite3.Row
        return [dict(x) for x in c.execute("select session_id, scopes, issued_at, expires_at, revoked_at from auth_sessions where client_label='t3 thread cli' order by issued_at")]
def report(s,row):
    ttl=(datetime.datetime.fromisoformat(row['expires_at'].replace('Z','+00:00'))-datetime.datetime.fromisoformat(row['issued_at'].replace('Z','+00:00'))).total_seconds()
    emit(s,'scopes: '+row['scopes']); emit(s,f'lifetime: {ttl:g} seconds ({ttl/60:g} minutes)'); emit(s,'revoked: '+str(row['revoked_at'] is not None))
    return ttl
try:
 for s,sha,cwd,port in SIDES:
    data=RUN/s; data.mkdir(); project=data/'demo'; project.mkdir()
    subprocess.run(['git','init','-q',str(project)],check=True)
    env={**os.environ,'T3CODE_DEV_SEED':'0'}
    r=records[s]={'start':time.monotonic(),'events':[],'data':data,'db':data/'userdata/statev2.sqlite','cwd':cwd,'env':env,'sha':sha}
    emit(s,'REAL CLI / Linux / Node / fresh isolated database / loopback server')
    emit(s,'commit '+sha)
    log=open(data/'server-private.log','w')
    p=subprocess.Popen(['node','apps/server/src/bin.ts','serve','--base-dir',str(data),'--port',str(port),'--host','127.0.0.1'],cwd=cwd,env=env,stdout=log,stderr=log); servers.append(p)
    deadline=time.monotonic()+60
    while not (data/'userdata/server-runtime.json').exists():
        if p.poll() is not None: raise RuntimeError('server exited; inspect private log')
        # Runtime filename differs across releases; use database + TCP readiness.
        import socket
        try:
            with socket.create_connection(('127.0.0.1',port),timeout=.1): pass
        except OSError: pass
        if time.monotonic()>deadline: raise RuntimeError('startup timeout')
        time.sleep(.1)
    out=cli(r,['project','add',str(project),'--title','Evidence demo']); emit(s,'$ t3 project add <demo>'); emit(s,'exit '+str(out.returncode)); emit(s,(out.stdout+out.stderr).strip())
    if out.returncode: raise RuntimeError('project registration failed')
    for args,label in [(['thread','list','--project',str(project),'--json'],'list registered project'),(['thread','list','--project',str(data/'missing'),'--json'],'list missing project'),(['thread','show','00000000-0000-4000-8000-000000000001','--json'],'show missing thread')]:
        old=len(rows(r)); emit(s,'$ t3 thread '+label)
        out=cli(r,args); emit(s,'exit '+str(out.returncode)); emit(s,(out.stdout+out.stderr).strip())
        new=rows(r)
        if len(new)!=old+1: raise RuntimeError('expected exactly one new session')
        report(s,new[-1])
    old={x['session_id'] for x in rows(r)}
    emit(s,'$ t3 thread list --project <demo> --json &')
    killlog=open(data/'killed-cli-private.log','w')
    p=subprocess.Popen(['node','apps/server/src/bin.ts','thread','list','--project',str(project),'--json','--base-dir',str(data)],cwd=cwd,env=env,stdout=killlog,stderr=killlog)
    deadline=time.monotonic()+30
    while True:
        new=[x for x in rows(r) if x['session_id'] not in old]
        if new:
            p.kill(); code=p.wait(); r['killed']=new[0]['session_id']; break
        if p.poll() is not None: raise RuntimeError('CLI exited before capture')
        if time.monotonic()>deadline: p.kill(); p.wait(); raise RuntimeError('session timeout')
        time.sleep(.001)
    emit(s,'Session observed; SIGKILL sent to captured CLI PID; exit '+str(128-code))
    row=next(x for x in rows(r) if x['session_id']==r['killed']); r['ttl']=report(s,row)
    out=cli(r,['auth','session','list','--json']); sessions=json.loads(out.stdout)
    # This command intentionally exposes no bearer tokens; retain only the killed-session membership.
    r['active_initial']=r['killed'] in out.stdout
    emit(s,'$ t3 auth session list --json'); emit(s,'Killed session listed as active: '+str(r['active_initial']))
    r['expiry']=datetime.datetime.fromisoformat(row['expires_at'].replace('Z','+00:00')).timestamp()
 emit('after','Waiting for the actual 10-minute expiry; no clock changes or database writes.')
 target=records['after']['expiry']+2
 while time.time()<target:
    time.sleep(min(30,target-time.time())); print('Expiry observation pending',flush=True)
 for s,r in records.items():
    emit(s,'After head session expiry (real wall clock):')
    out=cli(r,['auth','session','list','--json']); active=r['killed'] in out.stdout; r['active_final']=active
    emit(s,'$ t3 auth session list --json'); emit(s,'Killed session listed as active: '+str(active))
    row=next(x for x in rows(r) if x['session_id']==r['killed']); report(s,row)
    emit(s,'Observed at '+datetime.datetime.now(datetime.timezone.utc).isoformat())
finally:
 for p in servers:
    if p.poll() is None:
        p.terminate()
        try:p.wait(timeout=15)
        except subprocess.TimeoutExpired:p.kill();p.wait()
 for s,r in records.items():
    (RUN/(s+'.events.json')).write_text(json.dumps(r['events'],indent=2))
 (RUN/'summary.json').write_text(json.dumps({s:{k:r.get(k) for k in ['sha','ttl','active_initial','active_final']} for s,r in records.items()},indent=2))
 print('ARTIFACTS '+str(RUN),flush=True)
