#!/usr/bin/env python3
import copy,hashlib,html,importlib.util,json,re
from decimal import Decimal
from pathlib import Path
ROOT=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('frozen_goods_read',ROOT.parent/'parallel-ai-goods-printer-read8-20261011/compare.py');base=importlib.util.module_from_spec(spec);spec.loader.exec_module(base)
META=json.loads((ROOT/'structure-B.json').read_text())['tables'];base.META=META;equal=base.equal
def file(row):
 d=base.cast(row,'upload_file');url='http://terminal.goods-write.invalid/uploads/'+row['file_path'];d['preview_url']=d['external_url']=url;return d
base.file=file
def only(o,table,gid):return [r for r in o[table] if int(r['goods_id'])==gid]
def projection(w,gid):
 o=w['original'];rows=only(o,'goods',gid);assert len(rows)==1;without=copy.deepcopy(o);without['goods_sku']=[];g=base.goods(rows[0],without,basic=True)
 for row in only(o,'goods_sku',gid):
  x=base.cast(row,'goods_sku');x['goods_props']=json.loads(row['goods_props']) if row['goods_props'] else None;x['spec_value_ids']=json.loads(row['spec_value_ids']) if row['spec_value_ids'] else None;x['image']=None if int(row['image_id'])==0 else file(base.pick(o,'upload_file','file_id',row['image_id']));g['skuList'].append(x)
 g['skuList'].sort(key=lambda x:x['id']);return {'detail':g}
def physical(row,expected,table):
 assert set(row)==set(x['column_name'] for x in META[table]['columns'])
 for c in META[table]['columns']:
  k=c['column_name'];a=row[k]
  if k not in expected:assert k in ('id','goods_id','create_time','update_time');assert int(a)>0;continue
  e=expected[k];t=c['data_type']
  if 'int' in t:assert int(a)==int(e),(table,k,a,e)
  elif t in ('decimal','double','float'):assert Decimal(str(a))==Decimal(str(e)),(table,k,a,e)
  else:equal(a,e,table+'.'+k)
def expected_goods(form):
 d={c['column_name']:c['column_default'] for c in META['goods']['columns'] if c['column_name'] not in ('goods_id','create_time','update_time')};d.update({k:v for k,v in form.items() if k in d});d.update({'store_id':2147001112,'goods_price_min':form['goods_price'],'goods_price_max':form['goods_price'],'line_price_min':form['line_price'],'line_price_max':form['line_price'],'stock_total':form['stock_num'],'alone_grade_equity':'[]','delivery_type':json.dumps(form['delivery_type'],separators=(',',':')),'content':html.escape(form['content'],quote=True)});return d
def validate_effects(w,c,pre,gid,templates):
 b=w['before'];a=w['original'];equal(w['protect_before'],w['protect_after'],'natural/default');equal(w['protect_after'],pre['protect'],'sealed natural/default');baseline=pre['baseline']
 for t,rows in baseline.items():
  pk='goods_id' if t=='goods' else 'id' if t in ('goods_sku','goods_image') else None
  if pk:
   ids={r[pk] for r in rows};equal([r for r in a[t] if r[pk] in ids],rows,'historical.'+t)
  else:equal(a[t],rows,'all_unchanged.'+t)
 form=copy.deepcopy(templates['add']['form']);stage=c['stage'];changed=stage not in ('ADD','READ_ADD')
 if changed:form.update(templates['edit_changes_only'])
 g=only(a,'goods',gid);assert len(g)==1 and g[0]['goods_no']==templates['marker'];want=expected_goods(form)
 if stage=='STATE_UP':want['status']=10
 if stage in ('DELETE','READ_DELETE'):want['is_delete']=1
 physical(g[0],want,'goods');assert int(g[0]['stock_total'])==0 and int(g[0]['store_id'])==2147001112
 sku=only(a,'goods_sku',gid);images=only(a,'goods_image',gid);assert len(sku)==len(images)==1
 sk={x['column_name']:x['column_default'] for x in META['goods_sku']['columns'] if x['column_name'] not in ('id','create_time','update_time')};sk.update({'goods_id':gid,'store_id':2147001112,'goods_price':form['goods_price'],'line_price':form['line_price'],'stock_num':0,'goods_weight':form['goods_weight']});physical(sku[0],sk,'goods_sku');physical(images[0],{'goods_id':gid,'image_id':int(pre['file']['file_id']),'store_id':2147001112},'goods_image')
 if c['method']=='GET':equal(a,b,'read_no_effect');assert not set(w['operations'])&{'insert','update','delete','merge','replace'};return
 if stage=='ADD':
  assert len(a['goods'])==len(b['goods'])+1 and not only(b,'goods',gid);assert len(a['goods_sku'])==len(b['goods_sku'])+1 and len(a['goods_image'])==len(b['goods_image'])+1
 else:
  old=only(b,'goods',gid)[0];allowed={'goods_name','selling_point','goods_price_min','goods_price_max','line_price_min','line_price_max','update_time'} if stage=='EDIT' else {'is_delete','update_time'} if stage=='DELETE' else {'status','update_time'}
  for k,v in old.items():
   if k not in allowed:equal(g[0][k],v,'goods_narrow_change.'+k)
  if stage=='EDIT':
   for t in ('goods_sku','goods_image'):assert len(a[t])==len(b[t]) and only(a,t,gid)[0]['id']!=only(b,t,gid)[0]['id']
  else:
   equal(a['goods_sku'],b['goods_sku'],'state_delete_sku_unchanged');equal(a['goods_image'],b['goods_image'],'state_delete_image_unchanged')
