"""Benchmark tests for NoteFlow hot paths.

These benchmarks measure the performance of frequently-called code paths
to establish baselines and detect regressions.

Run with: pytest tests/benchmarks/ --benchmark-enable
Compare: pytest tests/benchmarks/ --benchmark-compare
Save baseline: pytest tests/benchmarks/ --benchmark-save=baseline
"""

from __future__ import annotations

from typing import TypeVar

import numpy as np
import pytest
from numpy.typing import NDArray
from pytest_benchmark.fixture import BenchmarkFixture

from noteflow.config.constants import DEFAULT_SAMPLE_RATE
from noteflow.infrastructure.asr.segmenter import AudioSegment, Segmenter, SegmenterConfig
from noteflow.infrastructure.asr.streaming_vad import EnergyVad, StreamingVad
from noteflow.infrastructure.audio.levels import RmsLevelProvider, compute_rms

_T = TypeVar("_T")


def _run_benchmark(benchmark: BenchmarkFixture, func: object, *args: object) -> object:
    """Run benchmark and return result as object.

    This helper captures the unknown return type from pytest-benchmark and
    explicitly returns it as object, allowing downstream casts to work properly.

    The cast is required because BenchmarkFixture.__call__ is untyped in
    pytest-benchmark (no type stubs available).
    """
    from typing import cast

    # cast required: pytest-benchmark lacks type stubs
    return cast(object, benchmark(func, *args))


def typed_benchmark(benchmark: BenchmarkFixture, expected_type: type[_T], func: object, *args: object) -> _T:
    """Run benchmark and return typed result for simple types (float, bool, int).

    BenchmarkFixture.__call__ is untyped but returns the result of func(*args).
    This wrapper provides explicit typing via the expected_type parameter.

    Args:
        benchmark: The pytest-benchmark fixture
        expected_type: The type that func returns (used for type inference)
        func: The function to benchmark
        *args: Arguments to pass to func
    """
    from typing import cast

    return cast(_T, _run_benchmark(benchmark, func, *args))


def benchmark_array(benchmark: BenchmarkFixture, func: object, *args: object) -> NDArray[np.float32]:
    """Run benchmark for functions returning float32 arrays.

    BenchmarkFixture.__call__ is untyped. This wrapper returns a properly typed NDArray.

    Args:
        benchmark: The pytest-benchmark fixture
        func: The function to benchmark (must return NDArray[np.float32])
        *args: Arguments to pass to func
    """
    from typing import cast

    return cast(NDArray[np.float32], _run_benchmark(benchmark, func, *args))


def benchmark_list(benchmark: BenchmarkFixture, func: object, *args: object) -> list[AudioSegment]:
    """Run benchmark for functions returning list of AudioSegment.

    BenchmarkFixture.__call__ is untyped. This wrapper returns a properly typed list.

    Args:
        benchmark: The pytest-benchmark fixture
        func: The function to benchmark (must return list[AudioSegment])
        *args: Arguments to pass to func
    """
    from typing import cast

    return cast(list[AudioSegment], _run_benchmark(benchmark, func, *args))


def benchmark_array_list(
    benchmark: BenchmarkFixture, func: object, *args: object
) -> list[NDArray[np.float32]]:
    """Run benchmark for functions returning list of float32 arrays.

    BenchmarkFixture.__call__ is untyped. This wrapper returns a properly typed list.

    Args:
        benchmark: The pytest-benchmark fixture
        func: The function to benchmark (must return list[NDArray[np.float32]])
        *args: Arguments to pass to func
    """
    from typing import cast

    return cast(list[NDArray[np.float32]], _run_benchmark(benchmark, func, *args))


# Standard audio chunk size (100ms at 16kHz)
CHUNK_SIZE = 1600
SAMPLE_RATE = DEFAULT_SAMPLE_RATE
# Typical partial buffer holds ~2s of audio (20 chunks x 100ms)
TYPICAL_PARTIAL_CHUNKS = 20
# dB floor for silence detection
DB_FLOOR = -60


@pytest.fixture
def audio_chunk() -> NDArray[np.float32]:
    """Generate a realistic audio chunk (100ms at 16kHz)."""
    return np.random.randn(CHUNK_SIZE).astype(np.float32) * 0.1


