#!/usr/bin/env python3
"""Native penalty ticker must survive crowd-HUD hide, then meters must return.
Reference suppresses ONLY the custom crowd draw entry in a private ROM copy.
Native queued minor penalty drives horizontal mode and ticker rendering.
"""
from pathlib import Path
import argparse,json,os,subprocess
from menu_protocol import refinements_enabled
from PIL import Image
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--state',type=Path,required=True);ap.add_argument('--baseline',type=Path,required=True);a=ap.parse_args();here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(parents=True,exist_ok=True)
code=bytearray(a.rom.read_bytes());code[0x2e001c:0x2e001e]=bytes.fromhex('4e75');(out/'reference.bin').write_bytes(code)
for name,rom in [('reference',out/'reference.bin'),('baseline',a.baseline),('fixed',a.rom)]:
 p=out/name;p.mkdir(exist_ok=True);(p/'commands.txt').write_text(f'100 load {a.state.resolve()}\n380 snap\n1800 snap\n2501 exit\n')
 (p/'hook.py').write_text('''from pathlib import Path
import json,struct
p=Path(outdir);horizontal=None;returned=None;redrawn=None
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def ww(a,v):v&=65535;wram[ram(a)]=v>>8;wram[ram(a+1)]=v&255
refined=romdata[0x258000:0x258002]==bytes.fromhex('6000')
def visible_bars():
 if not refined:return rw(0xe8fe)==0xb4a5
 if not rw(0xf8de)&2:return False
 n=core.retro_serialize_size();buf=C.create_string_buffer(n);assert core.retro_serialize(buf,n)
 if romdata[0x12c7c]==22:
  return (struct.unpack_from('<3H',buf.raw,74789+0xe000+24*128+33*2)==(0xa7e5,0xa7e6,0xa7e7)
      and struct.unpack_from('<3H',buf.raw,74789+0xe000+26*128+33*2)==(0xa7e8,0xa7e9,0xa7ea))
 return struct.unpack_from('<HH',buf.raw,74789+0xe000+25*128+6*2)==(0xa7e5,0xa7e6)
def test_tick(f):
 global horizontal,returned,redrawn
 if f==100 and refined:ww(0xf84a,0) # explicit enabled feature in the historical-state fixture
 if f==180:
  ww(0xc3a4,0x1680);ww(0xc3a6,0);ww(0xc3e6,0);ww(0xc468,144);wram[ram(0xc2ea)]|=5
 if f>180 and wram[ram(0xc2ec)]&128:
  if horizontal is None:horizontal=f
 if horizontal and f>horizontal+60 and not wram[ram(0xc2ec)]&128 and returned is None:returned=f
 if f==380:assert horizontal and wram[ram(0xc2ec)]&128,'penalty ticker did not appear'
 if returned and redrawn is None and visible_bars():redrawn=f
 if PROFILE=='fixed' and returned and f>=returned+60:assert redrawn and redrawn<=returned+60,('crowd bars not redrawn within60native frames of ticker return',returned,redrawn,hex(rw(0xe8fe)),hex(rw(0xf738)))
 if f==2500:
  row=dict(horizontal=horizontal,returned=returned,redrawn=redrawn,replay=hex(rw(0xf738)),shown=hex(rw(0xe8fe)),compact_shown=rw(0xf8de),view=hex(rw(0xc2ec)))
  assert returned,('penalty scene did not return',row)
  if PROFILE=='fixed':assert redrawn and redrawn<=returned+60,('crowd bars not redrawn after penalty return',row)
  (p/'result.json').write_text(json.dumps(row,indent=2)+'\\n')
'''.replace('PROFILE',repr(name)))
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env.update(GPGX_DEV='513',GPGX_HOOK=str(p/'hook.py'))
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(here/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
ref=Image.open(out/'reference/0000.png').convert('RGB');reports=[]
for name in ['baseline','fixed']:
 im=Image.open(out/name/'0000.png').convert('RGB');assert im.size==ref.size
 regions=[(216,top,248,top+8)for top in [192,208]]
 if refinements_enabled(a.rom):regions.append((48,200,64,208))
 if a.rom.read_bytes()[0x12c7c]==22:regions += [(264,top,288,top+8)for top in[192,208]]
 # Native ticker switches to H32/256px. New score bars at264..287 are then
 # outside the visible raster; their foreign-map guard is tested in the full
 # VRAM contract, while the real H40 return is checked by visible_bars above.
 visible_regions=[(left,top,min(right,im.width),min(bottom,im.height))for left,top,right,bottom in regions if left<im.width and top<im.height]
 mismatches=sum(im.getpixel((x,y))!=ref.getpixel((x,y))for left,top,right,bottom in visible_regions for y in range(top,bottom)for x in range(left,right))
 reports.append(dict(profile=name,damaged_pixels=mismatches,visible_pixel_regions=visible_regions,**json.loads((out/name/'result.json').read_text())))
assert reports[0]['damaged_pixels']>0,('old ticker bug not reproduced',reports)
assert reports[1]['damaged_pixels']==0,('native ticker still damaged',reports)
(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print('PASS native penalty ticker pixels match untouched reference; old damage',reports[0]['damaged_pixels'],'pixels; crowd bars return')
