#!/usr/bin/env python3
"""Offline verification; natural responses and source values stay private."""
import collections,copy,decimal,json,pathlib,re,sys
CASES=['DISHES','RECIPE_INFO','HARDWARE','CHECK_MENU','VERSION_LIST','VERSION_READ','CONSUME_LIST','VERSION_MISSING','VERSION_PAGE2']
VERSIONS=['VERSION_LIST','VERSION_READ','VERSION_PAGE2','VERSION_MISSING']
DATE_SHA='4749b69e8e4de3cfd0c05eb4be1a9cf182a7a22182d7c020a9cd48c60f7e44e3'
VERSION_SHA='36b6016d6c4bd6a12bbe0bf2c0a483775303c5b22d1791dcb0fa6832b07738b9'
MONEY=set('weight total_energy energy total_carbohydrate carbohydrate total_protein protein total_fat fat total_va total_ve total_dietary_fiber total_retinol total_vb1 total_vb2 total_vpp total_sodium total_calcium total_iron total_vc total_tenieratene total_potassium total_magnesium total_manganese total_zinc total_cuprum total_phosphorus total_selenium'.split())
def need(v,label):
    if not v:raise AssertionError(label)
def strict(a,b,p='$'):
    need(type(a) is type(b),'type '+p)
    if isinstance(a,dict):
        need(a.keys()==b.keys(),'fields '+p)
        for k in a:strict(a[k],b[k],p+'.'+k)
    elif isinstance(a,list):
        need(len(a)==len(b),'length '+p)
        for i,(x,y) in enumerate(zip(a,b)):strict(x,y,p+'['+str(i)+']')
    else:need(a==b,'value '+p)
def load(red,green,runtime):
    def read(p,f):return json.loads((pathlib.Path(p)/f).read_text())
    return {'red':{k:read(red,f) for k,f in [('report','report-private.json'),('responses','responses-private.json'),('source','expected-verified-private.json'),('readback','readback-verified-private.json')]},
            'green':{k:read(green,f) for k,f in [('report','report-private.json'),('responses','responses-private.json'),('source','expected-private.json'),('readback','readback-report-private.json')]},
            'version':{k:read(runtime,f) for k,f in [('report','version-final-report-private.json'),('responses','version-final-responses-private.json'),('source','version-final-source-private.json')]},
            'cleanup':read(runtime,'cleanup-report-private.json'),'time_meta':read(runtime,'version-date-metadata-private.json')}
def original_red_values(expected,actual,case,p='$'):
    if type(expected) is str and type(actual) in (int,float):
        field=p.rsplit('.',1)[-1]
        allowed=(case=='DISHES' and p.startswith('$.dishes[') and field in {'weight','power','carbohydrate','protein','fat'}) or (case=='RECIPE_INFO' and field in MONEY)
        need(allowed,'only known red decimal field '+p);need(decimal.Decimal(expected)==decimal.Decimal(str(actual)),'red original-table exact value '+p);return 1
    need(type(expected) is type(actual),'red other type '+p)
    if isinstance(expected,dict):
        need(expected.keys()==actual.keys(),'red fields '+p);return sum(original_red_values(v,actual[k],case,p+'.'+k) for k,v in expected.items())
    if isinstance(expected,list):
        need(len(expected)==len(actual),'red length '+p);return sum(original_red_values(a,b,case,p+'['+str(i)+']') for i,(a,b) in enumerate(zip(expected,actual)))
    need(expected==actual,'red value '+p);return 0
def events_ok(events,ends_count):
    ends=[e for e in events if e['kind']=='request_end'];qs=[e for e in events if e['kind']=='query_observed']
    need(len(ends)==ends_count and all(e['http_status']==200 and not e['fatal'] for e in ends),'actual HTTP endings')
    need(all(e['operation'] not in ('insert','update','delete','merge','replace') and e['callsites'] for e in qs),'observed read boundary')
    warnings=[e for e in qs if e['fingerprint_lexical_warning']]
    need(all(e['operation']=='other' for e in warnings),'unknown rendered SQL stays unknown')
    return {'raw_operations':dict(collections.Counter(e['operation'] for e in qs)),'unknown_rendered_sql':len(warnings),'unknown_fingerprint_counts':dict(collections.Counter(e['fingerprint'] for e in warnings))}
