import copy,datetime as dt,json,re
from decimal import Decimal
from pathlib import Path
TZ=dt.timezone(dt.timedelta(hours=8))
P=Path(__file__).resolve().parent
def eq(a,b,path='$'):
    if type(a) is not type(b): raise AssertionError(f'{path}: type {type(a).__name__} != {type(b).__name__}')
    if isinstance(a,dict):
        if a.keys()!=b.keys(): raise AssertionError(f'{path}: keys {sorted(a)} != {sorted(b)}')
        for k in a:eq(a[k],b[k],f'{path}.{k}')
    elif isinstance(a,list):
        if len(a)!=len(b):raise AssertionError(f'{path}: length {len(a)} != {len(b)}')
        for i,(x,y) in enumerate(zip(a,b)):eq(x,y,f'{path}[{i}]')
    elif a!=b:raise AssertionError(f'{path}: {a!r} != {b!r}')
def dto(table,row,schema):
    out={}
    for c in schema[table]['columns']:
        k=c['column_name'];v=row[k];t=c['data_type'].lower()
        if v is None:out[k]=None
        elif k in ('create_time','update_time'):out[k]=dt.datetime.fromtimestamp(int(v),TZ).strftime('%Y-%m-%d %H:%M:%S')
        elif 'int' in t:out[k]=int(v)
        elif t in ('decimal','numeric'):out[k]=str(Decimal(str(v)).quantize(Decimal('.01')))
        else:out[k]=v
    return out
def userdto(row,original,host,schema,grade=False):
    u=dto('user',row,schema)
    fs=[f for f in original['upload_file'] if int(f['file_id'])==int(row['avatar_id'])]
    assert len(fs)==1
    f=dto('upload_file',fs[0],schema);assert f['storage']=='local' and f['file_type']==10
    url='http://'+host+'/uploads/'+fs[0]['file_path']
    f['preview_url']=url;f['external_url']=url
    u['avatar_url']=url;u['avatar']=f
    if grade:u['grade']=None
    return u
def expected(w,schema):
    c=w['case'];o=w['original'];p=c['params'];n=c['name'];host=c['host']
    if not c['auth']:return {'status':401,'message':'请先登录后再访问','data':[]}
    if c['path']=='/terminal/recommend.Recommend/foodCategory':
        names=['粗杂粮','薯类','米面制品','深色蔬菜','浅色蔬菜','水果','奶类','豆类','坚果','海产品','畜禽类','其他类']
        return {'status':200,'message':'success','data':[{'id':i+1,'name':v} for i,v in enumerate(names)]}
    if c['name']=='FEEDBACK_LIST_DIAGNOSTIC':
        users={int(u['user_id']):u for u in o['user']}
        rows=[]
        start=int(dt.datetime.strptime(p['start_time'],'%Y-%m-%d').replace(tzinfo=TZ).timestamp())
        end=int(dt.datetime.strptime(p['end_time'],'%Y-%m-%d').replace(tzinfo=TZ).timestamp())+86400
        for r in o['user_feedback']:
            u=users.get(int(r['user_id']))
            # Actual ThinkPHP associative where syntax compiles this legacy nickname predicate as IN('like', nickname).
            if u and u['nick_name'] in ('like',p['nickname']) and int(r['cate'])==p['cate'] and int(r['status'])==p['status'] and start<=int(r['create_time'])<end:
                row=dto('user_feedback',r,schema);row.update(mobile=u['mobile'],nick_name=u['nick_name']);rows.append(row)
        rows.sort(key=lambda r:(r['status'],r['create_time']))
        assert rows
        return {'status':200,'message':'success','data':{'list':{'total':len(rows),'per_page':15,'current_page':1,'last_page':1,'data':rows}}}
    if c['path']=='/store/dishes.Dishes/all':return None
    if n.startswith('FEEDBACK'):
        rows=[r for r in o['user_feedback'] if int(r['id'])==int(p['id'])]
        if not rows:data=[]
        else:
            assert len(rows)==1;row=rows[0];data=dto('user_feedback',row,schema);us=[u for u in o['user'] if int(u['user_id'])==int(row['user_id'])];assert len(us)==1
            data.update(nick_name=us[0]['nick_name'],mobile=us[0]['mobile'],status_cn={0:'待处理',1:'已处理',2:'暂不处理',3:'已关闭'}[int(row['status'])],cate_cn={0:'',1:'APP使用疑问',2:'食物与热量',3:'Bug反馈',4:'APP优化建议',5:'功能异常',6:'其他'}[int(row['cate'])],images=json.loads(row['images']) if row['images'] else [])
        return {'status':200,'message':'success','data':data}
    if n.startswith('USERS'):
        rows=[r for r in o['user'] if int(r['is_delete'])==0 and (p.get('search','') in r['nick_name'] or p.get('search','') in r['mobile']) and int(r['gender'])==int(p['gender'])]
        rows.sort(key=lambda r:int(r['create_time']),reverse=True)
        project=lambda r:userdto(r,o,host,schema,True)
    else:
        points=n.startswith('POINTS');table='user_points_log' if points else 'user_balance_log';uid=int(p['userId' if points else 'user_id']);rows=[r for r in o[table] if int(r['user_id'])==uid]
        us={int(r['user_id']):r for r in o['user']}
        rows=[r for r in rows if int(r['user_id']) in us and p.get('search','') in us[int(r['user_id'])]['nick_name']]
        if not points and p.get('scene',0)>0:rows=[r for r in rows if int(r['scene'])==int(p['scene'])]
        if p.get('betweenTime'):
            a,b=[int(dt.datetime.strptime(x,'%Y-%m-%d').replace(tzinfo=TZ).timestamp()) for x in p['betweenTime']]
            rows=[r for r in rows if a<=int(r['create_time'])<b+86400]
        rows.sort(key=lambda r:(-int(r['create_time']),int(r['log_id'])))
        def project(r):
            x=dto(table,r,schema);x['user']=userdto(us[int(r['user_id'])],o,host,schema);return x
    total=len(rows);page=int(p.get('page',1));last=(total+14)//15;shown=[project(r) for r in rows[(page-1)*15:page*15]]
    data={'list':{'total':total,'per_page':15,'current_page':min(page,last) if last else 1,'last_page':last,'data':shown}}
    return {'status':200,'message':'success','data':data}
