#!/usr/bin/env python3
"""Actual metadata HTTP contracts: audited IDs/time only, no URL/type/order coercion."""
import argparse
from copy import deepcopy
from datetime import datetime
import importlib.util
import json
from pathlib import Path
import re
from zoneinfo import ZoneInfo

p=argparse.ArgumentParser();p.add_argument('b_runtime');p.add_argument('c_runtime');args=p.parse_args()
module=Path(__file__).resolve().parents[3]/'work/2026-09-28-smart-canteen-dm8-poc/http-harness/strict_response_compare.py'
spec=importlib.util.spec_from_file_location('strict',module);strict=importlib.util.module_from_spec(spec);spec.loader.exec_module(strict)
br,b,bm,bs=[strict.load_json(Path(args.b_runtime)/n) for n in ['report.json','responses.json','fixture_map.json','source-scenarios.json']]
cr,c,cm,cs=[strict.load_json(Path(args.c_runtime)/n) for n in ['report.json','responses.json','fixture_map.json','source-scenarios.json']]
suffix=['GROUP_ADD_PARENT','GROUP_ADD_CHILD','GROUP_LIST','GROUP_CYCLE_REJECT','GROUP_EDIT','GROUP_LIST_EDITED',
    'FILES_LIST','FILES_EDIT','FILES_LIST_RENAMED','MOVE_SINGLE','FILES_SINGLE_PARENT','MOVE_MULTIPLE',
    'FILES_CHILD_MULTIPLE','MOVE_UNGROUPED','FILES_UNGROUPED','FILES_IMAGE_FILTER','FILES_CLEANED','GROUPS_CLEANED']
expected={f'{m}_{s}' for m in ['S','T'] for s in suffix}
if set(b)!=expected or set(c)!=expected:raise SystemExit('Required 36 scenarios absent')
if br['source_sha256']!=cr['source_sha256']:raise SystemExit('Frozen source differs')
for r in [br,cr]:
    if r['unexpected_error'] is not None or r['failures'] or not r['natural_unchanged'] or any(r['independent_remaining'].values()) or not r['token_cleaned'] or r['storage_guard_attempts']:
        raise SystemExit('Source/audit/storage scope/cleanup gate failed')
    if r['cleanup']!={'files':4,'groups':4}:raise SystemExit('Unexpected fixture expansion or cleanup')
    if len(r['list_environment_guards'])!=14:raise SystemExit('Every Files/list needs actual-container proof')
    for guard in r['list_environment_guards']:
        if not guard['allowed'] or not guard['actual_new_root'] or guard['actual_version']!='0.0.1' or not guard['below_2_0_7'] or any(guard['deletion_targets_exist'].values()) or guard['version_sha256']!='c4183782e45ec544e34be623297955ef183135347f8b43c944d3579bd6a94e78':
            raise SystemExit('Unsafe list environment cannot be counted')
if any(bm[k]!=cm[k] for k in ['marker','tenant']):raise SystemExit('Synthetic input context differs')
rule={'$["trace_id"]':{'type':'string','pattern':'[0-9a-f]{13}','reason':'Validated distinct framework trace','require_distinct':True}}
def shape(v):
    kind=strict.kind(v)
    if kind=='object':return {'type':'object','properties':{k:shape(x) for k,x in sorted(v.items())} if v else []}
    if kind=='array':return {'type':'array','items':[shape(x) for x in v]}
    return {'type':'NULL' if kind=='null' else kind}
