"""Revised policy: human-only Quickgame; Allstar retains humans and adds CPU.

Actual controller menu circles plus declared native stale-choice/adapter/guard
fixtures. CPU movement and human input are qualified separately in Allstar.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import hashlib,json,os,shutil,struct,subprocess
from native_probe import NativeProbe

def verify(rom,here,out):
 rom=Path(rom).resolve();h=Path(here);o=Path(out);o.mkdir(parents=True,exist_ok=True);b=rom.read_bytes();assert b[0x22cd6c:0x22cd70]==b'ASCP';probe=NativeProbe(rom,h)
 cases=[dict(adapter=d,allstar=mode,choice=c)for d in[0,1]for mode in[0,1,99]for c in[0,1,2,3,4,65535]]
 def batch(item,selected=None):
  number,ss=item;selected=selected or probe;body=[]
  for i,z in enumerate(ss):
   body += [f' move.w #{z["adapter"]},0xffffd046',f' move.w #{z["allstar"]},0xfffff850',f' move.w #{z["choice"]},0xfffff84e',' clr.w 0xfffff06c',' jsr 0x229000',' jsr 0x22900c',f' move.w 0xfffff84e,0x{0xffff4000+20*i:x}',f' move.w 0xffffd04a,0x{0xffff4002+20*i:x}',f' move.w 0xfffff850,0x{0xffff4004+20*i:x}',f' move.w 0xfffff84c,0x{0xffff4006+20*i:x}',f' move.l 0xffffc328,0x{0xffff4008+20*i:x}',f' move.l 0xffffc32c,0x{0xffff400c+20*i:x}',' jsr 0x229004',f' move.l %d0,0x{0xffff4010+20*i:x}']
  raw=selected.run(o/str(number),'\n'.join(body),20*len(ss));rows=[]
  for i,z in enumerate(ss):
   c=z['choice'];effective=4 if c==4 and z['allstar']==1 else c if c<=3 and z['adapter']else 0
   players=[4,5,6,6,1][effective];sides=[[1,2,0,0],[1,2,2,0],[1,2,2,2],[1,1,2,2],[1,0,0,0]][effective]
   got=struct.unpack_from('>8HI',raw,20*i);assert got[:8]==(effective,players,z['allstar'],1,*sides),('native exclusive CPU mode/stale choice/adapter policy',z,got)
   ptr=got[8];label=b[ptr+2:ptr+22].decode('ascii');assert ('CPU'in label)==(effective==4),('native CPU label in wrong mode',z,label)
   rows.append(dict(spec=z,effective=effective,sides=sides,players=players,label=label))
  return rows
 with ThreadPoolExecutor(max_workers=3)as pool:contracts=sum(pool.map(batch,[(i//8,cases[i:i+8])for i in range(0,len(cases),8)]),[])
 # Flip the actual first mode guard, reproducing the earlier CPU allocation.
 mutant=bytearray(b);at=mutant.find(bytes.fromhex('0c780001f850'),0x229028,0x229080);assert at>=0 and mutant[at+6]==0x66;mutant[at+6]=0x67;path=o/'mutant-CPU-in-wrong-mode.bin';path.write_bytes(mutant)
 try:batch(('negative',[dict(adapter=1,allstar=0,choice=4)]),NativeProbe(path,h))
 except AssertionError as e:
  negative=str(e);assert 'exclusive CPU mode'in negative,negative
 else:raise AssertionError('Wrong-mode CPU allocation escaped native policy oracle')
 def menu(device):
  p=o/('menus-'+str(device));p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(bytes(65536));shutil.copy2(h/'testcases/capture_native.py',p/'capture_native.py');shutil.copy2(h/'testcases/exhibition_policy_hook.py',p/'hook.py')
  commands=['1100 press 1 Left 5','1250 press 1 Left 5','1400 press 1 Down 5'];expected={};choice=0
  def wanted(frame,mode,c):expected[str(frame)]=dict(allstar=mode,quick=1,choice=c,players=[4,5,6,6,1][c])
  for start,direction in[(1500,'Right'),(2500,'Left')]:
   for i in range(8):
    f=start+100*i;commands.append(f'{f} press 1 {direction} 5');choice=(choice+(1 if direction=='Right'else-1))%4 if device==1281 else 0;wanted(f+60,0,choice)
  commands += ['3500 press 1 Up 5','3700 press 1 Right 5','3900 press 1 Down 5']
  for i in range(10):
   f=4000+100*i;commands.append(f'{f} press 1 Right 5');choice=(i+1)%5 if device==1281 else(4 if i%2==0 else 0);wanted(f+60,1,choice)
  commands += ['5100 press 1 Left 5','5300 press 1 Up 5','5500 press 1 Left 5','5700 press 1 Down 5','5900 press 1 Right 5','6101 exit']
  wanted(5180,1,4);wanted(5800,0,0);wanted(5970,0,1 if device==1281 else 0)
  (p/'commands.txt').write_text('\n'.join(commands)+'\n');(p/'spec.json').write_text(json.dumps(dict(expected=expected,captures={},ROM_sha256=hashlib.sha256(b).hexdigest())))
  env={k:v for k,v in os.environ.items()if not k.startswith('GPGX_')};env.update(GPGX_DEV=str(device),GPGX_DEV2=str(device),GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(p/'hook.py'))
  with(p/'log').open('w')as log:subprocess.run(['python3',str(h/'test_core.py'),str(rom),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=90)
  return json.loads((p/'result.json').read_text())
 with ThreadPoolExecutor(max_workers=2)as pool:menus=list(pool.map(menu,[1281,513]))
 result=dict(ROM_sha256=hashlib.sha256(b).hexdigest(),native_contracts=contracts,real_controller_menus=menus,active_wrong_mode_negative=negative,Quickgame_only_humans=True,Allstar_CPU_and_four_human_splits=True,scope=__doc__);(o/'results.json').write_text(json.dumps(result,indent=2)+'\n');print('PASS36 native policy guards/stale-choice cases and58 real menu observations; CPU only in Allstar, four human splits retained; active wrong-mode negative',flush=True)
 return result
