import asyncio
import logging
import os
import tempfile

import numpy as np
import pytest
import soundfile as sf
from torchvision.io.video import av
from typing import List, Dict, Any, Optional

from vision_agents.plugins import silero
from vision_agents.core.vad.events import VADAudioEvent, VADPartialEvent
from plugins.plugin_test_utils import get_audio_asset, get_json_metadata
from getstream.video.rtc.track_util import PcmData

# Setup logging for the test
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


@pytest.fixture
def mia_mp3_path():
    """Return the path to the mia.mp3 test file."""
    return get_audio_asset("mia.mp3")


@pytest.fixture
def mia_json_path():
    """Return the path to the mia.json metadata file."""
    return get_audio_asset("mia.json")


@pytest.fixture
def mia_metadata():
    """Load the mia.json metadata."""
    return get_json_metadata("mia.json")


@pytest.fixture
def mia_wav_path(mia_mp3_path):
    """Convert the mp3 to wav for testing."""
    # Create a temporary wav file
    fd, wav_path = tempfile.mkstemp(suffix=".wav")
    os.close(fd)

    # Convert mp3 to wav using PyAV
    container = av.open(mia_mp3_path)
    audio_stream = container.streams.audio[0]
    sample_rate = audio_stream.sample_rate

    # Read all audio frames
    samples = []
    for frame in container.decode(audio_stream):
        # Convert to numpy array
        frame_array = frame.to_ndarray()
        if len(frame_array.shape) > 1:
            # Convert stereo to mono by averaging channels
            frame_array = np.mean(frame_array, axis=0)
        samples.append(frame_array)

    # Concatenate all samples
    audio_data = np.concatenate(samples)

    # Convert to float32 for soundfile (it expects values between -1.0 and 1.0)
    if audio_data.dtype == np.int16:
        audio_data = audio_data.astype(np.float32) / 32768.0
    elif audio_data.dtype == np.int32:
        audio_data = audio_data.astype(np.float32) / 2147483648.0

    container.close()

    # Save as wav using soundfile
    sf.write(wav_path, audio_data, sample_rate)

    yield wav_path

    # Clean up temporary file
    if os.path.exists(wav_path):
        os.remove(wav_path)


@pytest.fixture
def audio_data(mia_wav_path):
    """Load and prepare the audio data for testing."""
    # Load audio file
    data, sample_rate = sf.read(mia_wav_path)

    # Convert to mono if stereo
    if len(data.shape) > 1 and data.shape[1] > 1:
        data = np.mean(data, axis=1)

    return data, sample_rate


@pytest.fixture
async def vad_setup():
    """Create a Silero VAD instance with standard test configuration."""
    vad = silero.VAD(
        sample_rate=16000,  # Use the model's native sample rate
        speech_pad_ms=400,  # Increase padding to connect nearby speech segments
        min_speech_ms=100,  # Keep minimum low to catch shorter segments
        activation_th=0.2,  # Lower threshold for more sensitivity
        deactivation_th=0.15,  # Lower deactivation threshold
    )

    # Storage for detected segments
    detected_segments: List[Dict[str, Any]] = []

    return vad, detected_segments


async def process_audio_file(
    vad,
    data,
    original_sample_rate,
    detected_segments: List[Dict[str, Any]],
    partial_segments: Optional[List[Dict[str, Any]]] = None,
):
    """Process audio data with VAD and collect detected speech segments."""

    # Setup event handlers
    @vad.events.subscribe
    async def on_audio(event: VADAudioEvent):
        # Extract duration and samples from the event
        duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
        samples = event.audio_data if event.audio_data is not None else b""
        detected_segments.append({"duration": duration, "bytes": len(samples)})
        logger.info(
            f"Detected speech segment: {duration:.2f} seconds ({len(samples)} bytes)"
        )

    # Add handler for partial events if tracking them
    if partial_segments is not None:

        @vad.events.subscribe
        async def on_partial(event: VADPartialEvent):
            # Extract duration and samples from the event
            duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
            samples = event.audio_data if event.audio_data is not None else b""
            partial_segments.append({"duration": duration, "bytes": len(samples)})
            logger.info(
                f"Partial speech data: {duration:.2f} seconds ({len(samples)} bytes)"
            )

    # Resample if needed
    if original_sample_rate != vad.sample_rate:
        import scipy.signal

        num_samples = int(len(data) * vad.sample_rate / original_sample_rate)
        data = scipy.signal.resample(data, num_samples)

    # Convert to int16 PCM
    data = np.asarray(data, dtype=np.float32)
    pcm_samples = (data * 32768.0).astype(np.int16)

    # Process the audio data
    await vad.process_audio(
        PcmData(samples=pcm_samples, sample_rate=vad.sample_rate, format="s16")
    )

    # Ensure we flush any remaining speech
    await vad.flush()

    # Add a small delay to allow all events to be processed
    await asyncio.sleep(0.1)


