from copy import deepcopy
from pathlib import Path
import json
R=Path(__file__).resolve().parents[1]
EXPECTED_RECORD_FIELDS=['Ceremony','RP ID','Origin','Challenge State','Credential ID','User Verification Policy','Server Verification Result','Recovery Path','Negative Probe','Evidence','Residual Decision']
EXPECTED_POSITIVE_PATHS=['registration ceremony verifies expected RP/origin/fresh challenge/UV','authentication ceremony verifies registered credential and fresh challenge']
EXPECTED_NEGATIVE_PATHS=['expired challenge rejected','mismatched challenge rejected','unexpected origin rejected','unexpected RP ID rejected','missing required user verification rejected','materially weaker recovery shortcut rejected']
EXPECTED_FORBIDDEN_CONCLUSIONS=['A passkey is just a password stored more securely.','WebAuthn registration can trust a client-supplied challenge without server verification.','All passkeys have identical synchronization, backup or attestation properties.','Account recovery may safely use any weaker factor because it is only an exception path.']
EXPECTED_TRANSFER_CONTEXT='A synthetic staff portal uses device-bound credentials for a higher-risk role. Re-evaluate user-verification and recovery evidence without assuming the same backup/sync properties as the original fixture.'
EXPECTED_APPROVED_RECOVERY_PATHS=['backup_authenticator','supervised_identity_proofing_reenrollment']
EXPECTED_WEAKER_SHORTCUTS=['email_only_link']

def load_baseline(): return json.loads((R/'fixture/baseline-webauthn.json').read_text())

def validate_model_invariants(model=None):
    m=deepcopy(model or load_baseline()); failures=[]
    rp=m.get('relying_party',{}); p=m.get('ceremony_policy',{}); a=m.get('synthetic_account',{}); rec=m.get('recovery_policy',{}); ch=m.get('challenges',{})
    if rp!={'name':'MAKNOW Synthetic RP','rp_id':'login.example.test','expected_origin':'https://login.example.test'}: failures.append('relying_party')
    expected_policy={'user_verification':'required','challenge_ttl_seconds':120,'server_generates_challenge':True,'challenge_single_use':True,'verify_origin':True,'verify_rp_id':True}
    if p!=expected_policy: failures.append('ceremony_policy')
    if a.get('credential_id')!='cred-synth-001' or a.get('credential_registered') is not True: failures.append('registered_credential')
    if a.get('private_key_location')!='VIRTUAL_AUTHENTICATOR_ONLY_NO_SECRET_EXPORTED': failures.append('private_key_boundary')
    if a.get('public_key_material')!='SYNTHETIC_PUBLIC_KEY_ONLY': failures.append('public_key_boundary')
    for kind in ('registration','authentication'):
        st=ch.get(kind,{})
        if st.get('used') is not False: failures.append(kind+'_baseline_used')
        if not isinstance(st.get('issued_at'),int) or not isinstance(st.get('expires_at'),int) or st.get('expires_at')-st.get('issued_at')!=120: failures.append(kind+'_ttl_binding')
        if not isinstance(st.get('expected'),str) or not st.get('expected'): failures.append(kind+'_challenge')
    if rec.get('approved_paths')!=EXPECTED_APPROVED_RECOVERY_PATHS: failures.append('approved_recovery_paths')
    if rec.get('materially_weaker_shortcuts')!=EXPECTED_WEAKER_SHORTCUTS: failures.append('weaker_shortcuts')
    if rec.get('risk_based_decision_required') is not True: failures.append('risk_based_recovery')
    if rec.get('revoke_or_supersede_lost_credential_on_reenrollment') is not True: failures.append('credential_lifecycle_on_recovery')
    fields=m.get('record_fields',[])
    if fields!=EXPECTED_RECORD_FIELDS or len(set(fields))!=len(EXPECTED_RECORD_FIELDS): failures.append('record_schema')
    return {'pass':not failures,'failures':failures}

