#!/usr/bin/env python3
"""Schema and adversarial fail-closed tests; never production/browser evidence."""
import json, os, pathlib, subprocess, sys, importlib.util, tempfile, shutil
root=pathlib.Path(__file__).resolve().parent
spec=importlib.util.spec_from_file_location('auth_split_verify',root/'verify.py'); verifier=importlib.util.module_from_spec(spec); spec.loader.exec_module(verifier)
try:
 import jsonschema
except ImportError:
 print('SELFTEST=FAIL reason=jsonschema-validator-unavailable'); raise SystemExit(1)
schemas={p.name:json.loads(p.read_text()) for p in sorted((root/'schemas').glob('*.schema.json'))}
for name,schema in schemas.items():
 try: jsonschema.Draft202012Validator.check_schema(schema)
 except Exception as e: print(f'SELFTEST=FAIL schema={name} error={type(e).__name__}'); raise SystemExit(1)
print(f'SCHEMAS=PASS count={len(schemas)} draft=2020-12 checker=jsonschema-draft202012')
# Deliberately malformed and partial objects must be rejected by the real validators.
adversarial={
 'environment-input.schema.json':[{}, {'schema_version':'1.0.0','production_equivalent':False}],
 'case-oracle.schema.json':[{}, {'schema_version':'1.0.0','route':'/auth/unknown'}, {'canonical_outcome_sha256':'0'*64,'trace_ref':'trace','trace_sha256':'0'*64,'evidence_refs':[]}],
 'db-assertions.schema.json':[{}, {'schema_version':'1.0.0','case_id':'unrelated','expected_delta':{},'observed_delta':{}}],
 'sink-search.schema.json':[{}, {'schema_version':'1.0.0','marker_hits':0,'marker_classes':[]}],
 'redaction-manifest.schema.json':[{}, {'schema_version':'1.0.0','entries':[]}],
 'artifact-manifest.schema.json':[{}, {'schema_version':'1.0.0','files':[{'path':'../escape','sha256':'0'*64,'bytes':0}]}],
 'summary.schema.json':[{}, {'schema_version':'1.0.0','verdict':'PASS','counts':{'pass':999,'fail':0,'blocked':0}}]
}
for name,values in adversarial.items():
 v=jsonschema.Draft202012Validator(schemas[name],format_checker=jsonschema.FormatChecker())
 for i,value in enumerate(values):
  if v.is_valid(value): print(f'SELFTEST=FAIL accepted={name}:{i}'); raise SystemExit(1)
ids=verifier.expected_identities()
if not ids or len(ids)!=len(set(ids)):
 print('SELFTEST=FAIL expected-identities-not-closed'); raise SystemExit(1)
missing=set(ids); missing.pop()
if missing==ids or set([*ids,*ids])==ids and len([*ids,*ids])==len(ids):
 print('SELFTEST=FAIL identity-attack-undetected'); raise SystemExit(1)
print(f'IDENTITIES=PASS exact={len(ids)} missing-and-duplicate-negative=2')
p=subprocess.run([sys.executable,str(root/'verify.py'),'--input',str(root/'fixtures/self-test/absent-production'),'--manifest',str(root/'fixtures/self-test/absent-production/auth-sensitive-data-flow.json'),'--publication',str(root/'fixtures/self-test/absent-publication')],text=True,capture_output=True)
if p.returncode!=2 or not p.stdout.startswith('VERDICT=BLOCKED'):
 print('SELFTEST=FAIL fail_closed=0'); raise SystemExit(1)