async def process_audio_in_chunks(
    vad,
    data,
    original_sample_rate,
    detected_segments: List[Dict[str, Any]],
    partial_segments: Optional[List[Dict[str, Any]]] = None,
    chunk_size_ms: int = 20,
):
    """Process audio data in small chunks to simulate streaming."""

    # Setup event handlers
    @vad.events.subscribe
    async def on_audio(event: VADAudioEvent):
        # Extract duration and samples from the event
        duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
        samples = event.audio_data if event.audio_data is not None else b""
        detected_segments.append({"duration": duration, "bytes": len(samples)})
        logger.info(
            f"Detected speech segment: {duration:.2f} seconds ({len(samples)} bytes)"
        )

    # Add handler for partial events if tracking them
    if partial_segments is not None:

        @vad.events.subscribe
        async def on_partial(event: VADPartialEvent):
            # Extract duration and samples from the event
            duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
            samples = event.audio_data if event.audio_data is not None else b""
            partial_segments.append({"duration": duration, "bytes": len(samples)})
            logger.info(
                f"Partial speech data: {duration:.2f} seconds ({len(samples)} bytes)"
            )

    # Allow event handlers to register before processing audio
    await asyncio.sleep(0.01)

    # Resample if needed
    if original_sample_rate != vad.sample_rate:
        import scipy.signal

        num_samples = int(len(data) * vad.sample_rate / original_sample_rate)
        data = scipy.signal.resample(data, num_samples)

    # Calculate chunk size in samples for the target sample rate
    chunk_samples = int(vad.sample_rate * chunk_size_ms / 1000)
    logger.info(
        f"Processing audio in {chunk_size_ms}ms chunks ({chunk_samples} samples per chunk)"
    )

    # Process audio in chunks
    for i in range(0, len(data), chunk_samples):
        # Extract chunk
        chunk = data[i : i + chunk_samples]

        # Pad last chunk if needed
        if len(chunk) < chunk_samples:
            padded_chunk = np.zeros(chunk_samples)
            padded_chunk[: len(chunk)] = chunk
            chunk = padded_chunk

        # Convert to int16 PCM
        chunk = np.asarray(chunk, dtype=np.float32)
        chunk_pcm = (chunk * 32768.0).astype(np.int16)

        # Process the chunk
        await vad.process_audio(
            PcmData(samples=chunk_pcm, sample_rate=vad.sample_rate, format="s16")
        )

        # Add a small delay to simulate real-time processing
        await asyncio.sleep(0.001)

    # Ensure we flush any remaining speech
    await vad.flush()

    # Add a small delay to allow all events to be processed
    await asyncio.sleep(0.1)


