#!/usr/bin/env python3
import copy,hashlib,html,importlib.util,json,re
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from zoneinfo import ZoneInfo
ROOT=Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('strict_contract',ROOT.parent/'parallel-ai-goods-comment-20261011/compare.py');base=importlib.util.module_from_spec(spec);spec.loader.exec_module(base)
equal=base.equal;META=json.loads((ROOT/'structure-B.json').read_text())['tables'];TZ=ZoneInfo('Asia/Shanghai')
def date(v):return datetime.fromtimestamp(int(v),TZ).strftime('%Y-%m-%d %H:%M:%S')
def cast(row,table):
 out={}
 for c in META[table]['columns']:
  k=c['column_name'];v=row[k];t=c['data_type']
  out[k]=None if v is None else date(v) if k in ('create_time','update_time') else int(v) if 'int' in t else format(Decimal(str(v)),'.2f') if t=='decimal' else float(v) if t in ('double','float') else v
 return out
def pick(o,t,pk,v):
 rows=[r for r in o[t] if int(r[pk])==int(v)];assert len(rows)==1,(t,pk,v);return rows[0]
def file(row):
 d=cast(row,'upload_file');assert int(row['store_id'])==2147001112 and row['storage']=='local';url='http://terminal.goods-printer.invalid/uploads/'+row['file_path'];d['preview_url']=d['external_url']=url;return d
def goods(row,o,detail=False,basic=False):
 d=cast(row,'goods');d['alone_grade_equity']=json.loads(row['alone_grade_equity']) or [];d['delivery_type']=json.loads(row['delivery_type']) or [1,10,30];d['content']=html.unescape(row['content']);d['goods_sales']=int(row['sales_initial'])+int(row['sales_actual']);images=[]
 for r in o['goods_image']:
  if int(r['goods_id'])==int(row['goods_id']):
   x=cast(r,'goods_image');x['file']=file(pick(o,'upload_file','file_id',r['image_id']));images.append(x)
 images.sort(key=lambda r:r['id']);d['images']=images;d['goods_images']=[x['file'] for x in images];d['goods_image']=d['goods_images'][0]['preview_url'] if images else ''
 if basic or detail:
  d['skuList']=[]
  for r in o['goods_sku']:
   if int(r['goods_id'])==int(row['goods_id']):
    x=cast(r,'goods_sku');x['goods_props']=json.loads(r['goods_props']) or [];x['spec_value_ids']=json.loads(r['spec_value_ids']) or [];x['image']=file(pick(o,'upload_file','file_id',r['image_id']));d['skuList'].append(x)
  d['skuList'].sort(key=lambda r:r['id']);assert int(row['video_id'])==int(row['video_cover_id'])==0;d['video']=d['videoCover']=None
 else:d.pop('content')
 if detail:
  assert not o['goods_category_rel'] and not o['goods_service_rel'] and int(row['spec_type'])==10;d['categoryIds']=[];d['serviceIds']=[];d['isSpecLocked']=False
 return d
def printer(row):
 d=cast(row,'printer');d['printer_config']=json.loads(row['printer_config']);return d
