from copy import deepcopy
from pathlib import Path
import json
R=Path(__file__).resolve().parents[1]

EXPECTED_RECORD_FIELDS=['Lifecycle Event','Session Handle State','Cookie Attributes','Server-side State','Rotation Required','Invalidation Expected','Probe','Observation','Decision','Residual Risk']
EXPECTED_POSITIVE_PATHS=['anonymous session active','authentication rotates to a new authenticated handle','authenticated session remains valid until bounded invalidation']
EXPECTED_NEGATIVE_PATHS=['old pre-authentication handle rejected after rotation','logged-out handle rejected server-side','browser-only deletion is not accepted as server invalidation','unjustified cookie-scope expansion detected']
EXPECTED_FORBIDDEN_CONCLUSIONS=['HTTPS alone makes any session cookie configuration safe.','SameSite replaces all CSRF defenses in every application.','Deleting a browser cookie always invalidates server-side session authority.','A long random session identifier alone solves rotation, logout and reauthentication policy.']
EXPECTED_TRANSFER_CONTEXT='synthetic admin console requires reauthentication before changing recovery settings'
EXPECTED_COOKIE={'name':'__Host-session','Secure':True,'HttpOnly':True,'SameSite':'Strict','Path':'/','Domain':None}
EXPECTED_POLICY={'session_id_is_opaque_reference':True,'authoritative_session_state':'SERVER_SESSION_STORE','rotate_on_authentication':True,'rotate_on_privilege_change':True,'server_side_invalidation_on_logout':True,'browser_cookie_deletion_invalidates_server_session':False,'reauthentication_required_for':['recovery_settings.change']}
EXPECTED_HANDLE_KEYS={'anonymous','authenticated','reauthenticated'}

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

def validate_model_invariants(model=None):
    m=deepcopy(model or load_baseline()); c=m.get('cookie',{}); p=m.get('policy',{}); failures=[]
    for k,v in EXPECTED_COOKIE.items():
        if c.get(k)!=v: failures.append('cookie_'+k)
    for k,v in EXPECTED_POLICY.items():
        if p.get(k)!=v: failures.append('policy_'+k)
    req=m.get('record_fields',[])
    if req!=EXPECTED_RECORD_FIELDS or len(set(req))!=len(EXPECTED_RECORD_FIELDS): failures.append('record_schema')
    h=m.get('synthetic_handles',{})
    if set(h)!=EXPECTED_HANDLE_KEYS: failures.append('handle_keys')
    vals=[h.get(k) for k in sorted(EXPECTED_HANDLE_KEYS)]
    if any(not isinstance(v,str) or not v.strip() for v in vals) or len(set(vals))!=3: failures.append('handle_uniqueness')
    if m.get('external_target') is not None: failures.append('external_target')
    return {'pass':not failures,'failures':failures}

def validate_evidence_record(record, model=None):
    m=model or load_baseline(); req=m.get('record_fields',[])
    schema_ok=req==EXPECTED_RECORD_FIELDS and len(set(req))==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 baseline_lifecycle(model=None):
    m=deepcopy(model or load_baseline()); p=m['policy']; h=m['synthetic_handles']
    pre=h['anonymous']; post=h['authenticated'] if p['rotate_on_authentication'] else pre
    store={pre:{'state':'ANONYMOUS_ACTIVE','valid':True}}
    if p['rotate_on_authentication']:
        store[pre]['state']='STALE_AFTER_AUTH'; store[pre]['valid']=False; store[post]={'state':'AUTHENTICATED_ACTIVE','valid':True}
    else:
        store[pre]={'state':'AUTHENTICATED_ACTIVE_REUSED_HANDLE','valid':True}
    old_replay_allowed=bool(store.get(pre,{}).get('valid')) if pre!=post else True
    reauth_required='recovery_settings.change' in p['reauthentication_required_for']
    before_logout_valid=bool(store.get(post,{}).get('valid'))
    if p['server_side_invalidation_on_logout'] and post in store:
        store[post]['state']='INVALIDATED_BY_LOGOUT'; store[post]['valid']=False
    after_logout_valid=bool(store.get(post,{}).get('valid'))
    return {'pre_auth_handle':pre,'post_auth_handle':post,'rotated':post!=pre,'old_handle_rejected_after_auth':not old_replay_allowed,'post_auth_valid_before_logout':before_logout_valid,'reauthentication_required_for_recovery_change':reauth_required,'post_auth_valid_after_logout':after_logout_valid,'server_store':store}

def sensitive_action_gate(event='recovery_settings.change', reauthenticated=False, model=None):
    p=(model or load_baseline())['policy']; required=event in p.get('reauthentication_required_for',[])
    if required and not reauthenticated: return {'allowed':False,'classification':'REAUTHENTICATION_REQUIRED','event':event}
    return {'allowed':True,'classification':'SENSITIVE_ACTION_ALLOWED_AFTER_REAUTH' if required else 'SENSITIVE_ACTION_ALLOWED','event':event}