def verify_detected_speech(
    detected_segments: List[Dict[str, Any]],
    expected_duration: Optional[float] = None,
    expected_turns: Optional[int] = None,
    tolerance: float = 0.1,
):
    """
    Verify that the detected speech matches expectations.

    Args:
        detected_segments: List of detected speech segments
        expected_duration: Expected total duration of speech in seconds
        expected_turns: Expected number of speech turns
        tolerance: Tolerance for duration validation (e.g., 0.1 = ±10%)
    """
    # Verify that speech was detected
    assert len(detected_segments) > 0, "No speech segments were detected"

    # Verify number of turns if specified
    if expected_turns is not None:
        assert len(detected_segments) == expected_turns, (
            f"Expected {expected_turns} speech turns, got {len(detected_segments)}"
        )

    # Calculate total detected duration
    total_detected_duration = sum(segment["duration"] for segment in detected_segments)

    # Verify duration if specified
    if expected_duration is not None:
        # Calculate expected range with tighter tolerance (±10%)
        min_expected = expected_duration * (1 - tolerance)
        max_expected = expected_duration * (1 + tolerance)

        logger.info(f"Expected speech duration: {expected_duration:.2f} seconds")
        logger.info(f"Detected speech duration: {total_detected_duration:.2f} seconds")
        logger.info(
            f"Tolerance: ±{tolerance * 100:.0f}% ({min_expected:.2f} - {max_expected:.2f}s)"
        )

        if len(detected_segments) > 0:
            logger.info(f"Number of detected segments: {len(detected_segments)}")
            # Log the speech segments for inspection
            for i, segment in enumerate(detected_segments):
                logger.info(f"Speech segment {i + 1}: {segment['duration']:.2f}s")

        # Verify that the duration is within expected range
        assert min_expected <= total_detected_duration <= max_expected, (
            f"Expected speech duration {expected_duration}s (±{tolerance * 100:.0f}%), got {total_detected_duration}s"
        )


def verify_partial_events(
    partial_segments: List[Dict[str, Any]],
    detected_segments: List[Dict[str, Any]],
):
    """
    Verify that partial events were received before final speech events.

    Args:
        partial_segments: List of partial speech events
        detected_segments: List of final speech events
    """
    # Verify that at least one partial event was observed before each final audio event
    assert len(partial_segments) > 0, "No partial speech events were detected"

    # Each detected segment should have at least one corresponding partial event
    # For simplicity, we just check that we have at least one partial event per detected segment
    assert len(partial_segments) >= len(detected_segments), (
        f"Expected at least {len(detected_segments)} partial events, got {len(partial_segments)}"
    )


@pytest.mark.asyncio
async def test_silero_vad_initialization():
    """Test that the Silero VAD can be initialized with default parameters."""
    vad = silero.VAD()
    assert vad is not None
    assert vad.model is not None


@pytest.mark.asyncio
async def test_silero_vad_speech_detection(audio_data, mia_metadata, vad_setup):
    """Test that the Silero VAD can detect speech in an audio file."""
    vad, detected_segments = vad_setup
    data, sample_rate = audio_data

    # List to track partial events
    partial_segments: List[Dict[str, Any]] = []

    # Process the entire audio file
    await process_audio_file(
        vad, data, sample_rate, detected_segments, partial_segments
    )

    # Verify that speech was detected (without duration validation)
    assert len(detected_segments) > 0, "No speech segments were detected"
    logger.info(f"Detected {len(detected_segments)} speech segments")
    total_duration = sum(segment["duration"] for segment in detected_segments)
    logger.info(f"Total detected speech duration: {total_duration:.2f} seconds")

    # Verify partial events were received
    verify_partial_events(partial_segments, detected_segments)


@pytest.mark.asyncio
async def test_streaming_chunks_20ms(audio_data, mia_metadata):
    """
    Test that streaming chunks gives the same results as processing the entire file.
    This test verifies that the VAD works in streaming mode.
    """
    # Create a new VAD for this test using the same parameters as vad_setup
    vad = silero.VAD(
        sample_rate=16000,  # Use the model's native sample rate
        speech_pad_ms=400,  # Increase padding to connect nearby speech segments
        min_speech_ms=100,  # Keep minimum low to catch shorter segments
        activation_th=0.2,  # Lower threshold for more sensitivity
        deactivation_th=0.15,  # Lower deactivation threshold
    )

    # Lists to track detected segments and partial events
    detected_segments: List[Dict[str, Any]] = []
    partial_segments: List[Dict[str, Any]] = []
    data, sample_rate = audio_data

    # Process the audio in small chunks to simulate streaming
    await process_audio_in_chunks(
        vad, data, sample_rate, detected_segments, partial_segments, chunk_size_ms=20
    )

    # Verify that speech was detected (without duration validation)
    assert len(detected_segments) > 0, "No speech segments were detected"
    logger.info(f"Detected {len(detected_segments)} speech segments")
    total_duration = sum(segment["duration"] for segment in detected_segments)
    logger.info(f"Total detected speech duration: {total_duration:.2f} seconds")

    # Verify partial events were received
    verify_partial_events(partial_segments, detected_segments)