def normalize(name,body,mapping,sources):
    result=deepcopy(body);group_ids={value:index+101 for index,(_,value) in enumerate(sorted(mapping['groups'].items()))};file_ids={value:index+1001 for index,(_,value) in enumerate(sorted(mapping['files'].items()))}
    records=sources.get(name,{})
    file_rows={int(r['file_id']):r for r in records.get('files',[])}
    group_rows={int(r['group_id']):r for r in records.get('groups',[])}
    def owned_id(value,ids,zero=False):
        if strict.kind(value)!='number':raise ValueError('ID type changed, cannot map')
        if zero and value==0:return 0
        if value not in ids:raise ValueError('ID has no independently owned identity')
        return ids[value]
    def times(row,source):
        for field in ['create_time','update_time']:
            stamp=int(source[field]);expected=datetime.fromtimestamp(stamp,ZoneInfo('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S') if stamp>0 else ''
            if row[field]!=expected or strict.kind(row[field])!='string':raise ValueError('Date differs from independent source')
            row[field]='2000-01-01 00:00:00' if expected else ''
    def walk(value):
        if isinstance(value,list):
            for part in value:walk(part)
        elif isinstance(value,dict):
            if 'file_id' in value:
                ident=value['file_id'];source=file_rows.get(ident)
                if source is None or value['file_name']!=source['file_name'] or value['file_path']!=source['file_path'] or not value['file_path'].startswith(mapping['marker']):raise ValueError('File source ownership differs')
                times(value,source);value['file_id']=owned_id(ident,file_ids);value['group_id']=owned_id(value['group_id'],group_ids,True)
            elif 'group_id' in value:
                ident=value['group_id'];source=group_rows.get(ident)
                if source is None or value['name']!=source['name'] or not value['name'].startswith(mapping['marker']):raise ValueError('Group source ownership differs')
                times(value,source);value['group_id']=owned_id(ident,group_ids);value['parent_id']=owned_id(value['parent_id'],group_ids,True)
            for part in value.values():walk(part)
    walk(result);return result
cases=[]
for name in sorted(expected):
    bc=next(x for x in br['cases'] if x['scenario']==name);cc=next(x for x in cr['cases'] if x['scenario']==name)
    if any(bc[k]!=cc[k] for k in ['method','path','expected_business_status','positive_rows']):raise SystemExit('Case semantics differ')
    for response in [b[name],c[name]]:
        if response['http_status']!=200 or response['body'].get('status')!=bc['expected_business_status']:raise SystemExit('Unexpected HTTP/business status')
        if re.fullmatch('[0-9a-f]{13}',response['body'].get('trace_id','')) is None:raise SystemExit('Trace malformed')
        if response.get('wire_shape')!=shape(response['body']):raise SystemExit('Original wire {} / [] proof inconsistent')
    diffs=strict.compare(normalize(name,b[name]['body'],bm,bs),normalize(name,c[name]['body'],cm,cs),rules=rule)
    wire=strict.compare(b[name]['wire_shape'],c[name]['wire_shape'])
    cases.append({'scenario':name,'method':bc['method'],'path':bc['path'],'expected_business_status':bc['expected_business_status'],
        'source_projection_equal_both':bc['source_equal'] and cc['source_equal'],'positive_rows':bc['positive_rows'],
        'strict_differences':len(diffs),'wire_shape_differences':len(wire),'difference_paths_and_types':[{'path':p,'mysql_type':l,'dm_type':r} for p,l,r,_,_ in diffs]})
def runtime(r):
    e=[x for x in r['events'] if x.get('kind')=='query_observed']
    return {'observations':len(e),'fingerprints':len({x['fingerprint'] for x in e}),
        'operations':{op:sum(x['operation']==op for x in e) for op in sorted({x['operation'] for x in e})},
        'callsites':sorted({p for x in e for p in x['callsites']}),'lexical_warnings':sum(bool(x.get('lexical_warning')) for x in e)}
result={'date':'2026-10-10','exact_method_path_keys':12,'scenario_pairs':36,'strict_equal_pairs':sum(not x['strict_differences'] for x in cases),
    'wire_equal_pairs':sum(not x['wire_shape_differences'] for x in cases),'cases':cases,'source_sha256':br['source_sha256'],
    'audit':{lane:{k:r[k] for k in ['before','after','natural_unchanged','cleanup','independent_remaining','token_cleaned','storage_guard_attempts','list_environment_guards']} for lane,r in [('B',br),('C',cr)]},
    'runtime':{'B':runtime(br),'C':runtime(cr)},'host_header':br['host_header'],
    'normalization':'Source-audited generated file/group/parent IDs and own timestamps, plus valid distinct trace; no URL/name/value/type/array-order normalization',
    'scope':'Only 12 metadata methods under actual version 0.0.1/env guard; no upload/storage/delete or >=2.0.7 vendor cleanup acceptance'}
print(json.dumps(result,indent=2,ensure_ascii=False,default=str));raise SystemExit(any(x['strict_differences'] or x['wire_shape_differences'] for x in cases))
