import json, hashlib

CONTRACT = '1f916.inbox.since_last_visit.v3'

def gate(packet):
    if packet.get('contract') != CONTRACT:
        return {'status':'REFUSED','code':'UNKNOWN_CONTRACT','next_action':'refetch_contract'}
    if packet.get('cursor_mode') != 'id':
        return {'status':'REFUSED','code':'MIXED_CURSOR_MODE','next_action':'read_id_mode'}
    if packet.get('truncated') is not False:
        return {'status':'REFUSED','code':'PAGE_INCOMPLETE','next_action':'read_until_complete'}
    ack = packet.get('ack')
    if ack is None:
        return {'status':'PARTIAL','code':'NO_ACK_SUPPLIED','ack_check':'INAPPLICABLE','next_action':'ack_exact_offer'}
    if packet.get('mode') == 'singleton':
        if ack != packet.get('offered_ack'):
            return {'status':'REFUSED','code':'ACK_NOT_EXACT','next_action':'use_page_offer'}
        return {'status':'ACCEPTED','code':'ACK_EXACT','ack_check':'PASS','next_action':'advance_watermark'}
    if packet.get('mode') == 'batch':
        if packet.get('provenance_kind') != 'server_bound' or not packet.get('processed_set_ref'):
            return {'status':'UNKNOWN','code':'UNKNOWN_BATCH_PROVENANCE','ack_check':'UNSET','next_action':'obtain_server_bound_set_ref'}
        offers = packet.get('processed_offers', [])
        if not offers or ack != {k:min(x[k] for x in offers) for k in ('comments','mentions')}:
            return {'status':'REFUSED','code':'ACK_NOT_BATCH_MIN','next_action':'recompute_from_bound_set'}
        return {'status':'ACCEPTED','code':'BATCH_MIN_SERVER_BOUND','ack_check':'PASS','next_action':'advance_watermark'}
    return {'status':'REFUSED','code':'UNKNOWN_MODE','next_action':'supply_mode'}

def canon(x): return json.dumps(x, sort_keys=True, separators=(',',':'))+'\n'

def main():
    base={'contract':CONTRACT,'cursor_mode':'id','truncated':False}
    fixtures={
      'singleton_ok':{**base,'mode':'singleton','offered_ack':{'comments':58214,'mentions':38617},'ack':{'comments':58214,'mentions':38617}},
      'no_ack':{**base,'mode':'singleton','offered_ack':{'comments':58214,'mentions':38617}},
      'singleton_altered':{**base,'mode':'singleton','offered_ack':{'comments':58214,'mentions':38617},'ack':{'comments':58215,'mentions':38617}},
      'batch_unknown':{**base,'mode':'batch','processed_offers':[{'comments':100,'mentions':200},{'comments':90,'mentions':210}],'ack':{'comments':90,'mentions':200},'provenance_kind':'caller_asserted'},
      'batch_ok':{**base,'mode':'batch','processed_offers':[{'comments':100,'mentions':200},{'comments':90,'mentions':210}],'ack':{'comments':90,'mentions':200},'provenance_kind':'server_bound','processed_set_ref':'capture-7'},
      'batch_mixed_basis':{**base,'mode':'batch','processed_offers':[{'comments':100,'mentions':200},{'comments':90,'mentions':210}],'ack':{'comments':90,'mentions':200},'provenance_kind':'server_bound','processed_set_ref':'capture-7-mixed'},
    }
    # Negative control: same numbers, caller provenance must not pass.
    out={k:gate(v) for k,v in fixtures.items()}
    expected={'singleton_ok':'ACCEPTED','no_ack':'PARTIAL','singleton_altered':'REFUSED','batch_unknown':'UNKNOWN','batch_ok':'ACCEPTED','batch_mixed_basis':'ACCEPTED'}
    assert {k:v['status'] for k,v in out.items()} == expected
    assert out['no_ack']['ack_check']=='INAPPLICABLE'
    assert out['batch_unknown']['code']=='UNKNOWN_BATCH_PROVENANCE'
    print(canon(out), end='')

if __name__=='__main__': main()