@pytest.mark.asyncio
async def test_vad_with_connection_manager_format(audio_data, vad_setup):
    """Test VAD with PCM data in bytes format."""
    vad, detected_segments = vad_setup
    data, sample_rate = audio_data

    # List to track partial events
    partial_segments = []

    # Resample if needed
    if sample_rate != vad.sample_rate:
        import scipy.signal

        num_samples = int(len(data) * vad.sample_rate / sample_rate)
        data = scipy.signal.resample(data, num_samples)

    # Convert to int16 PCM bytes
    data = np.asarray(data, dtype=np.float32)
    pcm_bytes = (data * 32768.0).astype(np.int16).tobytes()

    @vad.events.subscribe
    async def on_audio(event: VADAudioEvent):
        # Extract duration and samples from the event
        duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
        samples = event.audio_data if event.audio_data is not None else b""
        detected_segments.append({"duration": duration, "bytes": len(samples)})
        logger.info(
            f"Detected speech segment: {duration:.2f} seconds ({len(samples)} bytes)"
        )

    @vad.events.subscribe
    async def on_partial(event: VADPartialEvent):
        # Extract duration and samples from the event
        duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
        samples = event.audio_data if event.audio_data is not None else b""
        partial_segments.append({"duration": duration, "bytes": len(samples)})
        logger.info(
            f"Partial speech data: {duration:.2f} seconds ({len(samples)} bytes)"
        )

    # Process the audio data as bytes
    pcm_array = np.frombuffer(pcm_bytes, dtype=np.int16)
    await vad.process_audio(
        PcmData(samples=pcm_array, sample_rate=vad.sample_rate, format="s16")
    )

    # Ensure we flush any remaining speech
    await vad.flush()

    # Add a small delay to allow all events to be processed
    await asyncio.sleep(0.1)

    # Verify that speech was detected with expected turns
    assert len(detected_segments) > 0, "No speech segments were detected"
    logger.info(f"Detected {len(detected_segments)} speech segments")

    # Verify partial events were received
    verify_partial_events(partial_segments, detected_segments)


@pytest.mark.asyncio
async def test_silence_no_turns():
    """Test that no 'audio' events are fired when processing silence."""
    # Create a VAD for this test
    vad = silero.VAD(
        sample_rate=16000,
        speech_pad_ms=300,
        min_speech_ms=250,
        activation_th=0.5,
        deactivation_th=0.35,
    )

    # Create 5 seconds of silence (zeros)
    silence_duration_seconds = 5
    silence_samples = np.zeros(
        vad.sample_rate * silence_duration_seconds, dtype=np.int16
    )

    # Flag to track if audio event was fired
    audio_event_fired = False
    partial_event_fired = False

    @vad.events.subscribe
    async def on_audio(event: VADAudioEvent):
        nonlocal audio_event_fired
        audio_event_fired = True
        duration_sec = (
            (event.duration_ms / 1000.0) if event.duration_ms is not None else 0.0
        )
        logger.info(f"Audio event detected on silence! Duration: {duration_sec:.2f}s")

    @vad.events.subscribe
    async def on_partial(event: VADPartialEvent):
        nonlocal partial_event_fired
        partial_event_fired = True
        duration_sec = (
            (event.duration_ms / 1000.0) if event.duration_ms is not None else 0.0
        )
        logger.info(f"Partial event detected on silence! Duration: {duration_sec:.2f}s")

    # Process the silence in chunks to simulate streaming
    chunk_size = 512
    for i in range(0, len(silence_samples), chunk_size):
        chunk = silence_samples[i : i + chunk_size]
        await vad.process_audio(
            PcmData(samples=chunk, sample_rate=vad.sample_rate, format="s16")
        )

    # Ensure we flush any remaining audio
    await vad.flush()

    # Add a small delay to allow all events to be processed
    await asyncio.sleep(0.1)

    # Verify that no audio or partial events were fired
    assert not audio_event_fired, "An audio event was fired when processing silence"
    assert not partial_event_fired, "A partial event was fired when processing silence"