def verify_wire(w,schema,source):
    eq(w['before'],w['original'],'physical before/after')
    assert w['http_status']==200
    for f,h in w['source_sha256'].items():assert source[f]==h,f
    path=w['case']['path'];module=path.split('/')[1];controller,action=path.split('/')[2:];relative='app/'+module+'/controller/'+controller.replace('.','/')+'.php'
    required={'cores/BaseController.php',relative}
    if w['case']['auth']:
        if path!='/terminal/recommend.Recommend/foodCategory':required.add('cores/BaseModel.php')
        if '/market.Points/' in path:required.update({'app/'+module+'/model/user/PointsLog.php','app/common/model/user/PointsLog.php'})
        elif '/user.Balance/' in path:required.update({'app/'+module+'/model/user/BalanceLog.php','app/common/model/user/BalanceLog.php'})
        elif '/User/list' in path:required.update({'app/'+module+'/model/User.php','app/common/model/User.php'})
        elif '/user.UserFeedback/' in path:required.update({'app/store/service/user/UserFeedback.php','app/store/model/user/UserFeedback.php','app/common/model/user/UserFeedback.php'})
        elif action=='foodCategory':required.update({'app/terminal/service/recommend/Recommend.php','app/common/enum/dishes/FoodCategory.php'})
    assert required<=w['source_sha256'].keys(),f'Missing actual source paths: {required-w["source_sha256"].keys()}'
    body=json.loads(w['raw']);assert set(body)=={'status','message','data','trace_id'}
    assert type(body['status']) is int and type(body['message']) is str and re.fullmatch('[0-9a-f]{13}',body['trace_id'])
    body.pop('trace_id');want=expected(w,schema)
    if want is None:
        assert body['status'] in (200,500)
        if w['case']['name']=='DISHES_ALL_DIAGNOSTIC':assert body=={'status':500,'message':'method not exist:think\\db\\Query->getAll','data':[]}
        return {'category':'diagnostic','body':body}
    eq(body,want)
    if w['case']['kind']=='positive':assert body['data'] and (not isinstance(body['data'],dict) or not 'list' in body['data'] or body['data']['list']['data'])
    return {'category':'positive' if w['case']['name']=='FEEDBACK_LIST_DIAGNOSTIC' else w['case']['kind'],'body':body}
def verify(directory,structure,source):
    out=[]
    for p in sorted(Path(directory).glob('wire-PILOT_LF_*.json')):
        w=json.loads(p.read_text())
        try:r=verify_wire(w,structure['tables'],source['files']);out.append({'scenario':w['scenario'],'pass':True,'category':r['category']})
        except Exception as e:out.append({'scenario':w['scenario'],'pass':False,'error':str(e)})
    return out
if __name__=='__main__':
    result={l:verify(P/'private-evidence'/l,json.loads((P/f'structure-{l}.json').read_text()),json.loads((P/'source-frozen-sha.json').read_text())) for l in 'BC'}
    (P/'comparison-original.json').write_text(json.dumps(result,ensure_ascii=False,indent=2)+'\n')
    print(json.dumps({l:{'total':len(r),'pass':sum(x['pass'] for x in r),'fail':[x for x in r if not x['pass']]} for l,r in result.items()},ensure_ascii=False))
