from pathlib import Path
import json,runpy,io,contextlib,os,tempfile
from lupa.luajit21 import LuaRuntime
R=Path(__file__).resolve().parents[1]
# Reuse typed sparse-memory fixtures from the original suite, not a game process.
with contextlib.redirect_stdout(io.StringIO()):old=runpy.run_path(str(R/'tests/test_hud.py'))
checks=[]
A,mech,model=old['arm_env']([(old['MECH'],1,77),(old['MECH'],4,78),(old['ARM'],2,5),(old['ARM'],3,6),(old['ARM'],5,7),(old['ARM'],6,8)])
out,link,meta=A[b'discover'](mech,model,True)
assert link==b'AMBIGUOUS' and len(out)==0 and meta[b'mechs']==2 and meta[b'candidates']==4
assert len(meta[b'mech_descriptors'])==2 and len(meta[b'candidate_descriptors'])==4
assert len(meta[b'trace'])>0 and meta[b'trace_bytes']<=131072 and meta[b'bytes']<=196608
checks.append('two_same_model_four_arms_remain_ambiguous_but_diagnostic_has_all_candidates')
A,mech,model=old['arm_env']([(old['MECH'],1,0),(old['MECH'],4,0),(old['ARM'],2,0)])
mech[b'unit']=0
out,link,meta=A[b'discover'](mech,model)
assert link==b'AMBIGUOUS' and len(out)==0
checks.append('zero_unit_is_not_strong_ownership')
# Exercise the HUD with controlled identity changes, candidate discovery and IO.
app=Path(tempfile.mkdtemp());os.environ['APPDATA']=str(app);(app/'Arrowhead/Helldivers2').mkdir(parents=True)
L=LuaRuntime(unpack_returned_tuples=True);L.globals().SRC=str(R/'src')
n=L.execute(r'''
local load=function(n)return dofile(SRC..'/'..n..'.lua')end
local N=load('native');local Z=load('zones');local stats={discover=0,read=0,draw=0,previous=0,validations=0}
local C={session='session1',peer=1,world='world1',ambiguous=false,seated=true,graph_error=false,relation_changed=false}
local vehicle={resource='35dbf54f016f3624',entity=100,goid=500,unit=700}
local sr={Application={},Network={},GameSession={},Gui={},Unit={}}
sr.Application.worlds=function()return {C.world,'ui'}end;sr.Application.main_world=function()return C.world end
sr.Network.game_session=function()return C.session end;sr.Network.peer_id=function()return C.peer end
sr.GameSession.in_session=function()return true end;sr.GameSession.objects_owned_by=function()return {10}end
sr.GameSession.game_object_is_type=function()return true end;sr.GameSession.game_object_field=function()return 20 end;sr.GameSession.game_object_exists=function()return true end
sr.Gui.resolution=function()return 1920,1080 end
sr.Unit.fake_parent_query=function()error('must never call inventoried APIs')end
local reader={sample=function()
 if not C.seated then return nil,'EMPTY','NOT_SEATED'end
 return {vehicle=vehicle,model=Z.models[vehicle.resource],hull=1800,hull_max=1800,hull_valid=true,zones={}}
end}
N.win={begin_sample=function()end,read=function()error('no raw memory access in this test')end}
N.graph=function()
 if C.graph_error then error('injected graph failure')end
 return {relation=function()return {status='VEHICLE',vehicle=C.relation_changed and {entity=999}or vehicle}end,validate=function()stats.validations=stats.validations+1 end}
end
local arms={discover=function(d,model,capture)
 stats.discover=stats.discover+1
 local a={resource='65489809a8181b96',entity=d.entity+1,goid=d.goid+1,unit=d.unit+1}
 local b={resource='df51fe8d62f294be',entity=d.entity+2,goid=d.goid+2,unit=d.unit+2}
 return C.ambiguous and {}or{a,b},C.ambiguous and 'AMBIGUOUS'or 'SOLE_MECH',
  {mechs=C.ambiguous and 2 or 1,candidates=C.ambiguous and 4 or 2,candidate_descriptors={a,b},trace=capture and {{address='123456',hex='abcd'}}or nil}
end,read=function(g,d)
 stats.read=stats.read+1;return {descriptor=d,hp=d.unit,max=10000,main=d.unit,source='main',valid=true,side=d.resource=='65489809a8181b96'and 'L'or 'R',zones={}}
end}
local D={mode='MECH',clear=function()end,reset=function()end,ensure=function()return true end,draw=function(w,h,v)stats.draw=stats.draw+1 end}
CowboyBingusModLoader={api=1};update=function(...)stats.previous=stats.previous+1;return 'old',nil,3 end
local cfg=function()return {debug=true,poll=.1}end
local S=load('hud')(sr,N,reader,function()return{}end,Z,cfg,{},function()return D end,arms,load('json'))
local count=0;local function check(v,msg)assert(v,msg);count=count+1 end
local function tick(n)for i=1,n or 1 do local a,b,c=update(.25);check(a=='old'and b==nil and c==3,'callback chain')end end
local function displayed()return S.arms and S.arms[1].hp end
local function changed(k,v)
 local n=stats.discover;vehicle[k]=v;tick();check(stats.discover==n+1,'full descriptor cache key: '..k);check(displayed()==vehicle.unit+1)
end
tick();check(displayed()==701 and stats.discover==1)
changed('goid',600);changed('unit',710);changed('resource','79e4b3d2da5e45e3');changed('entity',200)
for _,field in ipairs({'session','peer','world'})do local n=stats.discover;C[field]=tostring(C[field])..'2';tick();check(stats.discover==n+1,'scope reset '..field)end
C.seated=false;tick();check(not S.arms and not S.rows,'unseated clears everything');C.seated=true;local n=stats.discover;tick();check(stats.discover==n+1,'reenter rediscovers')
C.graph_error=true;tick();check(not S.arms and S.rows~=nil,'optional arm failure clears stale arms but preserves hull');n=stats.discover;tick();check(stats.discover==n,'error retry throttled');C.graph_error=false;tick(22);check(S.arms~=nil)
C.relation_changed=true;tick();check(not S.arms,'seat changed mid-sample');C.relation_changed=false;tick();check(S.arms~=nil)
C.ambiguous=true;tick(22);check(not S.arms and S.rows~=nil,'periodic rediscovery does not retain sole-model fallback with two models')
C.ambiguous=false;tick(22);check(S.arms~=nil,'can recover when unique again')
-- A fresh vehicle with ambiguity must still emit a diagnostic, not fake displayed HP.
vehicle.entity=300;C.ambiguous=true;tick();check(not S.arms)
check(S.diagnostic_count<=8,'diagnostic file bound')
_G.RESULT={count=count,stats=stats,state=S}
return count
''')
checks.append('hud_identity_and_session_invalidation_stale_clear_rediscovery_and_optional_errors')
files=list((app/'Arrowhead/Helldivers2').glob('mech_hud_spatial_*.json'))
assert 0<len(files)<=8
for p in files:
 d=json.loads(p.read_text());assert d['format']=='mech-spatial-diagnostic-v3';assert 'fake_parent_query'in d['api_functions']['Unit']