def expected(w,c):
 o=w['original'];p=c['params'] or {};name=c['name'];tenant=2147001111 if c['module']=='store' else 2147001112
 if c['method']=='POST':return []
 if name.startswith('GOODS_'):
  rows=[r for r in o['goods'] if int(r['store_id'])==tenant and int(r['is_delete'])==0]
  if name in ('GOODS_DETAIL','GOODS_BASIC'):
   r=pick(o,'goods','goods_id',p['goodsId']);assert int(r['store_id'])==tenant;return {'goodsInfo' if name=='GOODS_DETAIL' else 'detail':goods(r,o,name=='GOODS_DETAIL',True)}
  if name=='GOODS_LIST_IDS':
   rows=[r for gid in p['goodsIds'] for r in rows if int(r['goods_id'])==gid];return {'list':[goods(r,o) for r in rows]}
  assert name=='GOODS_LIST';rows=[r for r in rows if p.get('goodsName','') in r['goods_name']];rows.sort(key=lambda r:(int(r['sort']),-int(r['goods_id'])));n=len(rows);return {'list':{'total':n,'per_page':15,'current_page':1,'last_page':(n+14)//15,'data':[goods(r,o) for r in rows]}}
 rows=[r for r in o['printer'] if int(r['store_id'])==tenant and int(r['is_delete'])==0];rows=[r for r in rows if p.get('search','') in r['printer_name'] and (not p.get('printer_type') or r['printer_type']==p['printer_type'])];rows.sort(key=lambda r:(int(r['sort']),int(r['create_time'])));data=[printer(r) for r in rows]
 if name=='PRINTER_ALL':return {'list':data}
 n=len(data);return {'list':{'total':n,'per_page':15,'current_page':1,'last_page':(n+14)//15,'data':data}}
def effects(w,c):
 before=w['before'];after=w['original'];equal(w['protect_before'],w['protect_after'],'natural/default');assert w['protect_after']['printer_natural_count']==0
 for table,rows in before.items():
  if c['method']=='GET' or table!='printer':equal(after[table],rows,'effect.'+table)
 if c['method']=='GET':assert not any(op in ('insert','update','delete','replace','merge') for op in w['operations']);return
 tenant=2147001111 if c['module']=='store' else 2147001112;old={int(r['printer_id']):r for r in before['printer']};new={int(r['printer_id']):r for r in after['printer']};action=c['path'].rsplit('/',1)[1]
 if action=='add':
  assert set(old)<set(new) and len(new)==len(old)+1;added=new[next(iter(set(new)-set(old)))];assert int(added['store_id'])==tenant and int(added['is_delete'])==0
  for pk,r in old.items():equal(new[pk],r,'add.existing')
  equal({k:printer(added)[k] for k in c['form']},c['form'],'add.exact_form');assert set(added)==set(x['column_name'] for x in META['printer']['columns']);assert int(added['create_time'])>0 and int(added['update_time'])>0
 else:
  assert new.keys()==old.keys();pk=c['params']['printerId'];assert int(old[pk]['store_id'])==tenant
  for i,r in old.items():
   if i!=pk:equal(new[i],r,'update.other_row')
  allowed={'update_time','is_delete'} if action=='delete' else set(c['form'])|{'update_time'}
  for k,v in old[pk].items():
   if k not in allowed:equal(new[pk][k],v,'update.unlisted.'+k)
  if action=='delete':assert int(new[pk]['is_delete'])==1
  else:equal({k:printer(new[pk])[k] for k in c['form']},c['form'],'edit.exact_form')
def normalize(body,w,fixture):
 b=copy.deepcopy(body);b['trace_id']='VALIDATED_TRACE';ids=fixture['owned']['terminal'];maps={'goods_id':{ids['goods']:'terminal.goods'},'file_id':{ids['upload_file']:'terminal.file'},'image_id':{ids['upload_file']:'terminal.file'}}
 def walk(d):
  if isinstance(d,dict):
   if 'printer_id' in d:
    r=pick(w['original'],'printer','printer_id',d['printer_id']);assert d['create_time']==date(r['create_time']) and d['update_time']==date(r['update_time']);d['printer_id']=('store' if int(r['store_id'])==2147001111 else 'terminal')+'.printer';d['create_time']=d['update_time']='INDEPENDENT_ORIGINAL_VALIDATED_TIME'
   if 'id' in d:
    table='goods_sku' if 'goods_sku_id' in d else 'goods_image' if 'file' in d and 'image_id' in d else None;assert table and d['id']==ids[table];d['id']='terminal.'+table
   for k,x in list(d.items()):
    if k in maps:assert type(x) is int and x in maps[k];d[k]=maps[k][x]
    else:walk(x)
  elif isinstance(d,list):
   for x in d:walk(x)
 walk(b);return b
def verify():
 normalized={};reports={};http=0;success=0;toolreds=[];source=None
 for lane in ('B','C'):
  d=ROOT/'private-evidence'/lane;fixture=json.loads((d/'fixture-journal.json').read_text());cases={c['scenario']:c for p in d.glob('cases-*.json') for c in json.loads(p.read_text())};output=[]
  for line in (d/'private-wire.jsonl').read_text().splitlines():
   w=json.loads(line);s=w['scenario'];c=cases[s];b=json.loads(w['raw']);http+=1;assert w['http_status']==200 and set(b)=={'status','message','data','trace_id'} and type(b['status']) is int and type(b['message']) is str and re.fullmatch('[0-9a-f]{13}',b['trace_id']);effects(w,c)
   if source is None:source=w['source_sha256']
   equal(w['source_sha256'],source,s+'.actual_source');red=lane=='B' and s.startswith('PILOT_GP_READ_') and c['auth'] and b['status']==401
   if red:assert c['name'] in ('PRINTER_ALL','CROSS_GOODS','MISSING_GOODS','PRINTER_FILTER_EMPTY');equal(b['data'],[],s+'.tool_auth_data');toolreds.append(s)
   else:
    assert b['status']==c['expected_status'],s
    if b['status']==200:
     equal(b['data'],expected(w,c),s+'.full_source_projection');success+=1
     if c['name']!='PRINTER_FILTER_EMPTY':assert b['data'] or c['method']=='POST',s+'.nonempty'
    else:equal(b['data'],[],s+'.error_data')
    normalized[(lane,s)]=normalize(b,w,fixture)
   output.append({'scenario':s,'method':c['method'],'path':c['path'],'status':b['status'],'tool_cache_race_red':red,'original_full_projection':not red and b['status']==200,'raw_sha256':hashlib.sha256(w['raw'].encode()).hexdigest(),'elapsed_ms':w['elapsed_ms']})
  reports[lane]=output
 pairs=[]
 for (lane,s),b in normalized.items():
  if lane!='C':continue
  peer=s
  if s in ('PILOT_GP_READ_TERMINAL_PRINTER_ALL','PILOT_GP_READ_TERMINAL_CROSS_GOODS','PILOT_GP_READ_TERMINAL_MISSING_GOODS','PILOT_GP_READ_STORE_PRINTER_FILTER_EMPTY'):peer=s.replace('_READ_','_RECOVERY_')
  equal(normalized[('B',peer)],b,s+'.full_BC_body');pairs.append({'B':peer,'C':s})
 assert len(toolreds)==4 and len(pairs)==18
 return {'HTTP':http,'positive_full_original_projections_including_empty_boundary':success,'full_BC_pairs':pairs,'tool_cache_race_reds':toolreds,'business_reds':0,'source_sha256':source,'cases':reports}
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','full_BC_pairs')},ensure_ascii=False))
