#!/usr/bin/env python3
import base64, copy, hashlib, json, pathlib, re
from decimal import Decimal, ROUND_HALF_UP
HERE=pathlib.Path(__file__).resolve().parent
count=0
def need(ok, why):
    global count
    count+=1
    if not ok: raise AssertionError(why)
def same(a,b):
    if type(a) is not type(b): return False
    if isinstance(a,dict): return a.keys()==b.keys() and all(same(a[k],b[k]) for k in a)
    if isinstance(a,list): return len(a)==len(b) and all(same(x,y) for x,y in zip(a,b))
    return a==b
def rounded(v):
    # These frozen own records are all positive and integral after rounding.
    return int(v.quantize(Decimal('1'),rounding=ROUND_HALF_UP))
def expected(w):
    src=w['source_before']; staff=src['staff'][0]; recs={r['uuid']:r for r in src['recommend']}; dishes={r['uuid']:r for r in src['dishes']}
    stage=w['action'].startswith('get_stage_'); nutrient='nutrient' in w['action']; typ=w['params']['type']
    data={}
    rows=[]
    if w['kind']!='missing':
        field='carbohydrate' if nutrient else 'energy'; perday={}
        for m in src['menu']:
            if int(m['meal_type'])!=3: continue
            r=recs[m['recommend_uuid']]; d=dishes[m['dishes_uuid']]
            perday.setdefault(m['menu_date'],{'sum':Decimal(0),'rec':r})
            perday[m['menu_date']]['sum']+=Decimal(str(m['number']))*Decimal(str(d[field]))
        val='carbohydrate_weight' if nutrient else 'total_power'; pre='prescription_carbohydrate' if nutrient else 'prescription_power'
        if stage:
            actual=[d['sum']+Decimal(str(d['rec'][pre])) for d in perday.values()]
            heli=sum(90 <= (d['sum']+Decimal(str(d['rec'][pre])))/Decimal(str(d['rec'][val]))*100 <=110 for d in perday.values())
            rows=[{'name':staff['name'],'head_img':'','avg':rounded(sum(actual)/len(actual)),'heli_day':heli,'heli_day_zhanbi':rounded(Decimal(heli)/len(actual)*100)}]
        else:
            d=perday[w['params']['menu_data']]; total=d['sum']+Decimal(str(d['rec'][pre])); tui=Decimal(str(d['rec'][val]))
            rows=[{'real':rounded(total),'tui':rounded(tui),'chazhi':rounded(total)-rounded(tui),'czd':rounded(total/tui*100),'name':staff['name'],'head_img':''}]
    data['return_data']=rows
    if stage: data.update({'title':'相对合理人数' if typ=='1' else '需要改善人数','color':'#91CEA1' if typ=='1' else '#19BEF3'})
    elif nutrient: data.update({'title':'需减少摄入量人数' if typ=='1' else '需增加摄入量人数：','title_two':'摄入充足度＞110%' if typ=='1' else '摄入充足度＜90%','color':'#F29197' if typ=='1' else '#E9C738'})
    else: data.update({'title':'摄入偏高人数' if typ=='1' else '摄入过高人数','title_two':'110%＜摄入充足度≤120%' if typ=='1' else '摄入充足度>120%','color':'#F29197'})
    return {'code':0,'message':'获取信息成功','data':data}
