"""Tests for Segmenter state machine."""

from __future__ import annotations

import numpy as np
import pytest
from numpy.typing import NDArray

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.asr.segmenter import (
    AudioSegment,
    Segmenter,
    SegmenterConfig,
    SegmenterState,
)

# Test constants for custom configuration values
CUSTOM_SAMPLE_RATE_HZ = 44100
"""Custom sample rate in Hz for testing non-default configuration."""

MAX_SEGMENT_DURATION_SEC = 60.0
"""Maximum segment duration in seconds for testing force-emit behavior."""


class TestSegmenterInitialization:
    """Tests for Segmenter initialization."""

    def test_default_config(self) -> None:
        """Segmenter uses default config when not provided."""
        segmenter = Segmenter()

        assert segmenter.config.sample_rate == DEFAULT_SAMPLE_RATE, "default sample rate should match constant"
        assert segmenter.config.min_speech_duration == 0.3, "default min speech duration should be 0.3s"

    def test_custom_config(self) -> None:
        """Segmenter accepts custom configuration."""
        config = SegmenterConfig(sample_rate=CUSTOM_SAMPLE_RATE_HZ, max_segment_duration=MAX_SEGMENT_DURATION_SEC)
        segmenter = Segmenter(config=config)

        assert segmenter.config.sample_rate == CUSTOM_SAMPLE_RATE_HZ, "custom sample rate should be applied"
        assert segmenter.config.max_segment_duration == MAX_SEGMENT_DURATION_SEC, "custom max segment duration should be applied"

    def test_initial_state_is_idle(self) -> None:
        """Segmenter starts in IDLE state."""
        segmenter = Segmenter()

        assert segmenter.state == SegmenterState.IDLE, "segmenter should start in IDLE state"


class TestSegmenterStateTransitions:
    """Tests for Segmenter state machine transitions."""

    @pytest.fixture
    def segmenter(self) -> Segmenter:
        """Create segmenter with test-friendly config."""
        return Segmenter(
            config=SegmenterConfig(
                sample_rate=DEFAULT_SAMPLE_RATE,
                trailing_silence=0.1,
                leading_buffer=0.1,
                min_speech_duration=0.1,
            )
        )

    @staticmethod
    def make_audio(duration: float, sample_rate: int = DEFAULT_SAMPLE_RATE) -> NDArray[np.float32]:
        """Create test audio of specified duration."""
        return np.zeros(int(duration * sample_rate), dtype=np.float32)

    def test_idle_to_speech_on_voice_detected(self, segmenter: Segmenter) -> None:
        """Transition from IDLE to SPEECH when voice detected."""
        audio = self.make_audio(0.1)

        list(segmenter.process_audio(audio, is_speech=True))

        assert segmenter.state == SegmenterState.SPEECH, "state should transition to SPEECH when voice detected"

    def test_idle_stays_idle_on_silence(self, segmenter: Segmenter) -> None:
        """Stay in IDLE state when no speech detected."""
        audio = self.make_audio(0.1)

        list(segmenter.process_audio(audio, is_speech=False))

        assert segmenter.state == SegmenterState.IDLE, "state should remain IDLE when no speech detected"

    def test_speech_to_trailing_on_silence(self, segmenter: Segmenter) -> None:
        """Transition from SPEECH to TRAILING when speech ends."""
        speech_audio = self.make_audio(0.1)
        short_silence = self.make_audio(0.05)  # Less than trailing_silence threshold

        list(segmenter.process_audio(speech_audio, is_speech=True))
        list(segmenter.process_audio(short_silence, is_speech=False))

        assert segmenter.state == SegmenterState.TRAILING, "state should transition to TRAILING when speech ends"

    def test_trailing_to_idle_after_silence_threshold(self, segmenter: Segmenter) -> None:
        """Transition from TRAILING to IDLE after enough silence."""
        audio = self.make_audio(0.1)

        list(segmenter.process_audio(audio, is_speech=True))
        list(segmenter.process_audio(audio, is_speech=False))
        list(segmenter.process_audio(audio, is_speech=False))

        assert segmenter.state == SegmenterState.IDLE, "state should transition to IDLE after trailing silence"

    def test_trailing_to_speech_if_voice_resumes(self, segmenter: Segmenter) -> None:
        """Transition from TRAILING back to SPEECH if voice resumes."""
        audio = self.make_audio(0.05)

        list(segmenter.process_audio(audio, is_speech=True))
        list(segmenter.process_audio(audio, is_speech=False))
        assert segmenter.state == SegmenterState.TRAILING, "state should be TRAILING after brief silence"

        list(segmenter.process_audio(audio, is_speech=True))

        assert segmenter.state == SegmenterState.SPEECH, "state should return to SPEECH when voice resumes"