class TestSileroVAD:
    """Tests for Silero VAD implementation."""

    @pytest.mark.asyncio
    async def test_speech_detection(self):
        """Test that Silero VAD correctly detects speech in an audio file."""
        # Initialize the VAD with asymmetric thresholds
        vad = silero.VAD(
            sample_rate=16000,
            frame_size=512,
            activation_th=0.3,  # Activation threshold
            deactivation_th=0.2,  # Deactivation threshold (lower)
            speech_pad_ms=300,  # Increase padding to join nearby speech segments into one turn
            min_speech_ms=250,
            model_rate=16000,
            window_samples=512,  # Silero requires exactly 512 samples at 16kHz
            partial_frames=5,  # Emit partial every 5 frames
        )

        # Load an audio file with speech
        audio_path = get_audio_asset("formant_speech_16k.wav")
        audio_data, sample_rate = sf.read(audio_path, dtype="int16")
        assert sample_rate == 16000, f"Expected sample rate 16000, got {sample_rate}"

        # Create listeners to capture emitted speech segments and partial events
        detected_speech = []
        partial_events = []

        @vad.events.subscribe
        async def on_audio(event: VADAudioEvent, user=None):
            detected_speech.append(event)
            duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
            samples = event.audio_data if event.audio_data is not None else b""
            logger.info(f"Audio event: {duration:.2f}s ({len(samples)} samples)")

        @vad.events.subscribe
        async def on_partial(event: VADPartialEvent, user=None):
            partial_events.append(event)
            duration = event.duration_ms / 1000.0 if event.duration_ms else 0.0
            samples = event.audio_data if event.audio_data is not None else b""
            logger.info(f"Partial event: {duration:.2f}s ({len(samples)} samples)")

        # Process the audio data
        await vad.process_audio(
            PcmData(samples=audio_data, sample_rate=vad.sample_rate, format="s16")
        )

        # Ensure we flush any remaining speech
        await vad.flush()

        # Add a small delay to allow all events to be processed
        await asyncio.sleep(0.1)

        # Verify that speech was detected and partial events were received
        assert len(detected_speech) > 0, "No speech segments detected"
        logger.info(f"Detected {len(detected_speech)} speech segments")

        # Verify that partial events were received before the final audio event
        assert len(partial_events) > 0, "No partial events detected"
        assert len(partial_events) >= len(detected_speech), (
            f"Expected at least {len(detected_speech)} partial events, got {len(partial_events)}"
        )
        logger.info(f"Detected {len(partial_events)} partial events")

        # Clean up
        await vad.close()

    @pytest.mark.asyncio
    async def test_silence_no_turns(self):
        """Test that Silero VAD does not emit turns for pure silence."""
        # Initialize the VAD with asymmetric thresholds
        vad = silero.VAD(
            sample_rate=16000,
            frame_size=512,
            activation_th=0.5,
            deactivation_th=0.35,
            speech_pad_ms=30,
            min_speech_ms=250,
            model_rate=16000,
            window_samples=512,  # Silero requires exactly 512 samples at 16kHz
        )

        # Create 5 seconds of silence (16000 samples per second * 5 seconds)
        silence = np.zeros(16000 * 5, dtype=np.int16)

        # Create a listener to capture emitted speech segments
        detected_speech = []
        detected_partials = []

        @vad.events.subscribe
        async def on_audio(event: VADAudioEvent, user=None):
            detected_speech.append(event)

        @vad.events.subscribe
        async def on_partial(event: VADPartialEvent, user=None):
            detected_partials.append(event)

        # Process the silent audio data
        await vad.process_audio(
            PcmData(samples=silence, sample_rate=vad.sample_rate, format="s16")
        )

        # Ensure we flush any remaining buffer
        await vad.flush()

        # Add a small delay to allow all events to be processed
        await asyncio.sleep(0.1)

        # Verify that no speech was detected
        assert len(detected_speech) == 0, "Speech segments detected in silent audio"
        assert len(detected_partials) == 0, "Partial events detected in silent audio"

        # Clean up
        await vad.close()

    @pytest.mark.asyncio
    async def test_mixed_samplerate(self):
        """Test that Silero VAD handles mixed sample rates correctly."""
        # Initialize a listener for captured speech segments
        detected_speech_16k = []
        detected_speech_48k = []
        partial_events_16k = []
        partial_events_48k = []

        # First test with 16 kHz audio
        vad_16k = silero.VAD(
            sample_rate=16000,
            frame_size=512,
            activation_th=0.2,
            deactivation_th=0.15,
            speech_pad_ms=30,
            min_speech_ms=100,
            model_rate=16000,
            window_samples=512,  # Silero requires exactly 512 samples at 16kHz
            partial_frames=2,
        )

        # Spy/wrap is_speech for 16k
        max_p_16k = 0.0
        calls_16k = 0
        orig_is_speech_16k = vad_16k.is_speech

        async def spy_is_speech_16k(frame):
            nonlocal max_p_16k, calls_16k
            p = await orig_is_speech_16k(frame)
            calls_16k += 1
            if p > max_p_16k:
                max_p_16k = p
            return p

        vad_16k.is_speech = spy_is_speech_16k

        @vad_16k.events.subscribe
        async def on_audio_16k(event: VADAudioEvent, user=None):
            detected_speech_16k.append(event)

        @vad_16k.events.subscribe
        async def on_partial_16k(event: VADPartialEvent, user=None):
            partial_events_16k.append(event)

        # Load 16 kHz audio file
        audio_path_16k = get_audio_asset("formant_speech_16k.wav")
        audio_data_16k, sample_rate_16k = sf.read(audio_path_16k, dtype="int16")
        assert sample_rate_16k == 16000, (
            f"Expected sample rate 16000, got {sample_rate_16k}"
        )

        # Process the 16 kHz audio
        await vad_16k.process_audio(
            PcmData(samples=audio_data_16k, sample_rate=16000, format="s16")
        )
        await vad_16k.flush()
        await asyncio.sleep(0.1)
        logger.info(f"16k: is_speech calls={calls_16k}, max_p={max_p_16k:.3f}")

        # Now test with 48 kHz audio using the same parameters
        vad_48k = silero.VAD(
            sample_rate=48000,  # Input is 48 kHz
            frame_size=512 * 3,  # Scale frame size to match time duration
            activation_th=0.2,
            deactivation_th=0.15,
            speech_pad_ms=30,
            min_speech_ms=100,
            model_rate=16000,  # Model still runs at 16 kHz
            window_samples=512,  # Silero requires exactly 512 samples at 16kHz
            partial_frames=2,
        )

        # Spy/wrap is_speech for 48k
        max_p_48k = 0.0
        calls_48k = 0
        orig_is_speech_48k = vad_48k.is_speech

        async def spy_is_speech_48k(frame):
            nonlocal max_p_48k, calls_48k
            p = await orig_is_speech_48k(frame)
            calls_48k += 1
            if p > max_p_48k:
                max_p_48k = p
            return p

        vad_48k.is_speech = spy_is_speech_48k

        @vad_48k.events.subscribe
        async def on_audio_48k(event: VADAudioEvent, user=None):
            detected_speech_48k.append(event)

        @vad_48k.events.subscribe
        async def on_partial_48k(event: VADPartialEvent, user=None):
            partial_events_48k.append(event)

        # Load 48 kHz audio file
        audio_path_48k = get_audio_asset("formant_speech_48k.wav")
        audio_data_48k, sample_rate_48k = sf.read(audio_path_48k, dtype="int16")
        assert sample_rate_48k == 48000, (
            f"Expected sample rate 48000, got {sample_rate_48k}"
        )

        # Process the 48 kHz audio
        await vad_48k.process_audio(
            PcmData(samples=audio_data_48k, sample_rate=48000, format="s16")
        )
        await vad_48k.flush()
        await asyncio.sleep(0.1)
        logger.info(f"48k: is_speech calls={calls_48k}, max_p={max_p_48k:.3f}")

        # Verify both detected speech segments and partial events
        assert len(detected_speech_16k) > 0, (
            "No speech segments detected in 16 kHz audio"
        )
        assert len(detected_speech_48k) > 0, (
            "No speech segments detected in 48 kHz audio"
        )
        logger.info(
            f"Detected {len(detected_speech_16k)} speech segments in 16 kHz audio"
        )
        logger.info(
            f"Detected {len(detected_speech_48k)} speech segments in 48 kHz audio"
        )

        # Verify partial events
        assert len(partial_events_16k) > 0, "No partial events detected in 16 kHz audio"
        assert len(partial_events_48k) > 0, "No partial events detected in 48 kHz audio"
        # Don't require specific counts, just verify partials were emitted
        logger.info(
            f"Detected {len(partial_events_16k)} partial events in 16 kHz audio"
        )
        logger.info(
            f"Detected {len(partial_events_48k)} partial events in 48 kHz audio"
        )

        # Clean up
        await vad_16k.close()
        await vad_48k.close()

    @pytest.mark.asyncio
    async def test_bytearray_efficiency(self):
        """
        Test that the bytearray implementation is memory efficient.
        This is a basic test to ensure the implementation avoids O(n²) memory growth.
        """
        import tracemalloc

        # Initialize the VAD
        vad = silero.VAD(
            sample_rate=16000,
            frame_size=512,
            activation_th=0.3,
            deactivation_th=0.2,
            speech_pad_ms=30,
            min_speech_ms=250,
        )

        # Create a 2-second audio signal that's always above threshold to force buffering
        # Shorter duration to avoid timeout
        duration_sec = 2
        samples = np.random.randint(
            -10000, 10000, size=16000 * duration_sec, dtype=np.int16
        )

        # Start memory tracking
        tracemalloc.start()
        snapshot_start = tracemalloc.take_snapshot()

        # Track audio events
        detected_speech = []

        @vad.events.subscribe
        async def on_audio(event: VADAudioEvent, user=None):
            detected_speech.append(event)

        # Monkey-patch is_speech to always return high probability and force buffering
        original_is_speech = vad.is_speech

        async def mock_is_speech(frame):
            # Always return high probability to force speech detection
            return 0.9

        vad.is_speech = mock_is_speech

        # Process audio in smaller chunks for faster testing (200ms chunks)
        chunk_size = 3200  # 200ms at 16kHz
        for i in range(0, len(samples), chunk_size):
            chunk = samples[i : i + chunk_size]
            # Process the audio
            await vad.process_audio(
                PcmData(samples=chunk, sample_rate=16000, format="s16")
            )

        # Take memory snapshot after processing
        snapshot_end = tracemalloc.take_snapshot()
        tracemalloc.stop()

        # Compare memory usage
        stats = snapshot_end.compare_to(snapshot_start, "lineno")
        total_memory_bytes = sum(
            stat.size_diff for stat in stats if "vad/__init__.py" in str(stat)
        )
        total_memory_mb = total_memory_bytes / (1024 * 1024)

        # Check buffer size relative to audio size
        audio_size_bytes = samples.nbytes
        audio_size_mb = audio_size_bytes / (1024 * 1024)
        ratio = total_memory_mb / audio_size_mb if audio_size_mb > 0 else 0

        logger.info(
            f"Memory growth: {total_memory_mb:.2f} MB for {audio_size_mb:.2f} MB of audio (ratio: {ratio:.2f})"
        )

        # The ratio should be close to 1.0 for efficient buffering (no excessive copying)
        # Allow some overhead but ensure it's not O(n²)
        assert ratio < 5.0, f"Memory usage ratio too high: {ratio:.2f}"

        # Restore original is_speech method
        vad.is_speech = original_is_speech

        # Clean up
        await vad.flush()
        await vad.close()

    @pytest.mark.asyncio
    async def test_cuda_fallback(self):
        """
        Test that the Silero VAD falls back to CPU when CUDA is not available.
        This test will be skipped if CUDA is available.
        """
        import torch

        # Skip the test if CUDA is actually available
        if torch.cuda.is_available():
            pytest.xfail("CUDA is available, skipping fallback test")

        # Initialize VAD with CUDA device
        vad = silero.VAD(
            sample_rate=16000,
            window_samples=512,
            device="cuda:0",  # Request CUDA
        )

        # Check that the device fell back to CPU
        assert vad.device_name == "cpu", (
            "Failed to fall back to CPU when CUDA unavailable"
        )
        assert vad.device.type == "cpu", "Device is not CPU after fallback"

        # Create a short silence for inference
        silence = np.zeros(512, dtype=np.int16)

        # Try processing to ensure it works
        await vad.is_speech(PcmData(samples=silence, sample_rate=16000, format="s16"))

        # Clean up
        await vad.close()

    @pytest.mark.asyncio
    async def test_flush_api(self):
        """
        Test that flush() properly emits a speech turn even if the speech is not complete.
        """
        # Initialize the VAD with longer padding to ensure speech doesn't end naturally
        vad = silero.VAD(
            sample_rate=16000,
            window_samples=512,
            activation_th=0.3,
            deactivation_th=0.2,
            speech_pad_ms=1000,  # Long padding to ensure speech doesn't end naturally
            min_speech_ms=100,  # Short minimum to ensure even brief speech is detected
        )

        # Load an audio file with speech
        audio_path = get_audio_asset("formant_speech_16k.wav")
        audio_data, sample_rate = sf.read(audio_path, dtype="int16")
        assert sample_rate == 16000, f"Expected sample rate 16000, got {sample_rate}"

        # Get first second of audio (assumes speech starts quickly)
        half_audio = audio_data[: int(sample_rate)]  # First second

        # Create listeners to capture emitted speech segments
        detected_speech = []
        flush_triggered = False

        @vad.events.subscribe
        async def on_audio(event: VADAudioEvent, user=None):
            detected_speech.append({"event": event, "from_flush": flush_triggered})

        # Allow event handler to register
        await asyncio.sleep(0.01)

        # Process half the audio to get speech started but not completed
        await vad.process_audio(
            PcmData(samples=half_audio, sample_rate=sample_rate, format="s16")
        )

        # Mark that we're about to flush
        flush_triggered = True

        # Flush to force emission of the current speech
        await vad.flush()

        # Wait for all events to be processed
        await vad.events.wait(timeout=1.0)

        # Verify that at least one speech segment was emitted due to the flush
        assert len(detected_speech) > 0, "No speech segments detected after flush"
        assert detected_speech[-1]["from_flush"], (
            "Last speech segment was not triggered by flush"
        )

        # Clean up
        await vad.close()

    @pytest.mark.asyncio
    async def test_onnx_fallback(self):
        """
        Test that the Silero VAD falls back to PyTorch when ONNX is not available or fails.
        """

        # Initialize VAD with ONNX requested
        vad = silero.VAD(
            sample_rate=16000,
            window_samples=512,
            use_onnx=True,
        )

        # We know there's an issue with ONNX export in the current implementation
        # so just check that it falls back to PyTorch properly and doesn't crash
        assert not vad.use_onnx, "ONNX fallback to PyTorch should have occurred"
        assert vad.model is not None, "Model should have been loaded after fallback"

        # Create a short silence for inference
        silence = np.zeros(512, dtype=np.int16)

        # Try processing to ensure it works
        await vad.is_speech(PcmData(samples=silence, sample_rate=16000, format="s16"))

        # Clean up
        await vad.close()