print('ADVERSARIAL=PASS malformed=5 partial=5')
print('FAIL_CLOSED=PASS verifier_verdict=BLOCKED exit=2')
# Deterministic, visibly non-production fixtures exercise the whole public verifier.
factory=root/'generate_test_bundle.py'
with tempfile.TemporaryDirectory(prefix='auth-split-selftest-') as td:
 base=pathlib.Path(td)
 for scenario,expected_rc,reason in (
  ('valid',0,'FIXTURE_VERDICT=PASS'),
  ('fabricated-minimal',2,'expected-identity-set-mismatch'),
  ('disconnected-graph',2,'redaction-graph-root-count'),
 ):
  run=base/scenario; publication=run.with_name(run.name+'-publication')
  made=subprocess.run([sys.executable,str(factory),'--out',str(run),'--scenario',scenario],text=True,capture_output=True)
  if made.returncode:
   print(f'SELFTEST=FAIL fixture-generation={scenario} error={(made.stderr or made.stdout).strip()}'); raise SystemExit(1)
  checked=subprocess.run([sys.executable,str(root/'verify.py'),'--test-fixture','--input',str(run),'--manifest',str(run/'auth-sensitive-data-flow.json'),'--publication',str(publication)],text=True,capture_output=True)
  if checked.returncode!=expected_rc or reason not in checked.stdout:
   print(f'SELFTEST=FAIL fixture={scenario} rc={checked.returncode} output={checked.stdout.strip()}'); raise SystemExit(1)
  if scenario=='valid':
   common=[sys.executable,str(root/'verify.py'),'--test-fixture','--input',str(run),'--manifest',str(run/'auth-sensitive-data-flow.json'),'--publication',str(publication)]
   crypto=subprocess.run([*common,'--force-cryptography'],text=True,capture_output=True)
   crypto_available=verifier.cryptography_verifier() is not None
   if crypto_available:
    crypto_ok=crypto.returncode==0 and 'FIXTURE_VERDICT=PASS' in crypto.stdout
   else:
    crypto_ok=crypto.returncode==2 and 'reason=ed25519-validator-unavailable' in crypto.stdout
   if not crypto_ok:
    print(f'SELFTEST=FAIL backend=--force-cryptography rc={crypto.returncode} output={crypto.stdout.strip()}'); raise SystemExit(1)
   fallback=subprocess.run([*common,'--force-openssl-fallback'],text=True,capture_output=True)
   if fallback.returncode or 'FIXTURE_VERDICT=PASS' not in fallback.stdout:
    print(f'SELFTEST=FAIL backend=--force-openssl-fallback rc={fallback.returncode} output={fallback.stdout.strip()}'); raise SystemExit(1)
   unavailable_env={**os.environ,'AUTH_SPLIT_DISABLE_CRYPTOGRAPHY':'1','AUTH_SPLIT_OPENSSL':str(base/'missing-openssl')}
   unavailable=subprocess.run(common,text=True,capture_output=True,env=unavailable_env)
   if unavailable.returncode!=2 or 'reason=ed25519-validator-unavailable' not in unavailable.stdout:
    print(f'SELFTEST=FAIL no-backend rc={unavailable.returncode} output={unavailable.stdout.strip()}'); raise SystemExit(1)
   production=subprocess.run([sys.executable,str(root/'verify.py'),'--input',str(run),'--manifest',str(run/'auth-sensitive-data-flow.json'),'--publication',str(publication)],text=True,capture_output=True)
   if production.returncode!=2 or 'non-production-test-key' not in production.stdout:
    print(f'SELFTEST=FAIL production-accepted-test-key rc={production.returncode} output={production.stdout.strip()}'); raise SystemExit(1)
   repeated=base/'valid-repeat'
   remade=subprocess.run([sys.executable,str(factory),'--out',str(repeated),'--scenario','valid'],text=True,capture_output=True)
   first={p.relative_to(run).as_posix():p.read_bytes() for p in run.rglob('*') if p.is_file()}
   second={p.relative_to(repeated).as_posix():p.read_bytes() for p in repeated.rglob('*') if p.is_file()}
   if remade.returncode or first!=second:
    print('SELFTEST=FAIL fixture-not-byte-deterministic'); raise SystemExit(1)
 print(f'FIXTURE_VALID=PASS identities={len(ids)} sinks=32 signed=1 publication-copy=1 production-rejected=1 deterministic=1 forced-cryptography-tested=1 openssl-fallback=1 no-backend-blocked=1')
 print('FIXTURE_ATTACKS=PASS prior-fabricated-cases=102 disconnected-signed-graph=1')
print('SELFTEST=PASS')
