#!/usr/bin/env python3
"""Single fixed green palette change, rendered on exact uninstrumented ROMs.
Explicit four-star control/geometry fixture, not a human-input routing trial.
Native CRAM, SAT and actual libretro pictures provide graphics evidence.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,struct,hashlib,subprocess,os
from PIL import Image
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('--seed',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);new=a.rom.read_bytes();old=a.baseline.read_bytes();sha=lambda b:hashlib.sha256(b).hexdigest();assert len(old)==len(new)==3145728,'matched full-size pre-neon control required';changes=[i for i,(x,y)in enumerate(zip(new,old))if x!=y];assert changes and set(changes)<={0x18e,0x18f,0x5995a,0x5995b};assert old[0x5995a:0x5995c]==bytes.fromhex('0020')and new[0x5995a:0x5995c]==bytes.fromhex('00e0')
def pack(b):
 for off in range(0,len(b),8192):b[off+0x1fff]=sum(b[off:off+0x1ffe])&255;b[off+0x1ffe]=b[off+0x1fff]^255
 raw=bytearray(2*len(b));raw[1::2]=b;return raw
def battery():
 base=bytearray(a.seed.read_bytes()[1:16384:2]);base[0xe20:0x1ffe]=bytes(0x11de);banks=[]
 for slot,length in[(1,84),(2,31),(3,7)]:
  b=bytearray(base);struct.pack_into('>4s6H',b,0x1000,b'SESN',0,0,0,length,slot,3);struct.pack_into('>4s4H',b,0x1ea0,b'S3SV',1,slot,7,1);b[0x1ef0:0x1ef6]=bytes.fromhex('504304fb00ff');b[0x1ff6:0x1ffe]=bytes.fromhex('5500000202005a00');b[0x1eee:0x1ef0]=bytes.fromhex('50af');banks.append(b)
 b=bytearray(banks[0])+b''.join(banks);b[0x2e18:0x2e20]=bytes.fromhex('525001fe435401fe');b[0x4d20:0x4d22]=bytes.fromhex('50af');return pack(b)
def render(z):
 profile,team,end=z;rom=a.rom if profile=='current'else a.baseline;p=o/f'{profile}-{team}-{end}';p.mkdir(exist_ok=True);(p/'spec.json').write_text(json.dumps(dict(team=team,end=end,owner=2)));(p/'battery.srm').write_bytes(battery());(p/'commands.txt').write_text('1500 press 1 Start 5\n1900 press 1 Start 5\n5001 exit\n');env={k:v for k,v in os.environ.items()if not k.startswith('GPGX_')};env.update(GPGX_DEV='1281',GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/neon_raw_colors_hook.py'))
 with(p/'log').open('w')as log:subprocess.run(['python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 d=json.loads((p/'render.json').read_text());assert d['cram'][27]==(56 if profile=='current'else 8),'P3 native green word not uploaded';assert d['cram'][25]==192 and d['cram'][26]==7 and d['cram'][29]==3 and d['cram'][12]==110,'P1/P2/P4/goal colour changed';return(profile,team,end,p,d)
with ThreadPoolExecutor(max_workers=3)as pool:rows=list(pool.map(render,[(profile,team,end)for profile in['current','before']for team in[1,2]for end in[-1,1]]))
audits=[]
for _,team,end,p,d in[r for r in rows if r[0]=='current']:
 _,_,_,q,before=next(r for r in rows if r[0]=='before'and r[1:3]==(team,end));assert all(x==y for i,(x,y)in enumerate(zip(d['cram'],before['cram']))if i!=27),'unrelated CRAM entry changed'
 # Frontend stops can fall inside different native sprite-list construction
 # instructions. Partial work-list/SAT length is not a committed image oracle.
 # The separate256-DMA/48-render suite checks native sprite geometry/palettes.
 im=Image.open(p/'0000.png').convert('RGB');bi=Image.open(q/'0000.png').convert('RGB');assert im.size==bi.size;changed=0
 for x in range(im.width):
  for y in range(im.height):
   n,b=im.getpixel((x,y)),bi.getpixel((x,y))
   if n==b:continue
   if n[0]==b[0]==n[2]==b[2]==0 and n[1]>b[1]:changed+=1
 assert sum(1 for c in im.getdata()if c[0]==c[2]==0 and c[1]>200)>50,'neon was not actually visible'
 assert sum(1 for c in bi.getdata()if c[0]==c[2]==0 and c[1]>200)<50,'pre-neon control unexpectedly already bright'
 audits.append(dict(team=team,end=end,current_ROM_sha256=sha(new),reference_ROM_sha256=sha(old),current_image=str((p/'0000.png').relative_to(o)),reference_image=str((q/'0000.png').relative_to(o)),current_image_sha256=sha((p/'0000.png').read_bytes()),reference_image_sha256=sha((q/'0000.png').read_bytes()),changed_green_pixels=changed,fixed_CRAM_other_entries_exact=True,partial_native_worklists_not_compared=True,cold_boot_frames_not_claimed_geometry_identical=True))
report=dict(rom_sha256=sha(new),reference_sha256=sha(old),changed_addresses=[hex(i)for i in changes],P3_RGB3=[0,7,0],P1_P2_P4_goal_exact=True,no_runtime_hook=True,shared_fixed_palette='Some native crowd/background green pixels share this entry and become brighter too; other colours and team palettes stay unchanged.',actual_uninstrumented_ROM_screenshots=audits)
(o/'results.json').write_text(json.dumps(report,indent=2)+'\n');print('PASS neonP3: onepaletteword/checksum only;8 native uninstrumented-ROM pictures, all other CRAM entries exact; separate native geometry suite required; shared background green disclosed')
