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

EXPECTED_SUBJECTS={
    'alice':{'role':'Viewer','workspace_id':'w1'},
    'bob':{'role':'Editor','workspace_id':'w1'},
    'carol':{'role':'WorkspaceAdmin','workspace_id':'w1'},
    'dave':{'role':'Viewer','workspace_id':'w2'},
}
EXPECTED_RESOURCE_TYPES={
    'project:p1':('project','w1'),'file:f1':('file','w1'),'members:w1':('member_collection','w1'),
    'project:p2':('project','w2'),'file:f2':('file','w2'),'members:w2':('member_collection','w2'),
}
ACTION_RESOURCE_TYPES={
    'project.read':'project',
    'file.read':'file',
    'file.write':'file',
    'member.list':'member_collection',
    'member.manage':'member_collection',
}
EXPECTED_RECORD_FIELDS=['Subject/Role','Resource Scope','Action','Explicit Allow Rule','Default Decision','Enforcement Point','Positive Probe','Negative Probe','Evidence','Residual Policy Question']

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

def _deny(m, classification, subject_id, action, resource_id, role=None):
    out={'allowed':False,'classification':classification,'subject':subject_id,'action':action,'resource':resource_id,'enforcement_point':m['policy']['authoritative_enforcement_point']}
    if role is not None: out['role']=role
    return out

def authorize(subject_id, action, resource_id, model=None):
    m=deepcopy(model or load_baseline())
    if subject_id not in m['subjects']:
        return _deny(m,'UNKNOWN_SUBJECT_DENIED',subject_id,action,resource_id)
    if resource_id not in m['resources']:
        return _deny(m,'UNKNOWN_RESOURCE_DENIED',subject_id,action,resource_id,m['subjects'][subject_id]['role'])
    s=m['subjects'][subject_id]; r=m['resources'][resource_id]; role=s['role']
    if action not in ACTION_RESOURCE_TYPES:
        return _deny(m,'DEFAULT_DENY',subject_id,action,resource_id,role)
    if r.get('type')!=ACTION_RESOURCE_TYPES[action]:
        return _deny(m,'RESOURCE_ACTION_MISMATCH_DENIED',subject_id,action,resource_id,role)
    if m['policy'].get('workspace_scope_required',True) and s['workspace_id']!=r['workspace_id']:
        return _deny(m,'HORIZONTAL_SCOPE_DENIED',subject_id,action,resource_id,role)
    perms=set(m['roles'].get(role,[]))
    if action not in perms:
        return _deny(m,'VERTICAL_PRIVILEGE_DENIED',subject_id,action,resource_id,role)
    return {'allowed':True,'classification':'ALLOW','subject':subject_id,'role':role,'action':action,'resource':resource_id,'enforcement_point':m['policy']['authoritative_enforcement_point']}

def least_privilege_audit(model=None):
    m=deepcopy(model or load_baseline()); violations=[]
    for role,actual in m['roles'].items():
        required=set(m['owned_task_permissions'].get(role,[])); extra=sorted(set(actual)-required); missing=sorted(required-set(actual))
        if extra or missing: violations.append({'role':role,'extra':extra,'missing':missing})
    return {'pass':not violations,'violations':violations}

