#!/usr/bin/env python3
"""Only creates synthetic homes beneath the required scratch directory."""
import datetime, json, os, sqlite3
from pathlib import Path

ROOT = Path('/tmp/lead-32880-r1/evidence-capture')
STAMP = datetime.datetime.now(datetime.timezone.utc).isoformat().replace(':','-')
RUN = ROOT / ('fixtures-' + STAMP)
RUN.mkdir()
NOW = '2026-11-10T12:00:00.000Z'
MS = int(datetime.datetime.fromisoformat(NOW.replace('Z','+00:00')).timestamp()*1000)
SCHEMA = (ROOT/'codex-0.163.0-schema.sql').read_text()
MANIFEST = []

def uid(n): return f'019a5f00-0000-7000-8000-{n:012x}'
def line(record): return json.dumps({'timestamp':NOW,**record},separators=(',',':'))+'\n'
def prompt(text): return line({'type':'event_msg','payload':{'type':'user_message','message':text}})
def reply(text): return line({'type':'response_item','payload':{'type':'message','role':'assistant','content':[{'type':'output_text','text':text}]}})

class Fixture:
 def __init__(self,name,rows,description):
  self.name=name;self.home=RUN/name/'fakehome'; self.P=self.home/'code/project';self.Q=self.home/'code/other'; self.S=self.home/'Documents/Codex/2026-11-08/new-chat'
  for path in [self.P,self.Q,self.S,self.home/'.codex',self.home/'.claude',self.home/'.pi'] : path.mkdir(parents=True,exist_ok=True)
  self.db=sqlite3.connect(self.home/'.codex/state_5.sqlite');self.db.executescript(SCHEMA)
  self.f={'name':name,'rows':rows,'description':description,'home':str(self.home),'now':NOW,'roots':{'P':str(self.P),'Q':str(self.Q)},'files':[],'assignments':[],'rootInsertionOrder':[]}
 def project(self,n,roots):
  id=uid(100000+n)
  self.db.execute('INSERT INTO projects VALUES (?, ?, ?, ?, ?, ?)',(id,f'project-{n}','{}',n,MS,MS))
  for pos,path in roots:
   self.db.execute('INSERT INTO project_roots VALUES (?, ?, ?)',(id,pos,str(path)))
   self.f['rootInsertionOrder'].append({'projectId':id,'position':pos,'path':str(path)})
  return id
 def raw(self,n,cwd,contents,project=None,rollout=None,delta=-1000,home=None):
  codex=Path(home) if home else self.home/'.codex'
  file=codex/'sessions/2026/11/10'/f'rollout-2026-11-10T10-00-00-{uid(n)}{("_"+uid(rollout)) if rollout else ""}.jsonl'
  file.parent.mkdir(parents=True,exist_ok=True);file.write_text(contents);os.utime(file,ns=((MS+delta)*1000000,)*2)
  self.f['files'].append({'path':str(file),'threadId':uid(n),'rolloutId':uid(rollout) if rollout else uid(n),'cwd':str(cwd) if cwd else None,'mtimeMs':MS+delta,'records':contents.splitlines()})
  if home is None:
   self.db.execute('INSERT OR REPLACE INTO threads (id,rollout_path,created_at,updated_at,source,model_provider,cwd,title,sandbox_policy,approval_mode,project_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)',(uid(n),str(file),MS//1000,MS//1000,'vscode','synthetic',str(cwd or ''),'Synthetic fixture','read-only','never',project))
   self.f['assignments'].append({'threadId':uid(n),'projectId':project,'cwd':str(cwd) if cwd else None})
  return file
 def chat(self,n,cwd,text,project=None,delta=-1000,meta=None):
  return self.raw(n,cwd,line({'type':'session_meta','payload':{'id':uid(n),'cwd':str(cwd),**(meta or {})}})+prompt(text),project,delta=delta)
 def control(self):
  p=self.project(1,[(0,self.P)]);self.chat(1,self.P,'R01 direct control',p);return p
 def finish(self):
  self.db.commit();self.db.close();p=RUN/self.name/'fixture.json';p.write_text(json.dumps(self.f,indent=2)+'\n');MANIFEST.append(str(p))

f=Fixture('M1-composite',['R01','R02','R03','R04','R05','R14'],'Direct control, moved scratch, unassigned scratch, real other cwd, two rewinds with inherited history')
p=f.control();f.chat(2,f.S,'R02 assigned scratch',p,delta=-900);f.chat(3,f.S,'R03 unassigned scratch',delta=-800);f.chat(4,f.Q,'R04 real other cwd assigned to P',p,delta=-700)
def meta(base=None):return line({'type':'session_meta','payload':{'id':uid(5),'cwd':str(f.S),'history_mode':'paginated',**({'history_base':base} if base else {})}})
kept0=meta()+prompt('R05 keep original request')+reply('R05 keep original reply')
f.raw(5,f.S,kept0+prompt('R05 ABANDON original request')+reply('R05 ABANDON original reply'),p,delta=-600)
base0={'thread_id':uid(5),'end_ordinal_exclusive':3,'end_byte_offset':len(kept0.encode())}
kept1=meta(base0)+prompt('R05 keep first rewind request')+reply('R05 keep first rewind reply')
f.raw(5,f.S,kept1+prompt('R05 ABANDON first rewind request')+reply('R05 ABANDON first rewind reply'),p,rollout=51,delta=-500)
base1={'thread_id':uid(51),'end_ordinal_exclusive':3,'end_byte_offset':len(kept1.encode())}
f.raw(5,f.S,meta(base1)+prompt('R05 final second rewind request')+reply('R05 final second rewind reply'),p,rollout=52,delta=-400)
f.finish()

f=Fixture('M2-corrupt-db',['R06','R14'],'Direct control and assigned scratch; corrupt state database is deterministic read failure')
p=f.control();f.chat(2,f.S,'R02 scratch must remain excluded on DB failure',p);f.finish();(f.home/'.codex/state_5.sqlite').write_text('not a sqlite database\n')

f=Fixture('M3-root-order',['R07','R14'],'Root Q at position 1 is inserted before P at position 0; no direct control')
p=f.project(1,[(1,f.Q),(0,f.P)]);f.chat(7,f.S,'R07 position zero wins',p);f.finish()

for variant in ['relative','excluded','symlink-excluded']:
 f=Fixture('R08-'+variant,['R08','R14'],'Assigned root rejected: '+variant)
 if variant=='relative': root='relative-project'
 elif variant=='excluded': root=f.S
 else:
  root=f.home/'code/disguised-scratch';root.symlink_to(f.S,target_is_directory=True)
 p=f.project(1,[(0,root)]);f.chat(8,f.S,'R08 mapped invalid destination',p)
 if variant!='relative':f.f['roots']['destination']=str(root)
 f.finish()

for variant in ['absent','file','linked-worktree']:
 f=Fixture('R09-'+variant,['R09','R14'],'Mapped destination '+variant+' precedes valid P; scanner and direct recent have different validation')
 root=f.home/'code/destination'
 if variant=='file': root.write_text('synthetic regular file\n')
 if variant=='linked-worktree':
  root.mkdir();gitdir=f.home/'synthetic-repo/.git/worktrees/destination';gitdir.mkdir(parents=True)
  (root/'.git').write_text('gitdir: '+str(gitdir)+'\n');(gitdir/'commondir').write_text('../..\n');(f.home/'synthetic-repo/.git/config').write_text('[core]\n bare = false\n')
 p=f.project(1,[(0,root),(1,f.P)]);f.chat(9,f.S,'R09 direct recent destination',p);f.f['roots']['destination']=str(root);f.finish()

f=Fixture('R10-homes',['R10','R14'],'Same thread UUID in distinct homes maps independently; alias scanned once, disabled home omitted, Claude and Pi controls')
p=f.project(1,[(0,f.P)]);f.chat(10,f.S,'R10 primary home maps P',p)
other=f.home/'.codex-work';other.mkdir();otherdb=sqlite3.connect(other/'state_5.sqlite');otherdb.executescript(SCHEMA)
otherp=uid(100002);otherdb.execute('INSERT INTO projects VALUES (?,?,?,?,?,?)',(otherp,'other','{}',0,MS,MS));otherdb.execute('INSERT INTO project_roots VALUES (?,?,?)',(otherp,0,str(f.Q)))
of=f.raw(10,f.S,line({'type':'session_meta','payload':{'id':uid(10),'cwd':str(f.S)}})+prompt('R10 other home maps Q'),home=other)
otherdb.execute('INSERT INTO threads (id,rollout_path,created_at,updated_at,source,model_provider,cwd,title,sandbox_policy,approval_mode,project_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)',(uid(10),str(of),MS//1000,MS//1000,'vscode','synthetic',str(f.S),'Synthetic','read-only','never',otherp));otherdb.commit();otherdb.close()
disabled=f.home/'.codex-disabled';disabled.mkdir();f.raw(11,f.Q,line({'type':'session_meta','payload':{'id':uid(11),'cwd':str(f.Q)}})+prompt('R10 disabled should not appear'),home=disabled)
alias=f.home/'.codex-alias';alias.symlink_to(f.home/'.codex',target_is_directory=True)
f.f['providerInstances']={'codex-alias':{'driver':'codex','config':{'homePath':str(alias)}},'codex-work':{'driver':'codex','config':{'homePath':str(other)}},'codex-disabled':{'driver':'codex','enabled':False,'config':{'homePath':str(disabled)}}}
cf=f.home/'.claude/projects/synthetic/claude-control.jsonl';cf.parent.mkdir(parents=True);cf.write_text(line({'type':'user','cwd':str(f.P),'sessionId':'claude-control','message':{'role':'user','content':'R10 Claude preserved'}}));os.utime(cf,((MS-2000)/1000,)*2)
pf=f.home/'.pi/sessions/synthetic/pi-control.jsonl';pf.parent.mkdir(parents=True);pf.write_text(line({'type':'session','version':3,'id':'pi-control','cwd':str(f.Q)})+line({'type':'message','id':'message-1','parentId':None,'message':{'role':'user','content':[{'type':'text','text':'R10 Pi preserved'}]}}));os.utime(pf,((MS-3000)/1000,)*2)
f.finish()

f=Fixture('R11-age',['R11','R14'],'Fixed clock and exact cutoff; scan counts all three mapped sessions, recent accepts only exact 30-day cutoff')
p=f.project(1,[(0,f.P)])
for n,delta,title in [(111,-30*86400000-1000,'R11 older than cutoff'),(112,1000,'R11 future mtime'),(113,-30*86400000,'R11 exact cutoff inclusive')]:f.chat(n,f.S,title,p,delta=delta)
f.finish()

f=Fixture('R11-row-cap',['R11','R14'],'Oldest thread is beyond 100000 joined rows; no new truncation flag')
p=f.project(1,[(0,f.P)]);f.chat(114,f.S,'R11 beyond DB joined-row cap',p)
f.db.executemany('INSERT INTO threads (id,rollout_path,created_at,updated_at,source,model_provider,cwd,title,sandbox_policy,approval_mode,project_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)',((uid(200000+i),'no-rollout',MS//1000,MS//1000,'vscode','synthetic',str(f.S),'Filler','read-only','never',p) for i in range(100000)))
f.f['rowCap']='100000 filler joined rows inserted after target; target rowid 1 is beyond LIMIT 100000';f.finish()

f=Fixture('R12-repeat',['R12','R14'],'Mapped duplicate rollouts, completedSources replay, then append changes identity and reparses')
p=f.project(1,[(0,f.P)]);file=f.chat(12,f.S,'R12 repeated import',p,delta=-3000)
newest=f.raw(12,f.S,line({'type':'session_meta','payload':{'id':uid(12),'cwd':str(f.S)}})+prompt('R12 newer same-session copy'),p,rollout=121,delta=-2000)
f.f['repeat']={'file':str(newest),'append':prompt('R12 appended after completed import'),'originalMtimeMs':MS-2000};f.finish()

f=Fixture('R13-invalid',['R13','R14'],'Missing cwd, malformed input, Codex subagent, missing ID and no visible conversation are not rescued')
p=f.project(1,[(0,f.P)])
f.raw(130,None,line({'type':'session_meta','payload':{'id':uid(130)}})+prompt('R13 missing cwd'),p)
f.raw(131,None,'malformed json\n',p)
f.chat(132,f.S,'R13 subagent should not appear',p,meta={'source':{'subagent':'thread_spawn'}})
f.raw(133,f.S,line({'type':'session_meta','payload':{'cwd':str(f.S)}})+prompt('R13 no resumable ID'),p)
f.raw(134,f.S,line({'type':'session_meta','payload':{'id':uid(134),'cwd':str(f.S)}}),p)
f.finish()

f=Fixture('R14-cache-mutation',['R14'],'Stable warm/cold plus mapping mutation: cached candidates retain mapping until scan refresh')
p=f.project(1,[(0,f.P)]);f.chat(14,f.S,'R14 cache mutation',p);f.f['cacheMutation']={'projectId':p}
alias=f.home/'code/project-alias';alias.symlink_to(f.P,target_is_directory=True);f.f['persistedRoots']=[str(alias)];f.finish()

(RUN/'manifest.json').write_text(json.dumps(MANIFEST,indent=2)+'\n')
(ROOT/'fixtures-location.txt').write_text(str(RUN))
print(RUN)
print('\n'.join(MANIFEST))
