"""Trusted-manifest byte verification on temporary generated WAVs only."""
from pathlib import Path,PurePosixPath
from tempfile import gettempdir
from contextlib import contextmanager
import io,wave,struct,hashlib,json,re,uuid
MAX_BYTES=10_000_000
def make_wave(samples):
    f=io.BytesIO()
    with wave.open(f,'wb') as w:w.setnchannels(1);w.setsampwidth(2);w.setframerate(8000);w.writeframes(struct.pack('<'+'h'*len(samples),*samples))
    return f.getvalue()
def safe_path(root,name):
    if not isinstance(name,str) or not name or chr(92) in name or ':' in name:raise ValueError('relative POSIX path required')
    parts=PurePosixPath(name)
    if parts.is_absolute() or '..' in parts.parts:raise ValueError('path escapes fixture root')
    root=Path(root).resolve();p=root.joinpath(*parts.parts).resolve()
    if not p.is_relative_to(root):raise ValueError('resolved path escapes fixture root')
    return p
def digest(path):
    size=path.stat().st_size
    if size>MAX_BYTES:raise ValueError('local file size cap')
    h=hashlib.sha256();chunks=0
    with path.open('rb') as f:
        while True:
            b=f.read(4096)
            if not b:break
            h.update(b);chunks+=1
    return {'bytes':size,'sha256':h.hexdigest(),'chunks_read':chunks}
def verify(root,manifest):
    root=Path(root).resolve();entries=manifest['entries']
    if not 1<=len(entries)<=1000:raise ValueError('local entry count cap')
    if len({r['logical_id'] for r in entries})!=len(entries) or len({r['path'] for r in entries})!=len(entries):raise ValueError('duplicate reference identity/path')
    results=[];expected_paths=set()
    for r in entries:
        p=safe_path(root,r['path']);expected_paths.add(p)
        if type(r['bytes']) is not int or not 0<=r['bytes']<=MAX_BYTES or not re.fullmatch('[0-9a-f]{64}',r['sha256']):raise ValueError('invalid trusted reference')
        if not p.is_file():results.append({'logical_id':r['logical_id'],'path':r['path'],'status':'missing'});continue
        obs=digest(p);size_ok=obs['bytes']==r['bytes'];hash_ok=obs['sha256']==r['sha256']
        results.append({'logical_id':r['logical_id'],'path':r['path'],'status':'verified_bytes' if size_ok and hash_ok else 'size_mismatch' if not size_ok else 'hash_mismatch','size_match':size_ok,'hash_match':hash_ok,'observed':obs})
    unexpected=[]
    for p in root.rglob('*.wav'):
        rel=p.relative_to(root).as_posix();resolved=safe_path(root,rel)
        if resolved not in expected_paths and resolved.is_file():
            obs=digest(resolved);unexpected.append({'path':rel,'observed':obs,'byte_equal_to_reference_ids':[r['logical_id'] for r in entries if r['sha256']==obs['sha256'] and r['bytes']==obs['bytes']]})
        if len(unexpected)>1000:raise ValueError('local unexpected-file cap')
    return {'expected':results,'unexpected':unexpected,'authenticity_or_audio_quality_verified':False}
@contextmanager
def fixture_directory():
    base=Path(gettempdir()).resolve();root=base/('opera-audio-fixture-'+uuid.uuid4().hex)
    root.mkdir();assert root.resolve().is_relative_to(base)
    try:yield root
    finally:
        assert not root.is_symlink() and root.resolve().parent==base
        for name in ['intact.wav','missing.wav','changed.wav','truncated.wav','renamed.wav','renamed-copy.wav']:
            p=safe_path(root,name)
            if p.is_file():p.unlink()
        root.rmdir()  # Only succeeds when the generated flat fixture directory is empty.
def selftest():
    with fixture_directory() as tmp:
        root=Path(tmp).resolve();assert root.is_relative_to(Path(gettempdir()).resolve())
        names=['intact.wav','missing.wav','changed.wav','truncated.wav','renamed.wav'];entries=[]
        for i,name in enumerate(names):
            samples=[(j%200)-100 for j in range(3000)] if i==0 else [i,100+i,-100-i,0]
            p=safe_path(root,name);p.write_bytes(make_wave(samples));d=digest(p)
            entries.append({'logical_id':'audio-'+str(i+1),'version':'v1','path':name,'declared_format':'authored mono PCM16 WAV','bytes':d['bytes'],'sha256':d['sha256']})
        manifest={'trusted_reference_for_this_authored_exercise_only':True,'entries':entries}
        safe_path(root,'missing.wav').unlink()
        p=safe_path(root,'changed.wav');b=bytearray(p.read_bytes());b[-1]^=1;p.write_bytes(b)
        p=safe_path(root,'truncated.wav');p.write_bytes(p.read_bytes()[:-1])
        safe_path(root,'renamed.wav').rename(safe_path(root,'renamed-copy.wav'))
        result=verify(root,manifest)
        assert [r['status'] for r in result['expected']]==['verified_bytes','missing','hash_mismatch','size_mismatch','missing']
        assert result['expected'][0]['observed']['chunks_read']==2
        assert result['unexpected'][0]['byte_equal_to_reference_ids']==['audio-5']
        for bad in ['../outside.wav','C:/private.wav','/absolute.wav','a'+chr(92)+'b.wav']:
            try:safe_path(root,bad)
            except ValueError:pass
            else:raise AssertionError('unsafe path accepted')
        return {'reference_manifest':manifest,'observed_verification':result,'unsafe_path_cases_rejected':4,'real_user_files_read':False,'real_audio_listened':False}
if __name__=='__main__':print(json.dumps(selftest(),indent=2))
