#!/usr/bin/env python3
"""Private evidence mutation tests. All generated copies are removed automatically."""
import copy,hashlib,json,pathlib,shutil,sys,tempfile
import verify_catalog as v
def main(b,c):
    v.verify(b,c);checks=1;failed=[]
    def reject(label,lane,file,change,wire_case=None,report='green-report-private.json'):
        nonlocal checks
        with tempfile.TemporaryDirectory(prefix='cdx-catalog-mut-') as tmp:
            targets={}
            for key,source in [('B',b),('C',c)]:
                target=pathlib.Path(tmp)/key;shutil.copytree(source,target);targets[key]=target
            path=targets[lane]/file;data=json.loads(path.read_bytes());change(data);raw=json.dumps(data,ensure_ascii=False,separators=(',',':')).encode();path.write_bytes(raw)
            if wire_case:
                rp=targets[lane]/report;r=json.loads(rp.read_bytes());case=next(x for x in r['cases'] if x['case']==wire_case);case['wire_bytes']=len(raw);case['wire_sha256']=hashlib.sha256(raw).hexdigest();rp.write_text(json.dumps(r))
            checks+=1
            try:v.verify(targets['B'],targets['C'])
            except (AssertionError,KeyError,ValueError,TypeError):return
            failed.append(label)
    reject('string decimal becomes number','C','raw-green-ORDERS_SOURCE-private.json',lambda x:x['data']['data'][0].__setitem__('pay_price',1.25),'ORDERS_SOURCE')
    reject('date zero fraction returns','C','raw-green-FACE_1-private.json',lambda x:next(r for r in x['resultObject']['data'] if r['createTime']).__setitem__('createTime','2026-10-10 12:00:00.000000'),'FACE_1')
    reject('bool replaces integer','B','raw-DISHES_1-private.json',lambda x:x.__setitem__('code',False),'DISHES_1','report-private.json')
    reject('empty object replaces array','B','raw-CARD_MISSING-private.json',lambda x:x.__setitem__('data',{}),'CARD_MISSING','report-private.json')
    reject('face list order changed','C','raw-green-FACE_1-private.json',lambda x:x['resultObject']['data'].reverse(),'FACE_1')
    reject('response field missing','C','raw-green-CARD_SOURCE-private.json',lambda x:x['data'].pop('cash_balance'),'CARD_SOURCE')
    reject('unexpected response field','B','raw-ROTATION_SOURCE-private.json',lambda x:x['data'].__setitem__('unexpected',1),'ROTATION_SOURCE','report-private.json')
    reject('source digest changed','C','green-after-private.json',lambda x:x['ORDERS_SOURCE'].__setitem__('source_digest','0'*64))
    reject('source projection mutated','C','green-after-private.json',lambda x:x['CARD_SOURCE']['source_sets']['user'][0].__setitem__('name','mutant'))
    reject('credential column leaked in actor','C','green-spec-private.json',lambda x:x[0]['before']['source_sets']['actor'][0].__setitem__('pwd','disabled'))
    reject('actor no longer disabled','C','green-spec-private.json',lambda x:x[0]['before']['source_sets']['actor'][0].__setitem__('status',1))
    reject('old source SHA used','C','green-report-private.json',lambda x:x.__setitem__('source_sha256',v.ORIGINAL))
    reject('wrong actual method','B','report-private.json',lambda x:x['cases'][0].__setitem__('method','GET'))
    reject('wrong actual route','B','spec-private.json',lambda x:x[0].__setitem__('path','/alias/not-real'))
    reject('case omitted','C','green-report-private.json',lambda x:x['cases'].pop())
    reject('positive turned empty','C','green-report-private.json',lambda x:next(r for r in x['cases'] if r['case']=='FACE_1').__setitem__('positive',False))
    reject('source window not stable','C','green-report-private.json',lambda x:x['cases'][0].__setitem__('source_window_unchanged',False))
    reject('known red erased','C','report-private.json',lambda x:next(r for r in x['cases'] if r['case']=='ORDERS_SOURCE').__setitem__('source_pass',True))
    reject('initial native failures erased','C','native-stats-private.json',lambda x:x['stages']['catalog-original'].__setitem__('failures',0))
    reject('native requests dropped','C','native-stats-private.json',lambda x:x.__setitem__('requests',16))
    reject('HTTP fatal hidden','C','green-report-private.json',lambda x:x['cases'][0].__setitem__('fatal',True))
    reject('DML observed','C','green-report-private.json',lambda x:x.__setitem__('http_dml',1))
    reject('external gate lost','B','boundary-private.json',lambda x:x.__setitem__('external_stubs',False))
    reject('legacy fallback allowed','C','boundary-private.json',lambda x:x.__setitem__('legacy_ai_fallback_disabled',False))
    reject('worker remains','C','cleanup-private.json',lambda x:x.__setitem__('worker_state_exists',True))
    reject('port remains','B','cleanup-private.json',lambda x:x.__setitem__('port_listening',True))
    reject('cache remains','B','cleanup-private.json',lambda x:x.__setitem__('private_cache_files_remaining',1))
    reject('token remains','C','cleanup-private.json',lambda x:x.__setitem__('token_artifacts_remaining',1))
    print(json.dumps({'tests':checks,'failures':failed,'database_http_operations':0,'private_mutation_copies_removed':True}));return bool(failed)
if __name__=='__main__':
    if len(sys.argv)!=3:raise SystemExit('usage: test_verify_catalog.py B-private-evidence C-private-evidence')
    raise SystemExit(main(pathlib.Path(sys.argv[1]),pathlib.Path(sys.argv[2])))
