#!/usr/bin/env python3
"""Native overtime/shootout integration, isolated copied saves, no emulator window.

verify_shootout.py ROM /private/output --seed preferences.srm --playoffs three-playoffs.srm
The playoff fixture is produced by verify_season_slots.py. Tests shorten clocks
and position a puck across the goal line; no ROM instrumentation or fake goals.
"""
from pathlib import Path
import argparse,json,os,struct,subprocess
from menu_protocol import refinements_enabled

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('--case',help='run one named case')
ap.add_argument('--legacy-regulation-wait',action='store_true',help='reproduce the old delayed ordinary fixture that can acquire an unrelated regulation goal')
ap.add_argument('--teammates',type=int,choices=[2,3,4],default=2,help='adapter integration: three/four cooperative pads')
a=ap.parse_args();here=Path(__file__).resolve().parent
out=a.output.resolve();out.mkdir(parents=True,exist_ok=True);reports=[]
persistent_players=a.rom.read_bytes()[0x2eb000:0x2eb002]!=b'\xff\xff'

def put(path,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;path.write_bytes(raw)

def expected(pattern):
    counts=[0,0];goals=[0,0]
    for n in range(2*min(map(len,pattern))):
        side=n%2;goals[side]+=int(pattern[side][counts[side]]);counts[side]+=1
        if min(counts)>=3:
            win=counts[0]==counts[1] and goals[0]!=goals[1]
        else:
            win=(goals[0]>goals[1]+max(0,3-counts[1]) or
                 goals[1]>goals[0]+max(0,3-counts[0]))
        if win:return counts,goals,1 if goals[0]>goals[1] else 2
    raise AssertionError('test pattern never resolves')

def run(name,fmt,coop,side,pattern,playoff=False,clock=False,ot_goal=False,tie_score=0,period_choice=0):
    if a.case and name!=a.case:return
    if a.teammates>2 and not coop:return
    p=out/name;p.mkdir(exist_ok=True);save=p/'battery.srm'
    if playoff:
        save.write_bytes(a.playoffs.read_bytes())
        dirs=[f'{1000+i*200} press 1 Right 5' for i in range(fmt-1)]
        lines=dirs+['1600 press 1 Start 5','2200 press 1 Start 5','2800 press 1 Start 5','3400 press 1 Start 5']
        force=3500                 # inject before uncontrolled skaters can score
        if persistent_players:
            b=bytearray(save.read_bytes()[1::2])
            for o in range(0,len(b),8192):b[o+0x1ef0:o+0x1ef4]=b'PC\x01\xfe'
            put(save,b)
            lines=dirs+['1400 press 1 Down 5']+[f'{1500+i*80} press 1 Right 5' for i in range(a.teammates-1)]+['1780 press 1 Up 5','1900 press 1 Start 5','2500 press 1 Start 5','3100 press 1 Start 5','3700 press 1 Start 5']
            if refinements_enabled(a.rom):
                # The restored Matchups helper has already returned to native
                # pregame before3100. A fourth Start pauses the live match,
                # which the old away-result fixture mistook for active OT.
                lines.remove('3700 press 1 Start 5')
            force=3800
    else:
        b=bytearray(a.seed.read_bytes()[1::2][:8192]);b[0xe20:0x1ef6]=bytes(0x10d6)
        b[0x1000:0x1004]=b'SESN';struct.pack_into('>6H',b,0x1004,0,0,0,[84,31,7][fmt-1],fmt,3)
        put(save,b)
        lines=(['900 press 1 Down 5']+[f'{1000+i*100} press 1 Right 5' for i in range(a.teammates-1)]+['1400 press 1 Up 5'] if coop else [])
        lines+=['1500 press 1 Start 5','1900 press 1 Start 5'];force=2300 if a.teammates>2 else 3000
    if not playoff and not a.legacy_regulation_wait:force=1906  # first live play after the last scripted Start release
    lines+=['9000 snap','15000 snap','22000 snap','23000 exit']
    cmd=p/'commands.txt';cmd.write_text('\n'.join(lines)+'\n')
    case=dict(coop=coop,side=side,pattern=pattern,force_at=force,playoff=playoff,clock_test=clock,ot_goal=ot_goal,tie_score=tie_score,teammates=a.teammates,persistent_players=persistent_players,period_choice=period_choice,overtime_seconds=[300,420,600,1200][period_choice],first_live_setup=not playoff and not a.legacy_regulation_wait)
    env=dict(os.environ,GPGX_DEV='1281' if a.teammates>2 else '513',GPGX_SRM=str(save),
             GPGX_HOOK=str(here/'testcases/shootout_hook.py'),SHOOTOUT_CASE=json.dumps(case))
    with (p/'log.txt').open('w') as log:
        subprocess.run(['python3',str(here/'test_core.py'),str(a.rom.resolve()),str(cmd),str(p)],env=env,stdout=log,stderr=log,check=True)
    events=json.loads((p/'events.json').read_text())
    fixture=next(e for e in events if e['event']=='fixture')
    finish=next(e for e in events if e['event']=='finish')
    saved=next(e for e in events if e['event']=='saved')
    counts,tally,winner=([0,0],[0,0],1) if ot_goal else expected(pattern)
    assert finish['winner']==winner and finish['counts']==counts and finish['tally']==tally,(name,finish)
    assert finish['score']==[tie_score+int(j==winner-1) for j in range(2)] and finish['period']==3
    assert finish['sides']==fixture['sides'] and saved['crc']
    if clock:assert any(e['event']=='overtime-clock' and e['seconds']==[300,420,600,1200][period_choice] for e in events)
    attempts=[e for e in events if e['event']=='attempt']
    assert len(attempts)==sum(counts)
    human=fixture['sides'][0]
    for e in attempts:
        controls=e['controls'];pads=e['pads']
        assert all(c in (0,5,6,11,65535) for c in controls),(name,'invalid controlled actor',e)
        if e['side']+1==human:
            pad=e['attempt']%2 if coop else 0
            assert pads[pad]==human and pads[1-pad]==0 and controls[pad]==e['shooter'] and controls[1-pad]==65535,(name,e)
        else:
            assert pads==[human,0] and controls[1]==65535,(name,'defending pad',e)
    b=save.read_bytes()[1::2][:8192]
    assert b[0x1fff]==sum(b[:0x1ffe])%256 and b[0x1ffe]==(b[0x1fff]^255)
    if playoff:
        if persistent_players:assert b[0x1ef0:0x1ef4]==b'PC'+bytes([a.teammates,a.teammates^255]),(name,'count lost after native playoff save')
        oldpair=struct.unpack_from('>8H',bytes.fromhex(fixture['bracket']),fixture['pair']*16)
        if fmt==1:
            newpair=struct.unpack_from('>8H',bytes.fromhex(saved['bracket']),fixture['pair']*16)
            wins=list(oldpair[2:4]);wins[(winner-1)^((oldpair[7]>>8)&1)]+=1
            assert list(newpair[2:4])==wins and saved['game']==fixture['game']+1,(name,'native BO7 result',oldpair,newpair)
        else:
            progressed=[e for e in events if e['event']=='advance' and e['round']==fixture['round']+1]
            assert progressed,(name,'single-game round did not advance',saved)
            assert any(fixture['teams'][winner-1] in struct.unpack_from('>8H',bytes.fromhex(e['bracket']),(fixture['pair']//2)*16)[:2] for e in progressed),(name,'wrong qualified team',progressed)

        assert saved['password']!=fixture['password'],(name,'playoff password not updated')
    else:
        assert int.from_bytes(b[0x1008:0x100a],'big')==1 and int.from_bytes(b[0x100a:0x100c],'big')==[84,31,7][fmt-1]
        rows=[struct.unpack_from('>8H',b,0x1010+16*t) for t in range(32)]
        assert all(r[0]==1 and r[0]==r[1]+r[2]+r[3]+r[6] for r in rows)
        assert sum(r[4] for r in rows)==sum(r[5] for r in rows)
        for team,r in enumerate(rows):
            skaters=[struct.unpack_from('>BBH',b,0x1220+100*team+4*k) for k in range(25)]
            deciders=r[4]-sum(x[0] for x in skaters)
            assert 0<=deciders<=r[7] and sum(x[1] for x in skaters)<=2*sum(x[0] for x in skaters)
        for j,team in enumerate(fixture['teams']):
            r=rows[team];assert r[4]==tie_score+int(j==winner-1) and r[5]==tie_score+int(j!=winner-1)
            assert r[1]==int(j==winner-1) and r[3]==int(j!=winner-1) and r[7]==int(j==winner-1)
            assert sum(b[0x1220+100*team+4*k] for k in range(25))==tie_score+int(ot_goal and j==winner-1),'wrong normal / shootout scorer credit'
            gks=[b[0xe20+15*team+5*k:0xe20+15*team+5*k+5] for k in range(3)]
            assert sum(g[0] for g in gks)==1 and sum(g[1] for g in gks)==int(j==winner-1)
            assert sum((g[2]<<4)|(g[3]>>4) for g in gks)==tie_score+int(ot_goal and j!=winner-1),'wrong normal / shootout keeper goal credit'
    report=dict(case=name,attempts=counts,tally=tally,winner=winner,overtime_seconds=[300,420,600,1200][period_choice] if clock else None,
                original_sides=fixture['sides'],playoff=playoff,checksum=True)
    reports.append(report);(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n')
    print('PASS',name,counts,tally,flush=True)

for period_choice in range(4):
    run(f'normal-overtime-goal-ends-game-{period_choice}',1,True,1,['',''],clock=True,ot_goal=True,period_choice=period_choice)
for fmt in (1,2,3):
    run(f'league-{fmt}-coop-home-early',fmt,True,1,['110','000'],clock=fmt==1)
    run(f'league-{fmt}-coop-away-early',fmt,True,2,['000','110'])
run('single-home-paired-sudden-death',1,False,1,['0001','0000'])
run('single-away-second-sudden-death-round',1,False,2,['11100','11101'])
run('coop-home-sudden-death-equal-response',2,True,1,['00010','00011'])
run('existing-two-two-score-and-stats-preserved',3,True,1,['110','000'],tie_score=2)
run('coop-six-rounds-roster-wrap',3,True,1,['000001','000000'])
for fmt in (1,2,3):
    run(f'playoff-{fmt}-coop-home-result',fmt,True,None,['110','000'],True,clock=True,period_choice=fmt)
    run(f'playoff-{fmt}-coop-away-result',fmt,True,None,['000','110'],True)
print('PASS',len(reports),'native overtime/shootout result, controller, stats, save and playoff cases')
