from __future__ import annotations

import asyncio
import struct

from app.utils import audio_backlog

FRAME_SAMPLES = 320  # 20 ms @ 16 kHz mono
BYTES_PER_FRAME = FRAME_SAMPLES * 2


def _silent() -> bytes:
    return b"\x00" * BYTES_PER_FRAME


def _voiced(amp: int = 8000) -> bytes:
    return struct.pack("<%dh" % FRAME_SAMPLES, *([amp] * FRAME_SAMPLES))


def _queue(frames: list[bytes]) -> asyncio.Queue:
    q: asyncio.Queue = asyncio.Queue()
    for frame in frames:
        q.put_nowait(frame)
    return q


def _drain(q: asyncio.Queue) -> list[bytes]:
    out = []
    while True:
        try:
            out.append(q.get_nowait())
        except asyncio.QueueEmpty:
            break
    return out


def test_rms_threshold_from_dbfs() -> None:
    assert audio_backlog.rms_threshold_from_dbfs(0.0) == 32768
    assert audio_backlog.rms_threshold_from_dbfs(-50.0) == 103
    assert audio_backlog.rms_threshold_from_dbfs(-200.0) == 0


def test_frames_from_ms_rounds_up_with_minimum() -> None:
    assert audio_backlog.frames_from_ms(100, 20) == 5
    assert audio_backlog.frames_from_ms(90, 20) == 5  # arredonda pra cima
    assert audio_backlog.frames_from_ms(0, 20) == 1  # minimo
    assert audio_backlog.frames_from_ms(40, 20) == 2


def test_is_silent_frame() -> None:
    thr = audio_backlog.rms_threshold_from_dbfs(-50.0)
    assert audio_backlog.is_silent_frame(_silent(), thr) is True
    assert audio_backlog.is_silent_frame(_voiced(), thr) is False


def test_shed_below_keep_is_noop() -> None:
    q = _queue([_voiced()] * 2)
    result = audio_backlog.shed_queue_backlog(q, keep_frames=5, rms_threshold=103, blind=False)
    assert result.dropped == 0
    assert result.mode == "none"
    assert q.qsize() == 2


def test_shed_blind_drops_oldest_and_keeps_recent_in_order() -> None:
    frames = [_voiced(1000 + i) for i in range(8)]
    q = _queue(frames)
    result = audio_backlog.shed_queue_backlog(q, keep_frames=2, rms_threshold=103, blind=True)
    assert result.dropped == 6
    assert result.dropped_voiced == 6
    assert result.mode == "blind"
    assert _drain(q) == frames[-2:]


def test_shed_energy_drops_only_silence_preserving_speech_order() -> None:
    voiced = [_voiced(2000 + i * 100) for i in range(4)]
    # intercala voz e silencio: [V0, S, V1, S, V2, S, V3, S]
    frames = [voiced[0], _silent(), voiced[1], _silent(), voiced[2], _silent(), voiced[3], _silent()]
    q = _queue(frames)
    result = audio_backlog.shed_queue_backlog(q, keep_frames=2, rms_threshold=103, blind=False)
    # need = 8 - 2 = 6, mas so ha 4 frames silenciosos -> descarta os 4 silencios,
    # preserva os 4 de voz na ordem (nao corta fala).
    assert result.mode == "energy"
    assert result.dropped == 4
    assert result.dropped_silent == 4
    assert result.dropped_voiced == 0
    assert _drain(q) == voiced


def test_shed_energy_stops_at_need_even_with_more_silence() -> None:
    voiced = [_voiced(3000 + i) for i in range(2)]
    # 2 de voz + 6 de silencio, keep=4 -> need=4 -> descarta 4 silencios, sobra 4
    frames = [voiced[0], _silent(), _silent(), _silent(), voiced[1], _silent(), _silent(), _silent()]
    q = _queue(frames)
    result = audio_backlog.shed_queue_backlog(q, keep_frames=4, rms_threshold=103, blind=False)
    assert result.dropped == 4
    assert result.dropped_silent == 4
    remaining = _drain(q)
    assert q.qsize() == 0
    # sobra os 2 de voz (na ordem) + 2 silencios mais recentes
    assert remaining[0] == voiced[0]
    assert voiced[1] in remaining
    assert len(remaining) == 4


def test_shed_disabled_via_empty_queue() -> None:
    q = _queue([])
    result = audio_backlog.shed_queue_backlog(q, keep_frames=2, rms_threshold=103, blind=True)
    assert result.dropped == 0
    assert result.mode == "none"
