#!/usr/bin/env python3
"""Existing private HTTP evidence only. Auto time mapping is explicit/opt-in."""
import collections,copy,datetime,decimal,json,pathlib,re,sys
ZONE=datetime.timezone(datetime.timedelta(hours=8))
RED=[t+'_'+c for t in ('S','T') for c in ('EMPTY','ADD','LIST','DETAIL','EDIT','LIST_EDITED','DETAIL_EDITED','DELETE','SOFT_DELETED')]
GREEN=[t+'_'+c for t in ('S','T') for c in ('RETAINED_DETAIL','LIST','DETAIL','DELETE','EMPTY')]
OLD_SHA={'app/store/model/Coupon.php':'49fd20285d9c80a21f520a7a0c7651387a616cbcd50a0ae8e44473409cc75a8b','app/terminal/model/Coupon.php':'8b873c302b548856d0ad9e1cba92dd3f3ec01b240083e9392e33abe6727bf5d9'}
NEW_SHA={'app/store/model/Coupon.php':'c3707430deb8d3777020e559c1565836df68300ed8997782408b9325319797b7','app/terminal/model/Coupon.php':'c0cdd68be2172412e7e651a2f435458ae1f283f5b518b00e8c0c009255dfb5cd'}
def check(ok,label):
    if not ok:raise AssertionError(label)
def strict(a,b,path='$'):
    check(type(a) is type(b),'type '+path)
    if isinstance(a,dict):
        check(a.keys()==b.keys(),'fields '+path)
        for k in a:strict(a[k],b[k],path+'.'+k)
    elif isinstance(a,list):
        check(len(a)==len(b),'array length '+path)
        for i,(x,y) in enumerate(zip(a,b)):strict(x,y,path+'['+str(i)+']')
    else:check(a==b,'value '+path)
