from preflight import R,g,HEAD
from pathlib import Path
import csv,json,shutil,time
plan=json.loads((R/'plan.json').read_text());run=json.loads((R/'run.json').read_text());c=g.inspect();built=json.loads((R/'build.json').read_text())
assert run['window_result']=='PASS' and run['cleanup_verified'] and run['final']=='HEALTHY_STOPPED_NEW_IMAGE'
assert c['Image']==built['image_id'] and c['State']['Health']['Status']=='healthy'
assert c['HostConfig']['RestartPolicy']==plan['normal_restart']
assert g.sha(Path(plan['overlay']))==plan['overlay_hash'] and g.others()==plan['other_containers']
assert g.sha(g.CFG)==plan['config_hash'] and g.sha(g.CFG.parent/'2/model.plan')==plan['plan_hash']
assert all(g.sha(Path(p))==h for p,h in plan['pose_hashes'].items())
model=json.loads((R/'model-prepared.json').read_text());assert g.sha(Path(model['directory'])/'2/model.plan')==plan['fire_plan_hash']
assert g.sha(Path(model['directory'])/'config.pbtxt')==model['config_hash']
cfg=g.yaml.safe_load((g.ENV.parent/'metadata-active/compose.yaml').read_text());svc=cfg['services']['analytics']
assert svc['image']==c['Image'] and svc['restart']=='on-failure:3'
assert g.load_env_file(str(g.ENV))['CONTEXTRA_IMAGE']==built['tag']
assert not any(m['Destination'].startswith('/run/contextra-') for m in c['Mounts'])
import urllib.request
op=urllib.request.build_opener(urllib.request.ProxyHandler({}));env=g.load_env_file(str(g.ENV))
with op.open('http://127.0.0.1:'+env['FASTAPI_HOST_PORT']+'/api/v1/streams',timeout=3) as f:streams=json.load(f)['streams']
assert len(streams)==1 and streams[0]['state']=='stopped'
names=g.sh(['docker','ps','-a','--format','{{.Names}}']).splitlines();assert not any(n.startswith('codex-ltb1021-fire-api-') for n in names)
rows=[x for x in run['observations'] if x['state']=='running' and x['monotonic_ns']>=run['first_result_ns']+500000000]
first,last=rows[0],rows[-1];seconds=(last['monotonic_ns']-first['monotonic_ns'])/1e9
a=first['metadata_path']['performance'];b=last['metadata_path']['performance']
rates={k:(b['counts'].get(k,0)-a['counts'].get(k,0))/seconds for k in ('decoded','selected','infer','result','publisher_submit')}
rates['redis_sent']=(last['publisher']['metrics']['sent_frames']-first['publisher']['metrics']['sent_frames'])/seconds
stage_rates={k:(b['counts'][k]-1)/((b['last_ns'][k]-b['first_ns'][k])/1e9) for k in ('decoded','selected','infer','result','publisher_submit')}
final_metrics=run['observations'][-1]['publisher']['metrics'];last_path=run['observations'][-2]['metadata_path']
assert final_metrics['sent_frames']==run['redis_frames']==31 and last_path['processed']==31
summary=dict(head=HEAD,source_http_tests=2,image_http_tests=2,native_sdk_reused=True,builds=1,starts=1,stops=1,failures=0,image=built,
 runtime=dict(status='PASS',redis_frames=31,classes=[0,1],same_frame_samples=3,rtp_time='UNAVAILABLE/file input',events=0,capture='Off; bounded JSON3 only',
 throughput=dict(post_first_half_second_window_s=seconds,window_counter_fps=rates,stage_first_last_fps=stage_rates,stage_result_interval_s=(b['last_ns']['result']-b['first_ns']['result'])/1e9,source_file_fps=30,analysis_target=10,limitation='short window; poll alignment/count quantization; not sustained capacity'),
 sampling_drop=last_path['performance']['counts'].get('sampling_drop',0),decoded=last_path['performance']['counts']['decoded'],selected=last_path['performance']['counts']['selected'],queue_drop=last_path.get('queue_drop',0),stale=last_path.get('stale',0),observation_missed=last_path['performance']['observation_missed'],publisher_queue=final_metrics.get('queued',0),eof='NOT_REACHED; explicit Stop before EOF',cleanup_s=run['stop_status']['last_cleanup']['elapsed_s'],
 control={x['action']:dict(http=x['http'],response_ms=(x['returned_ns']-x['requested_ns'])/1e6) for x in run['calls']}),
 final=dict(analytics='healthy',stream='stopped',restart='on-failure:3 actual/persistent',original_camera_settings='CAS restored; next normal Start Medium RTSP',fire_model='RETAIN read-only version2/batch1/576; not selected by default',temporary_mounts='removed',capture='Off; JSON flag removed',non_targets='unchanged from this request preflight',task_containers=0,gpu_mib=g.gpu(),disk_free_gib=shutil.disk_usage(g.sh(['docker','info','--format','{{.DockerRootDir}}']).strip()).free/1024**3),
 limits=['raw FP16 numerical FAIL retained; exact offline visual acceptance separate','no natural EOF support acceptance','no Fire Event, sustained10fps, multi-channel or field accuracy acceptance'],next='FALL semantics; no required Fire integration blocker')
(R/'summary.json').write_text(json.dumps(summary,indent=2))
with (R/'throughput.csv').open('w') as f:
    w=csv.writer(f);w.writerow(['stage','window_counter_fps','window_s','stage_first_last_fps'])
    for k,v in rates.items():w.writerow([k,f'{v:.4f}',f'{seconds:.4f}',f'{stage_rates[k]:.4f}' if k in stage_rates else 'NOT_MEASURED'])
budget=json.loads((R/'budget.json').read_text());budget.update(completed_utc=time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),builds=1,starts=1,stops=1,failed_groups=0);(R/'budget.json').write_text(json.dumps(budget,indent=2))
print(json.dumps(summary,indent=2))
