import logging
import os
import tempfile
from typing import List
from unittest.mock import patch, Mock

import pytest
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QKeyEvent, QAction
from PyQt6.QtWidgets import (
    QMessageBox,
    QPushButton,
    QToolBar,
    QMenuBar,
    QTableView,
)
from pytestqt.qtbot import QtBot

from buzz.locale import _
from buzz.db.entity.transcription import Transcription
from buzz.db.service.transcription_service import TranscriptionService
from buzz.widgets.main_window import MainWindow
from buzz.widgets.transcriber.file_transcriber_widget import FileTranscriberWidget

mock_transcriptions: List[Transcription] = [
    Transcription(status="completed"),
    Transcription(status="canceled"),
    Transcription(status="failed", error_message=_("Error")),
]


def get_test_asset(filename: str):
    return os.path.join(os.path.dirname(__file__), "../../testdata/", filename)


class TestMainWindow:
    def test_should_set_window_title_and_icon(self, qtbot, transcription_service):
        window = MainWindow(transcription_service)
        qtbot.add_widget(window)
        assert window.windowTitle() == "Buzz"
        assert window.windowIcon().pixmap(QSize(64, 64)).isNull() is False
        window.close()

    def test_should_run_file_transcription_task(
        self, qtbot: QtBot, transcription_service
    ):
        window = MainWindow(transcription_service)

        self._import_file_and_start_transcription(window)

        open_transcript_action = self._get_toolbar_action(window, _("Open Transcript"))
        assert open_transcript_action.isEnabled() is False

        table_widget = self._get_tasks_table(window)
        qtbot.wait_until(
            self._get_assert_task_status_callback(table_widget, 0, "completed"),
            timeout=2 * 60 * 1000,
        )

        table_widget.setCurrentIndex(table_widget.model().index(0, 0))
        assert open_transcript_action.isEnabled()
        window.close()

    @staticmethod
    def _get_tasks_table(window: MainWindow) -> QTableView:
        return window.findChild(QTableView)

    def test_should_run_url_import_file_transcription_task(
        self, qtbot: QtBot, db, transcription_service
    ):
        window = MainWindow(transcription_service)
        menu: QMenuBar = window.menuBar()
        file_action = menu.actions()[0]
        import_url_action: QAction = file_action.menu().actions()[1]

        with patch(
            "buzz.widgets.import_url_dialog.ImportURLDialog.prompt"
        ) as prompt_mock:
            prompt_mock.return_value = "https://github.com/chidiwilliams/buzz/raw/main/testdata/whisper-french.mp3"
            import_url_action.trigger()

        file_transcriber_widget: FileTranscriberWidget = window.findChild(
            FileTranscriberWidget
        )
        run_button: QPushButton = file_transcriber_widget.findChild(QPushButton)
        run_button.click()

        table_widget = self._get_tasks_table(window)
        qtbot.wait_until(
            self._get_assert_task_status_callback(table_widget, 0, "completed"),
            timeout=2 * 60 * 1000,
        )

        window.close()

    @pytest.mark.timeout(300)
    def test_should_run_and_cancel_transcription_task(
        self, qtbot, db, transcription_service
    ):
        window = MainWindow(transcription_service)
        qtbot.add_widget(window)

        self._import_file_and_start_transcription(window, long_audio=True)

        table_widget = self._get_tasks_table(window)

        try:
            qtbot.wait_until(
                self._get_assert_task_status_callback(table_widget, 0, "in_progress"),
                timeout=60 * 1000,
            )
        except Exception:
            logging.error("Task never reached 'in_progress' status")
            assert False, "Task did not start as expected"

        logging.debug("Will cancel transcription task")

        table_widget.selectRow(0)
        
        # Force immediate processing of pending events before triggering cancellation
        qtbot.wait(100)
        
        window.toolbar.stop_transcription_action.trigger()
        
        # Give some time for the cancellation to be processed
        qtbot.wait(500)

        logging.debug("Will wait for task to reach 'canceled' status")

        try:
            qtbot.wait_until(
                self._get_assert_task_status_callback(table_widget, 0, "canceled"),
                timeout=30 * 1000,
            )
        except Exception:
            # On Windows, the cancellation might be slower, check final state
            final_status = self._get_status(table_widget, 0)
            logging.error(f"Task status after timeout: {final_status}")
            if "canceled" not in final_status.lower():
                assert False, f"Task did not cancel as expected. Final status: {final_status}"

        logging.debug("Task canceled")

        qtbot.wait(200)

        table_widget.selectRow(0)
        assert window.toolbar.stop_transcription_action.isEnabled() is False
        assert window.toolbar.open_transcript_action.isEnabled() is False

        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_load_tasks_from_cache(
        self, qtbot, transcription_dao, transcription_segment_dao, monkeypatch
    ):
        # Mock the queue worker to prevent it from processing tasks
        mock_queue_worker = Mock()
        mock_queue_worker.task_started = Mock()
        mock_queue_worker.task_progress = Mock()
        mock_queue_worker.task_download_progress = Mock()
        mock_queue_worker.task_error = Mock()
        mock_queue_worker.task_completed = Mock()
        mock_queue_worker.completed = Mock()
        mock_queue_worker.cancel_task = Mock()
        mock_queue_worker.add_task = Mock()
        mock_queue_worker.stop = Mock()
        
        monkeypatch.setattr("buzz.widgets.main_window.FileTranscriberQueueWorker", Mock(return_value=mock_queue_worker))
        
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        table_widget = self._get_tasks_table(window)
        assert table_widget.model().rowCount() == 3

        # Get all statuses and verify they match expected values
        statuses = [self._get_status(table_widget, i) for i in range(3)]
        expected_statuses = {"completed", "canceled", "failed"}
        assert set(statuses) == expected_statuses, f"Expected {expected_statuses}, got {statuses}"

        # Test that completed transcriptions enable the open action, others don't
        for i in range(3):
            table_widget.selectRow(i)
            status = self._get_status(table_widget, i)
            if status == "completed":
                assert window.toolbar.open_transcript_action.isEnabled()
            else:
                assert window.toolbar.open_transcript_action.isEnabled() is False
        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_clear_history_with_rows_selected(
        self, qtbot, transcription_dao, transcription_segment_dao
    ):
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        table_widget = self._get_tasks_table(window)
        table_widget.selectAll()

        with patch("PyQt6.QtWidgets.QMessageBox.exec") as question_message_box_mock:
            question_message_box_mock.return_value = QMessageBox.StandardButton.Yes
            window.toolbar.clear_history_action.trigger()

        assert table_widget.model().rowCount() == 0
        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_have_clear_history_action_disabled_with_no_rows_selected(
        self, qtbot, transcription_dao, transcription_segment_dao
    ):
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        assert window.toolbar.clear_history_action.isEnabled() is False
        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_open_transcription_viewer_when_menu_action_is_clicked(
        self, qtbot, transcription_dao, transcription_segment_dao
    ):
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        table_widget = self._get_tasks_table(window)

        # Find and select the completed transcription row
        completed_row = None
        for i in range(table_widget.model().rowCount()):
            if self._get_status(table_widget, i) == "completed":
                completed_row = i
                break

        assert completed_row is not None, "No completed transcription found"
        table_widget.selectRow(completed_row)

        window.toolbar.open_transcript_action.trigger()

        assert window.transcription_viewer_widget is not None

        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_open_transcription_viewer_when_return_clicked(
        self, qtbot, transcription_dao, transcription_segment_dao
    ):
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        table_widget = self._get_tasks_table(window)

        # Find and select the completed transcription row
        completed_row = None
        for i in range(table_widget.model().rowCount()):
            if self._get_status(table_widget, i) == "completed":
                completed_row = i
                break

        assert completed_row is not None, "No completed transcription found"
        table_widget.selectRow(completed_row)

        table_widget.keyPressEvent(
            QKeyEvent(
                QKeyEvent.Type.KeyPress,
                Qt.Key.Key_Return,
                Qt.KeyboardModifier.NoModifier,
                "\r",
            )
        )

        assert window.transcription_viewer_widget is not None

        window.close()

    @pytest.mark.parametrize("transcription_dao", [mock_transcriptions], indirect=True)
    def test_should_have_open_transcript_action_disabled_with_no_rows_selected(
        self, qtbot, transcription_dao, transcription_segment_dao
    ):
        window = MainWindow(
            TranscriptionService(transcription_dao, transcription_segment_dao)
        )
        qtbot.add_widget(window)

        assert window.toolbar.open_transcript_action.isEnabled() is False
        window.close()

    def test_import_folder_opens_file_transcriber_with_supported_files(
        self, qtbot, transcription_service
    ):
        window = MainWindow(transcription_service)
        qtbot.add_widget(window)

        with tempfile.TemporaryDirectory() as folder:
            # Create supported and unsupported files
            supported = ["audio.mp3", "video.mp4", "clip.wav"]
            unsupported = ["document.txt", "image.png"]
            subdir = os.path.join(folder, "sub")
            os.makedirs(subdir)
            nested = "nested.flac"

            for name in supported + unsupported:
                open(os.path.join(folder, name), "w").close()
            open(os.path.join(subdir, nested), "w").close()

            with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
                 patch.object(window, "open_file_transcriber_widget") as mock_open:
                mock_dir.return_value = folder
                window.on_import_folder_action_triggered()

            collected = mock_open.call_args[0][0]
            collected_names = {os.path.basename(p) for p in collected}
            assert collected_names == {"audio.mp3", "video.mp4", "clip.wav", "nested.flac"}

        window.close()

    def test_import_folder_does_nothing_when_cancelled(
        self, qtbot, transcription_service
    ):
        window = MainWindow(transcription_service)
        qtbot.add_widget(window)

        with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
             patch.object(window, "open_file_transcriber_widget") as mock_open:
            mock_dir.return_value = ""
            window.on_import_folder_action_triggered()

        mock_open.assert_not_called()
        window.close()

    def test_import_folder_does_nothing_when_no_supported_files(
        self, qtbot, transcription_service
    ):
        window = MainWindow(transcription_service)
        qtbot.add_widget(window)

        with tempfile.TemporaryDirectory() as folder:
            open(os.path.join(folder, "readme.txt"), "w").close()
            open(os.path.join(folder, "image.jpg"), "w").close()

            with patch("PyQt6.QtWidgets.QFileDialog.getExistingDirectory") as mock_dir, \
                 patch.object(window, "open_file_transcriber_widget") as mock_open:
                mock_dir.return_value = folder
                window.on_import_folder_action_triggered()

        mock_open.assert_not_called()
        window.close()

    @staticmethod
    def _import_file_and_start_transcription(
        window: MainWindow, long_audio: bool = False
    ):
        with patch(
            "PyQt6.QtWidgets.QFileDialog.getOpenFileNames"
        ) as open_file_names_mock:
            open_file_names_mock.return_value = (
                [
                    get_test_asset(
                        "audio-long.mp3" if long_audio else "whisper-french.mp3"
                    )
                ],
                "",
            )
            new_transcription_action = TestMainWindow._get_toolbar_action(
                window, _("New File Transcription")
            )
            new_transcription_action.trigger()

        file_transcriber_widget: FileTranscriberWidget = window.findChild(
            FileTranscriberWidget
        )
        run_button: QPushButton = file_transcriber_widget.findChild(QPushButton)
        run_button.click()

    @staticmethod
    def _get_assert_task_status_callback(
        table_widget: QTableView,
        row_index: int,
        expected_status: str,
    ):
        def assert_task_status():
            assert table_widget.model().rowCount() > 0
            assert expected_status in TestMainWindow._get_status(
                table_widget, row_index
            )

        return assert_task_status

    @staticmethod
    def _get_status(table_widget: QTableView, row_index: int):
        return table_widget.model().index(row_index, 9).data()

    @staticmethod
    def _get_toolbar_action(window: MainWindow, text: str):
        toolbar: QToolBar = window.findChild(QToolBar)
        return [action for action in toolbar.actions() if action.text() == text][0]
