"""Direct positioning-wrapper and actual native-callee context contracts.
Native movement callees are explicit recorder stubs in these ABI fixtures;
unmodified native motion/catches/releases are tested in the paired live suites.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import struct,subprocess,hashlib,json
from native_probe import NativeProbe
def verify(rom,control,here,out):
 h=Path(here);o=Path(out);o.mkdir(parents=True,exist_ok=True);image=Path(rom).read_bytes();base=Path(control).read_bytes();diff=[i for i,(x,y) in enumerate(zip(image,base)) if x!=y];assert set(diff)<={0x18e,0x18f,0x225002,0x225003,0x225006,0x225007},('unmatched native positioning control',diff[:32]);assert diff
 def assemble(name,body,origin):
  p=o/(name+'.s');p.write_text(' .text\n .globl _start\n_start:\n'+body);code=o/(name+'.code');subprocess.run([str(h.parent/'m68kasm.sh'),str(p),f'{origin:x}',str(code)],check=True,stdout=subprocess.DEVNULL);return code.read_bytes()
 patches=[]
 for at,dest,counter in [(0x105a2,0xffff3100,0xffff3000),(0x10750,0xffff3200,0xffff3002)]:
  body=f' movem.l %d0-%d7/%a0-%a6,0x{dest:x}\n move.w %sr,0x{dest+60:x}\n move.l %sp,0x{dest+62:x}\n addq.w #1,0x{counter:x}\n'+''.join(f' move.l #0x{0x55667700+i:x},%d{i}\n' for i in range(1,8))+' movea.l #0xffff2000,%a0\n movea.l #0xffff2100,%a1\n rts\n';code=assemble(f'callee-{at:x}',body,at);assert len(code)<112;patches.append((at,image[at:at+len(code)],code))
 specs=[dict(kind=k,sign=sign,gap=gap,ticks=tick) for k in ['far','near'] for sign in [-1,1] for gap in ([-40,-9,-8,0,8,9,40] if k=='far' else [-8,0,8]) for tick in [1,2,5,12]]
 caches=[0,1,2,3,10,127,128,255];probe=NativeProbe(rom,h)
 def batch(index,cart=probe,extra=(),negative=False):
  ss=specs[16*index:16*(index+1)];body=[];expect=[]
  for j,z in enumerate(ss):
   n=16*index+j;cache=caches[n%len(caches)];regs=[0x11223300+i for i in range(8)]+[0xffff1000+256*i for i in range(7)];regs[7]=z['ticks'];regs[11]=0xffffb04a;irq=n%8;sr=0x2000+(irq<<8)+(n%32);entry=0x225000 if z['kind']=='far' else 0x225004
   body += [' clr.l 0xffff3000',' lea 0xffff3100,%a0',' moveq #32,%d0',f'clear{j}: clr.w (%a0)+',f' dbra %d0,clear{j}',' lea 0xffff3200,%a0',' moveq #32,%d0',f'clearb{j}: clr.w (%a0)+',f' dbra %d0,clearb{j}',' clr.w 0xffffb04a',f' move.w #{220*z["sign"]},0xffffb05e',f' move.w #{110 if z["kind"]=="far" else 30},0xffffb74a',f' move.w #{220*z["sign"]+z["gap"]},0xffffb75e',' move.w #-2048,0xffffb772',' clr.w 0xffffb774',f' move.b #{cache},0xffffb08c']
   body += [f' move.l #0x{v:x},%d{i}' for i,v in enumerate(regs[:8])]+[f' movea.l #0x{v:x},%a{i}' for i,v in enumerate(regs[8:])]+[f' move.w #{sr},%sr',f' jsr 0x{entry:x}',f' movem.l %d0-%d7/%a0-%a6,0x{0xffff4000+204*j:x}',f' move.w %sr,0x{0xffff4000+204*j+60:x}',f' move.l %sp,0x{0xffff4000+204*j+62:x}',f' lea 0x{0xffff4000+204*j+66:x},%a1',' lea 0xffff3100,%a0',' moveq #32,%d0',f'tracker{j}: move.w (%a0)+,(%a1)+',f' dbra %d0,tracker{j}',' lea 0xffff3200,%a0',' moveq #32,%d0',f'brake{j}: move.w (%a0)+,(%a1)+',f' dbra %d0,brake{j}',' move.l 0xffff3000,(%a1)+',' clr.w %d0',' move.b 0xffffb08c,%d0',' move.w %d0,(%a1)+']
   active=z['kind']=='near' or abs(z['gap'])<=8;expect.append((regs,sr,active,cache))
  raw=cart.run(o/('negative' if negative else f'batch-{index}'),'\n'.join(body),204*len(ss),patches+list(extra));rows=[]
  for j,(z,want) in enumerate(zip(ss,expect)):
   regs,sr,active,cache=want;packet=raw[204*j:204*(j+1)];actual=list(struct.unpack_from('>15I',packet));expected=regs.copy();expected[0]=int(active);assert actual==expected,('positioning wrapper caller GPR',z,actual,expected);assert struct.unpack_from('>I',packet,62)[0]==0xffff9800 and struct.unpack_from('>H',packet,60)[0]&0xff00==sr&0xff00,('wrapper IRQ/SR/SP',z)
   tracker,brake=struct.unpack_from('>2H',packet,198);assert (tracker,brake)==(int(active),int(active and z['kind']=='far')),('actual native movement callees',z,tracker,brake)
   after_cache=struct.unpack_from('>H',packet,202)[0];want_cache=2 if z['kind']=='near' and 2<cache<128 else cache;assert after_cache==want_cache,('native near cache policy',z,cache,after_cache,want_cache)
   for called,offset,stack in [(tracker,66,0xffff97d0 if z['kind']=='far' else 0xffff97cc),(brake,132,0xffff97d0)]:
    if called:
     native=list(struct.unpack_from('>15I',packet,offset));assert native[1:]==regs[1:],('forwarded actual native context/ticks',z,native,regs);assert struct.unpack_from('>I',packet,offset+62)[0]==stack,('native callee stack',z,stack)
     if offset==132:assert native[0]==9,('native brake command',z,native[0])
   rows.append(dict(spec=z,native_tracker=tracker,native_brake=brake,original_ticks=regs[7],cache_before=cache,cache_after=after_cache))
  return rows
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(batch,range((len(specs)+15)//16)),[])
 # Remove only the second original-context restore, after a clobbering native
 # receive call. The same native-callee oracle must detect lost elapsed D7.
 needle=bytes.fromhex('4cef03fe0004');start=image.find(needle,0x225000,0x225100);at=image.find(needle,start+1,0x225100);assert start>=0 and at>start
 try:batch(0,extra=[(at,needle,bytes.fromhex('4e71')*3)],negative=True)
 except AssertionError as e:negative=str(e);assert 'forwarded actual native context/ticks' in negative,negative
 else:raise AssertionError('lost native forwarding context was not detected')
 report=dict(rom_sha256=hashlib.sha256(image).hexdigest(),control_sha256=hashlib.sha256(base).hexdigest(),cases=rows,negative_control=negative);(o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS',len(rows),'direct near/far wrapper native-callee context/IRQ/stack/cache cases; lost-D7 negative control',flush=True);return report