@pytest.fixture
def speech_chunk() -> NDArray[np.float32]:
    """Generate a speech-like audio chunk with higher energy."""
    return np.random.randn(CHUNK_SIZE).astype(np.float32) * 0.5


@pytest.fixture
def silence_chunk() -> NDArray[np.float32]:
    """Generate a silence chunk with very low energy."""
    return np.random.randn(CHUNK_SIZE).astype(np.float32) * 0.001


@pytest.fixture
def segmenter() -> Segmenter:
    """Create a segmenter with default config."""
    return Segmenter(config=SegmenterConfig(sample_rate=SAMPLE_RATE))


@pytest.fixture
def energy_vad() -> EnergyVad:
    """Create an energy VAD instance."""
    return EnergyVad()


@pytest.fixture
def streaming_vad() -> StreamingVad:
    """Create a streaming VAD instance."""
    return StreamingVad()


@pytest.fixture
def rms_provider() -> RmsLevelProvider:
    """Create an RMS level provider."""
    return RmsLevelProvider()


class TestComputeRmsBenchmark:
    """Benchmark tests for RMS computation (called 36,000x/hour)."""

    def test_compute_rms_typical_chunk(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark RMS computation on typical 100ms chunk."""
        result = typed_benchmark(benchmark, float, compute_rms, audio_chunk)
        assert 0 <= result <= 1, "RMS should be in valid range"

    def test_compute_rms_silence(
        self, benchmark: BenchmarkFixture, silence_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark RMS computation on silence."""
        result = typed_benchmark(benchmark, float, compute_rms, silence_chunk)
        assert result < 0.01, "Silence should have very low RMS"

    def test_compute_rms_speech(
        self, benchmark: BenchmarkFixture, speech_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark RMS computation on speech-like audio."""
        result = typed_benchmark(benchmark, float, compute_rms, speech_chunk)
        assert result > 0.1, "Speech should have higher RMS"


class TestVadBenchmark:
    """Benchmark tests for VAD processing (called 36,000x/hour)."""

    def test_energy_vad_process(
        self, benchmark: BenchmarkFixture, energy_vad: EnergyVad, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark single EnergyVad.process() call."""
        result = typed_benchmark(benchmark, bool, energy_vad.process, audio_chunk)
        assert isinstance(result, bool), "VAD should return boolean"

    def test_streaming_vad_process_chunk(
        self, benchmark: BenchmarkFixture, streaming_vad: StreamingVad, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark StreamingVad.process_chunk() call."""
        result = typed_benchmark(benchmark, bool, streaming_vad.process_chunk, audio_chunk)
        assert isinstance(result, bool), "VAD should return boolean"

    def test_energy_vad_speech_detection(
        self, benchmark: BenchmarkFixture, energy_vad: EnergyVad, speech_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark VAD on speech-like audio."""
        result = typed_benchmark(benchmark, bool, energy_vad.process, speech_chunk)
        assert result is True, "Should detect speech"

    def test_energy_vad_silence_detection(
        self, benchmark: BenchmarkFixture, energy_vad: EnergyVad, silence_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark VAD on silence."""
        result = typed_benchmark(benchmark, bool, energy_vad.process, silence_chunk)
        assert result is False, "Should detect silence"


class TestSegmenterBenchmark:
    """Benchmark tests for Segmenter state machine (called 36,000x/hour)."""

    def test_segmenter_idle_silence(
        self, benchmark: BenchmarkFixture, segmenter: Segmenter, silence_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark segmenter processing silence in IDLE state."""

        def process_idle() -> list[AudioSegment]:
            return list(segmenter.process_audio(silence_chunk, is_speech=False))

        result = benchmark_list(benchmark, process_idle)
        assert result == [], "No segments should be emitted in idle"

    def test_segmenter_speech_accumulation(
        self, benchmark: BenchmarkFixture, segmenter: Segmenter, speech_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark segmenter accumulating speech."""
        # First transition to SPEECH state
        list(segmenter.process_audio(speech_chunk, is_speech=True))

        def process_speech() -> list[AudioSegment]:
            return list(segmenter.process_audio(speech_chunk, is_speech=True))

        result = benchmark_list(benchmark, process_speech)
        # Should not emit unless max duration reached
        assert len(result) <= 1, "Should emit at most one segment"

    def test_segmenter_transition_idle_to_speech(
        self, benchmark: BenchmarkFixture, speech_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark state transition from IDLE to SPEECH."""

        def transition() -> list[AudioSegment]:
            seg = Segmenter(config=SegmenterConfig(sample_rate=SAMPLE_RATE))
            return list(seg.process_audio(speech_chunk, is_speech=True))

        result = benchmark_list(benchmark, transition)
        assert result == [], "Transition should not emit segment"


class TestRmsLevelProviderBenchmark:
    """Benchmark tests for RmsLevelProvider methods."""

    def test_get_rms(
        self, benchmark: BenchmarkFixture, rms_provider: RmsLevelProvider, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark get_rms() method."""
        result = typed_benchmark(benchmark, float, rms_provider.get_rms, audio_chunk)
        assert 0 <= result <= 1, "RMS should be normalized"

    def test_get_db(
        self, benchmark: BenchmarkFixture, rms_provider: RmsLevelProvider, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark get_db() method."""
        result = typed_benchmark(benchmark, float, rms_provider.get_db, audio_chunk)
        assert DB_FLOOR <= result <= 0, "dB should be in valid range"

    def test_rms_to_db_conversion(
        self, benchmark: BenchmarkFixture, rms_provider: RmsLevelProvider
    ) -> None:
        """Benchmark rms_to_db() conversion."""
        result = typed_benchmark(benchmark, float, rms_provider.rms_to_db, 0.5)
        assert result < 0, "Half amplitude should be negative dB"


class TestNumpyOperationsBenchmark:
    """Benchmark tests for NumPy operations used in hot paths."""

    def test_array_copy(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark array copy (used in partial buffer accumulation)."""
        result = benchmark_array(benchmark, audio_chunk.copy)
        assert result.shape == audio_chunk.shape, "Copy should preserve shape"

    def test_array_concatenate_small(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark concatenation of 5 chunks (~500ms audio)."""
        chunks = [audio_chunk.copy() for _ in range(5)]

        def concat() -> NDArray[np.float32]:
            return np.concatenate(chunks)

        result = benchmark_array(benchmark, concat)
        assert len(result) == CHUNK_SIZE * 5, "Should concatenate all chunks"

    def test_array_concatenate_large(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark concatenation of 20 chunks (~2s audio, typical partial)."""
        chunks = [audio_chunk.copy() for _ in range(20)]

        def concat() -> NDArray[np.float32]:
            return np.concatenate(chunks)

        result = benchmark_array(benchmark, concat)
        assert len(result) == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should concatenate all chunks"

    def test_array_square(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark np.square (used in RMS calculation)."""
        result = benchmark_array(benchmark, np.square, audio_chunk)
        assert result.dtype == np.float32, "Should preserve dtype"

    def test_array_mean(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark np.mean (used in RMS calculation)."""
        result = typed_benchmark(benchmark, float, np.mean, audio_chunk)
        assert isinstance(result, (float, np.floating)), "Mean should be scalar"


class TestBufferOperationsBenchmark:
    """Benchmark tests for list operations used in buffers."""

    def test_list_append(self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]) -> None:
        """Benchmark list append (partial buffer accumulation)."""
        buffer: list[NDArray[np.float32]] = []

        def append() -> None:
            buffer.append(audio_chunk.copy())

        benchmark(append)
        assert buffer, "Buffer should have items"

    def test_list_clear(self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]) -> None:
        """Benchmark list clear (partial buffer clearing)."""
        # Pre-fill buffer
        buffer = [audio_chunk.copy() for _ in range(20)]

        def clear_and_refill() -> None:
            buffer.clear()
            for _ in range(TYPICAL_PARTIAL_CHUNKS):
                buffer.append(audio_chunk.copy())

        benchmark(clear_and_refill)

    def test_sum_lengths_naive(self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]) -> None:
        """Benchmark naive sum of chunk lengths (OLD segmenter pattern)."""
        chunks = [audio_chunk.copy() for _ in range(20)]

        def sum_naive() -> int:
            return sum(len(chunk) for chunk in chunks)

        result = typed_benchmark(benchmark, int, sum_naive)
        assert result == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should sum all lengths"

    def test_cached_length(self, benchmark: BenchmarkFixture) -> None:
        """Benchmark cached length access (NEW segmenter pattern)."""
        cached_length = CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS

        def get_cached() -> int:
            return cached_length

        result = typed_benchmark(benchmark, int, get_cached)
        assert result == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should return cached value"


class TestPartialBufferComparisonBenchmark:
    """Benchmark comparing old list-based vs new pre-allocated buffer."""

    def test_old_list_append_and_concat(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark OLD pattern: list append + np.concatenate (20 chunks = 2s)."""
        def old_pattern() -> NDArray[np.float32]:
            buffer: list[NDArray[np.float32]] = []
            for _ in range(TYPICAL_PARTIAL_CHUNKS):
                buffer.append(audio_chunk.copy())
            return np.concatenate(buffer)

        result = benchmark_array(benchmark, old_pattern)
        assert len(result) == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should have all samples"

    def test_new_preallocated_buffer(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark NEW pattern: pre-allocated buffer (20 chunks = 2s)."""
        from noteflow.infrastructure.audio.partial_buffer import PartialAudioBuffer

        def new_pattern() -> NDArray[np.float32]:
            buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE)
            for _ in range(TYPICAL_PARTIAL_CHUNKS):
                buffer.append(audio_chunk)
            return buffer.get_audio()

        result = benchmark_array(benchmark, new_pattern)
        assert len(result) == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should have all samples"

    def test_preallocated_append_only(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark pre-allocated buffer append only (no get_audio)."""
        from noteflow.infrastructure.audio.partial_buffer import PartialAudioBuffer

        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE)

        def append_only() -> None:
            buffer.append(audio_chunk)

        benchmark(append_only)
        assert buffer.samples_buffered > 0, "Should have appended"

    def test_preallocated_get_audio_only(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark pre-allocated buffer get_audio only (pre-filled)."""
        from noteflow.infrastructure.audio.partial_buffer import PartialAudioBuffer

        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE)
        for _ in range(TYPICAL_PARTIAL_CHUNKS):
            buffer.append(audio_chunk)

        result = benchmark_array(benchmark, buffer.get_audio)
        assert len(result) == CHUNK_SIZE * TYPICAL_PARTIAL_CHUNKS, "Should have all samples"

    def test_realistic_old_pattern_10_cycles(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark OLD pattern: 10 cycles of accumulate/concat/clear."""
        def old_pattern_cycles() -> list[NDArray[np.float32]]:
            results: list[NDArray[np.float32]] = []
            for _ in range(10):
                buffer: list[NDArray[np.float32]] = []
                for _ in range(TYPICAL_PARTIAL_CHUNKS):
                    buffer.append(audio_chunk.copy())
                results.append(np.concatenate(buffer))
                buffer.clear()  # Note: doesn't help much, new list created next cycle
            return results

        result = benchmark_array_list(benchmark, old_pattern_cycles)
        assert len(result) == 10, "Should have 10 results"

    def test_realistic_new_pattern_10_cycles(
        self, benchmark: BenchmarkFixture, audio_chunk: NDArray[np.float32]
    ) -> None:
        """Benchmark NEW pattern: 10 cycles with buffer reuse."""
        from noteflow.infrastructure.audio.partial_buffer import PartialAudioBuffer

        # Buffer created once (simulates per-meeting initialization)
        buffer = PartialAudioBuffer(sample_rate=SAMPLE_RATE)

        def new_pattern_cycles() -> list[NDArray[np.float32]]:
            results: list[NDArray[np.float32]] = []
            for _ in range(10):
                for _ in range(TYPICAL_PARTIAL_CHUNKS):
                    buffer.append(audio_chunk)
                results.append(buffer.get_audio())
                buffer.clear()  # O(1) pointer reset, buffer reused
            return results

        result = benchmark_array_list(benchmark, new_pattern_cycles)
        assert len(result) == 10, "Should have 10 results"
