"""Tests for python_pkg.music_gen._music_speech module."""

from __future__ import annotations

from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch

import numpy as np
import pytest

from python_pkg.music_gen._music_speech import (
    BARK_MAX_CHARS,
    _generate_instrumental_for_song,
    _generate_vocals_for_song,
    _mix_audio,
    _resample_audio,
    _split_into_sentences,
    generate_speech,
)

if TYPE_CHECKING:
    from pathlib import Path


class TestSplitIntoSentences:
    """Tests for _split_into_sentences()."""

    def test_single_sentence(self) -> None:
        result = _split_into_sentences("Hello world.")
        assert result == ["Hello world."]

    def test_multiple_sentences(self) -> None:
        result = _split_into_sentences("First sentence. Second sentence. Third.")
        assert len(result) >= 1
        # All sentences should be present
        combined = " ".join(result)
        assert "First sentence." in combined
        assert "Second sentence." in combined

    def test_short_sentences_grouped(self) -> None:
        result = _split_into_sentences("Hi. Ok. Yes.")
        # Short sentences should be grouped together (< BARK_MAX_CHARS)
        assert len(result) == 1

    def test_long_text_splits(self) -> None:
        # Create text that exceeds BARK_MAX_CHARS when combined
        long_sentence = "A" * (BARK_MAX_CHARS - 10) + "."
        text = f"{long_sentence} {long_sentence}"
        result = _split_into_sentences(text)
        assert len(result) >= 2

    def test_empty_result_returns_original(self) -> None:
        # A single word with no sentence boundaries
        result = _split_into_sentences("hello")
        assert result == ["hello"]

    def test_whitespace_stripped(self) -> None:
        result = _split_into_sentences("  Hello world.  ")
        assert result[0] == "Hello world."

    def test_current_empty_in_else_branch(self) -> None:
        # First sentence exceeds BARK_MAX_CHARS so current is empty when else hit
        long_sent = "A" * (BARK_MAX_CHARS + 10) + "."
        short_sent = "Short."
        text = f"{long_sent} {short_sent}"
        result = _split_into_sentences(text)
        assert len(result) >= 2

    def test_all_sentences_too_long(self) -> None:
        # Each individual sentence is huge -- current is never empty at else
        s1 = "A" * (BARK_MAX_CHARS + 10) + "."
        s2 = "B" * (BARK_MAX_CHARS + 10) + "."
        text = f"{s1} {s2}"
        result = _split_into_sentences(text)
        assert len(result) >= 2

    def test_empty_string_input(self) -> None:
        # Empty string → sentences=[''], current stays '' after loop
        result = _split_into_sentences("")
        assert result == [""]


class TestResampleAudio:
    """Tests for _resample_audio()."""

    def test_same_rate_returns_unchanged(self) -> None:
        audio = np.array([1.0, 2.0, 3.0], dtype=np.float32)
        result = _resample_audio(audio, 44100, 44100)
        np.testing.assert_array_equal(result, audio)

    def test_resample_different_rate(self) -> None:
        audio = np.ones(100, dtype=np.float32)
        result = _resample_audio(audio, 44100, 22050)
        # Should be shorter since target rate is lower
        expected_length = int(len(audio) / 44100 * 22050)
        assert len(result) == expected_length
        assert result.dtype == np.float32


class TestMixAudio:
    """Tests for _mix_audio()."""

    def test_vocals_shorter_than_instrumental(self) -> None:
        instrumental = np.ones(100, dtype=np.float32)
        vocals = np.ones(50, dtype=np.float32)
        result = _mix_audio(instrumental, vocals)
        assert len(result) == 100

    def test_vocals_longer_than_instrumental(self) -> None:
        instrumental = np.ones(50, dtype=np.float32)
        vocals = np.ones(100, dtype=np.float32)
        result = _mix_audio(instrumental, vocals)
        assert len(result) == 50

    def test_same_length(self) -> None:
        instrumental = np.ones(100, dtype=np.float32)
        vocals = np.ones(100, dtype=np.float32)
        result = _mix_audio(instrumental, vocals)
        assert len(result) == 100

    def test_normalization_when_clipping(self) -> None:
        instrumental = np.ones(10, dtype=np.float32) * 2.0
        vocals = np.ones(10, dtype=np.float32) * 2.0
        result = _mix_audio(
            instrumental, vocals, vocal_volume=1.0, instrumental_volume=1.0
        )
        # Should be normalized so max <= 1.0
        assert np.max(np.abs(result)) <= 1.0 + 1e-6

    def test_no_normalization_needed(self) -> None:
        instrumental = np.ones(10, dtype=np.float32) * 0.1
        vocals = np.ones(10, dtype=np.float32) * 0.1
        result = _mix_audio(
            instrumental, vocals, vocal_volume=0.5, instrumental_volume=0.5
        )
        assert result.dtype == np.float32

    def test_output_type(self) -> None:
        instrumental = np.ones(10, dtype=np.float32) * 0.5
        vocals = np.ones(10, dtype=np.float32) * 0.5
        result = _mix_audio(instrumental, vocals)
        assert result.dtype == np.float32


