"""Native postgame window: pending shot timeout cannot erase Stars; meters retire."""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,struct,subprocess,os,hashlib
from PIL import Image,ImageChops
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);ap.add_argument('--state',type=Path,required=True);ap.add_argument('--current-state',type=Path);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(exist_ok=False,parents=True)
if a.rom.read_bytes()[0x12c7c]==22:
 from stars_hud_current import verify
 verify(a,h,o)
 raise SystemExit(0)
state=a.state.read_bytes();assert state.startswith(b'GENPLUS-GX 1.7.6');assert struct.unpack_from('<I',state,144468)[0]==0xa97e,'Use immutable natural pre-Stars capture, not an arbitrary mid-module state'
# Calibrate serialized VRAM from both actual existing four-cell meter sequences.
offsets=[]
for pal in (0x8000,0xa000,0xc000,0xe000):
 pattern=struct.pack('<4H',*[pal+0x7e5+i for i in range(4)]);pos=state.find(pattern)
 while pos>=0:
  v=pos-(0xe000+(24*64+27)*2)
  if v>=0 and v+65536<=len(state):
   words=struct.unpack_from('<4H',state,v+0xe000+(26*64+27)*2)
   if [x&0x7ff for x in words]==list(range(0x7e9,0x7ed)):offsets.append(v)
  pos=state.find(pattern,pos+1)
assert len(set(offsets))==1,('VRAM calibration ambiguous',offsets);vo=offsets[0]
(o/'hook.py').write_text('''from pathlib import Path
import json,struct
p=Path(outdir);spec=json.loads((p/'spec.json').read_text())
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def ww(a,v):wram[ram(a)]=v>>8;wram[ram(a+1)]=v&255
def serialize():
 n=core.retro_serialize_size();b=C.create_string_buffer(n);assert core.retro_serialize(b,n);return b.raw

def image():
 raw,width,height,pitch=frame[0]
 if pixfmt[0]==1:return Image.frombuffer('RGBA',(width,height),raw,'raw','BGRA',pitch,1).convert('RGB')
 out=bytearray()
 for y in range(height):
  for (v,) in struct.iter_unpack('<H',raw[y*pitch:y*pitch+width*2]):out+=bytes(((v>>11)<<3,((v>>5)&63)<<2,(v&31)<<3))
 return Image.frombytes('RGB',(width,height),bytes(out))
def test_tick(f):
 if f==100:
  assert not rw(0xc2ea)&0x8000,'Stars already began before the test'
  ww(0xf640,0x534d);ww(0xf646,spec['pending']);ww(0xf64c,int(spec['pending']>0));ww(0xf64e,0)
 if f in (150,400):image().save(p/('frame'+str(f)+'.png'))
 if f==400:
  raw=serialize();v=raw[spec['vram_offset']:spec['vram_offset']+65536];meters=[list(struct.unpack_from('<4H',v,0xe000+(row*64+27)*2)) for row in (24,26)]
  assert rw(0xc2ea)&0x8000,'Original Native Stars did not execute'
  (p/'result.json').write_text(json.dumps(dict(stars=True,score=[rw(0xc6da),rw(0xca3e)],period=rw(0xc466),clock=rw(0xc468),show=rw(0xf646),panel=rw(0xf64c),bars=rw(0xe8fe),meter_cells=meters),indent=2)+'\\n')
''')
def run(task):
 profile,image,pending=task;p=o/f'{profile}-{pending}';p.mkdir();(p/'spec.json').write_text(json.dumps(dict(pending=pending,vram_offset=vo)));(p/'commands.txt').write_text(f'100 load {a.state.resolve()}\n401 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='513',GPGX_HOOK=str(o/'hook.py'))
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(image.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 return profile,pending,json.loads((p/'result.json').read_text())
tasks=[('baseline',a.baseline,t) for t in (0,1,2,8,30,120,240)]+[('candidate',a.rom,t) for t in (0,1,2,8,30,120,240)]
with ThreadPoolExecutor(max_workers=4) as pool:results=list(pool.map(run,tasks))
reference=Image.open(o/'baseline-0/frame400.png').convert('RGB').crop((43,116,261,181));reports=[];reproduced=0
for profile,pending,result in results:
 im=Image.open(o/f'{profile}-{pending}/frame400.png').convert('RGB');panel=im.crop((43,116,261,181));exact=panel.tobytes()==reference.tobytes()
 if profile=='candidate':
  assert exact,('Native Stars panel changed/damaged',pending,ImageChops.difference(panel,reference).getbbox())
  assert result['show']==result['panel']==result['bars']==0,(pending,result)
  assert all(x==0x7ff for row in result['meter_cells'] for x in row),(pending,'meters remain',result)
 else:
  if not exact:reproduced+=1
 assert result['score']==[3,0] and result['period']==2 and result['clock']==0,(profile,pending,'gameplay changed',result)
 reports.append(dict(profile=profile,pending_timer=pending,native_panel_exact=exact,**result))
assert reproduced>=3,('Baseline did not reproduce reported timeout/window corruption',reproduced)
(o/'results.json').write_text(json.dumps(dict(rom_sha256=hashlib.sha256(a.rom.read_bytes()).hexdigest(),baseline_sha256=hashlib.sha256(a.baseline.read_bytes()).hexdigest(),state_sha256=hashlib.sha256(state).hexdigest(),scope='Immutable natural completed-game context; only pending HUD flags initialized, no score/clock/actor/physics writes; original Native Stars draws names and border',baseline_corruption_cases=reproduced,cases=reports),indent=2)+'\n');print('PASS',len(reports),'Native Stars contexts: all7candidate panels pixel-exact, meters absent, original score/clock; baseline corruption reproduced',reproduced)