class TestSegmenterEmission:
    """Tests for segment emission behavior."""

    @pytest.fixture
    def segmenter(self) -> Segmenter:
        """Create segmenter with test-friendly config."""
        return Segmenter(
            config=SegmenterConfig(
                sample_rate=DEFAULT_SAMPLE_RATE,
                trailing_silence=0.1,
                leading_buffer=0.1,
                min_speech_duration=0.0,
            )
        )

    @staticmethod
    def make_audio(duration: float, sample_rate: int = DEFAULT_SAMPLE_RATE) -> NDArray[np.float32]:
        """Create test audio of specified duration."""
        return np.ones(int(duration * sample_rate), dtype=np.float32)

    def test_emits_segment_after_trailing_silence(self, segmenter: Segmenter) -> None:
        """Emit segment when trailing silence threshold is reached."""
        audio = self.make_audio(0.2)

        segments_speech = list(segmenter.process_audio(audio, is_speech=True))
        segments_silence = list(segmenter.process_audio(audio, is_speech=False))

        assert not segments_speech, "no segment should be emitted during speech"
        assert len(segments_silence) == 1, "one segment should be emitted after trailing silence"
        assert isinstance(segments_silence[0], AudioSegment), "emitted segment should be AudioSegment"

    def test_emitted_segment_has_correct_timing(self, segmenter: Segmenter) -> None:
        """Emitted segment has correct start and end times."""
        audio = self.make_audio(0.2)

        list(segmenter.process_audio(audio, is_speech=True))
        segments = list(segmenter.process_audio(audio, is_speech=False))

        segment = segments[0]
        assert segment.start_time >= 0.0, "segment start time should be non-negative"
        assert segment.end_time > segment.start_time, "segment end time should be after start time"
        assert segment.duration > 0, "segment duration should be positive"

    def test_emitted_segment_contains_audio(self, segmenter: Segmenter) -> None:
        """Emitted segment contains concatenated audio."""
        audio = self.make_audio(0.2)

        list(segmenter.process_audio(audio, is_speech=True))
        segments = list(segmenter.process_audio(audio, is_speech=False))

        assert len(segments[0].audio) > 0, "emitted segment should contain audio data"

    def test_emits_on_max_duration(self) -> None:
        """Force emit segment when max duration is reached."""
        segmenter = Segmenter(
            config=SegmenterConfig(
                sample_rate=DEFAULT_SAMPLE_RATE,
                max_segment_duration=0.3,
            )
        )
        audio = self.make_audio(0.2)

        segments_1 = list(segmenter.process_audio(audio, is_speech=True))
        segments_2 = list(segmenter.process_audio(audio, is_speech=True))

        assert not segments_1, "no segment should be emitted before max duration"
        assert len(segments_2) == 1, "segment should be force-emitted when max duration reached"

    def test_min_speech_duration_filters_short_segments(self) -> None:
        """Segments shorter than min_speech_duration should be ignored."""
        segmenter = Segmenter(
            config=SegmenterConfig(
                sample_rate=DEFAULT_SAMPLE_RATE,
                min_speech_duration=0.5,
                trailing_silence=0.1,
            )
        )
        short_speech = self.make_audio(0.1)
        silence = self.make_audio(0.1)

        list(segmenter.process_audio(short_speech, is_speech=True))
        emitted = list(segmenter.process_audio(silence, is_speech=False))

        assert not emitted, "short segments below min duration should not be emitted"


class TestSegmenterFlush:
    """Tests for flush behavior."""

    @pytest.fixture
    def segmenter(self) -> Segmenter:
        """Create segmenter with test-friendly config."""
        return Segmenter(
            config=SegmenterConfig(
                sample_rate=DEFAULT_SAMPLE_RATE,
                trailing_silence=0.5,
                min_speech_duration=0.0,
            )
        )

    @staticmethod
    def make_audio(duration: float, sample_rate: int = DEFAULT_SAMPLE_RATE) -> NDArray[np.float32]:
        """Create test audio of specified duration."""
        return np.ones(int(duration * sample_rate), dtype=np.float32)

    def test_flush_returns_none_when_idle(self, segmenter: Segmenter) -> None:
        """Flush returns None when no pending audio."""
        result = segmenter.flush()

        assert result is None, "flush should return None when no pending audio"

    def test_flush_returns_segment_when_in_speech(self, segmenter: Segmenter) -> None:
        """Flush returns pending segment when in SPEECH state."""
        audio = self.make_audio(0.2)
        list(segmenter.process_audio(audio, is_speech=True))

        result = segmenter.flush()

        assert result is not None, "flush should return segment when in SPEECH state"
        assert isinstance(result, AudioSegment), "flushed result should be AudioSegment"

    def test_flush_returns_segment_when_in_trailing(self, segmenter: Segmenter) -> None:
        """Flush returns pending segment when in TRAILING state."""
        audio = self.make_audio(0.1)
        list(segmenter.process_audio(audio, is_speech=True))
        list(segmenter.process_audio(audio, is_speech=False))
        assert segmenter.state == SegmenterState.TRAILING, "state should be TRAILING before flush"

        result = segmenter.flush()

        assert result is not None, "flush should return segment when in TRAILING state"
        assert isinstance(result, AudioSegment), "flushed result should be AudioSegment"

    def test_flush_resets_to_idle(self, segmenter: Segmenter) -> None:
        """Flush resets state to IDLE."""
        audio = self.make_audio(0.2)
        list(segmenter.process_audio(audio, is_speech=True))

        segmenter.flush()

        assert segmenter.state == SegmenterState.IDLE, "flush should reset state to IDLE"


class TestSegmenterReset:
    """Tests for reset behavior."""

    def test_reset_clears_state(self) -> None:
        """Reset returns segmenter to initial state."""
        segmenter = Segmenter()
        audio = np.ones(1600, dtype=np.float32)

        list(segmenter.process_audio(audio, is_speech=True))
        assert segmenter.state == SegmenterState.SPEECH, "state should be SPEECH before reset"

        segmenter.reset()

        assert segmenter.state == SegmenterState.IDLE, "reset should return state to IDLE"


class TestAudioSegmentDataclass:
    """Tests for AudioSegment dataclass."""

    def test_duration_property(self) -> None:
        """Duration property calculates correctly."""
        segment = AudioSegment(
            audio=np.zeros(1600, dtype=np.float32),
            start_time=1.0,
            end_time=2.5,
        )

        assert segment.duration == 1.5, "duration should equal end_time minus start_time"