class TestGenerateSpeech:
    """Tests for generate_speech()."""

    def test_single_sentence(self, tmp_path: Path) -> None:
        mock_torch = MagicMock()
        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        mock_bark.generate_audio.return_value = np.zeros(24000, dtype=np.float32)

        np.zeros(24000, dtype=np.float32)

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "scipy": MagicMock(),
                    "scipy.io": MagicMock(),
                    "scipy.io.wavfile": MagicMock(),
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["Hello world."],
            ),
            patch("scipy.io.wavfile.write"),
        ):
            result = generate_speech("Hello world.", output_dir=tmp_path)

        assert result.parent == tmp_path
        assert result.suffix == ".wav"
        assert "speech" in result.name

    def test_multiple_sentences(self, tmp_path: Path) -> None:
        mock_torch = MagicMock()
        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        mock_bark.generate_audio.return_value = np.zeros(24000, dtype=np.float32)

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "scipy": MagicMock(),
                    "scipy.io": MagicMock(),
                    "scipy.io.wavfile": MagicMock(),
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["First sentence.", "Second sentence."],
            ),
            patch("scipy.io.wavfile.write"),
        ):
            result = generate_speech(
                "First sentence. Second sentence.",
                output_dir=tmp_path,
            )

        assert result.suffix == ".wav"

    def test_default_output_dir(self) -> None:
        mock_torch = MagicMock()
        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        mock_bark.generate_audio.return_value = np.zeros(24000, dtype=np.float32)

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "scipy": MagicMock(),
                    "scipy.io": MagicMock(),
                    "scipy.io.wavfile": MagicMock(),
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["Hello."],
            ),
            patch("scipy.io.wavfile.write"),
            patch("pathlib.Path.mkdir"),
        ):
            result = generate_speech("Hello.")

        assert "output" in str(result.parent)

    def test_patched_load_called(self, tmp_path: Path) -> None:
        """Ensure the patched_load inner function is actually invoked."""
        import sys

        mock_torch = MagicMock()
        original_load = MagicMock(return_value="loaded")
        mock_torch.load = original_load

        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        mock_bark.generate_audio.return_value = np.zeros(24000, dtype=np.float32)

        # Make preload_models call torch.load so patched_load runs
        def call_torch_load() -> None:
            sys.modules["torch"].load("model.pt")

        mock_bark.preload_models.side_effect = call_torch_load

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "scipy": MagicMock(),
                    "scipy.io": MagicMock(),
                    "scipy.io.wavfile": MagicMock(),
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["Hello."],
            ),
            patch("scipy.io.wavfile.write"),
        ):
            generate_speech("Hello.", output_dir=tmp_path)

        # The original_load should have been called via patched_load
        original_load.assert_called_once_with("model.pt", weights_only=False)

    def test_torch_load_restored_after_exception(self) -> None:
        mock_torch = MagicMock()
        original_load = mock_torch.load

        mock_bark = MagicMock()
        mock_bark.preload_models.side_effect = RuntimeError("test error")

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "scipy": MagicMock(),
                    "scipy.io": MagicMock(),
                    "scipy.io.wavfile": MagicMock(),
                    "bark": mock_bark,
                },
            ),
            pytest.raises(RuntimeError, match="test error"),
        ):
            generate_speech("Hello.")

        # torch.load should be restored
        assert mock_torch.load == original_load


