#!/usr/bin/env python3
"""Native exhaustive Mini fixtures, menu cycling, saved rounds and seven-game league."""
from pathlib import Path
import argparse,json,os,struct,subprocess,hashlib
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('--baseline',type=Path,required=True);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
divs=[3,0,0,3,1,2,2,1,2,0,3,0,3,2,0,2,1,1,1,0,1,1,3,3,2,0,0,2,3,3,1,2]
abbr='ANA BOS BUF CGY CAR CHI COL CBJ DAL DET EDM FLA LAK MIN MTL NSH NJD NYI NYR OTT PHI PIT SJS SEA STL TBL TOR UTA VAN VGK WAS WPG'.split()
sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
assert sha(a.baseline)=='083f45ffbfff3986d55fbe2ea13384916c4c5b4f1d0b6c5e21e39aa80af80e02','exact r8 Mini regression control required'

def core(rom,p,commands,hook=None,save=None):
 p.mkdir(exist_ok=True,parents=True);(p/'commands.txt').write_text(commands);env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
 if hook:env['GPGX_HOOK']=str(hook)
 if save:env['GPGX_SRM']=str(save)
 with (p/'log.txt').open('w') as f:subprocess.run(['python3',str(h/'test_core.py'),str(rom),str(p/'commands.txt'),str(p)],env=env,stdout=f,stderr=f,check=True,timeout=90)

p=o/'native-fixtures';p.mkdir(exist_ok=True)
(p/'driver.s').write_text(''' .text
 .globl _start
_start:
 move.w #0x2700,%sr
 movea.l #0xFFF800,%sp
 lea 0xFF4000,%a4
 moveq #0,%d4
team:
 moveq #0,%d7
round:
 move.w %d4,%d0
 move.w %d7,%d2
 jsr 0x2DBE00
 move.w %d3,(%a4)+
 move.b %d4,0xFFFFC331
 move.b %d3,0xFFFFC333
 move.w %d7,%d5
 movea.l %a4,%a2
 jsr 0x2DBE04
 adda.w #30,%a4
 move.w %d6,(%a4)+
 addq.w #1,%d7
 cmpi.w #7,%d7
 bcs.s round
 addq.w #1,%d4
 cmpi.w #32,%d4
 bcs.s team
 moveq #0,%d5
 move.b #10,0xFFFFC331
 move.b #9,0xFFFFC333
 jsr 0x2DBE04
 move.w %d6,0xFF3FE0
 moveq #7,%d5
 jsr 0x2DBE04
 move.w %d6,0xFF3FE2
 moveq #0,%d5
 move.b #3,0xFFFFC333
 jsr 0x2DBE04
 move.w %d6,0xFF3FE4
 move.l #0x4D494E49,0xFF3FF0
end: bra.s end
''');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2D9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
b=bytearray(a.rom.read_bytes());code=(p/'code.bin').read_bytes();assert b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',b,4,0x2d9000);(p/'driver.bin').write_bytes(b)
core(p/'driver.bin',p,'500 dump ff3ff0 4 done\n500 dump ff3fe0 6 legacy\n500 dump ff4000 7616 fixtures\n501 exit\n')
raws={r.split()[1]:bytes.fromhex(' '.join(r.split()[2:])) for r in (p/'commands.out').read_text().splitlines()};assert raws['done']==b'MINI';assert raws['legacy']==b'\xff'*6;raw=raws['fixtures'];fixtures=[]
for team in range(32):
 row=[]
 for k in range(7):
  r=raw[(team*7+k)*34:(team*7+k+1)*34];opp=struct.unpack('>H',r[:2])[0];assert opp!=team and divs[opp]==divs[team],(team,k,opp)
  assert struct.unpack('>H',r[32:])[0]==30
  pairs=[(team,opp)]+list(zip(r[2:32:2],r[3:32:2]));assert sorted(v for pair in pairs for v in pair)==list(range(32)),(team,k,pairs)
  assert all(x!=y and divs[x]==divs[y] for x,y in pairs),(team,k,pairs)
  row.append(opp)
 assert len(row)==len(set(row))==7,(team,row)
 assert all(row[k]!=row[(k+1)%7] for k in range(7)),(team,'adjacent cursor has same club',row)
 fixtures.append(dict(team=abbr[team],opponents=[abbr[x] for x in row]))
