#!/usr/bin/env python3
"""Native reset-vector tests for charge/animation eligibility, RNG, sound and physics.
Writes only isolated driver ROMs in the explicit private work folder.
"""
from pathlib import Path
import argparse,json,os,struct,subprocess
from verify_critical_sound import STUB


def main():
    ap=argparse.ArgumentParser(description=__doc__)
    ap.add_argument('rom',type=Path);ap.add_argument('work',type=Path)
    ap.add_argument('--sound-id',type=int,default=123)
    args=ap.parse_args();here=Path(__file__).resolve().parent
    work=args.work.resolve();work.mkdir(parents=True,exist_ok=True)
    source=(here/'testcases/critical_driver.s').read_text()
    cases=[('wrist-min',15,0x7fc,100,0,13),('wrist-boundary',30,0x7fc,100,0,13),
           ('slap-zero',31,0x7fc,0,0,13),('slap-half',31,0x7fc,50,0,13),
           ('slap-max',31,0x7fc,100,0,13),('slap-away',31,0x7fc,100,64,12),
           ('slap-reversed',31,0x7fc,100,128,13),('backhand',31,0x92e,100,0,13),
           ('backhand-max',255,0x92e,100,0,13),('other-animation',31,0,100,0,13),
           ('slap-invalid-slot',31,0x7fc,100,0,26)]
    reports=[]
    env={k:v for k,v in os.environ.items() if k not in ['GPGX_SRM','GPGX_HOOK','GPGX_WAV']};env['GPGX_DEV']='513'
    for name,charge,animation,rating,flags,slot in cases:
        p=work/name;p.mkdir(exist_ok=True)
        text=source.replace('move.b #13,0x66',f'move.b #{slot},0x66').replace('move.b #0,0x62',f'move.b #{flags},0x62')
        text=text.replace('move.w #192,0xFFBEDE',f'move.w #{animation},0x58(%a3)\n    move.w #{charge},0xFFBEDE\n    jsr 0x2AD000\n    move.w #192,0xFFBEDE')
        text=text.replace('jmp 0x2FF400','jmp 0xC4A8').replace('move.w #-1,-(%sp)','move.w #24,-(%sp)')
        text=text.replace('clr.w 0xFFF604','clr.w 0xFFF604\n    clr.l 0xFFF610\n    clr.l 0xFFF614')
        if flags & 128:text=text.replace('#-200','#200').replace('#-16320','#16320').replace('#-13056','#13056')
        stub=STUB.replace('cmpi.w #14,%d0',f'cmpi.w #{args.sound_id},%d0').replace('EXPECTED_SOUND','24')
        (p/'driver.s').write_text(text+stub)
        subprocess.run([str(here.parent/'m68kasm.sh'),str(p/'driver.s'),'2FEA00',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
        b=bytearray(args.rom.read_bytes());code=(p/'code.bin').read_bytes();assert len(code)<0x600
        b[0x2fea00:0x2fea00+len(code)]=code;struct.pack_into('>I',b,4,0x2fea00)
        b[0x11132:0x11138]=bytes.fromhex('4ef9002fed00')
        b[0x2ff800+26*10+13]=rating;b[0x2ff800+26*25+12]=rating
        (p/'driver.bin').write_bytes(b);(p/'commands.txt').write_text('1000 dump fff600 24 result\n1000 dump ffd066 4 random\n1001 exit\n')
        subprocess.run(['nice','-n','19','python3',str(here/'test_core.py'),str(p/'driver.bin'),str(p/'commands.txt'),str(p)],env=env,check=True,stdout=subprocess.DEVNULL)
        results={}
        for l in (p/'commands.out').read_text().splitlines():
            _,k,v=l.split(' ',2);results[k]=bytes.fromhex(v)
        raw=results['result'];normal,crit,errors=struct.unpack_from('>3H',raw);sound,ordinary,bad,caller=struct.unpack_from('>4H',raw,16)
        eligible=animation==0x7fc and charge>=31 and slot<26 and rating>0
        assert raw[8:12]==b'CRIT' and errors==bad==caller==0,(name,raw.hex())
        assert normal+crit==10000 and sound==crit and ordinary==normal,(name,raw.hex())
        if eligible:assert abs(crit/10000-rating/1000)<.015,(name,crit)
        else:assert crit==0 and results['random']==bytes.fromhex('12345678'),(name,'excluded shot rolled RNG')
        reports.append(dict(case=name,raw_charge=charge,animation=hex(animation),rating=rating,critical=crit,ordinary=normal,sound_id=args.sound_id,shots=10000))
    (work/'results.json').write_text(json.dumps(reports,indent=2)+'\n')
    print('PASS',len(reports),'native cases: slap eligibility, exclusions without RNG, probabilities, one sound, physics/stack')


if __name__=='__main__':main()
