#!/usr/bin/env python3
"""Native rewind boundary regression; original overflow reproduced, valid frames preserved."""
from pathlib import Path
import argparse,subprocess,struct,os,json,hashlib
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();out=a.output.resolve();out.mkdir(exist_ok=True,parents=True);here=Path(__file__).resolve().parent
site=Path('/home/jdc/mount/truenas/www/dela.jdc.se/nhl94');asm=here.parent/'m68kasm.sh' if (here.parent/'m68kasm.sh').exists() else site/'tools/m68kasm.sh';core=here/'test_core.py' if (here/'test_core.py').exists() else site/'tools/experimental/test_core.py'
rom=a.rom.read_bytes();assert rom[0xa51c:0xa520]==bytes.fromhex('610000e2'),'candidate must contain guarded rewind fix'
cases=[('full-tail-zero',0,0xaef2,16,0),('unfilled-zero',0,0xaef2,0,0),('ordinary-undo',196,98,16,196),('last-slot-undo',0xaef2,0xae90,16,0xaef2),('ordinary-rewind',196,0,16,98),('wrapped-rewind',0,98,16,0xaef2)]
body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xFFF800,%sp']
for i,(name,current,writer,flags,expected) in enumerate(cases):
 body += [' lea 0xFFFF0000,%a0',' move.w #0xB8B4/4-1,%d0',f'clear{i}: clr.l (%a0)+',f' dbra %d0,clear{i}',' lea 0xFFFFAF54,%a0',' moveq #48,%d0',f'marker{i}: move.w #0x1111,(%a0)+',f' dbra %d0,marker{i}',f' move.l #0x{0xffff0000+writer:X},0xFFFFB036',f' move.b #{flags},0xFFFFC2EC',f' movea.l #0x{0xffff0000+current:X},%a4',' jsr 0xA4F6',f' lea 0x{0xfff100+136*i:X},%a0',' move.l %a4,(%a0)+',' lea 0xFFFFB04A,%a1',' moveq #15,%d0',f'capture{i}: move.w (%a1),(%a0)+',' move.w 0x14(%a1),(%a0)+',' move.w 6(%a1),(%a0)+',' move.w 0x34(%a1),(%a0)+',' adda.w #128,%a1',f' dbra %d0,capture{i}',' move.w 0xFFFFB762,(%a0)+',' move.w 0xFFFFB89A,(%a0)+']
body+=[' move.l #0x52575250,0xFFFFF0FC','end: bra.s end'];(out/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(asm),str(out/'driver.s'),'2D9000',str(out/'driver.bin')],check=True,stdout=subprocess.DEVNULL);code=(out/'driver.bin').read_bytes();assert len(code)<0x3000 and rom[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code)
def run(label,old):
 p=out/label;p.mkdir(exist_ok=True);b=bytearray(rom);b[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',b,4,0x2d9000)
 if old:b[0xa51c:0xa520]=bytes.fromhex('d8fc0062')
 (p/'rom.bin').write_bytes(b);(p/'commands.txt').write_text(f'500 dump fff0fc 4 done\n500 dump fff100 {136*len(cases)} results\n501 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(core),str(p/'rom.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 rows={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (p/'commands.out').read_text().splitlines()};assert rows['done']==b'RWRP',label;return [rows['results'][136*i:136*(i+1)] for i in range(len(cases))]
old=run('original',True);new=run('fixed',False);report=[]
for i,(name,current,writer,flags,expected) in enumerate(cases):
 pointer=int.from_bytes(new[i][:4],'big');assert pointer==0xffff0000+expected,(name,hex(pointer),hex(expected))
 if i==0:
  assert int.from_bytes(old[i][:4],'big')==0xffffaf54 and old[i][4:]!=new[i][4:],'original overflow and wrong actor data must reproduce'
  assert new[i][4:]==bytes(132),'fixed boundary must decode genuine zero packet'
 else:assert old[i]==new[i],('valid native rewind changed',name)
 report.append(dict(case=name,passed=True,old_pointer=old[i][:4].hex(),fixed_pointer=new[i][:4].hex(),native_actor_data_identical=old[i][4:]==new[i][4:]))
r=dict(status='passed',rom_sha256=hashlib.sha256(rom).hexdigest(),cases=report,fix='A51C uses existing wrapped native advance A600 before decoding undo-rewind frame; originalAF54 out-of-buffer read reproduced');(out/'results.json').write_text(json.dumps(r,indent=2)+'\n');print('PASS',len(cases),'native rewind cases; old overflow reproduced, five valid paths identical')