# Independent fresh all-ambiguous start: original old code never dumped this case.
# Re-run with ambiguity from first frame so the eight-file cap does not hide the assertion.
source=Path(__file__).read_text().split("n=L.execute(r'''",1)[1].split("''')",1)[0]
source=source[:source.index('tick();check(displayed()==701')]+"C.ambiguous=true;tick();assert(S.arms==nil and S.rows~=nil);return S.diagnostic_count\n"
app2=Path(tempfile.mkdtemp());os.environ['APPDATA']=str(app2);(app2/'Arrowhead/Helldivers2').mkdir(parents=True)
L2=LuaRuntime(unpack_returned_tuples=True);L2.globals().SRC=str(R/'src');assert L2.execute(source)==1
f=list((app2/'Arrowhead/Helldivers2').glob('mech_hud_spatial_*.json'));assert len(f)==1
d=json.loads(f[0].read_text());assert d['link']=='AMBIGUOUS'and d['display_linked']==False and len(d['candidate_reads'])==2 and not d['arms']
checks.append('ambiguous_case_dumps_candidates_without_displaying_them_or_invoking_unverified_APIs')
report={'cases':checks,'hud_checks':n,'baseline_checks':88,'real_game_reads':0,'real_game_writes':0,'multiplayer_arm_link_fixed':False,'scope':'Legacy cache/diagnostic regressions with spatial module omitted; does not test spatial binding or prove ownership.'}
(R/'validation/multiplayer-tests.json').write_text(json.dumps(report,indent=2));print(json.dumps(report,indent=2))
