import copy,hashlib,json,re
from datetime import datetime
from pathlib import Path
from zoneinfo import ZoneInfo
ROOT=Path(__file__).resolve().parent;META=json.loads((ROOT/'structure-B.json').read_text())['tables'];TZ=ZoneInfo('Asia/Shanghai')
def equal(a,b,p='$'):
 if type(a) is not type(b):raise AssertionError(f'{p}: {type(a).__name__} != {type(b).__name__}')
 if isinstance(a,dict):
  assert a.keys()==b.keys(),(p,'keys',a.keys()^b.keys())
  for k in a:equal(a[k],b[k],p+'.'+k)
 elif isinstance(a,list):
  assert len(a)==len(b),(p,'length',len(a),len(b))
  for i,(x,y) in enumerate(zip(a,b)):equal(x,y,p+f'[{i}]')
 else:assert a==b,(p,'value mismatch',hashlib.sha256(json.dumps(a,ensure_ascii=False).encode()).hexdigest(),hashlib.sha256(json.dumps(b,ensure_ascii=False).encode()).hexdigest())
def no_password(v):
 if isinstance(v,dict):
  assert all(k.lower()!='password' for k in v),'Password field exposed'
  for x in v.values():no_password(x)
 elif isinstance(v,list):
  for x in v:no_password(x)
def cast(row,t):
 d={}
 for c in META[t]['columns']:
  k=c['column_name'];v=row[k]
  if t=='store_user' and k=='password':continue
  d[k]=None if v is None else datetime.fromtimestamp(int(v),TZ).strftime('%Y-%m-%d %H:%M:%S') if k in ('create_time','update_time') else int(v) if 'int' in c['data_type'] else v
 return d
def tree(rows,table,pk,parent=0,links=None):
 out=[]
 for row in rows:
  if int(row['parent_id'])!=parent:continue
  d=cast(row,table)
  if links is not None:
   relation=[cast(r,'store_menu_api') for r in links if int(r['menu_id'])==int(row[pk])];d['menuApi']=relation;d['apiIds']=[r['api_id'] for r in relation]
  children=tree(rows,table,pk,int(row[pk]),links)
  if children:d['children']=children
  out.append(d)
 return out
def expected(w,c,f):
 if c['expected_status']!=200:return []
 module=c['module'];name=c['name'];params=c['params'] or {}
 if module=='admin':
  n=w['natural']
  if name=='API_INDEX':return {'list':tree(n['store_api'],'store_api','api_id')}
  if name=='MENU_INDEX':return {'list':tree(n['store_menu'],'store_menu','menu_id',links=n['store_menu_api'])}
  rows=[r for r in n['store_menu'] if int(r['menu_id'])==params['menuId']];return {'info':cast(rows[0],'store_menu') if rows else None}
 tenant=2147001111 if module=='store' else 2147001112;rows=[r for r in w['original']['store_user'] if int(r['store_id'])==tenant and int(r['is_delete'])==0];assert len(rows)==1 and not w['original']['store_user_role'] and not w['original']['store_user_relation'] and not w['original']['store_role_menu']
 if name=='INFO':assert int(rows[0]['store_user_id'])==f['owned'][module];return {'userInfo':cast(rows[0],'store_user'),'roles':{'isSuper':1,'permissions':[]}}
 if params.get('user_type'):rows=[r for r in rows if int(r['user_type'])==params['user_type']]
 if name in ('LIST','CROSS_SEARCH'):
  if params.get('search'):rows=[r for r in rows if any(params['search'].lower() in r[k].lower() for k in ('user_name','real_name'))]
  rows.sort(key=lambda r:(int(r['sort']),-int(r['create_time'])));data=[]
  for r in rows:
   d=cast(r,'store_user');d.update({'roleIds':[],'roleNames':'','role':[],'users':[]});data.append(d)
  n=len(data);return {'list':{'total':n,'per_page':15,'current_page':1,'last_page':(n+14)//15,'data':data}}
 rows.sort(key=lambda r:(int(r['sort']),int(r['create_time'])));return {'list':[cast(r,'store_user') for r in rows]}
def normalize(body,f):
 d=copy.deepcopy(body);d['trace_id']='VALIDATED_TRACE';mapping={v:k+'.own_operator' for k,v in f['owned'].items()}
 def walk(x):
  if isinstance(x,dict):
   for k,v in list(x.items()):
    if k=='store_user_id':assert type(v) is int and v in mapping;x[k]=mapping[v]
    else:walk(v)
  elif isinstance(x,list):
   for v in x:walk(v)
 walk(d);return d
def verify():
 results={};norm={};source=None;full=0;total=0;naturalpasses=[]
 for lane in ('B','C'):
  p=ROOT/'private-evidence'/lane;f=json.loads((p/'fixture-journal.json').read_text());cases={c['scenario']:c for c in json.loads((p/'cases-NA.json').read_text())};out=[];seen=set()
  for line in (p/'private-wire.jsonl').read_text().splitlines():
   w=json.loads(line);s=w['scenario'];assert s not in seen;seen.add(s);c=cases[s];b=json.loads(w['raw']);total+=1;assert w['http_status']==200 and set(b)=={'status','message','data','trace_id'} and type(b['status']) is int and b['status']==c['expected_status'] and type(b['message']) is str and re.fullmatch('[0-9a-f]{13}',b['trace_id']);equal(b['message'],'success' if c['expected_status']==200 else '请先登录后再访问',s+'.independent_source_fixed_message');no_password(b)
   equal(w['before'],f['after'],s+'.before29');equal(w['original'],f['after'],s+'.after29');equal(w['natural'],f['natural_after'],s+'.natural_dictionaries_and_identity_hashes');equal(w['protect_before'],f['before_protect'],s+'.prior_scope');equal(w['protect_before'],w['protect_after'],s+'.prior_scope_unchanged');assert not set(w['operations'])&{'insert','update','delete','merge','replace'}
   if source is None:source=w['source_sha256'];equal(source,json.loads((ROOT/'source-main-sha.json').read_text())['files'],s+'.frozen_current_main_source')
   equal(w['source_sha256'],source,s+'.actual_source_identity');equal(b['data'],expected(w,c,f),s+'.complete_original_DTO');full+=1
   if c['module']=='admin' and c['name']!='MISSING_MENU':naturalpasses.append({'lane':lane,'scenario':s,'full_original_projection':True,'BC_equal_not_claimed':True})
   else:norm[(lane,s)]=normalize(b,f)
   out.append({'scenario':s,'method':'GET','path':c['path'],'status':b['status'],'full_original_DTO':True,'raw_sha256':hashlib.sha256(w['raw'].encode()).hexdigest(),'elapsed_ms':w['elapsed_ms']})
  assert len(seen)==13;results[lane]=out
 pairs=[]
 for lane,s in norm:
  if lane=='B':equal(norm[(lane,s)],norm[('C',s)],s+'.full_BC_body');pairs.append(s)
 assert len(pairs)==10 and len(naturalpasses)==6
 return {'HTTP':total,'complete_source_DTO_passes':full,'BC_own_and_boundary_full_body_pairs':pairs,'natural_positive_per_engine_not_BC_equal':naturalpasses,'business_reds':0,'source_sha256':source,'cases':results}
if __name__=='__main__':
 r=verify();(ROOT/'comparison.json').write_text(json.dumps(r,ensure_ascii=False,indent=2)+'\n');print(json.dumps({k:v for k,v in r.items() if k not in ('cases','source_sha256')},ensure_ascii=False))