def validate_policy_invariants(model=None):
    m=deepcopy(model or load_baseline()); failures=[]
    if m.get('subjects')!=EXPECTED_SUBJECTS: failures.append('subject_bindings')
    if set(m.get('roles',{}))!={'Viewer','Editor','WorkspaceAdmin'}: failures.append('role_set')
    got_resources={k:(v.get('type'),v.get('workspace_id')) for k,v in m.get('resources',{}).items()}
    if got_resources!=EXPECTED_RESOURCE_TYPES: failures.append('resource_type_scope_map')
    if m['policy'].get('default_decision')!='DENY': failures.append('default_decision')
    if m['policy'].get('workspace_scope_required') is not True: failures.append('workspace_scope_required')
    if m['policy'].get('authoritative_enforcement_point')!='SERVER_POLICY_EVALUATOR': failures.append('authoritative_enforcement_point')
    if m['policy'].get('ui_visibility_is_authorization_control') is not False: failures.append('ui_visibility_is_authorization_control')
    if m.get('record_fields')!=EXPECTED_RECORD_FIELDS or len(set(m.get('record_fields',[])))!=len(EXPECTED_RECORD_FIELDS): failures.append('record_schema')
    if least_privilege_audit(m)['pass'] is not True: failures.append('least_privilege_baseline')
    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 run_probe(case_id):
    m=load_baseline()
    if case_id=='baseline_authorization_matrix':
        obs=[authorize('alice','file.read','file:f1',m),authorize('bob','file.write','file:f1',m),authorize('carol','member.manage','members:w1',m),authorize('bob','member.manage','members:w1',m),authorize('alice','file.read','file:f2',m)]
        return {'classification':'BASELINE_AUTHORIZATION_EVIDENCE_OBSERVED','observations':obs}
    if case_id=='unknown_action_default_deny':
        o=authorize('carol','project.archive','project:p1',m); return {'classification':o['classification'],'observation':o}
    if case_id=='horizontal_cross_workspace_denial':
        o=authorize('alice','file.read','file:f2',m); return {'classification':o['classification'],'observation':o}
    if case_id=='vertical_editor_member_manage_denial':
        o=authorize('bob','member.manage','members:w1',m); return {'classification':o['classification'],'observation':o}
    if case_id=='ui_hidden_direct_request':
        hidden='member.manage' in m['ui']['Editor']['hidden_actions']; o=authorize('bob','member.manage','members:w1',m)
        return {'classification':'SERVER_ENFORCEMENT_CONFIRMED' if hidden and not o['allowed'] else 'ENFORCEMENT_MISMATCH','ui_hidden':hidden,'observation':o}
    if case_id=='editor_overgrant_mutation':
        x=deepcopy(m); x['roles']['Editor'].append('member.manage'); a=least_privilege_audit(x); o=authorize('bob','member.manage','members:w1',x)
        return {'classification':'LEAST_PRIVILEGE_EXPANSION_DETECTED' if (not a['pass'] and o['allowed']) else 'MUTATION_NOT_DETECTED','audit':a,'observation':o}
    if case_id=='evidence_field_removed':
        rec={k:'observed' for k in EXPECTED_RECORD_FIELDS if k!='Residual Policy Question'}; 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}
    checks=[]
    checks.append(validate_policy_invariants()['pass'])
    checks.append(all(observations[c['id']]['classification']==c['expected_class'] for c in cases))
    b=observations['baseline_authorization_matrix']['observations']
    checks += [b[0]['allowed'],b[1]['allowed'],b[2]['allowed'],not b[3]['allowed'],not b[4]['allowed']]
    checks += [observations['unknown_action_default_deny']['classification']=='DEFAULT_DENY',observations['horizontal_cross_workspace_denial']['classification']=='HORIZONTAL_SCOPE_DENIED',observations['vertical_editor_member_manage_denial']['classification']=='VERTICAL_PRIVILEGE_DENIED',observations['ui_hidden_direct_request']['classification']=='SERVER_ENFORCEMENT_CONFIRMED',observations['editor_overgrant_mutation']['classification']=='LEAST_PRIVILEGE_EXPANSION_DETECTED',observations['evidence_field_removed']['classification']=='EVIDENCE_INCOMPLETE']
    checks += [load_baseline()['policy']['authoritative_enforcement_point']=='SERVER_POLICY_EVALUATOR', load_baseline()['policy']['default_decision']=='DENY']
    checks += [authorize('eve','file.read','file:f1')['classification']=='UNKNOWN_SUBJECT_DENIED']
    checks += [authorize('alice','file.read','file:missing')['classification']=='UNKNOWN_RESOURCE_DENIED']
    checks += [authorize('dave','file.read','file:f1')['classification']=='HORIZONTAL_SCOPE_DENIED']
    checks += [authorize('alice','member.manage','members:w1')['classification']=='VERTICAL_PRIVILEGE_DENIED']
    checks += [authorize('bob','file.write','project:p1')['classification']=='RESOURCE_ACTION_MISMATCH_DENIED']
    checks += [authorize('carol','member.manage','file:f1')['classification']=='RESOURCE_ACTION_MISMATCH_DENIED']
    blank={k:'observed' for k in EXPECTED_RECORD_FIELDS}; blank['Evidence']=''; checks += [validate_evidence_record(blank)['pass'] is False]
    return {'cases':observations,'checks':checks,'pass':all(checks),'runtime_scope':'PYTHON_IN_MEMORY_SYNTHETIC_DEFENSIVE_AUTHORIZATION_FIXTURE_ONLY','external_target_used':False,'production_runtime_claimed':False}
