#!/usr/bin/env python3
"""Complete SR/GPR/SP contract for test-only post-render/frame observers.
Native callees are explicit RTS controls so the observer itself must preserve
all caller state. A D7-clobber mutant must fail the identical oracle.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import json,struct,subprocess,hashlib
from native_probe import NativeProbe
def verify(rom,here,out):
 h=Path(here);o=Path(out);o.mkdir(parents=True,exist_ok=True);b=Path(rom).read_bytes();probe=NativeProbe(rom,h);rows=[]
 def test_observer(name,slot):
  p=o/name;p.mkdir(exist_ok=True);subprocess.run([str(h.parent/'m68kasm.sh'),str(h/'testcases'/(name+'.s')),'2d9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);payload=(p/'code.bin').read_bytes();assert payload[-2:]==bytes.fromhex('4e75');basepatch=[(slot,b[slot:slot+4],bytes.fromhex('4e754e71'))]
  def batch(index,mutant=False):
   body=[];expect=[];ss=[(n//32,n%32) for n in range(16*index,16*index+16)]
   for j,(irq,ccr) in enumerate(ss):
    regs=[0x11223300+i for i in range(8)]+[0xffff1000+256*i for i in range(7)];sr=0x2000+(irq<<8)+ccr
    body += [f' move.l #{v},%d{i}' for i,v in enumerate(regs[:8])]+[f' movea.l #{v},%a{i}' for i,v in enumerate(regs[8:])]+[f' move.w #{sr},%sr',' jsr 0x2d9000',f' movem.l %d0-%d7/%a0-%a6,0x{0xffff4000+66*j:x}',f' move.w %sr,0x{0xffff4000+66*j+60:x}',f' move.l %sp,0x{0xffff4000+66*j+62:x}'];expect.append((regs,sr,0xffff9800))
   code=payload[:-2]+bytes.fromhex('42874e75') if mutant else payload;patches=basepatch+[(0x2d9000,b'\xff'*len(code),code)];raw=probe.run(p/('mutant' if mutant else str(index)),'\n'.join(body),66*len(ss),patches)
   for j,want in enumerate(expect):
    actual=(list(struct.unpack_from('>15I',raw,66*j)),*struct.unpack_from('>HI',raw,66*j+60));assert actual==want,('native observer register/SR/SP corruption',name,ss[j],actual,want)
   return dict(contexts=ss,sha256=hashlib.sha256(raw).hexdigest())
  with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(batch,range(16)))
  try:batch(0,True)
  except AssertionError as e:negative=str(e)
  else:raise AssertionError('D7-clobber negative control was not detected')
  return dict(observer=name,contexts=256,results=results,negative_control=negative)
 for name,slot in [('shot_bar_probe',0x2ed00c),('momentum_frame_probe',0x2ed000)]:rows.append(test_observer(name,slot))
 report=dict(rom_sha256=hashlib.sha256(b).hexdigest(),observers=rows);(o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS512 fullSR/GPR/SP observer contexts and2 active D7-clobber negative controls',flush=True);return report
if __name__=='__main__':
 import argparse
 ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();verify(a.rom,Path(__file__).resolve().parent,a.output)
