#!/usr/bin/env python3
"""CPU coaching guards/ABI/availability; real native keeper attributes match manual choice."""
from pathlib import Path
import argparse,subprocess,struct,os,json,hashlib
ap=argparse.ArgumentParser();ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();here=Path(__file__).resolve().parent;site=Path('/home/jdc/mount/truenas/www/dela.jdc.se/nhl94');asm=here.parent/'m68kasm.sh' if (here.parent/'m68kasm.sh').exists() else site/'tools/m68kasm.sh';core=here/'test_core.py' if (here/'test_core.py').exists() else site/'tools/experimental/test_core.py';out=a.output.resolve();out.mkdir(parents=True,exist_ok=True);rom=a.rom.read_bytes();assert rom[0x15aa4:0x15aaa]==bytes.fromhex('4ef9002dc800')
cases=[]
for side in [0,1]:
 cases +=[(f'EDM-cpu-{side}',dict(side=side),1),(f'CHI-best-already-starts-{side}',dict(side=side,club=5),0)]
 for diff in [0,2]:cases.append((f'difficulty-{side}-{diff}',dict(side=side,difficulty=diff),0))
 for pad in range(4):
  cases.append((f'human-own-{side}-{pad}',dict(side=side,pad=pad,human=side+1),0))
  cases.append((f'human-opposite-{side}-{pad}',dict(side=side,pad=pad,human=2-side),1))
 for role in [1,4,6,-1]:cases.append((f'non-goalie-{side}-{role}',dict(side=side,role=role),0))
 cases +=[(f'pulled-{side}',dict(side=side,selected=-1),0),(f'injured-best-{side}',dict(side=side,unavailable={1:-3}),2),(f'boxed-best-{side}',dict(side=side,unavailable={1:60}),2),(f'only-starter-available-{side}',dict(side=side,unavailable={1:-3,2:-4}),0),(f'already-best-{side}',dict(side=side,input=1,selected=1),1),(f'invalid-roster-{side}',dict(side=side,invalid=True),0)]
def run(label,specs,manual=False,full=False):
 p=out/label;p.mkdir(exist_ok=True);body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xFFF800,%sp']
 for i,(name,s,expected) in enumerate(specs):
  side=s.get('side',0);team=0xffc6ce+side*0x364;actor=0xffb04a+(5+side*6)*128;club=s.get('club',10);ptr=struct.unpack_from('>I',rom,0x30e+4*club)[0];selected=s.get('selected',0);roster=s.get('input',0)
  body+=[' lea 0xFFFFB000,%a0',' move.w #0x4800/4-1,%d0',f'zero{i}: clr.l (%a0)+',f' dbra %d0,zero{i}',f' move.w #{s.get("difficulty",1)},0xFFF07A',f' move.l #{0 if s.get("invalid") else ptr},0x{team+0x1e:X}',f' move.w #0x{0xb04a+side*768:X},0x{team+0x22:X}',f' move.w #{selected},0x{team+0x26:X}',f' move.w #{s.get("role",0)},0x{actor+0x34:X}',f' move.w #{5+side*6},0x{actor+0x52:X}',f' move.b #{64 if side else 0},0x{actor+0x62:X}',f' move.b #0xC1,0x{actor+0x63:X}']
  for j in range(26):body.append(f' move.w #{s.get("unavailable",{}).get(j,-1)},0x{team+0x66+2*j:X}')
  if 'human' in s:body.append(f' move.w #{s["human"]},0x{0xffc328+2*s["pad"]:X}')
  for reg in [0,1,2,4,5,6,7]:body.append(f' move.l #0x{0x10203040+reg*0x01010101:X},%d{reg}')
  body.append(f' move.l #0x12340000+{roster},%d3')
  for reg in [0,1,2,4,5,6]:body.append(f' movea.l #0x{0xffe100+32*reg:X},%a{reg}')
  body += [f' movea.l #0x{actor:X},%a3',' jsr 0x15AA4',f' movem.l %d0-%d7/%a0-%a6,0x{0xff4000+80*i:X}',f' move.w 0x{team+0x26:X},0x{0xff403c+80*i:X}',f' move.w 0x{actor+0x62:X},0x{0xff403e+80*i:X}',f' lea 0x{0xff4040+80*i:X},%a0',f' lea 0x{actor+0x66:X},%a1',' moveq #15,%d0',f'attrs{i}: move.b (%a1)+,(%a0)+',f' dbra %d0,attrs{i}']
 body+=[' move.l #0x474B4348,0xFF3FFC','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(asm),str(p/'driver.s'),'2D9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);code=(p/'code.bin').read_bytes();assert len(code)<0x3000;assert rom[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b=bytearray(rom);b[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',b,4,0x2d9000)
 if manual:b[0x15aa4:0x15aaa]=bytes.fromhex('08ab00060063')
 if not full:b[0x15aaa:0x15aac]=bytes.fromhex('4e75')
 (p/'rom.bin').write_bytes(b);(p/'commands.txt').write_text(f'500 dump ff3ffc 4 done\n500 dump ff4000 {80*len(specs)} results\n501 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(core),str(p/'rom.bin'),str(p/'commands.txt'),str(p)],env=env,stdout=log,stderr=log,check=True,timeout=60)
 rows={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (p/'commands.out').read_text().splitlines()};assert rows['done']==b'GKCH',(label,'native driver incomplete');return [rows['results'][80*i:80*i+80] for i in range(len(specs))]
reports=[]
# Keep each driver below the known free 0x3000-byte private test reservation.
for batch in range(0,len(cases),8):
 group=cases[batch:batch+8];rr=run(f'guards-{batch}',group)
 for (name,s,expected),row in zip(group,rr):
  regs=list(struct.unpack_from('>15I',row));assert regs[3]==0x12340000+expected,(name,hex(regs[3]),expected)
  for reg in [0,1,2,4,5,6,7]:assert regs[reg]==0x10203040+reg*0x01010101,(name,'D'+str(reg))
  for reg in [0,1,2,4,5,6]:assert regs[8+reg]==0xffe100+32*reg,(name,'A'+str(reg))
  assert regs[11]==0xffb04a+(5+s.get('side',0)*6)*128,(name,'A3')
  got=struct.unpack_from('>h',row,60)[0];wanted=expected if expected!=s.get('input',0) else s.get('selected',0);assert got==wanted,(name,got,wanted)
  assert row[63]==0x81,(name,'native displaced BCLR not preserved')
  assert row[64:]==bytes(16),(name,'coach wrote attributes instead of selecting a roster player')
  reports.append(dict(case=name,selected=expected,passed=True))
# Real native loader: coached keeper must be byte-identical to selecting that same
# existing keeper in the native game. No hook/stub in the loading/attribute path.
for side in [0,1]:
 coached=run(f'native-coach-{side}',[('coached',dict(side=side),1)],full=True)[0]
 reference=run(f'native-manual-{side}',[('manual',dict(side=side,input=1,selected=1),1)],manual=True,full=True)[0]
 assert coached[64:]==reference[64:] and coached[64]==1 and coached[73]==0x35,(side,'native Jarry attributes/jersey mismatch',coached.hex(),reference.hex())
 reports.append(dict(case=f'native-real-attributes-{side}',passed=True,attributes=coached[64:].hex()))
r=dict(status='passed',rom_sha256=hashlib.sha256(rom).hexdigest(),cases=reports,native_roster_attribute_equivalence=True);(out/'results.json').write_text(json.dumps(r,indent=2)+'\n');print('PASS',len(reports),'coach selection/guard/ABI cases; full native keeper loading equals manual roster choice')