class TestGenerateVocalsForSong:
    """Tests for _generate_vocals_for_song()."""

    def test_single_sentence(self) -> None:
        mock_torch = MagicMock()
        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        audio_array = np.zeros(24000, dtype=np.float32)
        mock_bark.generate_audio.return_value = audio_array

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["Hello."],
            ),
        ):
            vocals, sr = _generate_vocals_for_song("Hello.", "v2/en_speaker_6")

        assert sr == 24000
        np.testing.assert_array_equal(vocals, audio_array)

    def test_multiple_sentences(self) -> None:
        mock_torch = MagicMock()
        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        audio_array = np.ones(12000, dtype=np.float32)
        mock_bark.generate_audio.return_value = audio_array

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["First.", "Second."],
            ),
        ):
            vocals, sr = _generate_vocals_for_song(
                "First. Second.",
                "v2/en_speaker_6",
            )

        assert sr == 24000
        assert len(vocals) == 24000  # Two 12000-sample arrays concatenated

    def test_torch_load_restored(self) -> None:
        mock_torch = MagicMock()
        original_load = mock_torch.load

        mock_bark = MagicMock()
        mock_bark.preload_models.side_effect = RuntimeError("fail")

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "bark": mock_bark,
                },
            ),
            pytest.raises(RuntimeError, match="fail"),
        ):
            _generate_vocals_for_song("Hello.", "v2/en_speaker_6")

        assert mock_torch.load == original_load

    def test_patched_load_is_invoked(self) -> None:
        """Ensure patched_load inner function runs in _generate_vocals_for_song."""
        import sys

        mock_torch = MagicMock()
        original_load = MagicMock(return_value="loaded_model")
        mock_torch.load = original_load

        mock_bark = MagicMock()
        mock_bark.SAMPLE_RATE = 24000
        audio_array = np.zeros(24000, dtype=np.float32)
        mock_bark.generate_audio.return_value = audio_array

        def call_torch_load() -> None:
            sys.modules["torch"].load("weights.pt")

        mock_bark.preload_models.side_effect = call_torch_load

        with (
            patch.dict(
                "sys.modules",
                {
                    "torch": mock_torch,
                    "functools": __import__("functools"),
                    "numpy": np,
                    "bark": mock_bark,
                },
            ),
            patch(
                "python_pkg.music_gen._music_speech._split_into_sentences",
                return_value=["Hello."],
            ),
        ):
            _, sr = _generate_vocals_for_song("Hello.", "v2/en_speaker_6")

        assert sr == 24000
        # The original_load should have been called via patched_load
        original_load.assert_called_once_with("weights.pt", weights_only=False)


class TestGenerateInstrumentalForSong:
    """Tests for _generate_instrumental_for_song()."""

    def test_short_duration(self) -> None:
        mock_model = MagicMock()
        mock_param = MagicMock()
        mock_param.device = "cpu"
        mock_model.parameters.return_value = iter([mock_param])
        mock_model.config.audio_encoder.sampling_rate = 100

        audio = np.zeros(100 * 10, dtype=np.float32)

        with (
            patch(
                "python_pkg.music_gen._music_speech.select_model_size",
                return_value="small",
            ),
            patch(
                "python_pkg.music_gen._music_speech.load_model",
                return_value=(mock_model, MagicMock()),
            ),
            patch(
                "python_pkg.music_gen._music_speech.generate_segment",
                return_value=audio,
            ),
        ):
            instrumental, sr = _generate_instrumental_for_song("test", 10)

        assert sr == 100
        np.testing.assert_array_equal(instrumental, audio)

    def test_long_duration(self) -> None:
        mock_model = MagicMock()
        mock_param = MagicMock()
        mock_param.device = "cpu"
        mock_model.parameters.return_value = iter([mock_param])
        mock_model.config.audio_encoder.sampling_rate = 100

        audio = np.zeros(100 * 60, dtype=np.float32)

        with (
            patch(
                "python_pkg.music_gen._music_speech.select_model_size",
                return_value="small",
            ),
            patch(
                "python_pkg.music_gen._music_speech.load_model",
                return_value=(mock_model, MagicMock()),
            ),
            patch(
                "python_pkg.music_gen._music_speech._generate_long_audio",
                return_value=audio,
            ),
        ):
            _, sr = _generate_instrumental_for_song("test", 60)

        assert sr == 100
