#!/usr/bin/env python3
"""All four native human splits, physical input ports, keeper requests and inactive pads.

Uses private cold batteries and an explicitly hashed passive P3/P4 bank observer.
No controller-side/actor ownership words or successful responses are injected.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,os,json,hashlib,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--allstar',action='store_true');a=ap.parse_args()
h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
allstar_cpu=a.rom.read_bytes()[0x22cd6c:0x22cd70]==b'ASCP'
if allstar_cpu and not a.allstar:
 from verify_exhibition_policy import verify
 verify(a.rom,h,o)
 raise SystemExit(0)
has_allstar=a.rom.read_bytes()[0x22c000:0x22c002]!=b'\xff\xff'
left_inputs=['1100 press 1 Left 5']+(['1250 press 1 Left 5']if has_allstar and not a.allstar else [])
if a.allstar:assert has_allstar

sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
# Extend the previously established bank observer to save the temporary side too.
s=(h/'testcases/manual_keeper_banks.s').read_text().replace('    move.w 0xFFFFC320,0xFFF0D6','    move.w 0xFFFFC328,0xFFF852\n    move.w 0xFFFFC320,0xFFF0D6').replace('    move.w 0xFFFFC322,0xFFF0D8','    move.w 0xFFFFC32A,0xFFF854\n    move.w 0xFFFFC322,0xFFF0D8')
(o/'banks.s').write_text(s);subprocess.run([str(h.parent/'m68kasm.sh'),str(o/'banks.s'),'2EF400',str(o/'banks.bin')],check=True,stdout=subprocess.DEVNULL)
rom=bytearray(a.rom.read_bytes());code=(o/'banks.bin').read_bytes();assert rom[0x2ef400:0x2ef400+len(code)]==b'\xff'*len(code);rom[0x2ef400:0x2ef400+len(code)]=code
for at,target,expected in [(0xad66,0x2ef400,'3f38c32831f8c32cc328'),(0xadce,0x2ef440,'31f8c320c32431dfc320'),(0xae00,0x2ef480,'3f38c32a31f8c32ec32a'),(0xae68,0x2ef4c0,'31f8c322c32631dfc322')]:
 assert rom[at:at+10]==bytes.fromhex(expected);rom[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
(o/'observed.bin').write_bytes(rom)
def run(c,device=1281):
 split,pad,button=c;p=o/(f'split{split}-pad{pad+1}-{button}'+(f'-device{device}' if device!=1281 else ''));p.mkdir(exist_ok=True)
 (p/'battery.srm').write_bytes(bytes(65536))
 lines=left_inputs+['1400 press 1 Down 5']+[f'{1550+150*i} press 1 Right 5'for i in range(1 if split==4 and device==513 else split)]+['2300 press 1 Up 5']+[f'{2500+700*i} press 1 Start 5'for i in range(5)]+['9500 exit']
 (p/'commands.txt').write_text('\n'.join(lines)+'\n');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(h/'testcases/quickgame_input_hook.py'),QUICK_CASE=json.dumps(dict(split=split,pad=pad,button=button)),EXPECT_ALLSTAR=str(int(a.allstar)))
 with(p/'log').open('w')as log:subprocess.run(['python3',str(h/'test_core.py'),str(o/'observed.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=50)
 return json.loads((p/'result.json').read_text())

cases=[(4,0,'none')]+[(4,p,b)for p in range(4)for b in ['Right','Left','Y','B']]
with ThreadPoolExecutor(max_workers=3)as pool:results=list(pool.map(run,cases))
normal=[run((4,0,b),513)for b in ['none','Right','Left','Y','B']]
for cohort in [results,normal]:
 base=next(r for r in cohort if r['case']['button']=='none');sides=base['rows'][0]['sides'];assert sides==[1,0,0,0]
 rows={b:next(r for r in cohort if r['case']==dict(split=4,pad=0,button=b))['rows']for b in ['Right','Left','Y','B']}
 actor=base['rows'][0]['actors'][0]
 delta=rows['Right'][3]['objects'][actor][0]-rows['Left'][3]['objects'][actor][0];assert delta>15,('P1 directions',delta)
 assert any(r['actors'][0]!=actor for r in rows['B'][2:4]),'P1 B did not switch'
 assert any(r['actors'][0]==5 for r in rows['Y'][2:]),'P1 Y did not select home keeper'
 for result in cohort:
  for row in result['rows']:
   assert row['sides']==sides,('CPU opponent or inactive pad obtained human side',result['case'],row)
   assert all(not(row['objects'][i][2]&0x800)for i in range(6,12)),('CPU opposing actor has human flag',result['case'],row)
 # The native opponent must actually move without any away-pad input.
 first,last=base['rows'][0],base['rows'][-1]
 assert any(abs(last['objects'][i][axis]-first['objects'][i][axis])>10 for i in range(6,11)for axis in [0,1]),'CPU side did not skate autonomously'

def menu_circle(device):
 p=o/('menu-circle-'+str(device));p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(bytes(65536));lines=left_inputs+['1400 press 1 Down 5'];expected={};choice=0
 for start,direction in [(1500,'Right'),(2500,'Left')]:
  for i in range(5):
   f=start+200*i;lines.append(f'{f} press 1 {direction} 5')
   choice=((choice+(1 if direction=='Right'else -1))%5)if device==1281 else(4 if choice==0 else 0)
   expected[str(f+100)]=dict(choice=choice,allstar=int(allstar_cpu),quick=1,players=[4,5,6,6,1][choice])
 lines+=['3500 press 1 Left 5','3700 press 1 Up 5',f'3900 press 1 {"Left"if allstar_cpu else"Right"} 5','4100 press 1 Down 5']
 expected['3600']=dict(choice=4,allstar=int(allstar_cpu),quick=1,players=1);expected['4000']=dict(choice=0,allstar=int(not allstar_cpu),quick=1,players=4)
 for i in range(4):
  f=4300+200*i;lines.append(f'{f} press 1 Right 5');choice=(i+1)%4 if device==1281 else 0
  expected[str(f+100)]=dict(choice=choice,allstar=int(not allstar_cpu),quick=1,players=[4,5,6,6][choice])
 lines+=['5300 exit'];(p/'commands.txt').write_text('\n'.join(lines)+'\n');(p/'spec.json').write_text(json.dumps(dict(expected=expected,captures={})))
 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(h/'testcases/quickgame_cpu_menu_hook.py'),CPU_MENU_ADAPTER=str(int(device==1281)))
 with(p/'log').open('w')as log:subprocess.run(['python3',str(h/'test_core.py'),str(a.rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=50)
 return json.loads((p/'result.json').read_text())
with ThreadPoolExecutor(max_workers=2)as pool:menu_cycles=list(pool.map(menu_circle,[1281,513]))
report=dict(rom_sha256=sha(a.rom),CPU_mode='Allstar'if a.allstar else'Quickgame',adapter_cases=len(results),ordinary_pad_cases=len(normal),CPU_away_has_no_human_actors=True,autonomous_CPU_movement=True,P1_direction_B_Y=True,all_inactive_pads_stay_inactive=True,results=results,ordinary_pads=normal,real_menu_circles=menu_cycles)
(o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS '+('Allstar'if a.allstar else'Quickgame')+' CPU:22 cold boots, P1 direction/B/Y, opponent autonomous and entirely CPU, inactive pads excluded, adapter and ordinary pads')
