"""Original SMF fixtures: explicit/running status, channel change, missing initial status.
Only the note messages and tempo/end meta events generated here are supported.
"""
import pathlib,struct,json,csv
r=pathlib.Path(__file__).parent
def make(channel2=False,compress=False,omit_first=False):
 events=[(0,144,60,80),(120,144,60,0),(120,145 if channel2 else 144,64,80),(120,145 if channel2 else 144,64,0)]
 t=b'\x00\xff\x51\x03\x07\xa1\x20';last=None
 for i,(delta,status,key,velocity) in enumerate(events):
  t+=bytes([delta])
  if not(omit_first and i==0)and(not compress or status!=last):t+=bytes([status])
  t+=bytes([key,velocity]);last=status
 t+=b'\x00\xff\x2f\x00'
 return b'MThd'+struct.pack('>IHHH',6,0,1,480)+b'MTrk'+struct.pack('>I',len(t))+t
def parse(data):
 assert data[:4]==b'MThd'and data[14:18]==b'MTrk';p=22;tick=0;running=None;events=[]
 def vlq():
  nonlocal p
  n=0
  while True:
   v=data[p];p+=1;n=(n<<7)|(v&127)
   if v<128:return n
 while p<len(data):
  tick+=vlq();b=data[p]
  if b==255:
   p+=1;kind=data[p];p+=1;size=vlq();p+=size;running=None
   if kind==47:break
   if kind!=81:raise ValueError('Unsupported meta event in fixture parser')
   continue
  explicit=b>=128
  if explicit:
   if b>>4!=9:raise ValueError('Unsupported status in fixture parser')
   running=b;p+=1
  elif running is None:raise ValueError('No prior channel status')
  key,velocity=data[p:p+2];p+=2
  if key>127 or velocity>127:raise ValueError('Invalid data byte')
  events.append({'tick':tick,'status':running,'channel':(running&15)+1,'key':key,'velocity':velocity,'kind':'off' if velocity==0 else 'on','statusStored':explicit})
 return events
fixtures={'explicit.mid':make(),'running.mid':make(compress=True),'channel-change.mid':make(channel2=True,compress=True),'missing-first-status.mid':make(compress=True,omit_first=True)}
rows=[];results=[]
for name,data in fixtures.items():
 (r/name).write_bytes(data)
 try:
  events=parse(data);row={'file':name,'bytes':len(data),'result':'PASS','events':len(events),'explicitStatuses':sum(e['statusStored'] for e in events),'channels':','.join(map(str,sorted({e['channel'] for e in events})))}
  results.append({'file':name,'events':events})
 except ValueError as e:
  row={'file':name,'bytes':len(data),'result':str(e),'events':0,'explicitStatuses':0,'channels':''};results.append({'file':name,'error':str(e)})
 rows.append(row)
semantic=lambda es:[{k:v for k,v in e.items()if k!='statusStored'}for e in es]
assert semantic(results[0]['events'])==semantic(results[1]['events'])
assert [e['tick']for e in results[1]['events']]==[0,120,240,360]
assert [e['channel']for e in results[2]['events']]==[1,1,2,2]
assert [x['bytes']for x in rows]==[49,46,47,45]
assert results[3]['error']=='No prior channel status'
(r/'results.json').write_text(json.dumps({'scope':'Constructed MIDI file parsing only, no sound/DAW/driver performance test','rows':rows,'cases':results},indent=2)+'\n',encoding='utf-8')
with(r/'observations.csv').open('w',newline='',encoding='utf-8')as f:
 w=csv.DictWriter(f,fieldnames=list(rows[0]));w.writeheader();w.writerows(rows)
print(json.dumps(rows))