def normalized(body,coupon_id,allow_time):
    out=copy.deepcopy(body)
    def walk(v):
        if isinstance(v,dict):
            for k,x in list(v.items()):
                if k=='trace_id':check(type(x) is str and re.fullmatch('[a-f0-9]{13}',x),'trace type/format');v[k]='<REQUEST_TRACE>'
                elif k=='coupon_id':check(type(x) is int and x==coupon_id,'original owned ID');v[k]='<OWNED_COUPON_ID>'
                elif allow_time and k in ('create_time','update_time'):
                    check(type(x) is str and re.fullmatch(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}',x),'automatic time wire type/format');v[k]='<SOURCE_VERIFIED_AUTO_'+k+'>'
                else:walk(x)
        elif isinstance(v,list):
            for x in v:walk(x)
    walk(out);return out
def load(red,green):
    out={}
    for name,prefix,root in [('red','','red'),('green','green-','green')]:
        path=red if name=='red' else green
        out[name]={k:json.loads((path/(prefix+f)).read_text()) for k,f in [('report','report.json'),('responses','responses.json')]}
        source=path/(prefix+'source-owned.json')
        if not source.exists():source=path/(prefix+'source-private.json')
        out[name]['source']=json.loads(source.read_text())
    out['ids']=json.loads((green/'fixture_map.json').read_text())['coupon_ids'];return out
def verify_lane(lane,x):
    for stage,expected in [('red',RED),('green',GREEN)]:
        r=x[stage]['report'];check(r['lane']==lane and r['unexpected'] is None,'clean '+stage)
        check([c['case'] for c in r['cases']]==expected,'all ordered '+stage+' cases')
        check(r['natural_unchanged'] and r['before']==r['after'] and r['token_cleaned'] and r['external_attempts']==0,'natural/resource boundary '+stage)
        check(len(r['retained_owned'])==2 and all(v['is_delete']==1 and v['store_id']==1930501010 for v in r['retained_owned']),'two retained owned softdeleted '+stage)
        sha=OLD_SHA if stage=='red' else NEW_SHA
        check(all(r['source_sha256'].get(f)==s for f,s in sha.items()),'physical serializer '+stage)
        bad=[c['case'] for c in r['cases'] if not c['source_pass']]
        want=[] if lane=='B' or stage=='green' else [t+'_'+c for t in ('S','T') for c in ('LIST','DETAIL','LIST_EDITED','DETAIL_EDITED')]
        check(bad==want,'exact source red/green failures '+stage)
        events=r['events'];ends=[e for e in events if e['kind']=='request_end'];q=[e for e in events if e['kind']=='query_observed'];check(len(ends)==len(expected) and all(e['http_status']==200 and not e['fatal'] for e in ends),'actual HTTP ends '+stage)
        writes=collections.Counter(e['operation'] for e in q if e['operation'] in ('insert','update','delete','replace','merge'))
        check(writes==({'insert':2,'update':4} if stage=='red' else {'update':2}),'formal HTTP writes only '+stage)
        check(all(e['callsites'] and not e['fingerprint_lexical_warning'] for e in q),'observed sources '+stage)
    for name in [t+'_'+c for t in ('S','T') for c in ('LIST','DETAIL','LIST_EDITED','DETAIL_EDITED')]:
        src=x['red']['source'][name];data=x['red']['responses'][name]['body']['data'];actual=copy.deepcopy(data.get('detail',data.get('list',{}).get('data',[None])[0]))
        if lane=='C':
            for field in ('reduce_price','min_price','write_off_limit_amount'):
                check(type(actual[field]) is float,'known red decimal number only')
                check(decimal.Decimal(str(actual[field]))==decimal.Decimal(src['expected'][field]),'red decimal original-table value')
                actual[field]=src['expected'][field]
        strict(src['expected'],actual,name+'.red original-table except known amount types')
    # Every compared live row is strictly checked against independent original-table wire expectation.
    for name,src in x['green']['source'].items():
        body=x['green']['responses'][name]['body'];actual=body['data'].get('detail',body['data'].get('list',{}).get('data',[None])[0]);strict(src['expected'],actual,name+'.source')
        module='store' if name.startswith('S_') else 'terminal';raw=src['rows'][0];check(type(raw['coupon_id']) is int and raw['coupon_id']==x['ids'][module] and raw['store_id']==1930501010,'owned raw identity')
        for field in ('create_time','update_time'):
            check(type(raw[field]) is int,'raw auto time integer');expected=datetime.datetime.fromtimestamp(raw[field],ZONE).strftime('%Y-%m-%d %H:%M:%S');check(actual[field]==expected,'source auto time getter')
        tag=name[0];add=x['red']['source'][tag+'_ADD']['rows'][0];window=x['red']['responses'][tag+'_ADD']['request_time_window'];check(window[0]<=raw['create_time']<=window[1] and raw['create_time']==add['create_time'],'generated create_time request window')
        update_windows=[x['red']['responses'][tag+'_EDIT']['request_time_window'],x['red']['responses'][tag+'_DELETE']['request_time_window']]
        check(any(w[0]<=raw['update_time']<=w[1] for w in update_windows),'generated update_time request window')
    check(x['green']['report']['no_new_coupon_insert'] and x['green']['report']['no_physical_delete'],'reuse not create/delete')
def compare_loaded(b,c,allow_time=False):
    verify_lane('B',b);verify_lane('C',c);pairs=[];type_red=0
    for name in RED:
        if name.split('_',1)[1] in ('LIST','DETAIL','LIST_EDITED','DETAIL_EDITED'):
            br=b['red']['responses'][name]['body']['data'];cr=c['red']['responses'][name]['body']['data'];br=br.get('detail',br.get('list',{}).get('data',[None])[0]);cr=cr.get('detail',cr.get('list',{}).get('data',[None])[0])
            diffs=[k for k in br if type(br[k]) is not type(cr.get(k))];check(diffs==['reduce_price','min_price','write_off_limit_amount'],'only known three decimal type red');type_red+=len(diffs)
        else:
            module='store' if name.startswith('S_') else 'terminal';strict(normalized(b['red']['responses'][name]['body'],b['ids'][module],False),normalized(c['red']['responses'][name]['body'],c['ids'][module],False),name)
    for name in GREEN:
        module='store' if name.startswith('S_') else 'terminal';br=b['green']['responses'][name];cr=c['green']['responses'][name];strict(br['method'],cr['method']);strict(br['path'],cr['path']);strict(br['http_status'],cr['http_status']);strict(normalized(br['body'],b['ids'][module],allow_time),normalized(cr['body'],c['ids'][module],allow_time),name)
        pairs.append({'case':name,'method':br['method'],'path':br['path'],'strict_same_after_explicit_mapping':True})
    return {'status':'pass','mapping':{'own_id':True,'request_trace':True,'source_verified_auto_times':allow_time},'red_pairs':18,'red_type_differences':type_red,'green_pairs':10,'green_pass':10,'methods_observed':10,'pairs':pairs,'lanes':{l:{'red_natural':x['red']['report']['before'],'green_natural':x['green']['report']['after'],'retained_owned':x['green']['report']['retained_owned'],'source_sha256':x['green']['report']['source_sha256'],'natural_unchanged':True,'user_coupon_unchanged':True,'external_attempts':0,'token_cleaned':True} for l,x in [('B',b),('C',c)]},'limitations':['original automatic clock values differ, not literal equality','field serializer scope only, not wide DECIMAL recovery','retained own soft-deleted rows; no physical cleanup','terminal actual backend tested, no client/physical device proof','no issuance/use/payment/integration tests']}
if __name__=='__main__':
    try:
        args=sys.argv[1:];allow='--map-owned-auto-time' in args;args=[x for x in args if x!='--map-owned-auto-time'];check(len(args)==4,'four private red/green directories required');print(json.dumps(compare_loaded(load(pathlib.Path(args[0]),pathlib.Path(args[1])),load(pathlib.Path(args[2]),pathlib.Path(args[3])),allow),ensure_ascii=False,indent=2))
    except Exception as e:print(json.dumps({'status':'fail','class':type(e).__name__,'reason':str(e)}));raise SystemExit(1)
