#!/usr/bin/env python3
"""Exercise the complete season/native Play Mode circle, including empty Continue.

All inputs/saves are private. Run with --seed legacy-preferences.srm; optional
--empty-save captured.srm (repeatable) and --three-playoffs expanded.srm add cases.
"""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse, json, os, shutil, 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('--empty-save',type=Path,action='append',default=[])
ap.add_argument('--three-playoffs',type=Path)
ap.add_argument('--device',type=int,default=513,help='libretro pad device:513 ordinary six-button,1281 4-Way Play')
a=ap.parse_args();out=a.output.resolve();out.mkdir(parents=True,exist_ok=True)
here=Path(__file__).resolve().parent;cases=[]
def save(p,b):
 b[0x1fff]=sum(b[:0x1ffe])%256;b[0x1ffe]=b[0x1fff]^255
 raw=bytearray(len(b)*2);raw[1::2]=b;p.write_bytes(raw)
for slot in [1,2,3]:
 b=bytearray(a.seed.read_bytes()[1:16384:2]);b[0xe20:0x1ef6]=bytes(0x10d6)
 b[0x1000:0x1004]=b'SESN';struct.pack_into('>6H',b,0x1004,0,0,0,{1:84,2:31,3:7}[slot],slot,3)
 b[0x1f08:0x1f12]=bytes(10);seed=out/f'empty-{slot}.srm';save(seed,b)
 for coop in [False,True]:
  for direction in ['Right','Left']:cases.append((f'empty-{slot}-{coop}-{direction}',seed,slot,coop,direction,False))
for i,p in enumerate(a.empty_save):
 b=p.read_bytes()[1::2]
 slot=int.from_bytes(b[0x1ea6:0x1ea8],'big') if b[0x1ea0:0x1ea4]==b'S3SV' else int.from_bytes(b[0x100c:0x100e],'big')
 assert slot in [1,2,3]
 for direction in ['Right','Left']:cases.append((f'captured-{i}-{direction}',p,slot,False,direction,False))
if a.three_playoffs:
 for direction in ['Right','Left']:cases.append((f'three-playoffs-{direction}',a.three_playoffs,1,False,direction,True))

def run(c):
 name,seed,slot,coop,direction,po=c;case=out/name;case.mkdir(exist_ok=True)
 battery=case/'battery.srm';shutil.copy2(seed,battery)
 graph=[dict(slot=i,view=1,mode=1 if po else 0) for i in [1,2,3]]
 # r9: season playoffs no longer appear as native Continue Playoffs; these
 # fixtures have no standalone native playoff, so native view starts at New Playoffs.
 graph += [dict(view=0,mode=i) for i in [2,3,4]]
 if a.rom.read_bytes()[0x229000:0x229002]!=b'\xff\xff':
  for node in graph:node['quick']=0
  graph.append(dict(view=0,mode=0,quick=1))
 if a.rom.read_bytes()[0x22c000:0x22c002]!=b'\xff\xff':
  for node in graph:node['allstar']=0
  graph.append(dict(view=0,mode=0,quick=1,allstar=1))
 lines=[];checks=[]
 if coop:lines+=['1000 press 1 Down 5','1100 press 1 Right 5','1200 press 1 Up 5']
 index=slot-1;checks.append(dict(frame=1380,expected=graph[index]))
 for k in range(2*len(graph)):
  t=1450+k*90;lines.append(f'{t} press 1 {direction} 5')
  index=(index+(1 if direction=='Right' else -1))%len(graph)
  checks.append(dict(frame=t+65,expected=graph[index]))
 # Reverse and return to the exact initial slot: proves the loop remains responsive.
 for k in range(len(graph)):
  t=1450+(2*len(graph)+k)*90;reverse='Left' if direction=='Right' else 'Right';lines.append(f'{t} press 1 {reverse} 5')
  index=(index+(-1 if direction=='Right' else 1))%len(graph)
  checks.append(dict(frame=t+65,expected=graph[index]))
 lines += [f'{checks[-1]["frame"]+50} exit']
 (case/'checks.json').write_text(json.dumps(checks,indent=2)+'\n')
 cmd=case/'commands.txt';cmd.write_text('\n'.join(lines)+'\n')
 env=dict(os.environ,GPGX_DEV=str(a.device),GPGX_SRM=str(battery),GPGX_HOOK=str(here/'testcases/season_navigation_hook.py'))
 with (case/'log.txt').open('w') as log:subprocess.run(['python3',str(here/'test_core.py'),str(a.rom.resolve()),str(cmd),str(case)],env=env,check=True,stdout=log,stderr=log,timeout=20)
 result=json.loads((case/'results.json').read_text());assert len(result)==len(checks)
 return dict(case=name,checked=len(checks))
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),'navigation cases,',sum(r['checked'] for r in results),'full-circle/reverse checks')
