#!/usr/bin/env python3
"""Native buffered-friendly-interception eligibility, ABI and untouched memory.

Explicit reset-PC fixtures exercise the new pure guards. Unmodified gameplay,
including original native B24 interception, remains separately mandatory.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse, hashlib, json, struct
from native_probe import NativeProbe
from friendly_reception_scope import verify as scope

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)
a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=False)
image=a.rom.read_bytes()
# Keep the original failed61 -> exact releasedr2 local-repair scope intact.
# New requested dollar/frame code is proved separately against that immutable
# releasedr2, with explicit installer reservations and eight native operands.
# No friendly/physics/statistics/season/economy/palette range is newly allowed.
if a.baseline.suffix.lower()=='.json':
 references=json.loads(a.baseline.read_text());assert references['schema']=='friendly-ui-scope-v1'
 parent=Path(references['regression_parent']).read_bytes()
 released=Path(references['released_reference']).read_bytes()
 R2='c68333a8422b6e3754350ef2abd646951d85f6e2c2cd0f987dff1f184aa53b17'
 def composed_scope(before,after,reference):
  assert hashlib.sha256(reference).hexdigest()==R2,'exact releasedr2 scope bridge required'
  from main_menu_scope import prior_scope_image
  try:after=prior_scope_image(after)
  except AssertionError as error:raise AssertionError(('outside separately declared cosmetic UI scope',str(error)))
  original=scope(before,reference) # Original PARENT hash/allowlist/protected bytes unchanged.
  assert len(after)==len(reference)==3145728
  ui=[(0x120,0x180),(0x18e,0x190),
      (0xf7728,0xf772a),(0xf774c,0xf774e),(0xf7758,0xf775a),(0xf7762,0xf7764),
      (0xf7c08,0xf7c0a),(0xf7b82,0xf7b84),(0xf7bfc,0xf7bfe),(0xf80a2,0xf80a4),
      (0x250000,0x250c00),(0x258000,0x259000),(0x25a020,0x25a040),(0x25b000,0x25c000)]
  changes=[i for i,(x,y)in enumerate(zip(reference,after))if x!=y]
  outside=[i for i in changes if not any(lo<=i<hi for lo,hi in ui)]
  assert changes and not outside,('outside separately declared cosmetic UI scope',[hex(i)for i in outside[:32]])
  assert after[0x225800:0x226000]==reference[0x225800:0x226000],'friendly module changed'
  return dict(original_failed61_to_releasedr2=original,released_reference_sha256=R2,
              rom_sha256=hashlib.sha256(after).hexdigest(),cosmetic_UI_regions=ui,
              cosmetic_changed_bytes=len(changes),all_non_UI_bytes_exact_releasedr2=True,
              original_friendly_scope_not_expanded=True)
 scope_result=composed_scope(parent,image,released)
 rejected=[]
 for label,address in [('friendly-module',0x225800),('native-interception',0x105a2),
                       ('transfer-economy',0x234000),('season-accounting',0x2f8000),
                       ('physical-palette',0x59920),('outside-menu-operand',0xf7726)]:
  bad=bytearray(image);bad[address]^=1
  try:composed_scope(parent,bytes(bad),released)
  except AssertionError as error:
   assert 'outside separately declared cosmetic UI scope'in str(error),(label,error)
   rejected.append(dict(mutation=label,address=hex(address),rejected=True))
  else:raise AssertionError(('non-UI scope mutation escaped',label))
 wrong=bytearray(released);wrong[0x120]^=1
 try:composed_scope(parent,image,bytes(wrong))
 except AssertionError as error:assert 'exact releasedr2 scope bridge required'in str(error)
 else:raise AssertionError('wrong released scope bridge escaped')
 scope_result['active_non_UI_scope_negatives']=rejected
 scope_result['active_wrong_bridge_hash_negative']=True
else:
 parent=a.baseline.read_bytes();scope_result=scope(parent,image)

probe=NativeProbe(a.rom,h)
RAM_START,RAM_SIZE,PACKET=0xB000,0x4900,66
seed=bytes((i*31+i//251+17)&255 for i in range(RAM_SIZE))
sram=bytes((i*43+29)&255 for i in range(65536))

def fields(z):
 actor=z.get('actor',0);obj=0xB04A+128*(actor if actor<12 else 0);direction=z.get('direction',1)
 passer=z.get('passer',2 if actor<6 else 8)
 vals={0xF07A:(2,1),obj+0x34:(2,5),obj+0x52:(2,actor),obj+0x58:(2,0xB24),
       obj+0x62:(1,0x20|(128 if direction>0 else 0)|(64 if actor>=6 else 0)),obj+0x63:(1,0),
       obj:(2,0),obj+0x14:(2,220*direction),0xF086:(2,0x5042),0xF088:(2,actor+1),0xF08A:(2,64),
       0xBEE0:(2,actor),0xBEDA:(2,passer),0xB7AA:(2,0xFF00|passer),
       0xC2EA:(1,0),0xC2EC:(1,0),0xC2EE:(1,0),0xC2F2:(1,0),0xC2FA:(1,0),0xF738:(2,0)}
 for i in range(12):
  b=0xB04A+i*128
  if b!=obj:vals.update({b:(2,120),b+0x14:(2,-200*direction),b+0x34:(2,0),b+0x63:(1,0)})
 for at,v in z.get('edits',{}).items():
  assert at in vals,('undeclared fixture address',hex(at))
  vals[at]=(vals[at][0],v)
 return obj,[(at,size,value&((1<<(8*size))-1)) for at,(size,value) in vals.items()]

def signed(n):return n if n<32768 else n-65536

def model(z):
 obj,vals=fields(z);v={at:value for at,size,value in vals};actor=v[obj+0x52];flags=v[obj+0x62]
 yes=(v[0xF07A]==1 and not(flags&8) and not(v[obj+0x63]&4) and signed(v[obj+0x34])>0
      and not(v[0xC2EA]&17) and not(v[0xC2EC]&128) and not(v[0xC2EE]&128)
      and not(v[0xC2F2]&4) and not(v[0xC2FA]&5))
 if yes and flags&32:
  passer=v[0xBEDA];owner=v[0xB7AA]
  yes=(v[obj+0x58]==0xB24 and v[0xF086]==0x5042 and signed(v[0xF08A])>=0
       and actor<12 and v[0xF088]==actor+1 and (v[0xBEE0]==actor or (owner==actor and v[0xBEE0]==0xFF00|actor)) and passer<12
       and actor//6==passer//6 and (owner==actor or owner==0xFF00|passer)
       and not(v[0xC2EC]&1) and v[0xF738] not in range(0xA502,0xA507))
 entry=z.get('entry',0x225800)
 if entry==0x225808:yes=yes and v[obj+0x58]==0xB24 and v[0xB7AA]==actor
 if yes and entry==0x225804:
  x=signed(v[obj]);depth=signed(v[obj+0x14])*(1 if flags&128 else -1)
  yes=160<=depth<=252 and abs(x)<=92
  if yes:
   dist=264-depth
   for i in range(6 if actor<6 else 0,12 if actor<6 else 6):
    b=0xB04A+i*128;dy=signed(v[b+0x14])*(1 if flags&128 else -1)-depth
    if signed(v[b+0x34])>0 and not(v[b+0x63]&4) and 0<=dy<dist:
     cross=abs((signed(v[b])-x)*dist+x*dy)
     if cross<=12*(abs(x)+dist):yes=False;break
 return int(yes)

# All interrupt masks and all CCR states on positive incoming and owned catches.
cases=[]
for entry in (0x225800,0x225808):
 for n in range(256):
  actor=6*(n%2);z=dict(name='incoming' if entry==0x225800 else 'owned',actor=actor,
                     direction=1 if n%4<2 else -1,entry=entry,sr=0x2000|((n//32)<<8)|(n%32))
  if entry==0x225808:z['edits']={0xB7AA:actor,0xBEE0:0xFF00|actor}
  cases.append(z)
invalid=[('Normal',{0xF07A:0}),('Easy',{0xF07A:2}),('goalie',{0xB07E:0}),('off-ice',{0xB07E:65535}),
 ('human',{0xB0AC:0xA8}),('boxed',{0xB0AD:4}),('other-busy-animation',{0xB0A2:0x7FC}),
 ('other-busy-animation2',{0xB0A2:0x92E}),('magic',{0xF086:0}),('ttl-expired',{0xF08A:65535}),
 ('actor-range',{0xB09C:12}),('wrong-target',{0xBEE0:1}),('wrong-buffer',{0xF088:2}),
 ('other-owner',{0xB7AA:3}),('loose',{0xB7AA:65535}),('bad-flight-owner',{0xB7AA:0xFE02}),
 ('flight-actor-range',{0xB7AA:0xFF0C}),('different-passer',{0xBEDA:1}),
 ('other-side',{0xB7AA:0xFF08,0xBEDA:8}),('bad-passer',{0xB7AA:0xFF0C,0xBEDA:12}),
 ('whistle',{0xC2EA:1}),('game-over',{0xC2EA:16}),('pause',{0xC2EC:1}),('foreign',{0xC2EC:128}),
 ('announcement',{0xC2EE:128}),('shootout',{0xC2F2:4}),('special',{0xC2FA:1}),('special4',{0xC2FA:4})]
invalid += [(f'native-replay-{v:x}',{0xF738:v})for v in range(0xA502,0xA507)]
for name,edits in invalid:
 for irq in (0,1,3,7):
  for ccr in (0,7,16,31):cases.append(dict(name=name,edits=edits,sr=0x2000|(irq<<8)|ccr))
# Last live tick remains valid, maximum signed TTL valid; negative invalid.
for ttl in (0,1,32767,32768,65535):
 for owner in (0,0xFF02):cases.append(dict(name='TTL-boundary',edits={0xF08A:ttl,0xB7AA:owner},sr=0x271F))
# Ordinary nonbusy semantics must remain the original guard, irrespective of PB.
for n in range(256):
 cases.append(dict(name='ordinary-original-guard',edits={0xB0AC:128,0xF086:0,0xF08A:65535,0xC2EC:1,0xF738:0xA504},sr=0x2000|((n//32)<<8)|(n%32)))
# Actual finish-ready geometry is unchanged, both teams and attack directions.
for actor in (0,6):
 for direction in (-1,1):
  obj=0xB04A+actor*128;opp=0xB04A+(6 if actor==0 else 0)*128
  for depth in (159,160,220,252,253):
   for x in (-93,-92,0,92,93):
    cases.append(dict(name='native-finish-geometry',actor=actor,direction=direction,entry=0x225804,sr=0x231F,
                      edits={obj:x,obj+0x14:depth*direction,0xB7AA:actor}))
  for x,role,boxed in ((0,5,0),(90,5,0),(0,0,0),(0,5,4)):
   cases.append(dict(name='native-lane',actor=actor,direction=direction,entry=0x225804,sr=0x2700,
                     edits={opp:x,opp+0x14:240*direction,opp+0x34:role,opp+0x63:boxed,0xB7AA:actor}))
# Native accepted catches stamp FF|receiver rather than the flight target ID.
for entry in (0x225800,0x225804,0x225808):
 for actor in (0,6):
  for target in (actor,0xFF00|actor,1,0xFF01,65535):
   cases.append(dict(name='native-caught-target',entry=entry,actor=actor,edits={0xB7AA:actor,0xBEE0:target},sr=0x271F))
# Owned-opening fallback cannot admit a merely approaching pass or non-B24 busy action.
for edits in ({},{0xB7AA:0},{0xB7AA:0,0xF086:0},{0xB7AA:0,0xB0A2:0x7FC},{0xB7AA:0,0xB0AC:0xA8}):
 cases.append(dict(name='owned-opening-boundary',entry=0x225808,edits=edits,sr=0x251F))

def batch(specs,path,cart=probe):
 memory=bytearray(seed)
 body=[' lea 0x290000,%a0',' lea 0xffffb000,%a1',f' move.w #{RAM_SIZE//4-1},%d0',
       'initial_copy: move.l (%a0)+,(%a1)+',' dbra %d0,initial_copy']
 expected=[]
 for j,z in enumerate(specs):
  body+=[' move.w #0x2700,%sr'];obj,vals=fields(z)
  for at,size,value in vals:
   body.append(f' move.{"b"if size==1 else "w"} #0x{value:x},0xffff{at:04x}')
   memory[at-RAM_START:at-RAM_START+size]=value.to_bytes(size,'big')
  regs=[0x11223300+i for i in range(8)]+[0xFFFF1000+256*i for i in range(7)];regs[11]=0xFFFF0000|obj
  body += [f' move.l #0x{v:x},%d{i}'for i,v in enumerate(regs[:8])]
  body += [f' movea.l #0x{v:x},%a{i}'for i,v in enumerate(regs[8:])]
  out=0xFFFF4000+PACKET*j
  body += [f' move.w #0x{z["sr"]:x},%sr',f' jsr 0x{z.get("entry",0x225800):x}',
           f' move.w %sr,0x{out+60:x}',f' movem.l %d0-%d7/%a0-%a6,0x{out:x}',f' move.l %sp,0x{out+62:x}']
  want=model(z);regs[0]=want;expected.append((*regs,(z['sr']&~15)|(4 if not want else 0),0xFFFF9800))
 body+=[' move.w #0x2700,%sr',' lea 0xffffb000,%a0',f' lea 0x{0xFFFF4000+PACKET*len(specs):x},%a1',
        f' move.w #{RAM_SIZE//4-1},%d0','ram_capture: move.l (%a0)+,(%a1)+',' dbra %d0,ram_capture']
 raw=cart.run(path,'\n'.join(body),PACKET*len(specs)+RAM_SIZE,data=seed,data_address=0x290000,sram_data=sram,frames=100)
 for j,(z,want)in enumerate(zip(specs,expected)):
  got=struct.unpack_from('>15IHI',raw,PACKET*j)
  assert got==want,('native guard context/boolean',z,got,want)
 assert raw[PACKET*len(specs):]==memory,'pure guard wrote RAM outside its stack'
 assert(path/'probe.srm').read_bytes()==sram,'pure guard changed64KB SRAM'
 return len(specs)

with ThreadPoolExecutor(max_workers=3)as pool:
 count=sum(pool.map(lambda i:batch(cases[8*i:8*(i+1)],o/f'guard-{i}'),range((len(cases)+7)//8)))
# Two real admission mutations must be caught by unchanged negative contexts.
mutations=[]
for name,pattern,negative in (
 ('PB-magic',bytes.fromhex('0c785042f0866600'),dict(name='magic-negative',edits={0xF086:0},sr=0x271F)),
 ('same-team',bytes.fromhex('b7426b00'),dict(name='team-negative',edits={0xBEDA:8,0xB7AA:0xFF08},sr=0x271F))):
 start=image.find(pattern,0x225800,0x225FF0);assert start>=0,('missing guard for active mutation',name)
 branch=start+len(pattern)-2;b=bytearray(image);b[branch:branch+4]=bytes.fromhex('4e714e71')
 p=o/(name+'-mutant.bin');p.write_bytes(b)
 try:batch([negative],o/(name+'-negative'),NativeProbe(p,h))
 except AssertionError as e:mutations.append(dict(name=name,rejected=True,reason=str(e)[:150]))
 else:raise AssertionError(('guard-removal mutant escaped negative oracle',name))
report=dict(rom_sha256=hashlib.sha256(image).hexdigest(),scope=scope_result,native_guard_cases=count,
            all_interrupt_masks_and_CCR=True,original_native_interception_and_finisher_bytes_exact=True,
            full_GPR_SR_SP=True,whole_canary_RAM_unchanged=RAM_SIZE,whole_SRAM_unchanged=65536,
            active_guard_negatives=mutations,actual_gameplay_required=['finisher_hold','ai_smart_aim_gameplay','shot_plan'])
(o/'results.json').write_text(json.dumps(report,indent=2)+'\n')
print('PASS pure friendly native-interception buffer guards: full context/RAM/SRAM, original interception exact, active negatives',count,flush=True)
