#!/usr/bin/env python3
"""Strict offline verification of credential-free private wire/source evidence."""
import hashlib,json,pathlib,sys
HERE=pathlib.Path(__file__).resolve().parent
ORIGINAL={x.split(None,1)[1]:x.split(None,1)[0] for x in (HERE/'SOURCE_SHA256.txt').read_text().splitlines()}
CANDIDATE=json.loads((HERE/'CANDIDATE_TDD.json').read_bytes())
GREEN=dict(ORIGINAL)
for name,hashes in CANDIDATE['source_sha256'].items():
    assert ORIGINAL[name]==hashes['original']
    GREEN[name]=hashes['candidate']
B_NAMES=['DISHES_1','DISHES_2','RECOGNIZE_1','RECOGNIZE_2','ORDERS_SOURCE','ORDERS_EMPTY','CARD_SOURCE','CARD_MISSING','FACE_1','FACE_2','ROTATION_SOURCE','ROTATION_MISSING']
C_NAMES=[x for x in B_NAMES if x not in ['CARD_SOURCE','ROTATION_SOURCE']]
G_NAMES=['DISHES_1','ORDERS_SOURCE','ORDERS_EMPTY','CARD_SOURCE','FACE_1','FACE_2']
ROUTES={'dishes':('POST','/api/consume/dishes'),'recognize':('POST','/api/consume/dishesRecognizeImgList'),'orders':('POST','/api/consume/orderList'),'card':('POST','/api/consume/card'),'face':('GET','/p/api/getFaceDataList'),'rotation':('POST','/p/api/rotation')}
def strict(a,b,path='$'):
    assert type(a) is type(b),path+':type'
    if isinstance(a,dict):
        assert set(a)==set(b),path+':keys'
        for key in a: strict(a[key],b[key],path+'.'+str(key))
    elif isinstance(a,list):
        assert len(a)==len(b),path+':length'
        for i,(x,y) in enumerate(zip(a,b)): strict(x,y,path+f'[{i}]')
    else: assert a==b,path+':value'
def read(root,name):return json.loads((pathlib.Path(root)/name).read_bytes())
def verify_private_manifest(root):
    root=pathlib.Path(root);manifest=read(root,'EVIDENCE_MANIFEST.json')
    assert {p.name for p in root.iterdir()}==set(manifest)|{'EVIDENCE_MANIFEST.json'}
    for name,proof in manifest.items():
        assert pathlib.Path(name).name==name
        raw=(root/name).read_bytes()
        assert len(raw)==proof['bytes'] and hashlib.sha256(raw).hexdigest()==proof['sha256']
def stage(root,prefix,names,hashes,red=(),single=False):
    root=pathlib.Path(root);spec=read(root,prefix+'spec-private.json');spec=[spec] if single else spec
    after=read(root,prefix+'after-private.json');after={spec[0]['name']:after} if single else after
    report=read(root,prefix+'report-private.json');strict(report['source_sha256'],hashes)
    assert report['actor_unchanged'] is True
    assert report['http_dml']==report['new_fixture_dml']==report['ddl']==0
    cases={x['case']:x for x in report['cases']};assert set(cases)==set(names)
    assert {x['name'] for x in spec}==set(names)
    for entry in spec:
        name=entry['name'];case=cases[name]
        assert (entry['method'],entry['path'])==ROUTES[entry['kind']]
        assert (case['method'],case['path'])==ROUTES[entry['kind']]
        assert case['http_status']==200 and case['fatal'] is False
        before=entry['before'];post=after[name]
        assert before['source_digest']==post['source_digest'] and len(before['source_digest'])==64
        strict(before['source_sets'],post['source_sets'])
        assert case['source_window_unchanged'] is True
        for actor in before['source_sets']['actor']:
            assert set(actor)=={'id','uuid','account','type','status','full_row_digest'}
            digest=actor['full_row_digest']
            assert actor['type']==0 and actor['status']==0 and type(digest) is dict
            assert set(digest)=={'rows','sha256'} and digest['rows']==1 and len(digest['sha256'])==64
        raw_name='raw-'+('green-' if prefix=='green-' else '')+name+'-private.json'
        raw=(root/raw_name).read_bytes()
        assert len(raw)==case['wire_bytes'] and hashlib.sha256(raw).hexdigest()==case['wire_sha256']
        body=json.loads(raw)
        if name in red:
            assert case['source_pass'] is False and case['differences']
            try:strict(before['body'],body)
            except AssertionError:pass
            else:raise AssertionError('original red was erased')
        else:
            assert case['source_pass'] is True and case['differences']==[]
            strict(before['body'],body)
        if name not in ['DISHES_2','ORDERS_EMPTY','CARD_MISSING','ROTATION_MISSING']:
            assert case['positive'] is True
    return cases
def verify(b,c,require_cleanup=True):
    b=pathlib.Path(b);c=pathlib.Path(c)
    bc=stage(b,'',B_NAMES,ORIGINAL)
    cc=stage(c,'',C_NAMES,ORIGINAL,['ORDERS_SOURCE','FACE_1','FACE_2'])
    card=stage(c,'card-',['CARD_SOURCE'],ORIGINAL,single=True)
    green=stage(c,'green-',G_NAMES,GREEN)
    for root,lane,requests,assertions in [(b,'B',12,24),(c,'C',17,34)]:
        boundary=read(root,'boundary-private.json')
        for key in ['private_source_reflection','effective_ai_private_config_verified','legacy_ai_fallback_disabled','file_cache','redis_disabled','external_stubs']:
            assert boundary[key] is True
        assert boundary['real_queue'] is False and boundary['json_encode_param']==256
        native=read(root,'native-stats-private.json')
        assert native['requests']==requests and native['assertions']==assertions
        # C initial expected failures must remain visible; final green cannot erase them.
        assert native['stages']['catalog-original']['requests']==(12 if lane=='B' else 10)
        if lane=='C':
            assert native['stages']['catalog-original']['failures']==3
            assert native['stages']['card']['failures']==native['stages']['catalog-green']['failures']==0
        else:assert native['stages']['catalog-original']['failures']==0
        if require_cleanup:
            cleanup=read(root,'cleanup-private.json')
            assert cleanup['port_listening'] is cleanup['worker_state_exists'] is False
            assert cleanup['private_cache_files_remaining']==cleanup['token_artifacts_remaining']==0
            assert cleanup['new_pdo_owned_actor_verified'] is True
            assert cleanup['new_fixture_dml']==cleanup['ddl']==cleanup['business_rows_deleted']==0
    # Identical empty contracts can be strict-cross-compared; no natural snapshots mapped.
    for name in ['ORDERS_EMPTY','CARD_MISSING','ROTATION_MISSING']:
        strict(read(b,'raw-'+name+'-private.json'),read(c,'raw-'+name+'-private.json'))
    return {'status':'pass','method_candidates':6,'positive_partial':5,'limited_empty_only':1,
        'native_requests':{'B':12,'C':17},'new_fixture_dml':0,'ddl':0,
        'full_natural_cross_engine_equal_claimed':False,'wide_precision_p0_closed':False,
        'sql_fingerprint_gate_closed':False}
if __name__=='__main__':
    if len(sys.argv)!=3:raise SystemExit('usage: verify_catalog.py B-private-evidence C-private-evidence')
    verify_private_manifest(sys.argv[1]);verify_private_manifest(sys.argv[2])
    print(json.dumps(verify(sys.argv[1],sys.argv[2]),ensure_ascii=False,indent=2))
