from pathlib import Path
import json, copy, hashlib
ROOT=Path(__file__).resolve().parents[1]
BASE=json.loads((ROOT/'fixture/baseline-state.json').read_text())

def canonical_event_fingerprint(event_id,aggregate_key,event_type,payload):
    material={'event_id':event_id,'aggregate_key':aggregate_key,'event_type':event_type,'payload':payload}
    raw=json.dumps(material,sort_keys=True,separators=(',',':')).encode('utf-8')
    return hashlib.sha256(raw).hexdigest()

class OutboxState:
    def __init__(self):
        self.orders={BASE['service']['order_id']:BASE['service']['initial_order_state']}
        self.outbox={}
        self.synthetic_deliveries=[]
    def snapshot(self):
        return {'orders':copy.deepcopy(self.orders),'outbox':copy.deepcopy(self.outbox),'synthetic_deliveries':copy.deepcopy(self.synthetic_deliveries)}
    def confirm_with_outbox(self, fail_before_commit=False):
        before=self.snapshot()
        order_id=BASE['service']['order_id']; intent=BASE['intent']
        staged_orders=copy.deepcopy(self.orders); staged_outbox=copy.deepcopy(self.outbox)
        staged_orders[order_id]='CONFIRMED'
        fp=canonical_event_fingerprint(intent['event_id'],intent['aggregate_key'],intent['event_type'],intent['payload'])
        staged_outbox[intent['event_id']]={'event_id':intent['event_id'],'aggregate_key':intent['aggregate_key'],'event_type':intent['event_type'],'payload':copy.deepcopy(intent['payload']),'payload_fingerprint':fp,'mark_state':'PENDING','attempts':0,'age_seconds':0}
        if fail_before_commit:
            after=self.snapshot()
            return {'committed':False,'rolled_back':after==before,'order_state':self.orders[order_id],'outbox_count':len(self.outbox)}
        self.orders=staged_orders; self.outbox=staged_outbox
        return {'committed':True,'order_state':self.orders[order_id],'outbox_count':len(self.outbox),'mark_state':self.outbox[intent['event_id']]['mark_state'],'payload_fingerprint':fp}
    def relay_once(self,event_id,crash_after_publish=False):
        row=self.outbox[event_id]
        current_fp=canonical_event_fingerprint(row['event_id'],row['aggregate_key'],row['event_type'],row['payload'])
        if current_fp != row['payload_fingerprint']:
            return {'published':False,'classification':'EVENT_IDENTITY_COLLISION_REJECTED','mark_state':row['mark_state'],'attempts':row['attempts'],'payload_fingerprint_matches':False}
        row['attempts']+=1
        self.synthetic_deliveries.append({'event_id':row['event_id'],'aggregate_key':row['aggregate_key'],'payload_fingerprint':current_fp,'attempt':row['attempts']})
        if crash_after_publish:
            return {'published':True,'crashed_before_mark':True,'mark_state':row['mark_state'],'attempts':row['attempts'],'payload_fingerprint':current_fp}
        row['mark_state']='PUBLISHED'
        return {'published':True,'crashed_before_mark':False,'mark_state':row['mark_state'],'attempts':row['attempts'],'payload_fingerprint':current_fp}
    def reconcile(self,threshold_seconds):
        return [copy.deepcopy(v) for v in self.outbox.values() if v['mark_state']!='PUBLISHED' and v['age_seconds']>=threshold_seconds]