def validate_evidence_record(record, model=None):
    m=model or load_baseline(); schema_ok=m.get('record_fields')==EXPECTED_RECORD_FIELDS and len(set(m.get('record_fields',[])))==len(EXPECTED_RECORD_FIELDS)
    missing=[x for x in EXPECTED_RECORD_FIELDS if x not in record]; blank=[x for x in EXPECTED_RECORD_FIELDS if x in record and not str(record[x]).strip()]
    return {'pass':schema_ok and not missing and not blank,'schema_ok':schema_ok,'missing':missing,'blank':blank}

def _verify_ceremony_mutating(kind, m, challenge=None, now=None, origin=None, rp_id=None, uv_performed=True, credential_id=None):
    p=m['ceremony_policy']; rp=m['relying_party']; a=m['synthetic_account']; st=m['challenges'][kind]
    challenge=st['expected'] if challenge is None else challenge; now=st['issued_at']+10 if now is None else now; origin=rp['expected_origin'] if origin is None else origin; rp_id=rp['rp_id'] if rp_id is None else rp_id; credential_id=a['credential_id'] if credential_id is None else credential_id
    if st.get('used') is True: return {'ok':False,'classification':'CHALLENGE_REPLAY_REJECTED','reason':'challenge already used'}
    if now < st['issued_at'] or now > st['expires_at']: return {'ok':False,'classification':'EXPIRED_CHALLENGE_REJECTED','reason':'challenge outside validity window'}
    if challenge != st['expected']: return {'ok':False,'classification':'CHALLENGE_MISMATCH_REJECTED','reason':'challenge mismatch'}
    if p['verify_origin'] and origin != rp['expected_origin']: return {'ok':False,'classification':'ORIGIN_MISMATCH_REJECTED','reason':'unexpected origin'}
    if p['verify_rp_id'] and rp_id != rp['rp_id']: return {'ok':False,'classification':'RP_ID_MISMATCH_REJECTED','reason':'unexpected RP ID'}
    if p['user_verification']=='required' and uv_performed is not True: return {'ok':False,'classification':'USER_VERIFICATION_REQUIRED','reason':'required UV not observed'}
    if kind=='authentication' and a.get('credential_registered') is not True: return {'ok':False,'classification':'UNREGISTERED_CREDENTIAL_REJECTED','reason':'credential not registered'}
    if kind=='authentication' and credential_id != a.get('credential_id'): return {'ok':False,'classification':'UNKNOWN_CREDENTIAL_REJECTED','reason':'credential id not registered for synthetic account'}
    st['used']=True
    return {'ok':True,'classification':'VERIFIED_REGISTRATION' if kind=='registration' else 'VERIFIED_AUTHENTICATION','challenge_state':'fresh/matched/consumed','challenge_consumed':True,'origin':origin,'rp_id':rp_id,'credential_id':credential_id,'user_verification_observed':bool(uv_performed)}

def _verify_ceremony(kind, *, model=None, **kwargs): return _verify_ceremony_mutating(kind,deepcopy(model or load_baseline()),**kwargs)

def verify_single_use_sequence(kind='registration', model=None):
    m=deepcopy(model or load_baseline()); first=_verify_ceremony_mutating(kind,m); second=_verify_ceremony_mutating(kind,m)
    return {'first':first,'second':second,'pass':first.get('ok') is True and second.get('classification')=='CHALLENGE_REPLAY_REJECTED'}

def evaluate_recovery(path, model=None, risk_decision_recorded=True):
    m=model or load_baseline(); r=m['recovery_policy']
    if path in r['materially_weaker_shortcuts']: return {'ok':False,'classification':'WEAK_RECOVERY_REJECTED','path':path,'reason':'materially weaker synthetic shortcut requires rejection or separate justified redesign'}
    if path in r['approved_paths']:
        if r.get('risk_based_decision_required') and risk_decision_recorded is not True: return {'ok':False,'classification':'RECOVERY_RISK_DECISION_REQUIRED','path':path}
        return {'ok':True,'classification':'RECOVERY_PATH_BOUNDED_ACCEPTABLE','path':path,'credential_lifecycle_required':r['revoke_or_supersede_lost_credential_on_reenrollment']}
    return {'ok':False,'classification':'RECOVERY_PATH_UNREVIEWED','path':path}

