#!/usr/bin/python3
"""Tests for slpkg.multi_process.MultiProcess."""

import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

from slpkg.multi_process import MultiProcess


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def make_multi_process(
    tmp_path: Path,
    *,
    quiet: bool = False,
    process_log: bool = False,
    reinstall: bool = False,
) -> MultiProcess:
    """Construct MultiProcess bypassing __init__ (avoids config_load)."""
    obj: MultiProcess = object.__new__(MultiProcess)
    obj.colors = True
    obj.quiet = quiet
    obj.package_type = [".tgz", ".txz", ".tbz", ".tlz"]
    obj.log_packages = tmp_path / "log_packages"
    obj.process_log = process_log
    obj.process_log_file = tmp_path / "process.log"
    obj.red = ""
    obj.green = ""
    obj.yellow = ""
    obj.endc = ""
    obj.columns = 80
    obj.rows = 24
    obj.timestamp = "2026-01-01 00:00:00"
    obj.head_message = f"Timestamp: {obj.timestamp}"
    obj.bottom_message = "EOF - End of log file"
    obj.op_count = 0
    obj.op_total = 0
    obj.option_for_reinstall = reinstall

    obj.utils = MagicMock()
    obj.progress = MagicMock()
    obj.imp = MagicMock()
    obj.imp.done = "Done."
    obj.imp.skipped = "Skipped."
    obj.imp.failed = "Failed."
    obj.errors = MagicMock()
    return obj


# ---------------------------------------------------------------------------
# setup_counter
# ---------------------------------------------------------------------------


def test_setup_counter_sets_total(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path)
    obj.setup_counter(5)
    assert obj.op_total == 5


def test_setup_counter_resets_count(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path)
    obj.op_count = 3
    obj.setup_counter(10)
    assert obj.op_count == 0


# ---------------------------------------------------------------------------
# _write_log_head / _write_log_eof
# ---------------------------------------------------------------------------


def test_write_log_head_creates_file_when_process_log_enabled(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, process_log=True)
    obj._write_log_head()
    assert obj.process_log_file.exists()
    content = obj.process_log_file.read_text(encoding="utf-8")
    assert "Timestamp" in content


def test_write_log_head_skips_when_process_log_disabled(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, process_log=False)
    obj._write_log_head()
    assert not obj.process_log_file.exists()


def test_write_log_eof_writes_eof_marker(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, process_log=True)
    obj._write_log_eof()
    content = obj.process_log_file.read_text(encoding="utf-8")
    assert "EOF" in content


def test_write_log_eof_skips_when_process_log_disabled(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, process_log=False)
    obj._write_log_eof()
    assert not obj.process_log_file.exists()


# ---------------------------------------------------------------------------
# _error_process
# ---------------------------------------------------------------------------


def test_error_process_prints_message_when_not_quiet(tmp_path: Path, capsys) -> None:
    obj = make_multi_process(tmp_path, quiet=False)
    obj._error_process()
    out = capsys.readouterr().out
    assert "Error occurred" in out


def test_error_process_silent_when_quiet(tmp_path: Path, capsys) -> None:
    obj = make_multi_process(tmp_path, quiet=True)
    obj._error_process()
    assert capsys.readouterr().out == ""


# ---------------------------------------------------------------------------
# _run
# ---------------------------------------------------------------------------


def test_run_success_writes_log(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True, process_log=True)
    obj._run("echo hello")
    content = obj.process_log_file.read_text(encoding="utf-8")
    assert "hello" in content


def test_run_failure_raises_systemexit(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True)
    with pytest.raises(SystemExit):
        obj._run("false")


def test_run_prints_output_when_not_quiet(tmp_path: Path, capsys) -> None:
    obj = make_multi_process(tmp_path, quiet=False)
    obj._run("echo visible_output")
    out = capsys.readouterr().out
    assert "visible_output" in out


def test_run_quiet_uses_start_new_session_and_devnull_stdin(tmp_path: Path) -> None:
    """In quiet mode _run() must pass start_new_session=True and stdin=DEVNULL."""
    obj = make_multi_process(tmp_path, quiet=True)
    with patch("slpkg.multi_process.subprocess.Popen") as mock_popen:
        mock_proc = MagicMock()
        mock_proc.returncode = 0
        mock_popen.return_value.__enter__.return_value = mock_proc
        obj._run("echo ok")
    kwargs = mock_popen.call_args.kwargs
    assert kwargs.get("start_new_session") is True
    assert kwargs.get("stdin") == subprocess.DEVNULL


def test_run_quiet_uses_devnull_stdout_when_process_log_disabled(tmp_path: Path) -> None:
    """In quiet mode without process logging, stdout/stderr must go to DEVNULL (no PIPE)."""
    obj = make_multi_process(tmp_path, quiet=True, process_log=False)
    with patch("slpkg.multi_process.subprocess.Popen") as mock_popen:
        mock_proc = MagicMock()
        mock_proc.returncode = 0
        mock_popen.return_value.__enter__.return_value = mock_proc
        obj._run("echo ok")
    kwargs = mock_popen.call_args.kwargs
    assert kwargs.get("stdout") == subprocess.DEVNULL
    assert kwargs.get("stderr") == subprocess.DEVNULL


