"""Tests for StreamingVad and EnergyVad."""

from __future__ import annotations

from dataclasses import dataclass

import numpy as np

from noteflow.infrastructure.asr.streaming_vad import (
    EnergyVad,
    EnergyVadConfig,
    StreamingVad,
)


@dataclass(frozen=True, slots=True)
class EnergyVadState:
    """Snapshot of EnergyVad internal state for test assertions.

    Provides typed access to protected attributes without violating
    protected-member access rules.
    """

    is_speech: bool
    speech_frame_count: int
    silence_frame_count: int


def get_vad_state(vad: EnergyVad) -> EnergyVadState:
    """Extract internal state from EnergyVad for test assertions.

    Uses public accessors to avoid protected-member access while
    maintaining type safety through the returned dataclass.
    """
    return EnergyVadState(
        is_speech=vad.is_speech,
        speech_frame_count=vad.speech_frame_count,
        silence_frame_count=vad.silence_frame_count,
    )


class TestEnergyVadBasics:
    """Basic tests for EnergyVad."""

    def test_energy_vad_default_config(self) -> None:
        """EnergyVad uses default config when not provided."""
        vad = EnergyVad()

        assert vad.config.speech_threshold == 0.01, "Default speech threshold should be 0.01"
        assert vad.config.silence_threshold == 0.005, "Default silence threshold should be 0.005"

    def test_energy_vad_custom_config(self) -> None:
        """EnergyVad accepts custom configuration."""
        config = EnergyVadConfig(speech_threshold=0.02, min_speech_frames=5)
        vad = EnergyVad(config=config)

        assert vad.config.speech_threshold == 0.02, "Custom speech threshold should be applied"
        assert vad.config.min_speech_frames == 5, "Custom min_speech_frames should be applied"

    def test_initial_state_is_silence(self) -> None:
        """EnergyVad starts in silence state."""
        vad = EnergyVad()

        assert not get_vad_state(vad).is_speech, "VAD should start in silence state"


class TestEnergyVadDetection:
    """Tests for EnergyVad speech detection."""

    def test_detects_silence_for_zeros(self) -> None:
        """Silent audio detected as non-speech."""
        vad = EnergyVad()
        audio = np.zeros(1600, dtype=np.float32)

        result = vad.process(audio)

        assert not result, "Zero-amplitude audio should be detected as silence"

    def test_detects_speech_for_high_energy(self) -> None:
        """High energy audio eventually detected as speech."""
        vad = EnergyVad(config=EnergyVadConfig(min_speech_frames=2))
        # Audio with energy above threshold
        audio = np.ones(1600, dtype=np.float32) * 0.1

        vad.process(audio)
        result = vad.process(audio)

        assert result, "High energy audio should be detected as speech after min_speech_frames"

    def test_speech_requires_consecutive_frames(self) -> None:
        """Speech detection requires min_speech_frames consecutive frames."""
        vad = EnergyVad(config=EnergyVadConfig(min_speech_frames=3))
        audio = np.ones(1600, dtype=np.float32) * 0.1

        assert not vad.process(audio), "First frame should not trigger speech"
        assert not vad.process(audio), "Second frame should not trigger speech (need 3)"
        assert vad.process(audio), "Third consecutive frame should trigger speech"

    def test_silence_after_speech_requires_frames(self) -> None:
        """Transition to silence requires min_silence_frames."""
        config = EnergyVadConfig(min_speech_frames=1, min_silence_frames=2)
        vad = EnergyVad(config=config)
        speech = np.ones(1600, dtype=np.float32) * 0.1
        silence = np.zeros(1600, dtype=np.float32)

        vad.process(speech)
        assert get_vad_state(vad).is_speech, "Should be in speech state after processing speech audio"

        vad.process(silence)
        assert get_vad_state(vad).is_speech, "Should remain in speech state after first silence frame"

        vad.process(silence)
        assert not get_vad_state(vad).is_speech, "Should transition to silence after min_silence_frames"

    def test_hysteresis_prevents_chatter(self) -> None:
        """Hysteresis prevents rapid speech/silence toggling."""
        config = EnergyVadConfig(
            speech_threshold=0.01,
            silence_threshold=0.005,
            min_speech_frames=1,
            min_silence_frames=1,
        )
        vad = EnergyVad(config=config)

        # Just above speech threshold -> speech
        high = np.ones(1600, dtype=np.float32) * 0.015
        vad.process(high)
        assert get_vad_state(vad).is_speech, "Audio above speech threshold should trigger speech"

        # Between thresholds (below speech, above silence) -> stays speech
        mid = np.ones(1600, dtype=np.float32) * 0.007
        vad.process(mid)
        assert get_vad_state(vad).is_speech, "Audio in hysteresis zone should maintain speech state"

        # Below silence threshold -> silence
        low = np.ones(1600, dtype=np.float32) * 0.003
        vad.process(low)
        assert not get_vad_state(vad).is_speech, "Audio below silence threshold should transition to silence"


class TestEnergyVadReset:
    """Tests for EnergyVad reset behavior."""

    def test_energy_vad_reset_clears_state(self) -> None:
        """Reset returns VAD to initial state."""
        vad = EnergyVad(config=EnergyVadConfig(min_speech_frames=1))
        audio = np.ones(1600, dtype=np.float32) * 0.1
        vad.process(audio)

        vad.reset()

        state = get_vad_state(vad)
        assert not state.is_speech, "Reset should clear speech state"
        assert state.speech_frame_count == 0, "Reset should clear speech frame count"
        assert state.silence_frame_count == 0, "Reset should clear silence frame count"


class TestStreamingVad:
    """Tests for StreamingVad wrapper."""

    def test_default_engine_is_energy_vad(self) -> None:
        """StreamingVad uses EnergyVad by default."""
        vad = StreamingVad()

        assert isinstance(vad.engine, EnergyVad), "StreamingVad should use EnergyVad as default engine"

    def test_process_chunk_delegates_to_engine(self) -> None:
        """process_chunk delegates to underlying engine."""
        vad = StreamingVad()
        silence = np.zeros(1600, dtype=np.float32)

        result = vad.process_chunk(silence)

        assert not result, "process_chunk should return engine result for silence"

    def test_reset_delegates_to_engine(self) -> None:
        """reset delegates to underlying engine."""
        vad = StreamingVad()
        speech = np.ones(1600, dtype=np.float32) * 0.1

        vad.process_chunk(speech)
        vad.process_chunk(speech)
        vad.reset()

        assert isinstance(vad.engine, EnergyVad), "StreamingVad should use EnergyVad engine"
        assert not get_vad_state(vad.engine).is_speech, "StreamingVad reset should clear engine state"
