"""Tests for FasterWhisperEngine behavior without loading models."""

from __future__ import annotations

import sys
import types
from collections.abc import Iterable
from typing import Protocol

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

from noteflow.infrastructure.asr.engine import FasterWhisperEngine


# Test-local protocols matching whisper library interface for mock injection.
class WhisperWordProtocol(Protocol):
    """Protocol for word-level timing from whisper model."""

    word: str
    start: float
    end: float
    probability: float


class WhisperSegmentProtocol(Protocol):
    """Protocol for transcription segment from whisper model."""

    text: str
    start: float
    end: float
    words: Iterable[WhisperWordProtocol] | None
    avg_logprob: float
    no_speech_prob: float


class WhisperInfoProtocol(Protocol):
    """Protocol for transcription info from whisper model."""

    language: str
    language_probability: float


class WhisperModelProtocol(Protocol):
    """Protocol for whisper model interface used in testing."""

    def transcribe(
        self,
        audio: NDArray[np.float32],
        *,
        language: str | None = None,
        word_timestamps: bool = ...,
        beam_size: int = ...,
        vad_filter: bool = ...,
    ) -> tuple[Iterable[WhisperSegmentProtocol], WhisperInfoProtocol]:
        """Transcribe audio and return segments with info."""
        ...


class TestFasterWhisperEngine:
    """Tests for FasterWhisperEngine."""

    def test_transcribe_without_load_raises(self) -> None:
        """Calling transcribe before load_model raises RuntimeError."""
        engine = FasterWhisperEngine()
        audio = np.zeros(1600, dtype=np.float32)
        with pytest.raises(RuntimeError, match="Model not loaded"):
            list(engine.transcribe(audio))

    def test_load_invalid_model_size_raises(self) -> None:
        """Invalid model size raises ValueError when faster-whisper is available."""
        pytest.importorskip("faster_whisper")
        engine = FasterWhisperEngine()
        with pytest.raises(ValueError, match="Invalid model size"):
            engine.load_model(model_size="not-a-model")

    def test_load_model_with_stub_sets_state(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """load_model should set model and size when stubbed module is present."""

        class DummyModel:
            def __init__(
                self, model_size: str, device: str, compute_type: str, num_workers: int
            ) -> None:
                self.args: tuple[str, str, str, int] = (model_size, device, compute_type, num_workers)

        class _FakeWhisperModule(types.ModuleType):
            WhisperModel: type[DummyModel]

        fake_module = _FakeWhisperModule("faster_whisper")
        fake_module.WhisperModel = DummyModel
        monkeypatch.setitem(sys.modules, "faster_whisper", fake_module)

        engine = FasterWhisperEngine(compute_type="float32", device="cpu", num_workers=2)
        engine.load_model(model_size="base")

        assert engine.is_loaded is True, "Engine should be loaded after load_model call"
        assert engine.model_size == "base", "model_size should match the requested size"
        internal_model = engine.model
        assert internal_model is not None, "Internal model reference should be set"
        assert isinstance(internal_model, DummyModel), "Model should be instance of DummyModel"
        assert internal_model.args == ("base", "cpu", "float32", 2), (
            "Model constructor should receive correct arguments"
        )

    def test_load_model_wraps_errors(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """load_model should surface model construction errors as RuntimeError."""

        class FailingModel:
            def __init__(self, *_: object, **__: object) -> None:
                raise ValueError("boom")

        class _FailingWhisperModule(types.ModuleType):
            WhisperModel: type[FailingModel]

        fake_module = _FailingWhisperModule("faster_whisper")
        fake_module.WhisperModel = FailingModel
        monkeypatch.setitem(sys.modules, "faster_whisper", fake_module)

        engine = FasterWhisperEngine()
        with pytest.raises(RuntimeError, match="Failed to load model"):
            engine.load_model(model_size="base")

    def test_transcribe_with_stubbed_model(self) -> None:
        """transcribe should yield AsrResult objects when model is preset."""
        engine = FasterWhisperEngine()

        class DummyWord:
            word: str
            start: float
            end: float
            probability: float

            def __init__(self) -> None:
                self.word = "hi"
                self.start = 0.0
                self.end = 0.5
                self.probability = 0.9

        class DummySegment:
            text: str
            start: float
            end: float
            words: list[DummyWord]
            avg_logprob: float
            no_speech_prob: float

            def __init__(self) -> None:
                self.text = " hi "
                self.start = 0.0
                self.end = 1.0
                self.words = [DummyWord()]
                self.avg_logprob = -0.1
                self.no_speech_prob = 0.01

        class DummyInfo:
            language: str = "en"
            language_probability: float = 0.95

        class DummyModel:
            def transcribe(
                self,
                audio: NDArray[np.float32],
                *,
                language: str | None = None,
                word_timestamps: bool = True,
                beam_size: int = 5,
                vad_filter: bool = True,
            ) -> tuple[list[DummySegment], DummyInfo]:
                _ = audio, language, word_timestamps, beam_size, vad_filter
                return [DummySegment()], DummyInfo()

        # Inject test double via object.__setattr__ to bypass protected member check.
        # Test stub structurally matches WhisperModelProtocol defined in this module.
        object.__setattr__(engine, "_model", DummyModel())
        object.__setattr__(engine, "_model_size", "base")

        audio = np.zeros(1600, dtype=np.float32)
        results = list(engine.transcribe(audio))

        assert len(results) == 1, "Should yield one AsrResult for one segment"
        first = results[0]
        assert first.text == "hi", "AsrResult.text should be stripped segment text"
        assert first.words[0].word == "hi", "Word timing should preserve word text"
        assert engine.is_loaded is True, "Engine should remain loaded after transcription"

        engine.unload()
        assert not engine.is_loaded, "Engine should be unloaded after unload call"
