#!/usr/bin/env python3
"""Native manual-keeper pass after a real catch; physical B and physical pad map.

Manual ownership is an explicit fixture premise after the native catch. No pass,
receiver/vector, timer, whistle, score or successful outcome is injected.
The shortest asserted tap spans2frontend frames, as in goalie_request: a
1-frame tap can fall wholly between native polling ticks (retained diagnostics). Both
native options, both sides and1..4human pads use the native pass; native receiver assignment at
release is allowed. The separate b_switch suite proves early dispatch ownership.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,hashlib,subprocess,os,struct
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);ap.add_argument('--native-states',type=Path,required=True);ap.add_argument('--normal-states',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(exist_ok=True,parents=True);sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest();assert sha(a.baseline)=='2ca0be4ef69f1d875c5a25e1ef91e42bda6b87396c5471fa8a417eb652ec8d60','exact r12 required'
code=o/'banks.bin';subprocess.run([str(h.parent/'m68kasm.sh'),str(h/'testcases/manual_keeper_banks.s'),'2EF400',str(code)],check=True,stdout=subprocess.DEVNULL);code=code.read_bytes();profiles={}
for name,rom in [('baseline',a.baseline),('current',a.rom)]:
 r=bytearray(rom.read_bytes());assert set(r[0x2ef400:0x2ef400+len(code)])=={255};r[0x2ef400:0x2ef400+len(code)]=code
 for at,target,expected in [(0xad66,0x2ef400,'3f38c32831f8c32cc328'),(0xadce,0x2ef440,'31f8c320c32431dfc320'),(0xae00,0x2ef480,'3f38c32a31f8c32ec32a'),(0xae68,0x2ef4c0,'31f8c322c32631dfc322')]:
  assert r[at:at+10]==bytes.fromhex(expected);r[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
 path=o/(name+'.bin');path.write_bytes(r);profiles[name]=path
cases=[]
for adapter in [False,True]:
 for team in [0,1]:
  for count in range(1,(4 if adapter else 2)+1):
   for pad in range(count):
    for option in [0,1]:cases.append(dict(adapter=adapter,team=team,count=count,pad=pad,option=option,tap=2,delay=5))
for team in [0,1]:
 for pad in range(4):
  for tap,delay in [(2,1),(5,15)]:cases.append(dict(adapter=True,team=team,count=4,pad=pad,option=0,tap=tap,delay=delay))

def run(item):
 name,index,s=item;p=o/f'{name}-{index:03}';p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(s));(p/'commands.txt').write_text(f'100 load {((a.native_states/f'before-gk{6*s["team"]+5}.state') if s['adapter'] else (a.normal_states/f'gk{6*s["team"]+5}-clear.state')).resolve()}\n401 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='1281' if s['adapter'] else '513',GPGX_HOOK=str(h/'testcases/manual_keeper_pass_hook.py'))
 with(p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(profiles[name]),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 result=json.loads((p/'results.json').read_text());passed=result['pass_result'];assert result['catch_frame'] and result['manual_frame'] and result['press_frame'],('fixture not established',name,s)
 retained=bool(passed and passed['controls'][s['pad']]==6*s['team']+5)
 if name=='current':
  assert passed,('no real native pass',s)
  assert all(row['controls'][s['pad']]==6*s['team']+5 for row in result['trace'] if result['press_frame']<=row['frame']<result['release_frame']),('manual keeper switched before native release',s,passed)
  target=passed['target']&255;assert passed['target'] in (target,0xff00|target),('invalid native receiver encoding',passed['target']);assert target<12 and target//6==s['team'] and target%6!=5,('native pass target',s,passed)
  gx,gy=passed['goalie'];tx,ty=passed['targets'][target];vx,vy=passed['velocity'];assert (tx-gx)*vx+(ty-gy)*vy>0,('native outgoing vector',s,passed)
  assert passed['pending']==[0,0],('manual pass became CPU outlet request',s,passed)
  start=next(x for x in result['trace'] if x['manual']==result['manual_frame']);assert all(row['controls'][j]==start['controls'][j] for row in result['trace'] if result['press_frame']<=row['frame']<=passed['frame'] for j in range(s['count']) if j!=s['pad']),('other pad stolen',s)
 return dict(profile=name,case=index,spec=s,caught=result['catch_frame'],manual=result['manual_frame'],pressed=result['press_frame'],passed=None if passed is None else {k:passed[k] for k in ['frame','target','controls','velocity','keeper_flags','pending']},keeper_retained_through_pass=retained)
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,[(name,i,s) for name in ('baseline','current') for i,s in enumerate(cases)]))
old=[r for r in results if r['profile']=='baseline'];new=[r for r in results if r['profile']=='current'];assert any(not r['keeper_retained_through_pass'] for r in old),'old cursor switch not reproduced'
abi=o/'baseline-early-dispatch';subprocess.run(['python3',str(h/'verify_b_switch.py'),str(a.baseline),str(abi),'--observe-only'],check=True,stdout=subprocess.DEVNULL)
abi_rows=json.loads((abi/'results.json').read_text())['cases'];old_manual=[r for r in abi_rows if r['case']['kind']=='own_human_goalie'];assert len(old_manual)==12 and all(r['observed']!=r['case']['old'] for r in old_manual),'r12 early keeper-switch regression not reproduced'
report=dict(baseline_early_manual_keeper_switches=len(old_manual),rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),state_sha256={str(p.name):sha(p) for p in list(a.native_states.glob('*.state'))+list(a.normal_states.glob('*clear.state'))},scope=__doc__,paired_contexts=len(cases),old_receiver_cursor_changes_at_release=sum(not r['keeper_retained_through_pass'] for r in old),current_native_passes=len(new),results=results);(o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS',len(results),'paired native manual-keeper catch/B/target/vector/pad-retention cases; old cursor changes at release',report['old_receiver_cursor_changes_at_release'])
