#!/usr/bin/env python3
"""Native main-menu club text matches logos after a finished season.

Completed Full/Light/Mini slots retain their saved club after champion,
elimination or missing the playoffs. Pending new slots exercise all32 choices
and wrap/reverse. Active away and co-op seasons must show the user's club.
Private ROM probes observe the club passed to the native text renderer.
"""
from pathlib import Path
import argparse,json,os,shutil,struct,subprocess
from concurrent.futures import ThreadPoolExecutor
ap=argparse.ArgumentParser(description=__doc__)
ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path)
ap.add_argument('--seed',type=Path,required=True)
a=ap.parse_args();out=a.output.resolve();out.mkdir(parents=True,exist_ok=True)
here=Path(__file__).resolve().parent;b=bytearray(a.rom.read_bytes())
assert b[0xf7c72:0xf7c74]==bytes.fromhex('4eb9')
target=int.from_bytes(b[0xf7c74:0xf7c78],'big')
(out/'probe.s').write_text(f'''    .text
    .globl _start
_start:
    cmpi.w #4,%d7
    bne.s native
    jsr 0x{target:X}
    move.w %sr,-(%sp)
    move.w %d0,-(%sp)
    move.w (%a1,%d7.w),%d0
    move.w %d0,0xFFF0C6
    addq.w #1,0xFFF0C8
    move.w (%sp)+,%d0
    move.w (%sp)+,%sr
    rts
native:
    jmp 0x{target:X}
''')
subprocess.run([str(here.parent/'m68kasm.sh'),str(out/'probe.s'),'2FEA00',str(out/'probe-code.bin')],check=True,stdout=subprocess.DEVNULL)
code=(out/'probe-code.bin').read_bytes();assert set(b[0x2fea00:0x2fea00+len(code)])=={255}
b[0x2fea00:0x2fea00+len(code)]=code;b[0xf7c74:0xf7c78]=struct.pack('>I',0x2fea00)
(out/'probe.bin').write_bytes(b)
cases=[]
for slot,length in [(1,84),(2,31),(3,7)]:
 for flag in [3,4,5]:cases.append(dict(name=f'finished-{slot}-{flag}',slot=slot,length=length,flag=flag,games=length,coop=False,free=False))
 for games,coop in [(1,False),(1,True),(0,False)]:
  cases.append(dict(name=f'running-{slot}-{games}-{coop}',slot=slot,length=length,flag=0,games=games,coop=coop,free=games==0))
hook=out/'hook.py';hook.write_text('''from pathlib import Path
import json
p=Path(outdir);spec=json.loads((p/'spec.json').read_text());checks={r['frame']:r for r in spec['checks']};results=[];first_home=[None]
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def test_tick(f):
 if f not in checks:return
 c=checks[f];displayed=rw(0xf0c6);home=rw(0xd04c);user=rw(0xe3f4)
 if first_home[0] is None:first_home[0]=home
 expected=(first_home[0]+c['club'])%32 if spec['free'] else c['club']
 assert rw(0xf0c8)>0,('native team renderer not reached',spec['name'])
 assert displayed==expected,('wrong displayed club',spec['name'],c,dict(displayed=displayed,expected=expected,home=home,user=user,forced=rw(0xe3f0)))
 assert home==expected if spec['free'] or spec['coop'] or spec['flag']>=3 else home!=expected,('wrong actual home selection',spec['name'],c,home)
 logical=bytes(sram)[1:65536:2]
 assert logical[0x1000:0x1010]==bytes.fromhex(spec['header']),('viewer changed season ownership/progress',spec['name'])
 results.append(dict(**c,displayed=displayed,home=home,user=user))
 if f==max(checks):(p/'results.json').write_text(json.dumps(results,indent=2)+'\\n')
''')
def run(spec):
 p=out/spec['name'];p.mkdir(exist_ok=True);raw=bytearray(a.seed.read_bytes());logical=bytearray(raw[1::2]);assert len(logical)==32768
 struct.pack_into('>4s6H',logical,0x1000,b'SESN',spec['games'],9,spec['games'],spec['length'],spec['slot'] if not spec['games'] else 0,3)
 struct.pack_into('>H',logical,0x1210,spec['flag']);logical[0x1ea0:0x1ea4]=b'S3SV';struct.pack_into('>4H',logical,0x1ea4,1,spec['slot'],7,1)
 struct.pack_into('>4H',logical,0x1eae,0,3 if spec['coop'] else 1,8,6);struct.pack_into('>H',logical,0x1eda,0)
 logical[0x1fff]=sum(logical[:0x1ffe])&255;logical[0x1ffe]=logical[0x1fff]^255;raw[1::2]=logical;(p/'battery.srm').write_bytes(raw)
 spec=dict(spec,header=logical[0x1000:0x1010].hex());club=0 if spec['free'] else 8;checks=[dict(frame=1300,club=club)];cmd=['1400 press 1 Down 5','1500 press 1 Down 5'];frame=1600
 if spec['coop']:cmd=['1000 press 1 Down 5','1100 press 1 Right 5','1200 press 1 Up 5']+cmd
 if spec['free']:
  for direction,step in [('Right',1),('Left',-1)]:
   for i in range(32):
    cmd.append(f'{frame} press 1 {direction} 5');club=(club+step)%32;checks.append(dict(frame=frame+60,club=club));frame+=90
 else:
  for direction in ['Right','Left']:
   cmd.append(f'{frame} press 1 {direction} 5');checks.append(dict(frame=frame+60,club=8));frame+=90
 cmd += [f'{frame} snap',f'{frame+1} exit'];spec['checks']=checks;(p/'spec.json').write_text(json.dumps(spec,indent=2)+'\n');(p/'commands.txt').write_text('\n'.join(cmd)+'\n')
 env={k:v for k,v in os.environ.items() if k not in ['GPGX_HOOK','GPGX_SRM','GPGX_WAV']};env.update(GPGX_DEV='513',GPGX_HOOK=str(hook),GPGX_SRM=str(p/'battery.srm'))
 with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(here/'test_core.py'),str(out/'probe.bin'),str(p/'commands.txt'),str(p)],env=env,check=True,stdout=log,stderr=log,timeout=30)
 result=json.loads((p/'results.json').read_text());assert len(result)==len(checks)
 return dict(name=spec['name'],checks=len(result))
with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,cases))
(out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
print('PASS',len(results),'native menu cases,',sum(x['checks'] for x in results),'displayed/actual club checks')
