"""Native adapter power-cycle, movement and pause/resume QA on private saves."""
from pathlib import Path
import json,struct,sys
p=Path(outdir);spec=json.loads(os.environ['REBOOT_CASE']);events=[];started=None;pause_at=None;resumed_at=None;paused_seen=False;moved={};attempts={};movement_passed=set();phase='boot'
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def rb(a):return wram[ram(a)]
def sw(a):v=rw(a);return v-65536 if v&32768 else v
def press(pad,key,n=5):held[(pad-1,IDS[key])]=n
def emit(f,event,**kw):events.append(dict(frame=f,event=event,**kw));(p/'events.json').write_text(json.dumps(events,indent=2)+'\n')
def done(f):
 (p/'battery.srm').write_bytes(bytes(sram)[:65536]);(p/'result.json').write_text(json.dumps(dict(frame=f,events=events),indent=2)+'\n');print('PASS reboot',spec,flush=True);sys.exit(0)
def test_tick(f):
 global started,pause_at,resumed_at,paused_seen,phase
 b=bytes(sram)[1:65536:2];h=struct.unpack_from('>6H',b,0x1004)
 if f==1300:
  assert h[2]==2 and rw(0xf06a)==spec['slot'] and rw(0xd046) and rw(0xe3f6)==2
  assert b[0x1fff]==sum(b[:0x1ffe])%256;emit(f,'cold-boot',header=h,players=rw(0xd04a),adapter=rw(0xd046))
 if spec['stage']==2:
  if f in spec['checks']:assert rw(0xd046),('adapter lost after restart/menu switch',f)
  if f==spec['finish']:
   assert h[2]==2 and h[3]==spec['length'] and rw(0xf06a)==spec['slot'] and rw(0xf06c)==1 and rw(0xd048)==0
   assert b[0x1fff]==sum(b[:0x1ffe])%256;emit(f,'second-restart-menu-circles',header=h);done(f)
  return
 live=rw(0xc468)>0 and rw(0xc466)<=3 and rw(0xc2e8)>8 and not rb(0xc2ec)&128
 if started is None and f>2500 and live and rw(0xc468)<300 and not rb(0xc2f2)&4:
  assert [rw(0xc328+i*2) for i in range(4)]==[1]*spec['count']+[0]*(4-spec['count'])
  assert wram[ram(0xc331)]!=wram[ram(0xc333)];started=f;phase='game';emit(f,'new-native-game-after-reboot',count=spec['count'])
 if started is None:return
 elapsed=f-started
 for pad,at in [(3,120)]+([(4,240)] if spec['count']==4 else []):
  if pad in movement_passed or elapsed<at:continue
  if pad in moved:
   obj,x,onset=moved[pad]
   if f>=onset+30:
    actual=sw(0xb04a+128*obj)
    if live and not rb(0xc2ea)&17 and abs(actual-x)>5:
     movement_passed.add(pad);emit(f,'extra-pad-movement',pad=pad,object=obj,delta=actual-x,attempt=attempts[pad])
    else:emit(f,'movement-window-interrupted-or-blocked',pad=pad,delta=actual-x,live=live,flags=rb(0xc2ea))
    del moved[pad]
  elif live and not rb(0xc2ea)&17:
   obj=rw(0xc320+(pad-1)*2);assert 0<=obj<12
   # A whistle or a naturally selected keeper is not a skater-movement window.
   if obj%6!=5:
    attempts[pad]=attempts.get(pad,0)+1;assert attempts[pad]<=12,('extra pad repeatedly failed live movement',pad,attempts[pad])
    x=sw(0xb04a+128*obj);moved[pad]=(obj,x,f);press(pad,'Left' if x>0 else 'Right',30)
  if elapsed>=5000:assert pad in movement_passed,('no successful extra-pad live movement window',pad)
 if phase=='game' and len(movement_passed)==spec['count']-2 and elapsed>=400 and live and rw(0xf738) in [0,0xa500] and not rb(0xc2ea)&17:
  if rw(0xb7aa)==5 or elapsed>=2400:
   press(spec['count'],'Start');pause_at=f;phase='pausing';emit(f,'pause-from-extra-pad',pad=spec['count'],owner=rw(0xb7aa))
 if phase=='pausing' and f>=pause_at+80:
  assert rb(0xc2ec)&0x81,('pause menu did not open',rw(0xc2ec));assert rw(0xf064)==0
  press(spec['count'],'B',2);phase='paused';paused_seen=True;emit(f,'paused-menu-buttons')
 if phase=='paused' and f>=pause_at+160:
  if rb(0xc2ec)&0x81:assert rw(0xf064)==0;press(spec['count'],'C')
  resumed_at=f;phase='resuming'
 if phase=='resuming' and f>=resumed_at+200:
  assert not rb(0xc2ec)&128 and rw(0xd046),('Resume Game stalled',rw(0xc2ec))
  assert h[2]==2 and b[0x1fff]==sum(b[:0x1ffe])%256
  assert paused_seen;emit(f,'resume-and-close-unsaved-game',header=h);done(f)
 if f>=12000:raise AssertionError(('reboot/pause timeout',phase,started,rw(0xc466),rw(0xc468)))