def run_probe(case_id):
    m=load_baseline()
    if case_id=='baseline_registration': return _verify_ceremony('registration',model=m)
    if case_id=='baseline_authentication': return _verify_ceremony('authentication',model=m)
    if case_id=='expired_registration_challenge': return _verify_ceremony('registration',model=m,now=m['challenges']['registration']['expires_at']+1)
    if case_id=='mismatched_authentication_challenge': return _verify_ceremony('authentication',model=m,challenge='wrong-auth-challenge')
    if case_id=='origin_mismatch': return _verify_ceremony('authentication',model=m,origin='https://evil.example.test')
    if case_id=='rp_id_mismatch': return _verify_ceremony('authentication',model=m,rp_id='other.example.test')
    if case_id=='user_verification_missing': return _verify_ceremony('authentication',model=m,uv_performed=False)
    if case_id=='weak_recovery_shortcut': return evaluate_recovery('email_only_link',m)
    if case_id=='evidence_field_removed':
        rec={k:'observed' for k in EXPECTED_RECORD_FIELDS if k!='Residual Decision'}; v=validate_evidence_record(rec,m); return {'ok':False if not v['pass'] else True,'classification':'EVIDENCE_INCOMPLETE' if not v['pass'] else 'EVIDENCE_ACCEPTED','validation':v}
    raise KeyError(case_id)

def publisher_selftest():
    m=load_baseline(); cases=json.loads((R/'fixture/probe-cases.json').read_text())['cases']; observations={c['id']:run_probe(c['id']) for c in cases}; g=json.loads((R/'fixture/golden-policy.json').read_text()); req=json.loads((R/'requirements.json').read_text())
    blank={k:'observed' for k in EXPECTED_RECORD_FIELDS}; blank['Evidence']=' '
    alt=deepcopy(m); alt['synthetic_account']['credential_registered']=False
    checks=[validate_model_invariants()['pass'],all(observations[c['id']]['classification']==c['expected_class'] for c in cases),observations['baseline_registration']['ok'],observations['baseline_authentication']['ok'],not observations['expired_registration_challenge']['ok'],not observations['mismatched_authentication_challenge']['ok'],not observations['origin_mismatch']['ok'],not observations['rp_id_mismatch']['ok'],not observations['user_verification_missing']['ok'],not observations['weak_recovery_shortcut']['ok'],m['record_fields']==EXPECTED_RECORD_FIELDS,g['required_positive_paths']==EXPECTED_POSITIVE_PATHS,g['required_negative_paths']==EXPECTED_NEGATIVE_PATHS,g['forbidden_conclusions']==EXPECTED_FORBIDDEN_CONCLUSIONS,g['transfer_context']==EXPECTED_TRANSFER_CONTEXT,verify_single_use_sequence('registration')['pass'],verify_single_use_sequence('authentication')['pass'],_verify_ceremony('authentication',model=alt)['classification']=='UNREGISTERED_CREDENTIAL_REJECTED',_verify_ceremony('authentication',credential_id='unknown-credential')['classification']=='UNKNOWN_CREDENTIAL_REJECTED',evaluate_recovery('backup_authenticator')['classification']=='RECOVERY_PATH_BOUNDED_ACCEPTABLE',evaluate_recovery('backup_authenticator',risk_decision_recorded=False)['classification']=='RECOVERY_RISK_DECISION_REQUIRED',validate_evidence_record(blank)['pass'] is False,req['external_network_required'] is False,req['real_credentials_required'] is False,req['real_account_required'] is False,req['physical_authenticator_required'] is False,req['external_target_required'] is False]
    return {'cases':observations,'checks':checks,'pass':all(checks),'runtime_scope':'PYTHON_IN_MEMORY_SYNTHETIC_DEFENSIVE_WEBAUTHN_MODEL_ONLY','external_target_used':False,'real_credentials_used':False,'production_runtime_claimed':False}
