#!/usr/bin/env python3
"""Actual CPU frame declines a covered forced shot, retaining true possession.

Native C566 and possession/assist bookkeeping execute. Only the eventual C2F2
endpoint is observed, so this suite isolates the decision and cache ordering.
Full physical execution is independently tested by verify_ai_smart_aim_physics.
"""
from pathlib import Path
import argparse, hashlib, json, os, struct, subprocess
from smart_aim_test_support import setup

ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--gap',type=int,choices=[0,16,24],required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
cases=[dict(team=t,direction=d,gx=g) for t in (0,1) for d in (-1,1) for g in (0,7,20,-20)]
cases += [dict(**{key:value}) for key,value in [('difficulty',0),('difficulty',2),('human',True),('boxed',True),('whistle',True),('highlight',True),('view',True),('special',1),('wrong_owner',True),('loose',True)]]
# Add actual native B24 friendly-catch poses without removing any original case.
# General shot policy still excludes B24; only the owned catch-opening path may
# retain the existing buffer and use the unchanged native opening/lane rules.
cases += [dict(team=t,direction=d,gx=g,busy=True,animation=0xB24,friendly_interception=True)
          for t in (0,1)for d in(-1,1)for g in(0,7,20,-20)]
cases += [dict(busy=True,animation=0xB24,friendly_interception=True,**{key:value})
          for key,value in [('difficulty',0),('difficulty',2),('human',True),('boxed',True),('whistle',True),('highlight',True),('view',True),('special',1),('wrong_owner',True),('loose',True)]]
rows=[]
for index,s in enumerate(cases):
    p=o/str(index);p.mkdir(exist_ok=True);lines,addresses=setup(dict(s,index=5));slot=6*s.get('team',0)+2;base=0xffffc6ce+s.get('team',0)*0x364
    body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfffffe00,%sp']+lines
    if s.get('friendly_interception'):
        body += [f' move.w #{0xFF00|slot},0xffffbee0',f' move.w #{6*s.get("team",0)+1},0xffffbeda']
    body += [' move.w #0x5042,0xfffff086',f' move.w #{slot+1},0xfffff088',' move.w #64,0xfffff08a',' clr.l 0xfffff08c',' clr.w 0xffffbf80',f' move.w #7,0x{base+0x18:x}',f' move.w #9,0x{base+0x1a:x}',f' move.w #11,0x{base+0x1c:x}',' move.l #0x12345678,0xffffd066',' jsr 0x2dd010',' movem.l %d0-%d7/%a0-%a6,0xffff4000',' move.l %sp,0xffff403c',' move.w 0xffffbedc,0xffff4902',' move.w 0xffffb7aa,0xffff4904',f' move.w 0x{base+0x18:x},0xffff4906',' move.l 0xfffff086,0xffff4908',' move.w 0xfffff08a,0xffff490c',' move.l 0xffffd066,0xffff4910',' move.l #0x43415443,0xffff3ff0','end: bra.s end','observe:',' move.w #1,0xffff4900',f' move.w 0x{base+0x18:x},0xffff4914',' rts']
    (p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2d9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
    data=bytearray(a.rom.read_bytes());code=(p/'code.bin').read_bytes();assert data[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);data[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',data,4,0x2d9000);target=code.index(bytes.fromhex('33fc0001ffff4900'));data[0xc2f2:0xc2f8]=bytes.fromhex('4ef9')+struct.pack('>I',0x2d9000+target)
    (p/'driver.bin').write_bytes(data);(p/'commands.txt').write_text('100 dump ff3ff0 4 done\n100 dump ff4000 64 regs\n100 dump ff4900 22 observed\n101 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_AUTO_CREDITS']='0'
    with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
    out={line.split()[1]:bytes.fromhex(' '.join(line.split()[2:])) for line in (p/'commands.out').read_text().splitlines()};assert out['done']==b'CATC';obs=out['observed'];regs=struct.unpack('>16I',out['regs'])
    eligible=s.get('difficulty',1)==1 and not any(s.get(k) for k in ['human','boxed','whistle','highlight','view','special','wrong_owner','loose'])
    would_release=eligible and (not a.gap or abs(s.get('gx',20))>=20 or a.gap==16 and s.get('gx')==7)
    fired=int.from_bytes(obs[:2],'big');assert fired==int(would_release),(s,fired,would_release)
    assert int.from_bytes(obs[2:4],'big')==5,('opening probe did not restore prior Native aim',s,obs.hex())
    owner=65535 if s.get('loose') else 6*(1-s.get('team',0)) if s.get('wrong_owner') else slot
    assert int.from_bytes(obs[4:6],'big')==owner,('decision probe changed real puck possession',s)
    assert int.from_bytes(obs[6:8],'big')==(15 if would_release else 7),('native scorer accounting changed without a release or was omitted',s,obs.hex())
    if would_release:assert int.from_bytes(obs[20:22],'big')==15,('actual native1013E did not precede release',s)
    assert obs[8:14]==bytes(6),'buffer or TTL not cleared'
    assert obs[16:20]==bytes.fromhex('12345678'),('extra RNG draw',s)
    assert list(regs[:8])==[0x11223300+r for r in range(8)] and list(regs[8:15])==addresses and regs[15]==0xfffffe00,('native frame register/stack ABI',s,regs)
    rows.append(dict(case=s,forced_native_release=bool(fired),real_owner_preserved=True,aim_restored=True,buffer_cleared=True,cache_updated_only_for_actual_release=True,native_register_stack_rng_preserved=True))
(o/'results.json').write_text(json.dumps(dict(rom_sha256=hashlib.sha256(a.rom.read_bytes()).hexdigest(),minimum_gap=a.gap,cases=rows),indent=2)+'\n')
print('PASS',len(rows),'actual CPU catch/opening decisions, true ownership, cleared buffer, Native1013E cache ordering, ABI and RNG')
