#!/usr/bin/env python3
"""Tests for scitex_writer._utils._watch."""

import subprocess
from unittest.mock import MagicMock, patch

import pytest

from scitex_writer._utils._watch import watch_manuscript


class TestWatchManuscriptCompileScript:
    """Tests for watch_manuscript compile script handling."""

    def test_returns_early_when_compile_script_missing(self, tmp_path):
        """Verify returns immediately when compile script doesn't exist."""
        with patch("subprocess.Popen") as mock_popen:
            watch_manuscript(tmp_path)

            mock_popen.assert_not_called()

    def test_creates_correct_command(self, tmp_path):
        """Verify correct command is built for watch mode."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.return_value = ""  # Stop iteration immediately

        with patch("subprocess.Popen", return_value=mock_process) as mock_popen:
            watch_manuscript(tmp_path)

            expected_cmd = [str(compile_script), "-m", "-w"]
            mock_popen.assert_called_once()
            actual_cmd = mock_popen.call_args[0][0]
            assert actual_cmd == expected_cmd


class TestWatchManuscriptProcess:
    """Tests for watch_manuscript process handling."""

    def test_runs_process_with_correct_settings(self, tmp_path):
        """Verify Popen is called with correct settings."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.return_value = ""

        with patch("subprocess.Popen", return_value=mock_process) as mock_popen:
            watch_manuscript(tmp_path)

            call_kwargs = mock_popen.call_args[1]
            assert call_kwargs["cwd"] == tmp_path
            assert call_kwargs["stdout"] == subprocess.PIPE
            assert call_kwargs["stderr"] == subprocess.STDOUT
            assert call_kwargs["text"] is True
            assert call_kwargs["bufsize"] == 1

    def test_waits_for_process_completion(self, tmp_path):
        """Verify process.wait is called with timeout."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.return_value = ""

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path, timeout=30)

            mock_process.wait.assert_called_once_with(timeout=30)


class TestWatchManuscriptCallback:
    """Tests for watch_manuscript callback handling."""

    def test_calls_callback_on_compilation_event(self, tmp_path):
        """Verify callback is called when 'Compilation' appears in output."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.side_effect = [
            "Starting...\n",
            "Compilation complete\n",
            "",  # End iteration
        ]

        callback = MagicMock()

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path, on_compile=callback)

            callback.assert_called_once()

    def test_callback_not_called_without_compilation_keyword(self, tmp_path):
        """Verify callback is not called for non-compilation output."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.side_effect = [
            "Starting...\n",
            "Processing files...\n",
            "",
        ]

        callback = MagicMock()

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path, on_compile=callback)

            callback.assert_not_called()

    def test_callback_error_does_not_stop_watch(self, tmp_path):
        """Verify callback errors are caught and logged."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.side_effect = [
            "Compilation complete\n",
            "",
        ]

        callback = MagicMock(side_effect=Exception("Callback failed"))

        with patch("subprocess.Popen", return_value=mock_process):
            # Should not raise
            watch_manuscript(tmp_path, on_compile=callback)


class TestWatchManuscriptExceptions:
    """Tests for watch_manuscript exception handling."""

    def test_keyboard_interrupt_terminates_process(self, tmp_path):
        """Verify KeyboardInterrupt terminates the process."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.side_effect = KeyboardInterrupt()

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path)

            mock_process.terminate.assert_called_once()

    def test_generic_exception_terminates_process(self, tmp_path):
        """Verify generic exceptions terminate the process."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash\necho 'compiling'")

        mock_process = MagicMock()
        mock_process.stdout.readline.side_effect = RuntimeError("Connection lost")

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path)

            mock_process.terminate.assert_called_once()


class TestWatchManuscriptParameters:
    """Tests for watch_manuscript parameter defaults."""

    def test_interval_default(self, tmp_path):
        """Verify interval parameter has default value."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash")

        mock_process = MagicMock()
        mock_process.stdout.readline.return_value = ""

        with patch("subprocess.Popen", return_value=mock_process):
            # Should work without interval parameter
            watch_manuscript(tmp_path)

    def test_timeout_none_by_default(self, tmp_path):
        """Verify timeout defaults to None."""
        compile_script = tmp_path / "compile"
        compile_script.write_text("#!/bin/bash")

        mock_process = MagicMock()
        mock_process.stdout.readline.return_value = ""

        with patch("subprocess.Popen", return_value=mock_process):
            watch_manuscript(tmp_path)

            mock_process.wait.assert_called_once_with(timeout=None)


if __name__ == "__main__":
    import os

    import pytest

    pytest.main([os.path.abspath(__file__)])
