#!/usr/bin/env python3
"""Native Auto Replays menu, shared preference persistence and real goal/manual replay."""
from pathlib import Path
import argparse,json,os,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--seed',type=Path,required=True);ap.add_argument('--state',type=Path,required=True)
a=ap.parse_args();here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(parents=True,exist_ok=True);reports=[]
def run(name,raw,lines,hook,dev='513',extra=None):
 p=out/name;p.mkdir(exist_ok=True)
 if raw is not None:(p/'battery.srm').write_bytes(raw)
 (p/'commands.txt').write_text('\n'.join(lines)+'\n');(p/'hook.py').write_text(hook)
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV=dev,GPGX_HOOK=str(p/'hook.py'))
 if raw is not None:env['GPGX_SRM']=str(p/'battery.srm')
 if extra:env.update(extra)
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(here/'test_core.py'),str(a.rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=40)
 result=json.loads((p/'result.json').read_text());reports.append(dict(name=name,**result));return (p/'battery.srm').read_bytes() if raw is not None else None
common='''from pathlib import Path
import json,struct
p=Path(outdir);rows=[]
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def observe(f,wanted,slot=None):
 b=bytes(sram)[1:65536:2]
 assert rw(0xf0de)==wanted,('replay preference',f,rw(0xf0de),wanted)
 if slot is not None:assert rw(0xf06a)==slot,(f,rw(0xf06a),slot)
 assert [rw(x) for x in [0xd050,0xd052,0xd056,0xd072,0xf004]]==[0,0,2,2,0],('game settings changed',f)
 assert b[0x1fff]==sum(b[:0x1ffe])%256 and b[0x3fff]==sum(b[0x2000:0x3ffe])%256,('checksum',f)
 rows.append(dict(frame=f,value=wanted,slot=rw(0xf06a),record=b[0x2e18:0x2e1c].hex()))
 (p/'result.json').write_text(json.dumps(dict(observations=rows),indent=2)+'\\n')
'''
# Started slots deliberately keep their game options locked; replay preference is editable.
b=bytearray(a.seed.read_bytes()[1:16384:2]);b[0xe20:0x1ffe]=bytes(0x11de);banks=[]
for slot,length in [(1,84),(2,31),(3,7)]:
 c=bytearray(b);struct.pack_into('>4s6H',c,0x1000,b'SESN',1,11,1,length,0,3);struct.pack_into('>4s4H',c,0x1ea0,b'S3SV',1,slot,7,1)
 struct.pack_into('>H',c,0x1eca,0xff00);struct.pack_into('>H',c,0x1eba,1)
 c[0x1ef0:0x1ef4]=b'PC\x01\xfe';c[0x1ef4:0x1ef6]=bytes([0,255]);c[0x1ff6:0x1ffd]=bytes([0x54,0,0,2,2,0,0x5a]);banks.append(c)
logical=bytearray(banks[0])+b''.join(banks)
logical[0x2e18:0x2e1c]=b'RP\x00\xff';logical[0x2e1c:0x2e20]=b'CT\x00\xff'
def pack(b):
 for off in range(0,len(b),8192):b[off+0x1fff]=sum(b[off:off+0x1ffe])%256;b[off+0x1ffe]=b[off+0x1fff]^255
 raw=bytearray(len(b)*2);raw[1::2]=b;return raw
raw=pack(logical)
lines=[f'{1000+80*i} press 1 Down 5' for i in range(10)]+['2050 snap','2100 press 1 Right 5','2250 snap']
lines += [f'{2300+70*i} press 1 Up 5' for i in range(10)]
lines += ['3250 press 1 Right 5','3400 press 1 Right 5','3701 exit']
hook=common+'''def test_tick(f):
 if f==900:observe(f,0,1)
 if f==2200:observe(f,1,1)
 if f==3350:observe(f,1,2)
 if f==3650:observe(f,1,3)
'''
raw=run('menu-off-switch-slots',raw,lines,hook)
hook=common+'''def test_tick(f):
 if f==900:observe(f,1,3)
 if f==2200:observe(f,0,3)
'''
raw=run('cold-on',raw,[*[f'{1000+80*i} press 1 Down 5' for i in range(10)],'2100 press 1 Left 5','2200 snap','2201 exit'],hook)
run('cold-on-confirm',raw,['1101 exit'],common+'''def test_tick(f):
 if f==1000:observe(f,0,3)
''')
# Invalid tag, enum and complement fall back to the requested default Off.
for name,record in [('tag',b'XX\x01\xfe'),('enum',b'RP\x09\xf6'),('complement',b'RP\x01\x01')]:
 b=bytearray(raw[1::2]);b[0x2e18:0x2e1c]=record
 run('invalid-'+name,pack(b),['1101 exit'],common+'''def test_tick(f):
 if f==1000:observe(f,1,3)
''')
# Reuse only the existing native controller shot setup, then independently watch
# the actual goal with Off. Manual pause -> Instant Replay must remain available.
setup=(here/'shot_panel_hook.py').read_text().split('def test_tick(f):')[0]
body=(here/'shot_panel_hook.py').read_text().split('def test_tick(f):')[1].split(" if spec['kind'] in ['timeout','pause'] and f>=released:")[0]
hook=setup+'def setup_tick(f):'+body+'''
goal_at=None;manual_at=None
def test_tick(f):
 global goal_at,manual_at
 if f==100:ww(0xf0de,1)
 if f<=200:setup_tick(f)
 if f>=200:
  assert not 0xa501<=rw(0xf738)<=0xa506,('automatic replay while Off',f)
  if rw(0xc6da)+rw(0xca3e)==1 and goal_at is None:goal_at=f
 if goal_at and f==goal_at+240:
  assert rw(0xf73c)==0 and rw(0xf73e)==0xa501
  held[(0,IDS['Start'])]=5;manual_at=f
 if manual_at and f==manual_at+100:held[(0,IDS['Down'])]=5
 if manual_at and f==manual_at+180:held[(0,IDS['C'])]=5
 if manual_at and f==manual_at+300:
  assert rb(0xc2ee)&8 and rw(0xcf20)==1,('manual replay unavailable while Off',f,hex(rw(0xc2ee)),rw(0xcf20))
  snap('manual-replay-off');(p/'result.json').write_text(json.dumps(dict(native_goal=True,automatic_replay=False,manual_replay=True,goal_frame=goal_at))+'\\n');raise SystemExit(0)
 assert f<1700,('Off/manual replay timeout',goal_at,manual_at)
'''
run('goal-off-manual',None,[f'100 load {a.state.resolve()}','1801 exit'],hook,'1281',dict(SHOT_PANEL_CASE=json.dumps(dict(kind='goal',pad=1))))
(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print('PASS',len(reports),'replay preference cases: locked-slot edits, switches/reboots, corrupt records, real goal Off/manual replay')
