"""Native winding-shot colour regression, sharing only the captured-shot setup."""
from pathlib import Path
exec(compile((Path(__file__).resolve().parent/'shot_panel_hook.py').read_text(),str(Path(__file__).resolve().parent/'shot_panel_hook.py'),'exec'),globals())
# test_core executes hooks in its globals, so its __file__ is test_core.py.
base_tick=test_tick
colour_spec=json.loads(os.environ['SHOT_COLOUR_CASE']);stages={};observations=[];pending_shots={};last_rendered=None;pending_render=None;deferred_render=[];red_seen=False;red_frames=0;red_phases=set()
# Serialized CPU context is the core1.7.6 ABI, calibrated with native D-register
# sentinels and a known loop PC. Serialization observes without advancing it.
cellcmd=romdata.find(bytes.fromhex('48e7c0007012d043ef48'),0x2ed000,0x2ed800)
assert cellcmd>=0,'native HUD cellcmd not found'
cellcmd_end=romdata.index(bytes.fromhex('4e75'),cellcmd,cellcmd+96)+2
def native_pc():
 n=core.retro_serialize_size();saved=C.create_string_buffer(n);assert core.retro_serialize(saved,n)
 assert saved.raw.startswith(b'GENPLUS-GX 1.7.6'),'unverified CPU context format'
 return struct.unpack_from('<I',saved.raw,144468)[0]
def red_pixels_visible():
 raw,width,height,pitch=frame[0]
 if pixfmt[0]==1:
  return any(raw[y*pitch+4*x+2]>200 and raw[y*pitch+4*x+1]<40 and raw[y*pitch+4*x]<40 for y in range(202 if left_panel else 154,206 if left_panel else 158) for x in range(9 if left_panel else 273,39 if left_panel else 303))
 return any(((v>>11)<<3)>200 and (((v>>5)&63)<<2)<40 and ((v&31)<<3)<40 for y in range(202 if left_panel else 154,206 if left_panel else 158) for x in range(9 if left_panel else 273,39 if left_panel else 303) for v in [struct.unpack_from('<H',raw,y*pitch+2*x)[0]])

def test_tick(f):
 global last_rendered,pending_render,red_seen,red_frames
 base_tick(f)
 if f in pending_shots:
  name=pending_shots.pop(f);snap(name)
  im=Image.open(p/(name+'.png'));pixels=[im.getpixel((x,y)) for y in range(202 if left_panel else 154,206 if left_panel else 158) for x in range(9 if left_panel else 273,39 if left_panel else 303)]
  good=lambda c: min(c)>200 if name=='white' else c[0]>170 and 70<c[1]<200 and c[2]<40 if name=='orange' else c[0]>200 and c[1]<40 and c[2]<40
  assert any(good(c) for c in pixels),('native screen missing charge colour',colour_spec,name,set(pixels))
  if name=='red':red_seen=True
 if f==100:
  ww(0xf07a,colour_spec['difficulty'])
  if left_panel:
   for addr in [0xef80,0xef82,0xef84,0xef86]:ww(addr,0)
 # Deliberately exercise both former blink phases during the native windup.
 if 145<=f<=205:ww(0xf64a,4 if f%2 else 0)
 if f<152 or not rw(0xf64c):return
 if pending_render is not None:
  assert f==pending_render['frame']+1 and (rw(0xe8f4)&255)>=pending_render['fill'],('native HUD update did not commit next frame',colour_spec,f,pending_render,rw(0xe8f4))
  pending_render=None
 if rw(0xf648) and not rw(0xf646):
  if left_panel and (not rw(0xef84) or rw(0xef86)):return
  charge=rw(0xef82) if left_panel else rw(0xf644);fill=max(0,min(30,(charge-15)*30//16));key=rw(0xef80) if left_panel else rw(0xe8f4);colour=key>>8;actual_fill=key&255
  logical_charge=charge
  if actual_fill!=fill:
   pc=native_pc()
   assert cellcmd<=pc<cellcmd_end and last_rendered is not None and last_rendered['frame']==f-1 and key==last_rendered['key'] and charge>=last_rendered['charge'],('charge bar blanked/blinked outside a pending native render',colour_spec,f,charge,key,fill,hex(pc),last_rendered)
   pending_render=dict(frame=f,charge=charge,fill=fill,pc=hex(pc));deferred_render.append(pending_render.copy())
   charge=last_rendered['charge'];fill=last_rendered['fill']
  assert actual_fill==fill,('charge bar blanked/blinked',colour_spec,f,charge,key,fill)
  wanted=1 if fill<10 else 5 if fill<30 else 10
  assert colour==wanted,('wrong shot colour',colour_spec,f,charge,colour,wanted)
  buf=bytes(rb((0xef00 if left_panel else 0xea00)+i) for i in range(128));nibbles=[x for b in buf for x in (b>>4,b&15)]
  assert set(nibbles)<={wanted,4 if wanted==1 else 7},('unexpected composed pixels',colour_spec,f,set(nibbles))
  if fill:
   assert nibbles.count(wanted)==4*fill,('fill lost',colour_spec,f,fill,nibbles.count(wanted))
   name={1:'white',5:'orange',10:'red'}[wanted]
   if name not in stages:pending_shots[f+2]=name;stages[name]=dict(frame=f,charge=charge,fill=fill)
  last_rendered=dict(frame=f,charge=charge,fill=fill,key=key)
  observations.append(dict(frame=f,charge=charge,logical_charge=logical_charge,fill=fill,colour=colour))
  if red_seen and logical_charge==31:
   assert red_pixels_visible(),('full charge visibly blanked',colour_spec,f,hex(key))
   red_frames+=1;red_phases.add(4 if f%2 else 0)
 if released is not None and f>=released+8:
  # The serialized core stores native 3-bit RGB values. Palette 1 index 5
  # is the game's existing orange; no CRAM entry is changed by this feature.
  v=vram();size=core.retro_serialize_size();buf=C.create_string_buffer(size);assert core.retro_serialize(buf,size)
  cram=struct.unpack_from('<64H',buf.raw,offset+65536)
  assert cram[1]==0x1ff and cram[16+5]==0x26 and cram[16+10]==7,('native colour palette changed',colour_spec,cram[:32])
  assert set(stages)=={'white','orange','red'},('missing stages',colour_spec,stages)
  assert pending_render is None and red_frames>=4 and red_phases=={0,4},('incomplete full-red visibility/commit coverage',colour_spec,pending_render,red_frames,red_phases)
  report=dict(case=colour_spec,stages=stages,samples=len(observations),observations=observations,native_palette_unchanged=True,bar_observation=('post-native-render complete scratch packet' if left_panel else 'frontend scratch state'),deferred_native_render=deferred_render,full_red_visible_frames=red_frames,full_red_blink_phases=sorted(red_phases))
  (p/'colour-result.json').write_text(json.dumps(report,indent=2)+'\n');sys.exit(0)