def reauthentication_transition(model=None, event='recovery_settings.change'):
    m=deepcopy(model or load_baseline()); p=m['policy']; h=m['synthetic_handles']; current=h['authenticated']
    store={current:{'state':'AUTHENTICATED_ACTIVE','valid':True}}
    before=sensitive_action_gate(event,False,m)
    required=event in p.get('reauthentication_required_for',[])
    nxt=current
    if required and p.get('rotate_on_privilege_change'):
        nxt=h['reauthenticated']; store[current]['state']='STALE_AFTER_REAUTH'; store[current]['valid']=False; store[nxt]={'state':'REAUTHENTICATED_ACTIVE','valid':True}
    after=sensitive_action_gate(event,True,m) if required else sensitive_action_gate(event,False,m)
    rotated=nxt!=current
    old_rejected=not bool(store.get(current,{}).get('valid')) if rotated else False
    new_valid=bool(store.get(nxt,{}).get('valid'))
    ok=required and (not before['allowed']) and after['allowed'] and rotated and old_rejected and new_valid
    return {'classification':'REAUTHENTICATION_BOUNDARY_ENFORCED' if ok else 'REAUTHENTICATION_BOUNDARY_WEAK','event':event,'required':required,'pre_reauth_handle':current,'post_reauth_handle':nxt,'rotated':rotated,'old_handle_rejected_after_reauth':old_rejected,'post_reauth_valid':new_valid,'before_reauth':before,'after_reauth':after,'server_store':store}

def run_probe(case_id):
    m=load_baseline()
    if case_id=='baseline_session_lifecycle':
        return {'classification':'BASELINE_SESSION_LIFECYCLE_OBSERVED','observation':baseline_lifecycle(m),'cookie':m['cookie']}
    if case_id=='rotation_disabled_at_authentication':
        x=deepcopy(m); x['policy']['rotate_on_authentication']=False; o=baseline_lifecycle(x)
        return {'classification':'SESSION_ROTATION_MISSING' if not o['rotated'] else 'ROTATION_PRESENT','observation':o}
    if case_id=='browser_cookie_deleted_without_server_invalidation':
        h=m['synthetic_handles']['authenticated']; server_state={'handle':h,'valid':True}; browser_cookie_present=False
        return {'classification':'CLIENT_COOKIE_REMOVAL_NOT_SERVER_INVALIDATION' if (not browser_cookie_present and server_state['valid']) else 'SERVER_INVALIDATED','browser_cookie_present':browser_cookie_present,'server_state':server_state}
    if case_id=='cookie_scope_broadened':
        x=deepcopy(m); x['cookie']['Domain']='example.test'; x['cookie']['name']='session'; inv=validate_model_invariants(x)
        return {'classification':'COOKIE_SCOPE_EXPANSION_DETECTED' if not inv['pass'] else 'SCOPE_ACCEPTED','validation':inv,'cookie':x['cookie']}
    if case_id=='stale_pre_auth_session_replay':
        o=baseline_lifecycle(m)
        return {'classification':'STALE_SESSION_REJECTED' if o['old_handle_rejected_after_auth'] else 'STALE_SESSION_ACCEPTED','observation':o}
    if case_id=='logout_server_invalidation':
        o=baseline_lifecycle(m)
        return {'classification':'SERVER_SESSION_INVALIDATED' if not o['post_auth_valid_after_logout'] else 'SERVER_SESSION_STILL_ACTIVE','observation':o}
    if case_id=='evidence_field_removed':
        rec={k:'observed' for k in EXPECTED_RECORD_FIELDS if k!='Residual Risk'}; v=validate_evidence_record(rec,m)
        return {'classification':'EVIDENCE_INCOMPLETE' if not v['pass'] else 'EVIDENCE_ACCEPTED','validation':v}
    raise KeyError(case_id)

def publisher_selftest():
    cases=json.loads((R/'fixture/probe-cases.json').read_text())['cases']; observations={c['id']:run_probe(c['id']) for c in cases}; b=run_probe('baseline_session_lifecycle')['observation']; m=load_baseline(); c=m['cookie']; g=json.loads((R/'fixture/golden-policy.json').read_text()); req=json.loads((R/'requirements.json').read_text()); rt=reauthentication_transition(m)
    checks=[validate_model_invariants()['pass'], all(observations[x['id']]['classification']==x['expected_class'] for x in cases), b['rotated'], b['old_handle_rejected_after_auth'], b['post_auth_valid_before_logout'], not b['post_auth_valid_after_logout'], b['reauthentication_required_for_recovery_change'], c['Secure'], c['HttpOnly'], c['SameSite']=='Strict', c['Path']=='/', c['Domain'] is None, m['policy']['server_side_invalidation_on_logout'], m['policy']['browser_cookie_deletion_invalidates_server_session'] is False, m['policy']['authoritative_session_state']=='SERVER_SESSION_STORE']
    checks += [m['record_fields']==EXPECTED_RECORD_FIELDS, len(set(m['synthetic_handles'].values()))==3, m['external_target'] is None]
    checks += [sensitive_action_gate(reauthenticated=False,model=m)['classification']=='REAUTHENTICATION_REQUIRED', rt['classification']=='REAUTHENTICATION_BOUNDARY_ENFORCED', rt['rotated'], rt['old_handle_rejected_after_reauth'], rt['post_reauth_valid']]
    alt=deepcopy(m); alt['synthetic_handles']={'anonymous':'x_anon','authenticated':'x_auth','reauthenticated':'x_reauth'}; ab=baseline_lifecycle(alt); ar=reauthentication_transition(alt)
    checks += [ab['rotated'] and ab['old_handle_rejected_after_auth'], ar['classification']=='REAUTHENTICATION_BOUNDARY_ENFORCED']
    checks += [req['external_network_required'] is False and req['real_credentials_required'] is False and req['external_target_required'] is False]
    checks += [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]
    return {'cases':observations,'reauthentication_transition':rt,'checks':checks,'pass':all(checks),'runtime_scope':'PYTHON_IN_MEMORY_SYNTHETIC_DEFENSIVE_SESSION_FIXTURE_ONLY','external_target_used':False,'production_runtime_claimed':False}
