#!/usr/bin/env python3
"""Keeper deadline native ABI/guards and genuine input-only timeout regression."""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,struct,subprocess,os,hashlib
ap=argparse.ArgumentParser(description=__doc__);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)
cases=[dict(team=t,timer=v,active=0<=v<4) for t in (0,1) for v in (-1,0,1,2,3,4,5,180)]
for t in (0,1):
 for key,val in [('role',4),('human',8),('stop',1),('stop',16),('penalty',1),('shootout',1),('faceoff',4),('ask',0),('pending_owner',-1),('owner',-1),('owner',6*(1-t)+5)]:cases.append(dict(team=t,timer=2,active=False,**{key:val}))
abi=[]
for batch in range((len(cases)+7)//8):
 ss=cases[batch*8:batch*8+8];p=o/f'abi-{batch}';p.mkdir(exist_ok=True);body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfff800,%sp'];expected=[]
 for n,s in enumerate(ss):
  index=6*s['team']+5;g=0xffb04a+128*index;actor=bytearray(bytes.fromhex('5aa55aa5')*32);vals=[(0x34,s.get('role',0)),(0x48,s['timer']),(0x52,index)];flags=s['team']*64+s.get('human',0)
  body += [f' lea 0x{g:x},%a0',' moveq #31,%d0',f'clear{n}: move.l #0x5aa55aa5,(%a0)+',f' dbra %d0,clear{n}']
  for off,val in vals:body.append(f' move.w #{val},0x{g+off:x}');struct.pack_into('>H',actor,off,val&65535)
  body += [f' move.b #{flags},0x{g+0x62:x}',f' move.w #{s.get("owner",index)},0xffb7aa',f' move.w #{s.get("ask",1)},0xfff064',f' move.w #{s.get("pending_owner",index)},0xfff066',f' move.b #{s.get("stop",0)},0xffc2ea',f' move.w #{s.get("penalty",0)},0xffd412',f' move.b #{s.get("shootout",0)},0xffc2fa',f' move.b #{s.get("faceoff",0)},0xffc2f2'];actor[0x62]=flags
  regs=[0x11223300+i for i in range(8)];addresses=[g]+[0xff1000+i*256 for i in range(1,7)]
  for i,v in enumerate(regs):body.append(f' move.l #0x{v:x},%d{i}')
  for i,v in enumerate(addresses):body.append(f' movea.l #0x{v:x},%a{i}')
  at=0xff4000+224*n;body += [' move.w #0x271f,%sr',' jsr 0x2db400',f' movem.l %d0-%d7/%a0-%a6,0x{at:x}',f' move.w %sr,0x{at+64:x}',f' move.l %sp,0x{at+68:x}',f' lea 0x{g:x},%a0',f' lea 0x{at+80:x},%a1',' moveq #31,%d0',f'copy{n}: move.l (%a0)+,(%a1)+',f' dbra %d0,copy{n}']
  if s['active']:struct.pack_into('>H',actor,0x48,4)
  expected.append((regs+addresses,bytes(actor)))
 body += [' move.l #0x4f555444,0xff3ff0','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2d9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
 r=bytearray(a.rom.read_bytes());code=(p/'code.bin').read_bytes();assert len(code)<0x3000 and set(r[0x2d9000:0x2d9000+len(code)])=={255};r[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',r,4,0x2d9000);(p/'driver.bin').write_bytes(r);(p/'commands.txt').write_text(f'500 dump ff3ff0 4 done\n500 dump ff4000 {224*len(ss)} result\n501 exit\n')
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 data={line.split()[1]:bytes.fromhex(' '.join(line.split()[2:])) for line in (p/'commands.out').read_text().splitlines()};assert data['done']==b'OUTD'
 for n,(s,(regs,actor)) in enumerate(zip(ss,expected)):
  raw=data['result'][n*224:(n+1)*224];assert list(struct.unpack_from('>15I',raw))==regs,('registers',s);assert struct.unpack_from('>H',raw,64)[0]==0x271f,('CCR',s);assert struct.unpack_from('>I',raw,68)[0]==0xfff800,('stack',s);assert raw[80:208]==actor,('whole object',s);abi.append(s)
fixtures=[('catch-2883-keeper5','e9f1f6f846159893fd9af690172d9fa6d06f5be75969079fb2e40cc2792fe93e'),('catch-5401-keeper5','f41a76860326cd22e8b74db5959ccebc6445035c764619cef34e97d319f2f569')];live=[]
for label,digest in fixtures:
 state=a.fixtures_root.resolve()/'goalie-b-outlet-20261030/user-playback'/label/'native.state';assert hashlib.sha256(state.read_bytes()).hexdigest()==digest,('fixture hash',state)
 for profile,rom in [('baseline',a.baseline),('candidate',a.rom)]:
  for delay in [10,180,208,209,210,235,237,238,300]:
   for button in ['B','Y','C']:live.append((label,profile,rom,state,dict(delay=delay,button=button,duration=2)))
def run(item):
 label,profile,rom,state,spec=item;p=o/f"{label}-{profile}-{spec['button']}-{spec['delay']}";p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(spec));end=100+spec['delay']+160;(p/'commands.txt').write_text(f'100 load {state}\n{end} exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='1281',GPGX_HOOK=str(h/'outlet_deadline_gameplay_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)
 return dict(fixture=label,profile=profile,spec=spec,result=json.loads((p/'live.json').read_text()))
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,live))
regressions=0
for row in results:
 result=row['result'];spec=row['spec'];partner=next(r for r in results if r['fixture']==row['fixture'] and r['profile']!=row['profile'] and r['spec']==spec)['result']
 if row['profile']=='candidate' and result['eligible'] and spec['button'] in ['B','Y']:
  assert result['passed'],('timely request lost',row)
  if spec['button']=='B':assert result['passed']['target']==result['passed']['controlled'],('wrong selected receiver',row)
  if not partner['passed']:regressions+=1
 if spec['button']=='C' or not result['eligible']:assert result['passed'] is None,('unrequested/post-deadline pass',row)
assert regressions>=2,('baseline did not reproduce timeout race',regressions)
(o/'results.json').write_text(json.dumps(dict(rom_sha256=hashlib.sha256(a.rom.read_bytes()).hexdigest(),baseline_sha256=hashlib.sha256(a.baseline.read_bytes()).hexdigest(),native_abi=abi,live=results,repaired_baseline_cases=regressions,scope='Native whole-object/register/CCR/stack guards; genuine keeper states with controller inputs only. Negative possession timer means timeout already queued, even before visible STOP. No gameplay RAM forcing.'),indent=2)+'\n');print('PASS',len(abi),'native ABI/guards;',len(results),'paired input-only contexts;',regressions,'baseline deadline failures repaired')
