"""Offline record and node-name proof. Optional independent pinned-data/model reparse.
No raw game models are packaged, downloaded or modified by this test.
"""
from pathlib import Path
import argparse,json,struct,hashlib,gzip,sys
from lupa.luajit21 import LuaRuntime
R=Path(__file__).resolve().parents[1];sys.path.insert(0,str(R/'tools'))
from archive_writer import murmur64
from parse_unit_components import parse_types,find_section,dlhash
p=argparse.ArgumentParser();p.add_argument('--source-dir',type=Path);p.add_argument('--model-dir',type=Path);args=p.parse_args()
s=json.loads((R/'validation/mount-schema.json').read_text());schemas=json.loads((R/'validation/model-node-schema.json').read_text())
model=next(x for x in schemas['models']if x['id']=='79e4b3d2da5e45e3');assert model['sha256']==s['model_sha256']
raw=bytes.fromhex(model['joint_map_hex']);n=model['joint_count'];hashes=[f'{struct.unpack_from("<I",raw,4*n+4*i)[0]:08x}'for i in range(n)]
L=LuaRuntime();M=L.execute((R/'src/mount_policy.lua').read_text());Z=L.execute((R/'src/zones.lua').read_text())
assert s['types']['MountComponent']['size']==120 and s['types']['MountInfo']['size']==24
assert [v['offset']for v in s['types']['MountInfo']['members']]==[0,8,12,16,20,21]
assert s['types']['AttachableComponent']['size']==64
assert [v['offset']for v in s['types']['AttachableComponent']['members'][3:6]]==[12,24,36]
assert len(s['profiles'])==4 and len(s['records'])==12
arms=set()
for body,slots in s['profiles'].items():
 actual=M.specs(L.table_from({'resource':body}));record=s['records'][body]['MountComponent'];b=bytes.fromhex(record['record_hex']);assert len(b)==120
 decoded=[]
 for i in range(5):
  res,node,side,name=struct.unpack_from('<QIII',b,i*24)
  if res:decoded.append({'resource':f'{res:016x}','node_hash':f'{node:08x}','mount_side':side,'name_hash':f'{name:08x}'})
 assert decoded==record['infos']and {v['mount_side']for v in decoded}=={1,2}
 assert len(slots)==2 and set(slots)=={'L','R'}
 for v in decoded:
  side={1:'R',2:'L'}[v['mount_side']];spec=slots[side];name=spec['name'];assert actual[side]['resource']==spec['resource']==v['resource']and actual[side]['name']==name
  assert Z.side_by_resource[spec['resource']]==side and Z.models[body]['arms'][spec['resource']]
  h=f'{murmur64(name.encode())>>32:08x}';assert h==spec['name_hash']==v['node_hash'];assert hashes.count(h)==1 and hashes.index(h)==spec['serialized_node_index_evidence_only']
  a=s['records'][v['resource']]['AttachableComponent'];ab=bytes.fromhex(a['record_hex']);assert len(ab)==64
  assert list(struct.unpack_from('<3f',ab,12))==a['offset']==[0,0,0]
  assert list(struct.unpack_from('<3f',ab,24))==a['rotation']==[0,0,0]
  assert f'{struct.unpack_from("<I",ab,36)[0]:08x}'==a['offset_node']=='00000000';arms.add(v['resource'])
assert len(arms)==8 and M.specs(L.table_from({'resource':'unknown'}))is None
for resource,components in s['records'].items():
 for kind,row in components.items():
  sec=s['types'][kind+'Data'];ix,records=sec['descriptor']['members'];size=s['types'][kind]['size'];base=sec['data_offset']
  assert row['record_offset']==base+records['offset']+row['index']*size
  assert 0<=row['index']<records['array_len']and base<=row['index_record_offset']<base+ix['size']
  assert (row['index_record_offset']-base)%16==0
if args.source_dir:
 tl=gzip.decompress((args.source_dir/'dl_library.dl_typelib.gz').read_bytes());blob=gzip.decompress((args.source_dir/'generated_entities.dl_bin.gz').read_bytes())
 assert hashlib.sha256(tl).hexdigest()==s['typelib_sha256']and hashlib.sha256(blob).hexdigest()==s['entities_sha256'];types=parse_types(tl)
 for name,v in s['types'].items():
  assert types[dlhash(name)]==v.get('descriptor',v)
  if name.endswith('Data')and 'data_offset'in v:assert find_section(blob,dlhash(name))==(v['section_offset'],v['data_offset'],v['size'])
 for resource,components in s['records'].items():
  for kind,row in components.items():
   sec=s['types'][kind+'Data'];base=sec['data_offset'];ix=sec['descriptor']['members'][0];found=[]
   for offset in range(base,base+ix['size'],16):
    if struct.unpack_from('<Q',blob,offset)[0]==int(resource,16):found.append(offset)
   assert found==[row['index_record_offset']]and struct.unpack_from('<I',blob,found[0]+8)[0]==row['index']
   offset=row['record_offset'];assert blob[offset:offset+s['types'][kind]['size']].hex()==row['record_hex']
if args.model_dir:
 b=(args.model_dir/'79e4b3d2da5e45e3.unit-main.bin').read_bytes();assert hashlib.sha256(b).hexdigest()==s['model_sha256']
 offset=struct.unpack_from('<I',b,52)[0];assert struct.unpack_from('<I',b,offset)[0]==n
 assert b[offset+16+128*n:offset+16+136*n]==raw
runtime=(R/'src/spatial_runtime.lua').read_text();assert 'has(u,spec.name)'in runtime and 'node(u,spec.name)'in runtime and 'mount_indices[side]~=i'in runtime
assert not any(f'position(u,{v})'in runtime for v in [82,83,171,190])
print(json.dumps({'bodies':4,'zero_offset_arms':8,'hash_verified_unique_mount_names':4,'serialized_indices_not_runtime_indices':True,'pinned_source_reparsed':bool(args.source_dir),'installed_base_model_reparsed':bool(args.model_dir),'live_mount_API_checked':False,'active_model_overrides_enumerated':False}))