def normalize(body,w,gid,fileid):
 d=copy.deepcopy(body);d['trace_id']='VALIDATED_TRACE'
 def walk(v):
  if isinstance(v,dict):
   new=int(v.get('goods_id',0))==gid
   if new:
    table='goods_sku' if 'goods_sku_id' in v else 'goods_image' if 'file' in v else 'goods';raw=only(w['original'],table,gid)[0]
    for k in ('create_time','update_time'):
     if k in v:assert v[k]==base.date(raw[k]);v[k]='INDEPENDENT_ORIGINAL_VALIDATED_TIME'
    if 'id' in v:assert v['id']==int(raw['id']);v['id']='new.'+table
    v['goods_id']='new.goods'
   for k,x in list(v.items()):
    if k in ('file_id','image_id') and x!=0:assert type(x) is int and x==fileid;v[k]='own.file'
    else:walk(x)
  elif isinstance(v,list):
   for x in v:walk(x)
 walk(d);return d
def verify():
 source=None;normalized={};results={};count=0
 for lane in ('B','C'):
  p=ROOT/'private-evidence'/lane;pre=json.loads((p/'preflight.json').read_text());j=json.loads((p/'run-journal.json').read_text());assert j['complete'] and j['own_token_absent'];gid=j['new_goods_id'];templates=json.loads((ROOT/'request-values.json').read_text());cases={c['scenario']:c for c in j['cases']};out=[]
  for line in (p/'private-wire.jsonl').read_text().splitlines():
   w=json.loads(line);s=w['scenario'];c=cases[s];b=json.loads(w['raw']);count+=1;assert w['http_status']==200 and set(b)=={'status','message','data','trace_id'} and type(b['status']) is int and b['status']==200 and type(b['message']) is str and re.fullmatch('[0-9a-f]{13}',b['trace_id']);validate_effects(w,c,pre,gid,templates)
   equal(b['data'],projection(w,gid) if c['method']=='GET' else [],s+'.complete_original_DTO');equal(b['message'],'success' if c['method']=='GET' else {'ADD':'添加成功','EDIT':'更新成功','STATE_UP':'操作成功','STATE_DOWN':'操作成功','DELETE':'删除成功'}[c['stage']],s+'.message')
   if source is None:source=w['source_sha256']
   equal(w['source_sha256'],source,s+'.actual_source');normalized[(lane,s)]=normalize(b,w,gid,int(pre['file']['file_id']));out.append({'scenario':s,'method':c['method'],'path':c['path'],'status':200,'complete_original_DTO_and_effects':True,'raw_sha256':hashlib.sha256(w['raw'].encode()).hexdigest(),'elapsed_ms':w['elapsed_ms']})
  assert len(out)==8;results[lane]=out
 pairs=[]
 for lane,s in normalized:
  if lane=='B':equal(normalized[(lane,s)],normalized[('C',s)],s+'.full_BC_body');pairs.append(s)
 return {'HTTP':count,'complete_BC_body_pairs':pairs,'positive_source_and_effect_checks':count,'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')}))