def run_probe(case_id):
    event_id=BASE['intent']['event_id']
    if case_id=='baseline_transactional_outbox_commit':
        s=OutboxState(); before=s.snapshot(); rollback=s.confirm_with_outbox(fail_before_commit=True); rollback_state_unchanged=(s.snapshot()==before); committed=s.confirm_with_outbox(False)
        return {'case_id':case_id,'classification':'DOMAIN_AND_INTENT_COMMITTED',**committed,'rollback_verified':rollback['rolled_back'] and rollback_state_unchanged and rollback['order_state']=='PENDING' and rollback['outbox_count']==0,'rollback_state_unchanged':rollback_state_unchanged,'event_id':event_id,'aggregate_key':BASE['intent']['aggregate_key']}
    if case_id=='direct_dual_write_publish_failure':
        s=OutboxState(); order_id=BASE['service']['order_id']; s.orders[order_id]='CONFIRMED'; publish_failed=True
        return {'case_id':case_id,'classification':'DUAL_WRITE_GAP_OBSERVED','order_state':s.orders[order_id],'publish_failed':publish_failed,'durable_intent_present':event_id in s.outbox,'outbox_count':len(s.outbox)}
    if case_id=='relay_publish_before_mark_crash_retry':
        s=OutboxState(); s.confirm_with_outbox(); first=s.relay_once(event_id,crash_after_publish=True); actual_after_crash=s.outbox[event_id]['mark_state']; second=s.relay_once(event_id,crash_after_publish=False)
        deliveries=[x for x in s.synthetic_deliveries if x['event_id']==event_id]; fps=[x['payload_fingerprint'] for x in deliveries]
        return {'case_id':case_id,'classification':'DUPLICATE_DELIVERY_CANDIDATE','event_id':event_id,'first_attempt':first,'actual_mark_state_after_crash':actual_after_crash,'retry_attempt':second,'delivery_count_for_event':len(deliveries),'same_event_id_republished':len(deliveries)==2,'same_payload_fingerprint_republished':len(set(fps))==1 and len(fps)==2,'delivery_payload_fingerprints':fps,'final_mark_state':s.outbox[event_id]['mark_state']}
    if case_id=='reconciliation_old_pending_intent':
        s=OutboxState(); s.confirm_with_outbox(); row=s.outbox[event_id]; row['age_seconds']=BASE['reconciliation']['old_pending_age_seconds']; row['attempts']=BASE['reconciliation']['repeated_attempts']; found=s.reconcile(BASE['reconciliation']['stuck_threshold_seconds'])
        return {'case_id':case_id,'classification':'STUCK_INTENT_DETECTED','threshold_seconds':BASE['reconciliation']['stuck_threshold_seconds'],'age_seconds':row['age_seconds'],'attempts':row['attempts'],'found_count':len(found),'found_event_ids':[x['event_id'] for x in found],'mark_state_after_reconcile':row['mark_state'],'auto_marked_success':row['mark_state']=='PUBLISHED'}
    if case_id=='evidence_field_removed':
        required={'Intent ID','Aggregate Key','Domain Commit','Outbox Commit','Relay Attempt','Publish Observation','Mark State','Age','Reconciliation Decision','Residual Consumer Risk'}
        row={k:'x' for k in required}; row.pop('Residual Consumer Risk'); missing=sorted(required-set(row))
        return {'case_id':case_id,'classification':'EVIDENCE_INCOMPLETE' if missing==['Residual Consumer Risk'] else 'UNEXPECTED','missing':missing}
    raise KeyError(case_id)

def publisher_selftest():
    cases=json.loads((ROOT/'fixture/probe-cases.json').read_text())['cases']; exp={c['id']:c['expected_class'] for c in cases}; obs=[run_probe(c['id']) for c in cases]
    checks=[o['classification']==exp[o['case_id']] for o in obs]
    b=next(o for o in obs if o['case_id']=='baseline_transactional_outbox_commit'); d=next(o for o in obs if o['case_id']=='direct_dual_write_publish_failure'); r=next(o for o in obs if o['case_id']=='relay_publish_before_mark_crash_retry'); q=next(o for o in obs if o['case_id']=='reconciliation_old_pending_intent')
    checks += [b['order_state']=='CONFIRMED',b['outbox_count']==1,b['mark_state']=='PENDING',b['rollback_verified'] is True,b['rollback_state_unchanged'] is True,d['order_state']=='CONFIRMED',d['publish_failed'] is True,d['durable_intent_present'] is False,r['delivery_count_for_event']==2,r['same_event_id_republished'] is True,r['same_payload_fingerprint_republished'] is True,r['actual_mark_state_after_crash']=='PENDING',r['first_attempt']['mark_state']=='PENDING',r['final_mark_state']=='PUBLISHED',q['found_count']==1,q['auto_marked_success'] is False,q['mark_state_after_reconcile']=='PENDING']
    s=OutboxState(); s.confirm_with_outbox(); row=s.outbox[BASE['intent']['event_id']]; t=BASE['reconciliation']['stuck_threshold_seconds']; row['age_seconds']=t-1; checks.append(s.reconcile(t)==[]); row['age_seconds']=t; checks.append(len(s.reconcile(t))==1)
    return {'cases':obs,'checks':checks,'pass':all(checks),'runtime_scope':'PYTHON_IN_MEMORY_SYNTHETIC_FIXTURE_ONLY','database_server_runtime_claimed':False,'broker_runtime_claimed':False,'production_runtime_claimed':False}