(o/'native-fixtures.json').write_text(json.dumps(fixtures,indent=2)+'\n')
# Retain all-club/format/logo/opponent-lock coverage through actual controller inputs.
seed16=o/'preferences16.srm';preference=bytearray(a.seed.read_bytes()[1:16384:2]);preference[0xe20:0x1ef6]=bytes(0x10d6);preference[0x1fff]=sum(preference[:0x1ffe])%256;preference[0x1ffe]=preference[0x1fff]^255;raw=bytearray(16384);raw[1::2]=preference;seed16.write_bytes(raw);cmd=['python3',str(h/'verify_season_teams.py'),str(a.rom.resolve()),str(o/'all-club-menu'),'--seed',str(seed16)]
with (o/'all-club-menu.log').open('w') as f:subprocess.run(cmd,stdout=f,stderr=f,check=True,timeout=180)
# Native menu reproduction: r8's historical round10 offset31 creates EDM vs DET.
menu_hook=o/'menu-hook.py';menu_hook.write_text('''from pathlib import Path
import json
def rw(a):return(wram[ram(a)]<<8)|wram[ram(a+1)]
def test_tick(f):
 if f==1300:Path(outdir,'fixture.json').write_text(json.dumps(dict(user=rw(0xe3f4),opponent=rw(0xe3f8),teams=[rw(0xd04c),rw(0xd04e)],game_row=bytes(wram[ram(0xe3a2+i)] for i in range(20)).decode('ascii',errors='replace'))))
''')
for label,rom in [('r8',a.baseline),('current',a.rom)]:
 p=o/('reported-edmonton-'+label);p.mkdir(exist_ok=True);b=bytearray(seed16.read_bytes()[1::2]);b[0xe20:0x1ef6]=bytes(0x10d6);struct.pack_into('>4s6H',b,0x1000,b'SESN',0,0,0,8,3,3);struct.pack_into('>HIHHHH',b,0x1212,0x5343,0,0,0,10,3);b[0x1fff]=sum(b[:0x1ffe])%256;b[0x1ffe]=b[0x1fff]^255;r=bytearray(16384);r[1::2]=b;(p/'battery.srm').write_bytes(r);core(rom.resolve(),p,'1301 exit\n',menu_hook,p/'battery.srm')
