#!/usr/bin/env python3
"""Native global Crit Shots persistence, strict Off and native release regressions."""
from pathlib import Path
import argparse,json,os,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('--baseline',type=Path,required=True);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,replay=0):
 b=bytes(sram)[1:65536:2]
 assert rw(0xf0ec)==wanted,('crit preference',f,rw(0xf0ec),wanted)
 assert rw(0xf0de)==replay,('replay preference changed',f,rw(0xf0de),replay)
 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[0x2e1c:0x2e20].hex()))
 (p/'result.json').write_text(json.dumps(dict(observations=rows),indent=2)+'\\n')
'''

def make_seed(GP,OPTION):
 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',GP,11,GP,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)
 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
 logical[0x2e18:0x2e1c]=b'RP\x00\xff'  # explicit saved On; unrelated Replay edit remains checked
 logical[0x2e1c:0x2e20]=bytes([0x43,0x54,OPTION,OPTION^255])
 raw=pack(logical)
 return raw

candidate=a.rom
for profile,image in [('baseline',a.baseline),('candidate',candidate)]:
 a.rom=image
 for slot in (1,2,3):
  for option in (0,1):
   raw=make_seed(1,option);lines=[f'{1000+220*i} press 1 Right 5' for i in range(slot-1)];t=1100+220*(slot-1)
   lines += [f'{t+80*i} press 1 Down 5' for i in range(11)]
   press=t+1000;direction='Right' if option==0 else 'Left';lines += [f'{press} press 1 {direction} 5',f'{press+150} snap',f'{press+200} exit']
   wanted=option if profile=='candidate' else 1-option
   hook=common+f"def test_tick(f):\n if f=={press+130}:\n  assert rw(0xe3f0)==1 and rw(0xf06c)==1,('not a played season',f)\n  observe(f,{wanted},{slot})\n"
   result=run(f'{profile}-played-s{slot}-value{option}',raw,lines,hook)
   stored=result[1::2][0x2e1c:0x2e20];assert stored==bytes([0x43,0x54,wanted,wanted^255]),(profile,slot,stored,wanted)
   if profile=='candidate':
    run(f'cold-played-s{slot}-value{option}',result,['1100 snap','1101 exit'],common+f"def test_tick(f):\n if f==1000:observe(f,{option},{slot})\n")
# Before a first match, both On and Off remain editable. Existing global native
# mode persistence and physical shot behavior retain their separate43-case suite.
a.rom=candidate
for option in (0,1):
 raw=make_seed(0,option);lines=[f'{1100+80*i} press 1 Down 5' for i in range(11)]+[f'2100 press 1 {"Right" if option==0 else "Left"} 5','2301 exit']
 run(f'new-season-edit-{option}',raw,lines,common+f"def test_tick(f):\n if f==2250:\n  assert rw(0xe3e8)==0,('already played',rw(0xe3e8))\n  observe(f,{1-option},1)\n")
# Auto Replays stays editable even in a played season; all game settings retained.
raw=make_seed(1,0)
lines=[f'{1100+80*i} press 1 Down 5' for i in range(10)]+['2100 press 1 Right 5','2301 exit']
run('played-replay-still-editable',raw,lines,common+"def test_tick(f):\n if f==2250:observe(f,0,1,replay=1)\n")
(out/'results.json').write_text(json.dumps(dict(candidate=str(candidate),baseline=str(a.baseline),cases=reports),indent=2)+'\n')
print('PASS',len(reports),'real controller Crit lock/new-season edit/cold-boot/slot and unrelated Replay behavior cases; oldROM reproduces bug')
