#!/usr/bin/env python3
"""Native 1..4-player selection, per-season saves and cold-boot playoff starts.

Requires a private valid 64KB three-playoff fixture from verify_season_slots.py.
Uses actual controller input to select counts. Every match starts in a new core
process from the written battery, without loading a state or forcing side words.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,os,struct,subprocess
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);ap.add_argument('--playoffs',type=Path,required=True)
ap.add_argument('--roundtrip-only',action='store_true',help='only the mixed-count slot/native-mode/reboot regression')
a=ap.parse_args();out=a.output.resolve();out.mkdir(parents=True,exist_ok=True);here=Path(__file__).resolve().parent
hook=out/'hook.py';hook.write_text('''from pathlib import Path
import json,struct,sys,hashlib
p=Path(outdir);spec=json.loads(os.environ['PLAYER_CASE']);seen=[];started=None;brackets={}
def rw(a):return (wram[ram(a)]<<8)|wram[ram(a+1)]
def rb(a):return wram[ram(a)]
def finish(f):
 (p/'battery.srm').write_bytes(bytes(sram)[:65536]);(p/'result.json').write_text(json.dumps(dict(frame=f,checks=seen),indent=2)+'\\n');sys.exit(0)
def test_tick(f):
 global started
 b=bytes(sram)[1:65536:2]
 if f in spec['checks']:
  expected=spec['checks'][f];slot,count=expected if isinstance(expected,list) else (spec['slot'],expected)
  assert rw(0xf0d0)==count,('wrong preferred count',spec,f,rw(0xf0d0))
  assert rw(0xf06a)==slot and rw(0xf06c)==1 and rw(0xd046)==int(spec['device']==1281)
  effective=min(count,4 if spec['device']==1281 else 2)
  assert rw(0xf07c)==(effective if effective>=3 else 0)
  assert b[0x1fff]==sum(b[:0x1ffe])%256 and b[0x1ffe]==(b[0x1fff]^255)
  assert b[0x1ef0:0x1ef4]==b'PC'+bytes([count,count^255])
  if spec['playoff']:assert rw(0xe3f0)==3 and rw(0xd048)==1
  else:assert rw(0xd048)==0
  # Only preferences/checksums may change while navigating: all season data stays.
  original=(p/'seed.srm').read_bytes()[1::2]
  o=slot*8192 if spec.get('roundtrip') else 0
  assert b[0xe20:0x1ea0]==original[o+0xe20:o+0x1ea0],('active season data changed on player selection',f)
  assert all(b[o+0xe20:o+0x1ea0]==original[o+0xe20:o+0x1ea0] for o in [8192,16384,24576])
  if spec['playoff']:
   bracket=bytes(wram[ram(a)] for a in list(range(0xce66,0xcef0))+list(range(0xcef4,0xcf14)))
   if slot in brackets:assert bracket==brackets[slot],('playoff bracket/results changed with player count',f,slot)
   brackets[slot]=bracket
  seen.append(dict(frame=f,count=count,effective=effective,players=rw(0xd04a)))
  if spec['stage'] in ['choose','roundtrip'] and f==max(spec['checks']):finish(f)
 if spec['stage']=='game':
  live=rw(0xc2e8)>8 and rw(0xc468)>0 and rw(0xc468)<300 and rw(0xc466)<=3 and not rb(0xc2ec)&128 and not rb(0xc2f2)&4
  if started is None and f>2000 and live:
   expected=min(spec['count'],4 if spec['device']==1281 else 2);side=rw(0xc328)
   sides=[rw(0xc328+2*i) for i in range(4)]
   assert side in [1,2] and sides==[side]*expected+[0]*(4-expected),('native match started with wrong pads',spec,sides)
   user=int.from_bytes(b[0x1006:0x1008],'big')-1
   if spec['playoff']:assert rb(0xc331 if side==1 else 0xc333)==user,('controls assigned to other club',spec,user)
   started=f;held.clear();seen.append(dict(frame=f,native_match=True,sides=sides,clubs=[rb(0xc331),rb(0xc333)]))
  if started and f>=started+120:finish(f)
 if f>=10000:raise AssertionError(('no native match',spec,rw(0xc466),rw(0xc468),rw(0xc2ec)))
spec['checks']={int(k):v for k,v in spec['checks'].items()}
''')
def put(p,b):
    for o in range(0,len(b),8192):b[o+0x1fff]=sum(b[o:o+0x1ffe])%256;b[o+0x1ffe]=b[o+0x1fff]^255
    raw=bytearray(len(b)*2);raw[1::2]=b;p.write_bytes(raw)
def run_core(p,spec,lines):
    p.mkdir(exist_ok=True);(p/'commands.txt').write_text('\n'.join(lines)+'\n');(p/'seed.srm').write_bytes((p/'battery.srm').read_bytes())
    env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')}
    env.update(GPGX_DEV=str(spec['device']),GPGX_DEV2=str(spec['device']),GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(hook),PLAYER_CASE=json.dumps(spec))
    with (p/'log.txt').open('w') as log:subprocess.run(['python3',str(here/'test_core.py'),str(a.rom.resolve()),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=30)
    return json.loads((p/'result.json').read_text())
def run(case):
    playoff,slot,count=case;length={1:84,2:31,3:7}[slot];name=f"{'playoff' if playoff else 'league'}-{slot}-pads{count}";p=out/name;p.mkdir(exist_ok=True)
    if playoff:
        b=bytearray(a.playoffs.read_bytes()[1::2]);assert len(b)==32768
        # Make the requested slot active; the normal slot manager resumes its bank.
        b[:8192]=b[slot*8192:(slot+1)*8192];struct.pack_into('>HHH',b,0x1ea6,slot,7,1)
    else:
        b=bytearray(a.seed.read_bytes()[1:16384:2]);b[0xe20:0x1ef6]=bytes(0x10d6)
        struct.pack_into('>4s6H',b,0x1000,b'SESN',0,0,0,length,slot,3)
        b[0x1f08:0x1f12]=bytes(10);b+=bytearray(24576)
    for o in range(0,len(b),8192):b[o+0x1ef0:o+0x1ef4]=b'PC\x01\xfe'
    put(p/'battery.srm',b)
    checks={1300:1};lines=['1400 press 1 Down 5'];t=1500;current=1
    # Full forward and reverse circles prove One<->Four wrap and distinct Three/Four labels.
    for direction in ['Right']*4+['Left']*4+['Right']*(count-1):
        lines.append(f'{t} press 1 {direction} 5');current=1+(current-1+(1 if direction=='Right' else -1))%4;checks[t+100]=current;t+=170
    checks[t+100]=count;lines +=[f'{t+100} snap',f'{t+101} exit']
    spec=dict(playoff=playoff,slot=slot,count=count,device=1281,stage='choose',checks=checks)
    chosen=run_core(p,spec,lines)
    reports=[];previous=p/'battery.srm'
    for device in ([1281,513,1281] if count>=3 else [1281]):
        q=out/(name+'-boot'+str(len(reports)));q.mkdir(exist_ok=True);(q/'battery.srm').write_bytes(previous.read_bytes())
        spec=dict(playoff=playoff,slot=slot,count=count,device=device,stage='game',checks={1300:count})
        lines=['1300 snap']+[f'{1600+i*600} press 1 Start 5' for i in range(12)]+['9900 snap','10001 exit']
        reports.append(run_core(q,spec,lines))
        previous=q/'battery.srm'
    return dict(case=name,selection_checks=len(chosen['checks']),cold_starts=len(reports))
results=[]
if not a.roundtrip_only:
    with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(run,[(po,slot,count) for po in [False,True] for slot in [1,2,3] for count in [1,2,3,4]]))
    (out/'results.json').write_text(json.dumps(results,indent=2)+'\n')
p=out/'mixed-count-roundtrip';p.mkdir(exist_ok=True);b=bytearray(a.playoffs.read_bytes()[1::2])
for o in range(0,len(b),8192):b[o+0x1ef0:o+0x1ef4]=b'PC\x01\xfe'
put(p/'battery.srm',b)
inputs=[(1400,'Down'),(1500,'Right'),(1700,'Right'),(1900,'Right'),(2100,'Up'),(2300,'Right'),(2500,'Down'),(2600,'Right'),(2800,'Up'),(3000,'Right'),(3200,'Down'),(3300,'Right'),(3500,'Right'),(3700,'Up'),(3900,'Left'),(4100,'Left'),(4300,'Right'),(4500,'Right'),(4700,'Right'),(4900,'Right'),(5100,'Right'),(5300,'Right')]
checks={1300:[1,1],2000:[1,4],2400:[2,1],2700:[2,2],3100:[3,1],3600:[3,3],4000:[2,2],4200:[1,4],4400:[2,2],4600:[3,3],5400:[1,4]}
spec=dict(stage='roundtrip',roundtrip=True,playoff=True,slot=1,count=4,device=1281,checks=checks)
first=run_core(p,spec,[f'{t} press 1 {key} 5' for t,key in inputs]+['5401 exit'])
q=out/'mixed-count-reboot';q.mkdir(exist_ok=True);(q/'battery.srm').write_bytes((p/'battery.srm').read_bytes())
spec['checks']={1300:[1,4],1500:[2,2],1700:[3,3],1900:[2,2],2100:[1,4]}
second=run_core(q,spec,['1400 press 1 Right 5','1600 press 1 Right 5','1800 press 1 Left 5','2000 press 1 Left 5','2101 exit'])
(out/'roundtrip-results.json').write_text(json.dumps(dict(before_reboot=first,after_reboot=second),indent=2)+'\n')
(out/'results.json').write_text(json.dumps(results,indent=2)+'\n');print('PASS',len(results),'league/playoff native player-choice cases;',sum(r['cold_starts'] for r in results),'cold match starts, adapter fallback and restoration')
print('PASS mixed Full4/Light2/Mini3 counts, native-mode detour, reboot, unchanged bracket/results')