# ---------------------------------------------------------------------------
# process_and_log — non-quiet (direct mode)
# ---------------------------------------------------------------------------


def test_process_and_log_non_quiet_returns_true_on_success(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=False)
    result = obj.process_and_log("echo ok", "pkg.txz", "Installing")
    assert result is True


def test_process_and_log_non_quiet_raises_on_failure(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=False)
    with pytest.raises(SystemExit):
        obj.process_and_log("false", "pkg.txz", "Installing")


# ---------------------------------------------------------------------------
# process_and_log — quiet (multiprocessing mode)
# ---------------------------------------------------------------------------


def test_process_and_log_quiet_increments_op_count(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True)
    obj.setup_counter(3)

    with patch("slpkg.multi_process.Process") as mock_proc_cls, \
         patch("shutil.get_terminal_size") as mock_ts:
        mock_ts.return_value.columns = 80
        mock_proc = MagicMock()
        mock_proc.exitcode = 0
        mock_proc_cls.return_value = mock_proc

        obj.process_and_log("echo ok", "pkg.txz", "Installing")

    assert obj.op_count == 1


def test_process_and_log_quiet_returns_true_on_success(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True)

    with patch("slpkg.multi_process.Process") as mock_proc_cls, \
         patch("shutil.get_terminal_size") as mock_ts:
        mock_ts.return_value.columns = 80
        mock_proc = MagicMock()
        mock_proc.exitcode = 0
        mock_proc_cls.return_value = mock_proc

        result = obj.process_and_log("echo ok", "pkg.txz", "Installing")

    assert result is True


def test_process_and_log_quiet_returns_false_on_failure(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True)

    with patch("slpkg.multi_process.Process") as mock_proc_cls, \
         patch("shutil.get_terminal_size") as mock_ts:
        mock_ts.return_value.columns = 80
        mock_proc = MagicMock()
        mock_proc.exitcode = 1
        mock_proc_cls.return_value = mock_proc

        result = obj.process_and_log("exit 1", "pkg.txz", "Installing")

    assert result is False


def test_process_and_log_quiet_joins_process_2_after_terminate(tmp_path: Path) -> None:
    """process_2 (spinner) must be joined after terminate() so the zombie is reaped."""
    obj = make_multi_process(tmp_path, quiet=True)
    with patch("slpkg.multi_process.Process") as mock_proc_cls, \
         patch("shutil.get_terminal_size") as mock_ts:
        mock_ts.return_value.columns = 80
        mock_proc1 = MagicMock()
        mock_proc1.exitcode = 0
        mock_proc2 = MagicMock()
        mock_proc2.exitcode = 0
        mock_proc2.is_alive.return_value = False
        mock_proc_cls.side_effect = [mock_proc1, mock_proc2]
        obj.process_and_log("echo ok", "pkg.txz", "Installing")
    mock_proc2.terminate.assert_called_once()
    mock_proc2.join.assert_called()


def test_process_and_log_quiet_does_not_increment_when_count_as_new_false(tmp_path: Path) -> None:
    obj = make_multi_process(tmp_path, quiet=True)
    obj.setup_counter(3)

    with patch("slpkg.multi_process.Process") as mock_proc_cls, \
         patch("shutil.get_terminal_size") as mock_ts:
        mock_ts.return_value.columns = 80
        mock_proc = MagicMock()
        mock_proc.exitcode = 0
        mock_proc_cls.return_value = mock_proc

        obj.process_and_log("echo ok", "pkg.txz", "Installing", count_as_new=False)

    assert obj.op_count == 0


# ---------------------------------------------------------------------------
# process (static)
# ---------------------------------------------------------------------------


def test_process_static_success_does_not_raise(tmp_path: Path) -> None:
    MultiProcess.process("echo ok")  # must not raise


def test_process_static_failure_raises_systemexit() -> None:
    with pytest.raises(SystemExit):
        MultiProcess.process("exit 2")


def test_process_static_wget_failure_does_not_raise() -> None:
    # wget/curl/lftp/aria2c failures are silently ignored
    MultiProcess.process("wget --non-existent-flag 2>/dev/null || true")  # must not raise


def test_process_static_keyboard_interrupt_raises_systemexit() -> None:
    with patch("subprocess.run", side_effect=KeyboardInterrupt):
        with pytest.raises(SystemExit) as exc:
            MultiProcess.process("echo ok")
    assert exc.value.code == 1


def test_process_static_unexpected_exception_raises_systemexit() -> None:
    with patch("subprocess.run", side_effect=RuntimeError("boom")):
        with pytest.raises(SystemExit) as exc:
            MultiProcess.process("echo ok")
    assert exc.value.code == 1