def verify_lane(lane,x):
    rd=x['red'];gr=x['green'];vr=x['version'];cleanup=x['cleanup'];n=0
    for stage in (rd,gr):
        r=stage['report'];need(r['lane']==lane and r['unexpected'] is None,'lane/clean');need([c['case'] for c in r['cases']]==CASES,'nine ordered scenarios')
        need(r['source_unchanged'] and r['source_before']==r['source_after'] and r['natural_user_unchanged'] and r['http_dml']==0,'source unchanged')
        need(r['boundary']['private_source_reflection'] and r['boundary']['file_cache'] and r['boundary']['redis_disabled'] and r['boundary']['external_stubs'] and not r['boundary']['real_queue'],'actual private boundary')
        need(r['boundary']['original_debug'] is False and r['boundary']['diagnostic_query_observation_debug'] is True,'explicit observation limitation')
        observed=events_ok(r['events'],9);need(observed['unknown_rendered_sql']==(11 if lane=='C' else 0),'frozen observation warnings not expanded')
        need(stage['readback']['errors']==[] and stage['readback']['new_http']==stage['readback']['database_writes']==0,'independent source oracle')
        for name in CASES:
            resp=stage['responses'][name];body=resp['body'];need(resp['method']=='POST' and resp['http_status']==200,'method/status')
            need(body.keys()=={'code','message','data'} and type(body['code']) is int and body['code']==0,'exact original envelope')
            if name=='HARDWARE':need(type(body['message']) is bool,'original hardware boolean message')
            else:need(body['message']==({'DISHES':'获取成功','RECIPE_INFO':'获取成功','CHECK_MENU':'查询成功','CONSUME_LIST':'查询成功'}.get(name,'')) and type(body['message']) is str,'original message')
            if stage is rd and lane=='C':n+=original_red_values(stage['source'][name],body['data'],name)
            else:strict(stage['source'][name],body['data'],name+'.original-table')
    need(gr['report']['source_sha256']['application/p/logic/DateMenu.php']==DATE_SHA,'real green DateMenu SHA')
    need([c['case'] for c in vr['report']['cases']]==VERSIONS and vr['report']['unexpected'] is None,'four version requests')
    need(vr['report']['version_natural_unchanged'] and vr['report']['before']==vr['report']['after'] and vr['report']['actor_unchanged'] and vr['report']['new_actor_rows']==vr['report']['database_writes']==0,'version boundary')
    need(vr['report']['source_sha256']['application/p/logic/Equipment.php']==VERSION_SHA,'real final Equipment SHA');need(events_ok(vr['report']['events'],4)['unknown_rendered_sql']==(4 if lane=='C' else 0),'frozen final observation warnings')
    for name in VERSIONS:
        resp=vr['responses'][name];need(resp['method']=='POST' and resp['http_status']==200 and type(resp['body']['code']) is int and resp['body']['code']==0 and resp['body']['message']=='','version envelope');need(resp['body'].keys()=={'code','message','data'},'version envelope fields');strict(vr['source'][name]['expected'],resp['body']['data'],name+'.version-source')
        c=next(c for c in vr['report']['cases'] if c['case']==name);need(c['source_pass'] and c['difference'] is None,'version source flag')
        rows=resp['body']['data'];rows=rows['row'] if name in ('VERSION_LIST','VERSION_PAGE2') else ([] if rows is None else [rows])
        need(all(type(r['create_time']) is str and re.fullmatch(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}',r['create_time']) for r in rows),'actual second-format wire')
    need(cleanup['independent_new_connection'] and cleanup['port_not_listening'] and not cleanup['state_file_exists'] and cleanup['cache_files_remaining']==0 and cleanup['owned_actor_unchanged'] and cleanup['owned_actor_funds_zero'] and cleanup['natural_user_unchanged'] and cleanup['natural_user_before']==cleanup['natural_user_after_excluding_exact_owned_pk'] and cleanup['database_writes']==0,'independent cleanup/retention')
    actor=cleanup['owned_actor_retained'];need(type(actor['id']) is int and actor['id']>0 and re.fullmatch('CDXDMENU10'+lane+'[A-F0-9]{8}',actor['marker']) and re.fullmatch('[A-F0-9]{8}(?:-[A-F0-9]{4}){3}-[A-F0-9]{12}',actor['uuid']) and type(actor['type']) is int and type(actor['status']) is int and actor['type']==actor['status']==0,'exact retained disabled actor')
    positive=[c['case'] for c in gr['readback']['cases'] if c['nonempty']];need(all(c['pass'] and c['difference'] is None for c in gr['readback']['cases']),'all nine final source assertions')
    return {'red_decimal_type_differences':n,'red_http':9,'decimal_green_http':9,'version_green_http':4,'source_pass_final_nine':9,'positive_cases':positive,'natural_user':cleanup['natural_user_before'],'retained_actor':actor,'http_dml':0,'fixture_user_inserts':1,'funds_unchanged_zero':True,'private_resources_cleared':True,'green9_query_operations':events_ok(gr['report']['events'],9),'final4_query_operations':events_ok(vr['report']['events'],4)}
def verify_loaded(b,c):
    lb=verify_lane('B',b);lc=verify_lane('C',c);need(lc['red_decimal_type_differences']>0,'real decimal red')
    strict(b['version']['responses']['VERSION_MISSING']['body'],c['version']['responses']['VERSION_MISSING']['body'],'cross-engine original missing UUID')
    return {'status':'pass','full_cross_engine_natural_responses_equal':False,'methods':[
        {'method':'POST','path':p,'coverage':'dual-engine-nonempty-partial' if name not in ('HARDWARE','CHECK_MENU') else 'B-nonempty-C-empty-limited'} for name,p in [('DISHES','/p/dateMenu/getDishes'),('RECIPE_INFO','/p/dateMenu/getRecipeInfo'),('HARDWARE','/p/date_menu/hardwareInfo'),('CHECK_MENU','/p/dateMenu/checkMenuList'),('VERSION_LIST','/p/equipment/VersionList'),('VERSION_READ','/p/equipment/versionRead'),('CONSUME_LIST','/p/equipment/consumeList')]],
        'dual_engine_positive_methods':5,'limited_methods':2,'lanes':{'B':lb,'C':lc},'global_wide_decimal_p0_closed':False,'sql_fingerprint_classification_gate_closed':False,'natural_ids_or_business_values_normalized':False}
if __name__=='__main__':
    try:
        need(len(sys.argv)==7,'six private red/green/runtime paths required');print(json.dumps(verify_loaded(load(*sys.argv[1:4]),load(*sys.argv[4:7])),ensure_ascii=False,indent=2))
    except Exception as e:print(json.dumps({'status':'fail','class':type(e).__name__,'reason':str(e)}));raise SystemExit(1)
