"""Native momentum option contracts. Explicit routine fixtures are not matches."""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import json,struct,hashlib,math
BASE=[2,5,20,20,12,12,20,20,20,0x15,20,20,20,20,6,20]
FIELDS=[2,3,4,5,6,7,8,10,11,12,13,15]
def scaled(base,pct):
 out=base.copy()
 for i in FIELDS:
  p=-pct if i in [4,5] else pct;n=base[i]*p;delta=abs(n)//100*(1 if n>=0 else -1);out[i]=max(0,min(max(30,base[i]),base[i]+delta))
 return out
def easy(base,human):
 out=base.copy()
 for i in FIELDS:out[i]=(7 if human else 13) if i in [4,5] else 30 if human else 6
 out[14]=14 if human else 2
 return out

def gates(self):
 specs=[dict(target=n,mode=mode,irq=irq,ccr=ccr) for n in [0,2,4] for mode in [0,1,99] for irq in range(8) for ccr in range(32)]
 def trial(batch):
  ss=specs[batch*16:batch*16+16];body=[];expected=[]
  for j,z in enumerate(ss):
   init,regs=self.registers(12,-34);body+=[f' move.w #{z["mode"]},0xfffff84a']+init+[f' move.w #{0x2000+(z["irq"]<<8)+z["ccr"]},%sr',f' jsr 0x{0x226010+2*z["target"]:x}',f' movem.l %d0-%d7/%a0-%a6,0x{0xffff4000+j*66:x}',f' move.w %sr,0x{0xffff4000+j*66+60:x}',f' move.l %sp,0x{0xffff4000+j*66+62:x}']
   if z['mode']==1:regs[z['target']]=0
   expected.append((regs,0x2000+(z['irq']<<8)+z['ccr'],0xfffff900))
  raw=self.native_run(self.o/f'gate-{batch}','\n'.join(body),66*len(ss))
  for j,want in enumerate(expected):
   actual=(list(struct.unpack_from('>15I',raw,j*66)),*struct.unpack_from('>HI',raw,j*66+60));assert actual==want,('effective percentage complete register/CCR/IRQ/SP',ss[j],actual,want)
  return dict(contexts=ss,output_sha256=hashlib.sha256(raw).hexdigest())
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,range((len(specs)+15)//16)))
 print('PASS',len(specs),'effective percentage gate complete native ABI contexts',flush=True);return dict(cases=len(specs),batches=rows)

def events(self):
 event_rows=[(0,0,0,0,290),(2,0,0,0,290),(2,4,0,0,290),(2,4,8,0,290),(2,4,8,5,290),(2,4,8,5,289),(0,0,0,0,289)]
 layouts=[[0,0,0,0],[1,0,0,0],[2,0,0,0],[1,2,0,0],[0,0,1,0],[0,0,0,2],[1,1,1,1],[1,2,1,2]];specs=[dict(diff=d,mode=m,sides=s) for d in [0,1,2] for m in [0,1,99] for s in layouts];record_size=10+8+192+104
 def trial(index,z,profile='current'):
  body=[' lea 0xffffb000,%a0',' move.w #0x4850/4-1,%d0','zero: clr.l (%a0)+',' dbra %d0,zero',' move.w #0x4352,0xfffff700',f' move.w #{z["diff"]},0xfffff07a',f' move.w #{z["mode"]},0xfffff84a',' move.w #290,0xfffff70c',' lea 0xffff4000,%a4']
  for p,side in enumerate(z['sides']):body += [f' move.w #{side},0x{0xffffc328+2*p:x}']
  pct=9 if z['mode']==1 else 0
  for a,v in [(0xf702,950 if pct else 0),(0xf70e,pct),(0xf72a,-pct),(0xf732,pct),(0xf734,-pct)]:body += [f' move.w #{v},0xffff{a:04x}']
  for i in range(12):
   obj=0xffffb04a+128*i;side=1 if i<6 else 2;cpct=pct if side==1 else -pct;raw=scaled(BASE,cpct);body += [f' move.w #{0 if i%6==5 else 1},0x{obj+0x34:x}',f' move.b #{64 if side==2 else 0},0x{obj+0x62:x}',f' move.b #1,0x{0xfffff71c+i:x}',f' move.b #{cpct&255},0x{0xfffff710+i:x}']
   for j,v in enumerate(BASE):body += [f' move.b #{v},0x{0xfffff740+16*i+j:x}',f' move.b #{raw[j]},0x{obj+0x66+j:x}']
  for hs,hc,aw,ac,clock in event_rows:
   for addr,value in [(0xc6ce,hs),(0xc6de,hc),(0xca32,aw),(0xca42,ac),(0xc468,clock)]:body += [f' move.w #{value},0xffff{addr:04x}']
   body += [' jsr 0x2e0000']
   for addr in [0xf702,0xf70e,0xf72a,0xf72c,0xf72e,0xf704,0xf706,0xf708,0xf70a]:body += [f' move.w 0xffff{addr:04x},(%a4)+']
   for i in range(12):
    body += [f' lea 0x{0xffffb04a+128*i+0x66:x},%a0']+[f' move.l {off}(%a0),(%a4)+' for off in [0,4,8,12]]
   for base in [0xc6ce,0xca32]:body += [f' lea 0xffff{base+0x32:04x},%a0']+[f' move.w {2*i}(%a0),(%a4)+' for i in range(26)]
  image=(self.a.baseline if profile=='parent' else self.a.rom).read_bytes();patches=[(at,bytes.fromhex('4e75'),image[at:at+2]) for at in [0x2e4004,0x2e5000,0x2ed000]];raw=self.native_run(self.o/f'events-{profile}-{index}','\n'.join(body),len(event_rows)*record_size,patches,profile);rows=[]
  for i,ev in enumerate(event_rows):
   data=raw[i*record_size:(i+1)*record_size];values=struct.unpack_from('>9h',data);attrs=[list(data[18+16*n:18+16*(n+1)]) for n in range(12)];energy=struct.unpack_from('>52H',data,18+192)
   if z['mode']==1 and profile=='current':assert values[:3]==(0,0,0),('Off retained earned percent',z,i,values)
   assert values[5:9]==ev[:4],('events not consumed (would bank credit)',z,i,values,ev)
   for actor,actual in enumerate(attrs):
    side=1 if actor<6 else 2;want=easy(BASE,side in z['sides']) if z['diff']==2 else BASE if z['mode']==1 and profile=='current' else scaled(BASE,values[1 if side==1 else 2]);assert actual==want,('actual all12 full loaded actor attributes',z,i,actor,actual,want)
   if z['diff']==2:
    assert all(v==(4096 if 1+n//26 in z['sides'] else 0) for n,v in enumerate(energy)),('Easy full26 energies disabled by momentum',z,i,energy)
   else:assert set(energy)=={0},('momentum changed fatigue',z,i,energy)
   rows.append(dict(event=ev,momentum=values[:5],consumed=values[5:9],attrs=attrs,energy=energy))
  return raw,dict(spec=z,rows=rows)
 def item(pair):
  i,z=pair;raw,r=trial(i,z)
  if z['mode']==0:
   old,parent=trial(i,z,'parent');assert raw==old,('On deviates from complete native r4 event/rating/Easy state',z);r['actual_native_parent_exact']=True
  return r
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(item,enumerate(specs)))
 print('PASS',len(rows),'native On/Off/invalid event/cache/Easy profiles, all12 actors and all26 energies',flush=True);return dict(cases=rows,scope='Explicit actual team-counter increments and complete cached attribute fixtures, no matches/results claimed.')

def entries(self):
 specs=[dict(mode=m,diff=d,side=t,human=hu) for m in [0,1,99] for d in [0,1,2] for t in [0,1] for hu in [0,1]];rows=[]
 image=self.a.rom.read_bytes();patches=[(0xfa9f8,bytes.fromhex('4e75'),image[0xfa9f8:0xfa9fa]),(0x8e4c,bytes.fromhex('4e75'),image[0x8e4c:0x8e4e]),(0x1ff002,bytes.fromhex('4e75'),image[0x1ff002:0x1ff004])]
 def trial(batch):
  ss=specs[batch*12:batch*12+12];body=[]
  for j,z in enumerate(ss):
   obj=0xffffb04a+768*z['side'];team=0xffffc6ce+0x364*z['side'];body += [' move.w #0x4352,0xfffff700',f' move.w #{z["mode"]},0xfffff84a',f' move.w #{z["diff"]},0xfffff07a',' move.w #10,0xfffff70e',' move.w #-10,0xfffff72a',f' lea 0x{team:x},%a2',f' lea 0x{obj:x},%a3',f' move.b #{64*z["side"]+8*z["human"]},0x62(%a3)',' move.w #1,0x34(%a3)']
   for pad in range(4):body += [f' move.w #{z["side"]+1 if pad==3 and z["human"] else 0},0x{0xffffc328+2*pad:x}']
   base=0xffff4000+72*j
   body += [' moveq #50,%d0',' moveq #100,%d1',' move.l #0xa0000,%d4',' jsr 0x2e000c',f' move.w %d0,0x{base:x}',' moveq #50,%d0',' jsr 0x2e0018',f' move.w %d0,0x{base+2:x}',' moveq #50,%d0',' jsr 0x2e0014',f' move.w %d1,0x{base+4:x}']
   # Real crowd loader tail, with only the preceding native callee explicitly stubbed.
   for k,v in enumerate(BASE):body += [f' move.b #{v},0x{obj+0x66+k:x}']
   body += [' jsr 0x2e0004']
   for k in range(16):body += [f' move.b 0x{obj+0x66+k:x},0x{base+6+k:x}']
  raw=self.native_run(self.o/f'entries-{batch}','\n'.join(body),72*len(ss),patches);result=[]
  for j,z in enumerate(ss):
   rating,crit,roll=struct.unpack_from('>3H',raw,72*j);pct=0 if z['mode']==1 else (-10 if z['side'] else 10);expected=50+(50*pct//100);want_rating=(100 if z['human'] else 20) if z['diff']==2 else expected;want_roll=1000 if z['diff']==2 and z['human'] else expected+30;actual=list(raw[72*j+6:72*j+22]);want_attr=easy(BASE,bool(z['human'])) if z['diff']==2 else scaled(BASE,pct)
   assert (rating,crit,roll)==(want_rating,want_rating,want_roll),('actual roster/crit roll/show ignored Off or Easy',z,(rating,crit,roll),(want_rating,want_rating,want_roll));assert actual==want_attr,('actual loader ignored Off or Easy',z,actual,want_attr);result.append(dict(spec=z,rating=rating,crit=crit,roll=roll,loaded=actual))
  return result
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(trial,range((len(specs)+11)//12)),[])
 print('PASS',len(rows),'actual crowd roster/crit/loader entries with declared preceding-callee stubs',flush=True);return dict(cases=rows,scope='Native crowd entry routines execute. Only preceding formatter/loader and final native text sink are explicit RTS stubs; live complete-game validation is separate.')

def game(self):
 p=self.o/'game-observer';p.mkdir(exist_ok=True);code=self.asm(p,(self.h/'testcases/momentum_frame_probe.s').read_text(),0x2d9000);b=bytearray(self.a.rom.read_bytes());needle=bytes.fromhex('4eb9002ed000');at=b.find(needle,0x2e0000,0x2e0800);assert at>=0 and b.find(needle,at+1,0x2e0800)<0;assert b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b[0x2d9000:0x2d9000+len(code)]=code;b[at:at+6]=bytes.fromhex('4eb9002d9000');rom=p/'rom.bin';rom.write_bytes(b);rows=[]
 for difficulty in [0,1]:
  q=self.o/f'complete-off-{difficulty}';q.mkdir(exist_ok=True);(q/'battery.srm').write_bytes(self.battery(momentum=(1,1,1,1),others=0));self.core(q,rom,['1000 press 1 Right 5','1100 press 1 Right 5','1200 press 1 Down 5','1500 press 1 Start 5','1900 press 1 Start 5','90000 snap','180000 snap','180001 exit'],dict(GPGX_DEV='513',GPGX_SRM=str(q/'battery.srm'),GPGX_HOOK=str(self.h/'testcases/momentum_game_hook.py'),CASE_DIR=str(q),CASE_KIND='cpu',FINAL_FRAME='180000',MOMENTUM_DIFFICULTY=str(difficulty)))
  checks=json.loads((q/'checks.json').read_text());live=json.loads((q/'momentum-live.json').read_text());assert checks and all(x['difficulty']==difficulty for x in checks);rows.append(dict(difficulty=difficulty,completed=checks,committed_off_actor_samples=len(live['samples'])))
 easy_probe=self.o/'easy-observer';easy_probe.mkdir(exist_ok=True);code=self.asm(easy_probe,(self.h/'testcases/easy_energy_probe.s').read_text(),0x2d9000);b=bytearray(self.a.rom.read_bytes());needle=bytes.fromhex('4eb9002ee400');at=b.find(needle,0x2e0000,0x2e0800);assert at>=0 and b.find(needle,at+1,0x2e0800)<0;assert b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b[0x2d9000:0x2d9000+len(code)]=code;b[at:at+6]=bytes.fromhex('4eb9002d9000');easy_rom=easy_probe/'rom.bin';easy_rom.write_bytes(b);inputs=[]
 for count in [1,2,3,4]:
  q=self.o/f'easy-off-input-{count}';q.mkdir(exist_ok=True);battery=bytearray(self.battery(momentum=(1,1,1,1),others=0)[1::2])
  for bank in range(0,len(battery),8192):battery[bank+0x1ef0:bank+0x1ef6]=bytes([80,67,count,count^255,2,253])
  (q/'battery.srm').write_bytes(self.pack(battery));(q/'spec.json').write_text(json.dumps(dict(count=count,end=18000 if count==3 else 12000)));self.core(q,easy_rom,['1500 press 1 Start 5','1900 press 1 Start 5','9000 snap',str(18001 if count==3 else 12001)+' exit'],dict(GPGX_DEV='1281' if count>2 else '513',GPGX_SRM=str(q/'battery.srm'),GPGX_HOOK=str(self.h/'testcases/momentum_easy_input_hook.py')));inputs.append(json.loads((q/'results.json').read_text()))
 print('PASS',len(rows),'complete native Off games and4 real-input Easy Off games; skills/energy/bar rendering',flush=True);return dict(cases=rows,easy_off_input=inputs)


def negative(self):
 import subprocess
 p=self.o/'negative-loader';p.mkdir(exist_ok=True);b=bytearray(self.a.rom.read_bytes());assert b[0x226018:0x22601a]==bytes.fromhex('6000');b[0x226018:0x22601c]=bytes.fromhex('4e754e71');rom=p/'disabled-off-gate.bin';rom.write_bytes(b)
 with(p/'log').open('w') as log:r=subprocess.run(['python3',str(self.h/'verify_momentum.py'),str(rom),str(p/'entries'),'--baseline',str(self.a.baseline.resolve()),'--seed',str(self.a.seed.resolve()),'--state',str(self.a.state.resolve()),'--section','entries'],stdout=log,stderr=log,timeout=120)
 reason=(p/'log').read_text();assert r.returncode and 'actual loader ignored Off or Easy' in reason,('active stale-percent loader mutant was not specifically detected',r.returncode,reason[-1500:]);print('PASS actual stale-percent Off loader negative control',flush=True);return dict(expected_failure=reason[-2200:])
