import json, pathlib, subprocess, sys, tempfile, shutil, hashlib
ROOT=pathlib.Path.cwd()/"quality/acceptance/auth-split-origin"
GEN=ROOT/"generate_test_bundle.py"; VER=ROOT/"verify.py"
def sha(b): return hashlib.sha256(b).hexdigest()
def rows(p): return [json.loads(x) for x in p.read_text().splitlines()]
def putrows(p,rs): p.write_text(''.join(json.dumps(x,sort_keys=True,separators=(',',':'),ensure_ascii=False)+'\n' for x in rs))
def putjson(p,x): p.write_text(json.dumps(x,sort_keys=True,separators=(',',':'),ensure_ascii=False)+'\n')
def reclose(run,pub):
    cases=rows(run/'cases.ndjson'); searches=rows(run/'sink-search.ndjson'); db=rows(run/'db-assertions.ndjson'); crash=rows(run/'crash-injection.ndjson')
    payload=sorted(p.relative_to(run).as_posix() for p in run.rglob('*') if p.is_file() and p.name not in {'summary.json','artifact-manifest.json','SHA256SUMS'})
    files=[{'path':n,'sha256':sha((run/n).read_bytes()),'bytes':(run/n).stat().st_size} for n in payload]
    putjson(run/'artifact-manifest.json',{'schema_version':'1.0.0','run_id':cases[0]['run_id'],'generated_at':'2026-08-23T00:00:00Z','files':files})
    af=[{**x,'producer_version':'auth-split-fixture-v1','case_ids':[c['case_id'] for c in cases]} for x in files]
    putjson(run/'summary.json',{'schema_version':'1.0.0','run_id':cases[0]['run_id'],'environment_valid':True,'production_equivalent':True,'counts':{'pass':len(cases)+len(searches)+len(db)+len(crash),'fail':0,'blocked':0},'verdict':'PASS','artifacts':af})
    retained=sorted(p.relative_to(run).as_posix() for p in run.rglob('*') if p.is_file() and p.name!='SHA256SUMS')
    (run/'SHA256SUMS').write_text(''.join(f'{sha((run/n).read_bytes())}  {n}\n' for n in retained))
    if pub.exists(): shutil.rmtree(pub)
    shutil.copytree(run,pub)
def mutate(name,run):
    cs=rows(run/'cases.ndjson'); ds=rows(run/'db-assertions.ndjson'); ss=rows(run/'sink-search.ndjson')
    if name=='missing-identity':
        i=next(i for i,c in enumerate(cs) if c['phase']!='crash'); cid=cs.pop(i)['case_id']; ds=[d for d in ds if d['case_id']!=cid]; putrows(run/'cases.ndjson',cs); putrows(run/'db-assertions.ndjson',ds)
    elif name=='duplicate-identity':
        c=dict(cs[0]); c['case_id']='case-duplicate-identity'; c['request_fixture_id']='request-duplicate'; c['oracle_id']='oracle-duplicate'; c['db_assertions']=['db-duplicate-identity']; cs.append(c); d=dict(ds[0]); d['case_id']=c['case_id']; d['assertion_id']='db-duplicate-identity'; ds.append(d); putrows(run/'cases.ndjson',cs); putrows(run/'db-assertions.ndjson',ds)
    elif name=='missing-ref': cs[0]['db_assertions']=['db-does-not-exist']; putrows(run/'cases.ndjson',cs)
    elif name=='duplicate-ref': cs[0]['db_assertions']=[cs[0]['db_assertions'][0],cs[0]['db_assertions'][0]]; putrows(run/'cases.ndjson',cs)
    elif name=='bad-outcome-hash': cs[0]['canonical_outcome_sha256']='1'*64; putrows(run/'cases.ndjson',cs)
    elif name=='bad-trace-hash': cs[0]['trace_sha256']='1'*64; putrows(run/'cases.ndjson',cs)
    elif name=='bad-evidence-hash': cs[0]['evidence_refs'][0]['sha256']='1'*64; putrows(run/'cases.ndjson',cs)
    elif name=='missing-db': ds=ds[1:]; putrows(run/'db-assertions.ndjson',ds)
    elif name=='duplicate-db': ds.append(dict(ds[0])); putrows(run/'db-assertions.ndjson',ds)
    elif name=='missing-sink': ss=ss[1:]; putrows(run/'sink-search.ndjson',ss)
    elif name=='duplicate-sink': ss.append(dict(ss[0])); putrows(run/'sink-search.ndjson',ss)
    elif name=='missing-marker': ss[0]['marker_hits']-=1; putrows(run/'sink-search.ndjson',ss)
    elif name=='duplicate-marker': ss[0]['marker_hits']+=1; putrows(run/'sink-search.ndjson',ss)
EXPECT={'missing-identity':'expected-identity-set-mismatch','duplicate-identity':'duplicate-expected-identity','missing-ref':'case-reference-closure','duplicate-ref':'schema:case-oracle.schema.json:ValidationError','bad-outcome-hash':'canonical-outcome-digest:case-001','bad-trace-hash':'trace-digest','bad-evidence-hash':'evidence-digest','missing-db':'case-reference-closure','duplicate-db':'duplicate-db-assertion','missing-sink':'enabled-probe-coverage-not-exact','duplicate-sink':'enabled-probe-coverage-not-exact','missing-marker':'schema:sink-search.schema.json:ValidationError','duplicate-marker':'marker-coverage-not-exact'}
base=pathlib.Path(tempfile.mkdtemp(prefix='auth-adversarial-'))
try:
 for rep in (1,2):
  run=base/f'baseline-{rep}'; pub=base/f'baseline-{rep}-publication'; subprocess.run([sys.executable,str(GEN),'--out',str(run),'--scenario','valid'],check=True,stdout=subprocess.DEVNULL)
  p=subprocess.run([sys.executable,str(VER),'--test-fixture','--input',str(run),'--manifest',str(run/'auth-sensitive-data-flow.json'),'--publication',str(pub)],text=True,capture_output=True)
  if p.returncode!=0 or 'FIXTURE_VERDICT=PASS' not in p.stdout: raise SystemExit(f'BASELINE_FAIL rep={rep} rc={p.returncode} out={p.stdout}')
  print(f'BASELINE rep={rep} PASS')
 for name,want in EXPECT.items():
  for rep in (1,2):
   run=base/f'{name}-{rep}'; pub=base/f'{name}-{rep}-publication'; subprocess.run([sys.executable,str(GEN),'--out',str(run),'--scenario','valid'],check=True,stdout=subprocess.DEVNULL); mutate(name,run); reclose(run,pub)
   p=subprocess.run([sys.executable,str(VER),'--test-fixture','--input',str(run),'--manifest',str(run/'auth-sensitive-data-flow.json'),'--publication',str(pub)],text=True,capture_output=True)
   out=p.stdout.strip()
   if p.returncode!=2 or f'reason={want}' not in out: raise SystemExit(f'ATTACK_FAIL name={name} rep={rep} want={want} rc={p.returncode} out={out}')
   print(f'ATTACK name={name} rep={rep} rc=2 reason={want}')
 print(f'ATTACK_SUITE=PASS attacks={len(EXPECT)} repetitions=2')
finally:
 shutil.rmtree(base,ignore_errors=True)
 print(f'SCRATCH_REMOVED={not base.exists()}')
