#!/usr/bin/env python3
"""Replay the reported r5 catch from a genuine recorded pre-catch state; no game RAM forcing."""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,hashlib,os,subprocess
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);ap.add_argument('--fixtures-root',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);state=a.fixtures_root/'goalie-control-defaults-20261030/user-playback/frame-9750.state';sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();assert sha(a.baseline)=='a7e8bef0e8c78da6d37eacb432769af3f6d4b99e0196e451875c7991656ca182'
(o/'hook.py').write_text('''from pathlib import Path
import json
p=Path(outdir);s=json.loads((p/'spec.json').read_text());rows=[];caught=None;pressed=None
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def test_tick(f):
 global caught,pressed
 if f<100:return
 # Exact held input from recorded r5 frames9751..9757; then fork only inputs.
 if f==100:held[(0,IDS['Down'])]=7
 if caught is None and rw(0xb7aa)==5:caught=f
 if caught is not None and s['delay'] is not None and pressed is None and f>=caught+s['delay']:
  held[(0,IDS['B'])]=s['duration'];pressed=f
 row=dict(frame=f,owner=rw(0xb7aa),control=rw(0xc320),flags=rw(0xb2ca+0x62),pending=[rw(0xf064),rw(0xf066)],passer=rw(0xbeda),target=rw(0xbee0),stop=wram[ram(0xc2ea)]&17,clock=rw(0xc468),caught=caught,pressed=pressed)
 rows.append(row)
 if f==299:(p/'results.json').write_text(json.dumps(rows,indent=2)+'\\n')
''')
specs=[dict(delay=None,duration=0),dict(delay=10,duration=2),dict(delay=10,duration=5),dict(delay=1,duration=2)]
def run(item):
 profile,rom,spec=item;p=o/f"{profile}-delay{spec['delay']}-tap{spec['duration']}";p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(spec));(p/'commands.txt').write_text(f'100 load {state.resolve()}\n180 snap\n300 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='1281',GPGX_DEV2='1281',GPGX_HOOK=str(o/'hook.py'))
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 rows=json.loads((p/'results.json').read_text());caught=next(r for r in rows if r['owner']==5);assert caught['frame']==107,('recorded catch did not reproduce',profile,caught)
 if profile=='baseline':assert caught['flags']&0x0800 and caught['control']==5,('reported keeper takeover not reproduced',caught)
 else:
  assert not caught['flags']&0x0800 and caught['control']<5,('keeper still manual',caught)
  passed=next((r for r in rows if r['owner']!=5 and r['passer']==5 and not r['stop']),None)
  if spec['delay'] is None:assert passed is None and all(not r['flags']&0x0800 and r['control']==caught['control'] for r in rows if caught['frame']<=r['frame'] and r['owner']==5),('unrequested pass/control takeover',passed)
  else:
   assert passed and passed['target']==caught['control']==passed['control'],('wrong requested current skater',spec,passed,caught)
   assert all(r['control']==caught['control'] for r in rows if caught['frame']<=r['frame']<=passed['frame']),('pass button changed skater',spec)
 return dict(profile=profile,spec=spec,caught=caught,passed=next((r for r in rows if r['owner']!=5 and r['passer']==5 and not r['stop']),None))
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,[('baseline',a.baseline,specs[0])]+[('current',a.rom,s) for s in specs]))
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),state_sha256=sha(state),contexts=results,scope='Genuine recorded pre-catch state; exact recorded input prefix through acceptance, then controller-only branches. No position/physics/clock/object/control writes.'),indent=2)+'\n');print('PASS reported manual-keeper catch reproduced in r5; current catch AI/current-skater outlets and no-input hold;5 contexts')
