"""Owned short nonsegmented RFC4733 event fixture. Four-byte payload plus authored RTP metadata."""
import struct,json
from fractions import Fraction
def payload(event,end,duration,volume=10,reserved=False):
    if type(event)is not int or event not in range(10)or type(end)is not bool or type(duration)is not int or not 1<=duration<=65535 or type(volume)is not int or not 0<=volume<=63:raise ValueError('fixture sender supports digits0..9 with positive16-bit duration')
    # Reserved=True is solely a receiver robustness test, not a conforming sender example.
    return struct.pack('>BBH',event,(128 if end else 0)|(64 if reserved else 0)|volume,duration)
def parse(raw):
    if len(raw)!=4:raise ValueError('one simple telephone-event block only, not redundant/multiple payloads')
    event,flags,duration=struct.unpack('>BBH',raw)
    if event not in range(10):raise ValueError('event outside fixture digit0..9 subset')
    return {'event':event,'E':bool(flags&128),'volume':flags&63,'duration_ticks':duration,'reserved_bit_ignored':True}
def aggregate(packets,clock=8000):
    if type(clock)is not int or clock<=0:raise ValueError('declared positive negotiated clock')
    state={};events=[];trace=[]
    for packet in packets:
        fields=parse(bytes.fromhex(packet['payload_hex']));key=(packet['session'],packet['ssrc'],packet['timestamp'],fields['event'])
        if any(type(packet[k])is not int for k in ['ssrc','timestamp','sequence'])or not 0<=packet['ssrc']<2**32 or not 0<=packet['timestamp']<2**32 or not 0<=packet['sequence']<2**16:raise ValueError('bounded authored RTP metadata')
        if fields['duration_ticks']==0:
            trace.append({'sequence':packet['sequence'],'action':'ignore_zero_duration_digit_not_state'});continue
        if key not in state:
            if len(state)>=32:raise ValueError('bounded exercise event count')
            state[key]={'maximum_duration':0,'closed':False}
        entry=state[key]
        if entry['closed']:
            action='repeated_end_ignored'if fields['E']else'late_interim_ignored'
            if fields['E']and fields['duration_ticks']!=entry['maximum_duration']:raise ValueError('inconsistent repeated final duration outside fixture')
        else:
            entry['maximum_duration']=max(entry['maximum_duration'],fields['duration_ticks']);action='interim_update'
            if fields['E']:
                if fields['duration_ticks']<entry['maximum_duration']:raise ValueError('final duration smaller than observed update')
                entry['closed']=True;action='emit_one_completed_event';events.append({'session':key[0],'ssrc':key[1],'start_timestamp':key[2],'digit':str(key[3]),'duration_ticks':entry['maximum_duration'],'duration_seconds':str(Fraction(entry['maximum_duration'],clock))})
        trace.append({'sequence':packet['sequence'],'event_identity':list(key),'fields':fields,'action':action,'maximum_duration_ticks':entry['maximum_duration'],'closed':entry['closed']})
    return {'clock_hz':clock,'packet_count':len(packets),'end_reports':sum(parse(bytes.fromhex(x['payload_hex']))['E']for x in packets),'completed_events':events,'trace':trace,'uncompleted_event_identities':[list(k)for k,v in state.items()if not v['closed']]}
def demo():
    packets=[]
    for seq,ts,dur,end in [(100,32000,160,False),(101,32000,640,False),(102,32000,800,True),(103,32000,800,True),(104,32000,800,True),(105,48000,240,False),(106,48000,640,True)]:
        packets.append({'session':'authored-session','ssrc':123,'timestamp':ts,'sequence':seq,'payload_hex':payload(5,end,dur).hex()})
    result=aggregate(packets);assert result['packet_count']==7 and result['end_reports']==4 and len(result['completed_events'])==2
    assert [x['duration_seconds']for x in result['completed_events']]==['1/10','2/25']and [x['start_timestamp']for x in result['completed_events']]==[32000,48000]
    with_late=aggregate(packets+[{'session':'authored-session','ssrc':123,'timestamp':32000,'sequence':99,'payload_hex':payload(5,False,80).hex()}]);assert with_late['completed_events']==result['completed_events']and with_late['trace'][-1]['action']=='late_interim_ignored'
    other=aggregate(packets+[{**packets[2],'ssrc':124}]);assert len(other['completed_events'])==3
    assert parse(payload(5,True,800,reserved=True))==parse(payload(5,True,800))
    zero=aggregate([{**packets[0],'payload_hex':struct.pack('>BBH',5,0,0).hex()}]);assert not zero['completed_events']and zero['trace'][0]['action']=='ignore_zero_duration_digit_not_state'
    no_end=aggregate(packets[:2]);assert not no_end['completed_events']and len(no_end['uncompleted_event_identities'])==1
    try:parse(b'123')
    except ValueError:bad_length=True
    else:raise AssertionError('short payload accepted')
    return {'authored_packets_not_network_capture':packets,'main':result,'no_end_report':no_end,'late_interim_does_not_reopen':True,'same_digit_timestamp_different_ssrc_is_separate':True,'reserved_receiver_bit_ignored':True,'zero_duration_digit_ignored':True,'short_block_rejected':bad_length,'scope':'one simple short event per payload; no timestamp wrap, long segmentation, RFC2198, actual signaling, loss recovery or event cache lifecycle implemented','real_calls_or_packets_sent':False}
if __name__=='__main__':print(json.dumps(demo(),indent=2))
