import multiprocessing
import os
import platform
from unittest.mock import Mock, patch

import pytest
import sounddevice
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QValidator, QKeyEvent
from PyQt6.QtWidgets import (
    QApplication,
    QMessageBox,
)
from pytestqt.qtbot import QtBot

from buzz.locale import _
from buzz.__version__ import VERSION
from buzz.widgets.audio_devices_combo_box import AudioDevicesComboBox
from buzz.widgets.transcriber.advanced_settings_dialog import AdvancedSettingsDialog
from buzz.widgets.transcriber.hugging_face_search_line_edit import (
    HuggingFaceSearchLineEdit,
)
from buzz.widgets.transcriber.languages_combo_box import LanguagesComboBox
from buzz.widgets.transcriber.temperature_validator import TemperatureValidator
from buzz.widgets.about_dialog import AboutDialog
from buzz.settings.settings import Settings
from buzz.transcriber.transcriber import (
    TranscriptionOptions,
)
from buzz.widgets.transcriber.transcription_options_group_box import (
    TranscriptionOptionsGroupBox,
)
from tests.mock_sounddevice import MockInputStream, mock_query_devices
from .mock_qt import MockNetworkAccessManager, MockNetworkReply

if platform.system() == "Linux":
    try:
        multiprocessing.set_start_method("spawn", force=True)
    except RuntimeError:
        pass


@pytest.fixture(scope="module", autouse=True)
def audio_setup():
    with patch("sounddevice.query_devices") as query_devices_mock, patch(
        "sounddevice.InputStream", side_effect=MockInputStream
    ), patch("sounddevice.check_input_settings"):
        query_devices_mock.return_value = mock_query_devices
        sounddevice.default.device = 3, 4
        yield


class TestLanguagesComboBox:
    def test_should_show_sorted_whisper_languages(self, qtbot):
        languages_combox_box = LanguagesComboBox("en")
        qtbot.add_widget(languages_combox_box)
        assert languages_combox_box.itemText(0) == _("Detect Language")
        assert languages_combox_box.itemText(1) == _("Afrikaans")

    def test_should_select_en_as_default_language(self, qtbot):
        languages_combox_box = LanguagesComboBox("en")
        qtbot.add_widget(languages_combox_box)
        assert languages_combox_box.currentText() == _("English")

    def test_should_select_detect_language_as_default(self, qtbot):
        languages_combo_box = LanguagesComboBox(None)
        qtbot.add_widget(languages_combo_box)
        assert languages_combo_box.currentText() == _("Detect Language")


class TestAudioDevicesComboBox:
    def test_get_devices(self):
        audio_devices_combo_box = AudioDevicesComboBox()

        assert audio_devices_combo_box.itemText(0) == "Background Music"
        assert audio_devices_combo_box.itemText(1) == "Background Music (UI Sounds)"
        assert audio_devices_combo_box.itemText(2) == "BlackHole 2ch"
        assert audio_devices_combo_box.itemText(3) == "MacBook Pro Microphone"
        assert audio_devices_combo_box.itemText(4) == "Null Audio Device"

        assert audio_devices_combo_box.currentText() == "MacBook Pro Microphone"

    def test_select_default_mic_when_no_default(self):
        sounddevice.default.device = -1, 1

        audio_devices_combo_box = AudioDevicesComboBox()
        assert audio_devices_combo_box.currentText() == "Background Music"


@pytest.fixture(scope="module", autouse=True)
def clear_settings():
    settings = Settings()
    settings.clear()


class TestAboutDialog:
    def test_should_check_for_updates(self, qtbot: QtBot):
        reply = MockNetworkReply(data={"name": "v" + VERSION})
        manager = MockNetworkAccessManager(reply=reply)
        dialog = AboutDialog(network_access_manager=manager)
        qtbot.add_widget(dialog)

        mock_message_box_information = Mock()
        QMessageBox.information = mock_message_box_information

        with qtbot.wait_signal(dialog.network_access_manager.finished):
            dialog.check_updates_button.click()

        mock_message_box_information.assert_called_with(
            dialog, "", _("You're up to date!")
        )


