"""Run beside audio_lab.py and the five supplied WAVs: python audio_lab_test.py"""
from array import array
from contextlib import redirect_stdout
import hashlib
import io
import json
from pathlib import Path
import tempfile
import unittest
import wave
import audio_lab as lab
HERE=Path(__file__).resolve().parent

class AudioLabTests(unittest.TestCase):
    def setUp(self):
        self.samples,self.rate=lab.load_pcm(HERE/'jingle-faded.wav')
    def test_source_duration_and_boundaries(self):
        measured=lab.measure(self.samples,self.rate)
        self.assertEqual((measured['samples'],measured['seconds'],measured['boundaryCount']),(384000,8,15))
        self.assertEqual(measured['maxBoundaryJump'],0)
    def test_peak_target(self):
        result,report=lab.normalized(self.samples,self.rate,'peak',-3)
        self.assertAlmostEqual(report['after']['samplePeakDbfs'],-3,places=3)
        self.assertEqual(report['after']['railSamples'],0)
        self.assertEqual(len(result),len(self.samples))
    def test_rms_target(self):
        _,report=lab.normalized(self.samples,self.rate,'rms',-18)
        self.assertAlmostEqual(report['after']['rmsDbfs'],-18,places=3)
    def test_one_gain_not_compression(self):
        result,report=lab.normalized(self.samples,self.rate,'peak',-3)
        gain=10**(report['gainDb']/20)
        self.assertTrue(all(abs(after-before*gain)<=.5 for before,after in zip(self.samples,result)))
    def test_all_distributed_normalized_pcm_matches(self):
        for src,out,mode,target in [('jingle-faded.wav','jingle-faded-peak-minus3.wav','peak',-3),('jingle-faded.wav','jingle-faded-rms-minus18.wav','rms',-18),('jingle-abrupt.wav','jingle-abrupt-rms-minus18.wav','rms',-18)]:
            original,rate=lab.load_pcm(HERE/src);expected,_=lab.normalized(original,rate,mode,target)
            actual,actual_rate=lab.load_pcm(HERE/out)
            self.assertEqual((actual,actual_rate),(expected,rate))
    def test_published_metrics_match(self):
        for record in json.loads((HERE/'audio-metrics.json').read_text()):
            samples,rate=lab.load_pcm(HERE/record['file'])
            self.assertEqual(record,{'file':record['file'],**lab.measure(samples,rate)})
    def test_silence_rejected(self):
        with self.assertRaisesRegex(ValueError,'Silence'):lab.normalized(array('h',[0]*100),48000,'rms',-18)
    def test_clip_risk_rejected(self):
        with self.assertRaisesRegex(ValueError,'range'):lab.normalized(self.samples,self.rate,'rms',-1)
    def test_nonfinite_target_rejected(self):
        for target in [float('nan'),float('inf')]:
            with self.assertRaises(ValueError):lab.normalized(self.samples,self.rate,'peak',target)
    def test_stereo_rejected(self):
        with tempfile.TemporaryDirectory() as folder:
            p=Path(folder)/'stereo.wav'
            with wave.open(str(p),'wb') as w:
                w.setnchannels(2);w.setsampwidth(2);w.setframerate(48000);w.writeframes(b'\0'*400)
            with self.assertRaisesRegex(ValueError,'mono'):lab.load_pcm(p)
    def test_truncated_pcm_rejected(self):
        with tempfile.TemporaryDirectory() as folder:
            p=Path(folder)/'short.wav';p.write_bytes((HERE/'jingle-faded.wav').read_bytes()[:-2])
            with self.assertRaisesRegex(ValueError,'Truncated'):lab.load_pcm(p)
    def test_existing_output_is_preserved(self):
        with tempfile.TemporaryDirectory() as folder:
            p=Path(folder)/'keep.wav';p.write_bytes(b'KEEP')
            with self.assertRaisesRegex(ValueError,'existing'):lab.main(['normalize',str(HERE/'jingle-faded.wav'),str(p),'--mode','peak','--target-dbfs','-3'])
            self.assertEqual(p.read_bytes(),b'KEEP')
    def test_rejected_target_leaves_no_output(self):
        with tempfile.TemporaryDirectory() as folder:
            p=Path(folder)/'not-created.wav'
            with self.assertRaises(ValueError):lab.main(['normalize',str(HERE/'jingle-faded.wav'),str(p),'--mode','rms','--target-dbfs','-1'])
            self.assertFalse(p.exists())
    def test_write_roundtrip_without_source_mutation(self):
        source=HERE/'jingle-faded.wav';before=hashlib.sha256(source.read_bytes()).digest()
        with tempfile.TemporaryDirectory() as folder:
            p=Path(folder)/'new.wav'
            with redirect_stdout(io.StringIO()):lab.main(['normalize',str(source),str(p),'--mode','rms','--target-dbfs','-18'])
            self.assertEqual(p.read_bytes(),(HERE/'jingle-faded-rms-minus18.wav').read_bytes())
        self.assertEqual(before,hashlib.sha256(source.read_bytes()).digest())
    def test_turning_clipped_plateau_down_does_not_restore_shape(self):
        plateau=array('h',[32767]*32+[-32768]*32)
        reduced,report=lab.normalized(plateau,48000,'rms',-18)
        self.assertEqual(report['after']['railSamples'],0)
        self.assertEqual(len(set(reduced[:32])),1)
        self.assertEqual(len(set(reduced[32:])),1)
    def test_boundary_spacing_not_automatically_detected(self):
        with self.assertRaisesRegex(ValueError,'whole number'):lab.measure(self.samples,self.rate,.00001)

if __name__=='__main__':unittest.main(verbosity=2)