old=json.loads((o/'reported-edmonton-r8/fixture.json').read_text());new=json.loads((o/'reported-edmonton-current/fixture.json').read_text());assert old['user']==new['user']==10 and old['opponent']==9 and divs[new['opponent']]==3,(old,new)
assert old['game_row'].startswith('1/8') and new['game_row'].startswith('1/7'),(old,new)
# Seven actual pre-game Simulate Match actions, with a clean core reboot after4.
league_hook=o/'league-hook.py';league_hook.write_text('''from pathlib import Path
import json,struct
p=Path(outdir);spec=json.loads((p/'spec.json').read_text());results=[]
def rw(a):return(wram[ram(a)]<<8)|wram[ram(a+1)]
def test_tick(f):
 if f in spec['pregame']:
  k=spec['pregame'].index(f)+spec['start_game'];user=rw(0xe3f4);opponent=rw(0xe3f8)
  results.append(dict(game=k+1,user=user,opponent=opponent,teams=[rw(0xd04c),rw(0xd04e)]))
 if f==9200 and spec['start_game']==4:
  b=bytes(sram)[1:16384:2];gp=int.from_bytes(b[0x1008:0x100a],'big');assert gp==7 and all(int.from_bytes(b[0x1010+16*t:0x1012+16*t],'big')==7 for t in range(32)),('an eighth regular match was recorded',gp)
  Path(outdir,'after-seven.json').write_text(json.dumps(dict(games=gp,postseason_flag=int.from_bytes(b[0x1210:0x1212],'big'),season_length=int.from_bytes(b[0x100a:0x100c],'big'),playmode=rw(0xd048),menu=bytes(wram[ram(0xe3a2+i)] for i in range(20)).decode('ascii',errors='replace'))))
 if f in spec['recorded']:
  k=spec['recorded'].index(f)+spec['start_game']+1;b=bytes(sram)[1:16384:2]
  assert int.from_bytes(b[0x1008:0x100a],'big')==k,('wrong completed games',k,b[0x1000:0x1010].hex())
  assert b[0x1fff]==sum(b[:0x1ffe])%256 and b[0x1ffe]==(b[0x1fff]^255)
  rows=[struct.unpack_from('>8H',b,0x1010+16*t) for t in range(32)]
  assert all(r[0]==k and r[1]+r[2]+r[3]+r[6]==k for r in rows),('unequal simulated GP',k,rows)
  assert sum(r[4] for r in rows)==sum(r[5] for r in rows)
  for team,row in enumerate(rows):
   assert sum(b[0x1220+100*team+4*j] for j in range(25))==row[4]
   goalies=[b[0xe20+15*team+5*j:0xe20+15*team+5*j+5] for j in range(3)]
   assert sum(g[0] for g in goalies)==k
   assert sum((g[2]<<4)|(g[3]>>4) for g in goalies)==row[5]
  results[-1]['all32_GP']=k
  (p/'league.json').write_text(json.dumps(results,indent=2))
''')
league=[];previous=o/'reported-edmonton-current/battery.srm'
for phase in range(2):
 p=o/f'seven-game-phase{phase}';p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(previous.read_bytes());commands=[];pregame=[];recorded=[]
 for j in range(4 if phase==0 else 3):
  t=1500+2000*j;commands += [f'{t} press 1 Start 5',f'{t+400} press 1 Down 5',f'{t+500} press 1 C 5',f'{t+1200} press 1 Start 5'];pregame.append(t+300);recorded.append(t+1000)
 (p/'spec.json').write_text(json.dumps(dict(start_game=4*phase,pregame=pregame,recorded=recorded)))
 end=9300 if phase==0 else 7300
 commands += ([f'{end} snap','7400 press 1 Start 5','7800 press 1 Down 5','7900 press 1 C 5','8800 press 1 Start 5','9200 snap','9301 exit'] if phase==1 else [f'{end} snap',f'{end+101} exit']);core(a.rom.resolve(),p,'\n'.join(commands)+'\n',league_hook,p/'battery.srm');league += json.loads((p/'league.json').read_text());previous=p/'battery.srm'
assert len(league)==7 and all(r['user']==10 and divs[r['opponent']]==3 for r in league),league
assert len(set(r['opponent'] for r in league[:7]))==len(set(r['opponent'] for r in league))==7,league
assert [r['teams'].index(10) for r in league]==[0,1,0,1,0,1,0],league
(o/'seven-game-league.json').write_text(json.dumps(league,indent=2)+'\n')
after_seven=json.loads((o/'seven-game-phase1/after-seven.json').read_text());assert after_seven['season_length']==7 and after_seven['postseason_flag'] in [1,2,3,4,5] and not after_seven['menu'].startswith('8/'),after_seven
result=dict(after_seven=after_seven,rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),native_fixture_cases=224,all32_clubs=True,all_simulated_pairs_divisional=True,seven_distinct_opponents_once=True,reported_fixture=dict(old=old,new=new),seven_game_league_with_reboot=league,all_club_menu=json.loads((o/'all-club-menu/results.json').read_text()))
(o/'results.json').write_text(json.dumps(result,indent=2)+'\n');print('PASS224 native Mini fixtures, all32 clubs, all15 simulated games each round; seven own-division rivals once; EDM/DET reproduced on r8 and corrected; real-input all-format club/logo/cursor/round recording checks')