class TestAdvancedSettingsDialog:
    def test_should_update_advanced_settings(self, qtbot: QtBot):
        dialog = AdvancedSettingsDialog(
            transcription_options=TranscriptionOptions(
                temperature=(0.0, 0.8),
                initial_prompt="prompt",
                enable_llm_translation=False,
                llm_model="",
                llm_prompt=""
            )
        )
        qtbot.add_widget(dialog)

        transcription_options_mock = Mock()
        dialog.transcription_options_changed.connect(transcription_options_mock)

        assert dialog.windowTitle() == _("Advanced Settings")
        assert dialog.temperature_line_edit.text() == "0.0, 0.8"
        assert dialog.initial_prompt_text_edit.toPlainText() == "prompt"
        assert dialog.enable_llm_translation_checkbox.isChecked() is False
        assert dialog.llm_model_line_edit.text() == "gpt-4.1-mini"
        assert dialog.llm_prompt_text_edit.toPlainText() == _("Please translate each text sent to you from English to Spanish.")

        dialog.temperature_line_edit.setText("0.0, 0.8, 1.0")
        dialog.initial_prompt_text_edit.setPlainText("new prompt")
        dialog.enable_llm_translation_checkbox.setChecked(True)
        dialog.llm_model_line_edit.setText("model")
        dialog.llm_prompt_text_edit.setPlainText("Please translate this text")

        assert transcription_options_mock.call_args[0][0].temperature == (0.0, 0.8, 1.0)
        assert transcription_options_mock.call_args[0][0].initial_prompt == "new prompt"
        assert transcription_options_mock.call_args[0][0].enable_llm_translation is True
        assert transcription_options_mock.call_args[0][0].llm_model == "model"
        assert transcription_options_mock.call_args[0][0].llm_prompt == "Please translate this text"


class TestTemperatureValidator:
    validator = TemperatureValidator(None)

    @pytest.mark.parametrize(
        "text,state",
        [
            ("0.0,0.5,1.0", QValidator.State.Acceptable),
            ("0.0,0.5,", QValidator.State.Intermediate),
            ("0.0,0.5,p", QValidator.State.Invalid),
        ],
    )
    def test_should_validate_temperature(self, text: str, state: QValidator.State):
        assert self.validator.validate(text, 0)[0] == state


@pytest.mark.skipif(
    platform.system() == "Linux" and os.environ.get("XDG_SESSION_TYPE") == "wayland",
    reason="Skipping on Wayland sessions due to Qt popup issues"
)
class TestHuggingFaceSearchLineEdit:
    def test_should_update_selected_model_on_type(self, qtbot: QtBot):
        widget = HuggingFaceSearchLineEdit(
            default_value="",
            network_access_manager=self.network_access_manager()
        )
        qtbot.add_widget(widget)

        mock_model_selected = Mock()
        widget.model_selected.connect(mock_model_selected)

        self._set_text_and_wait_response(qtbot, widget)
        mock_model_selected.assert_called_with("openai/whisper-tiny")

    def test_should_show_list_of_models(self, qtbot: QtBot):
        widget = HuggingFaceSearchLineEdit(
            default_value="",
            network_access_manager=self.network_access_manager()
        )
        qtbot.add_widget(widget)

        self._set_text_and_wait_response(qtbot, widget)

        assert widget.popup.count() > 0
        assert "openai/whisper-tiny" in widget.popup.item(0).text()

    def test_should_select_model_from_list(self, qtbot: QtBot):
        widget = HuggingFaceSearchLineEdit(
            default_value="",
            network_access_manager=self.network_access_manager()
        )
        qtbot.add_widget(widget)

        mock_model_selected = Mock()
        widget.model_selected.connect(mock_model_selected)

        self._set_text_and_wait_response(qtbot, widget)

        # press down arrow and enter to select next item
        QApplication.sendEvent(
            widget.popup,
            QKeyEvent(
                QKeyEvent.Type.KeyPress, Qt.Key.Key_Down, Qt.KeyboardModifier.NoModifier
            ),
        )
        QApplication.sendEvent(
            widget.popup,
            QKeyEvent(
                QKeyEvent.Type.KeyPress,
                Qt.Key.Key_Enter,
                Qt.KeyboardModifier.NoModifier,
            ),
        )

        mock_model_selected.assert_called_with("openai/whisper-tiny.en")

    @staticmethod
    def network_access_manager():
        reply = MockNetworkReply(
            data=[{"id": "openai/whisper-tiny"}, {"id": "openai/whisper-tiny.en"}]
        )
        return MockNetworkAccessManager(reply=reply)

    @staticmethod
    def _set_text_and_wait_response(qtbot: QtBot, widget: HuggingFaceSearchLineEdit):
        with qtbot.wait_signal(widget.network_manager.finished):
            widget.setText("openai/whisper-tiny")
            widget.textEdited.emit("openai/whisper-tiny")


class TestTranscriptionOptionsGroupBox:
    def test_should_update_model_type(self, qtbot):
        widget = TranscriptionOptionsGroupBox()
        qtbot.add_widget(widget)

        mock_transcription_options_changed = Mock()
        widget.transcription_options_changed.connect(mock_transcription_options_changed)

        widget.model_type_combo_box.setCurrentIndex(1)

        mock_transcription_options_changed.assert_called()