records={};drifts={};native_count=0
for lane in ('B','C'):
    p=HERE/'private'/lane
    attempts=json.loads((p/'http-attempts.json').read_text())
    need(len(attempts)==12,lane+' exact HTTP12')
    need(all(a['complete'] for a in attempts),lane+' completed')
    attempts={a['label']:a for a in attempts}
    wires=sorted(p.glob('wire-*.json'));need(len(wires)==12,lane+' exact native wires12')
    journal=json.loads((p/'fixture-journal.json').read_text())
    need(journal['committed'] and len(journal['events'])==6,lane+' fixture6 commit')
    for ev in journal['events']: need(ev['success'] and ev['rowCount']==1,lane+' insert exact1')
    records[lane]={}
    for f in wires:
        w=json.loads(f.read_text());label=w['label'];body=w['body']
        need(w['status']==200,lane+label+' status')
        need(w['source_unchanged'] and same(w['source_before'],w['source_after']),lane+label+' own exact')
        need(hashlib.sha256(body.encode()).hexdigest()==w['body_sha256'],lane+label+' body SHA')
        need(base64.b64decode(attempts[label]['body_base64'])==body.encode(),lane+label+' real HTTP body')
        got=json.loads(body);want=expected(w);need(same(got,want),lane+label+' full independent DTO/types')
        report_exec=[]
        for e in w['native']:
            native_count+=1
            if e['layer']=='actual PDO.prepare': need(e['prepared'],lane+label+' prepared')
            elif e['layer']=='actual PDOStatement.execute':
                need(e['success'],lane+label+' executed')
                sql=e['queryString'];need(not re.search(r'\b(?:INSERT|UPDATE|DELETE|ALTER|DROP|CREATE|MERGE)\b',sql,re.I),lane+label+' readonly')
                if sql.startswith('CALL '):
                    need(sql=='CALL SP_TABLEDEF(?, ?)',lane+label+' exact metadata')
                    binds=e['bound'];need(binds['1']['value']=='ZHCT_APP' and binds['1']['type']==2 and binds['2']['type']==2,lane+label+' metadata twoSTR')
                    need(binds['2']['value'].lower() in ('ydy_identity_type','ydy_config','ydy_menu_recommend','ydy_date_menu','ydy_dishes','ydy_staff'),lane+label+' metadata table')
                elif sql.startswith('SELECT '):
                    if 'LEFT JOIN' in sql:
                        report_exec.append(e)
                        need(w['params']['uuid'] in sql or any(v['value']==w['params']['uuid'] for v in e['bound'].values()),lane+label+' exact UUID SQL')
                        need('2026-10-08' in sql or any(v['value']=='2026-10-08' for v in e['bound'].values()),lane+label+' exact date SQL')
                    elif 'USER_TAB_COLUMNS' in sql or 'USER_CONSTRAINTS' in sql:
                        need(sql in ('SELECT COLUMN_NAME,DATA_TYPE,NULLABLE,DATA_DEFAULT FROM USER_TAB_COLUMNS WHERE TABLE_NAME=? ORDER BY COLUMN_ID',"SELECT cc.COLUMN_NAME FROM USER_CONSTRAINTS c JOIN USER_CONS_COLUMNS cc ON c.CONSTRAINT_NAME=cc.CONSTRAINT_NAME WHERE c.TABLE_NAME=? AND c.CONSTRAINT_TYPE='P'"),lane+label+' exact original metadata SELECT')
                        need(e['bound']['1']['type']==2 and e['bound']['1']['value'].lower() in ('ydy_identity_type','ydy_config','ydy_menu_recommend'),lane+label+' metadata exact STR table')
                    else: need('ydy_config' in sql or 'ydy_identity_type' in sql,lane+label+' ctor dictionary only')
                else: need(sql.startswith('SHOW '),lane+label+' session metadata only')
        need(len(report_exec)==1,lane+label+' exact report query1')
        # The actual DM PDO reports -1 for every SELECT before fetching; preserve it.
        need(report_exec[0]['rowCount']==(-1 if lane=='C' else (0 if w['kind']=='missing' else 3)),lane+label+' original native rowCount convention')
        need(w['last_error'] is None,lane+label+' no PHP error')
        records[lane][label]=body
    before=json.loads((p/'batch-before.json').read_text());after=json.loads((p/'batch-after.json').read_text())
    drifts[lane]={k:{'before':before['sentinel'][k],'after':after['sentinel'][k]} for k in before['sentinel'] if before['sentinel'][k]!=after['sentinel'][k]}
    before.pop('sentinel');after.pop('sentinel');need(same(before,after),lane+' final own/actor/config/identity exact')
need(records['B']==records['C'],'B/C full wire body bytes equal all12')
mutations=0
w=json.loads((HERE/'private/B/wire-get_day_teampower_popout-positive.json').read_text());want=expected(w)
for field in ('real','tui','chazhi','czd','name','head_img'):
    bad=copy.deepcopy(want);bad['data']['return_data'][0].pop(field);need(not same(bad,want),'missing DTO field '+field);mutations+=1
for field,value in (('real','600'),('tui',600.0),('czd',True),('name','wrong-owner'),('head_img','https://invalid.example/')):
    bad=copy.deepcopy(want);bad['data']['return_data'][0][field]=value;need(not same(bad,want),'wrong DTO type/value '+field);mutations+=1
for field,value in (('title',''),('title_two',''),('color','#000'),('return_data',[])):
    bad=copy.deepcopy(want);bad['data'][field]=value;need(not same(bad,want),'constant/nonempty '+field);mutations+=1
for key in ('code','message','data'):
    bad=copy.deepcopy(want);bad.pop(key);need(not same(bad,want),'missing top '+key);mutations+=1
summary={'status':'PASS_FOUR_METHODS_PARTIAL_SCENARIOS_ONLY','HTTP':24,'wire':24,'actual_native_events':native_count,'assertions':count,'DTO_mutations_rejected':mutations,'full_BC_body_pairs':12,'fixture_INSERT_per_lane':6,'business_DML_DDL':0,'source_unchanged_all_cases':True,'sentinel_drift':drifts,'sentinel_drift_not_attributed_to_this_readonly_batch':True}
print(json.dumps(summary,ensure_ascii=False,indent=2))
