"""Tests for MindRoom user service installation helpers."""

from __future__ import annotations

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

import pytest
from typer.testing import CliRunner

from mindroom.cli.main import app
from mindroom.services.config import (
    InstallResult,
    ServiceActionResult,
    ServiceManager,
    ServiceStatus,
    build_service_command,
    find_uv,
    install_uv,
)
from mindroom.services.launchd import _generate_plist
from mindroom.services.launchd import _get_log_args as _get_launchd_log_args
from mindroom.services.launchd import _get_log_command as _get_launchd_log_command
from mindroom.services.launchd import _restart_service as _restart_launchd_service
from mindroom.services.launchd import _start_service as _start_launchd_service
from mindroom.services.launchd import _stop_service as _stop_launchd_service
from mindroom.services.manager import get_service_manager
from mindroom.services.runtime import ServiceConfigMissingError, resolve_service_environment
from mindroom.services.systemd import _generate_unit_file, _get_unit_name
from mindroom.services.systemd import _get_log_args as _get_systemd_log_args
from mindroom.services.systemd import _restart_service as _restart_systemd_service
from mindroom.services.systemd import _start_service as _start_systemd_service
from mindroom.services.systemd import _stop_service as _stop_systemd_service

runner = CliRunner()


def test_build_service_command_pins_mindroom_version(tmp_path: Path) -> None:
    """The service command must not resolve a newer MindRoom release on restart."""
    uv_path = tmp_path / "uv"
    uv_path.touch()

    command = build_service_command(uv_path, package_version="2026.8.1")

    assert command == [
        str(uv_path),
        "tool",
        "run",
        "--from",
        "mindroom==2026.8.1",
        "mindroom",
        "run",
    ]


@pytest.mark.parametrize(
    ("package_version", "expected_requirement"),
    [
        ("2026.8.1.post1.dev0+g7b7439571.d20260801", "mindroom==2026.8.1"),
        ("2026.8.1rc1.post1.dev0+g7b7439571.d20260801", "mindroom==2026.8.1rc1"),
    ],
)
def test_build_service_command_pins_source_checkout_to_published_version(
    tmp_path: Path,
    package_version: str,
    expected_requirement: str,
) -> None:
    """A VCS development version should resolve to its published predecessor."""
    command = build_service_command(
        tmp_path / "uv",
        package_version=package_version,
    )

    assert command[4] == expected_requirement


def test_find_uv_prefers_extra_paths(tmp_path: Path) -> None:
    """find_uv returns an executable passed through extra_paths first."""
    uv_path = tmp_path / "uv"
    uv_path.touch()
    uv_path.chmod(0o755)

    assert find_uv(extra_paths=[uv_path]) == uv_path


@patch("subprocess.run")
def test_install_uv_success(mock_run: MagicMock) -> None:
    """install_uv reports success when curl and sh both succeed."""
    mock_run.return_value = MagicMock(stdout="install script")

    success, message = install_uv()

    assert success is True
    assert message == "uv installed successfully"
    curl_call, shell_call = mock_run.call_args_list
    assert curl_call.kwargs["text"] is True
    assert isinstance(shell_call.kwargs["input"], str)
    assert shell_call.kwargs["text"] is True


@patch("subprocess.run")
def test_install_uv_failure(mock_run: MagicMock) -> None:
    """install_uv returns a user-facing failure message on subprocess errors."""
    mock_run.side_effect = subprocess.CalledProcessError(1, "curl")

    success, message = install_uv()

    assert success is False
    assert "Failed to install uv" in message


@patch("mindroom.services.manager.platform.system", return_value="Darwin")
def test_get_service_manager_macos(mock_system: MagicMock) -> None:
    """Darwin platforms use the launchd manager."""
    manager = get_service_manager()

    assert isinstance(manager, ServiceManager)
    mock_system.assert_called_once()


@patch("mindroom.services.manager.platform.system", return_value="Linux")
def test_get_service_manager_linux(mock_system: MagicMock) -> None:
    """Linux platforms use the systemd manager."""
    manager = get_service_manager()

    assert isinstance(manager, ServiceManager)
    mock_system.assert_called_once()


@patch("mindroom.services.manager.platform.system", return_value="Windows")
def test_get_service_manager_unsupported(mock_system: MagicMock) -> None:
    """Unsupported platforms fail with a clear RuntimeError."""
    with pytest.raises(RuntimeError, match="Unsupported platform"):
        get_service_manager()

    mock_system.assert_called_once()


def test_systemd_unit_runs_mindroom() -> None:
    """The generated systemd unit starts MindRoom and restarts on failure."""
    with patch("mindroom.services.config.distribution_version", return_value="2026.8.1"):
        unit = _generate_unit_file(
            Path("/usr/bin/uv"),
            {
                "MINDROOM_CONFIG_PATH": "/Users/test/Mind Room/config.yaml",
                "MINDROOM_STORAGE_PATH": "/Users/test/Mind Room/data%root",
                "PATH": "/Users/test/.local/bin:/usr/bin",
            },
        )

    assert _get_unit_name() == "mindroom.service"
    assert "Description=MindRoom" in unit
    assert "ExecStart=/usr/bin/uv tool run --from mindroom==2026.8.1 mindroom run" in unit
    assert 'Environment="MINDROOM_CONFIG_PATH=/Users/test/Mind Room/config.yaml"' in unit
    assert 'Environment="MINDROOM_STORAGE_PATH=/Users/test/Mind Room/data%%root"' in unit
    assert 'Environment="PATH=/Users/test/.local/bin:/usr/bin"' in unit
    assert "Restart=on-failure" in unit


def test_launchd_plist_runs_mindroom(tmp_path: Path) -> None:
    """The generated launchd plist starts MindRoom and writes logs."""
    service_environment = {
        "MINDROOM_CONFIG_PATH": str(tmp_path / "config.yaml"),
        "MINDROOM_STORAGE_PATH": str(tmp_path / "mindroom_data"),
        "PATH": f"{tmp_path}/bin:/usr/bin",
    }
    with patch("mindroom.services.config.distribution_version", return_value="2026.8.1"):
        plist_data = _generate_plist(tmp_path / "uv", tmp_path, tmp_path / "logs", service_environment)
    rendered = plistlib.dumps(plist_data)

    assert plist_data["Label"] == "chat.mindroom.local"
    assert plist_data["ProgramArguments"] == [
        str(tmp_path / "uv"),
        "tool",
        "run",
        "--from",
        "mindroom==2026.8.1",
        "mindroom",
        "run",
    ]
    assert plist_data["EnvironmentVariables"] == service_environment
    assert b"StandardOutPath" in rendered


def test_launchd_log_command_uses_explicit_files() -> None:
    """The launchd log command avoids zsh nomatch failures from an empty glob."""
    command = _get_launchd_log_command()

    assert "*.log" not in command
    assert "stdout.log" in command
    assert "stderr.log" in command


def test_launchd_log_args_use_explicit_files() -> None:
    """The launchd log args execute the same explicit files without shell parsing."""
    args = _get_launchd_log_args()

    assert args[0:2] == ["tail", "-f"]
    assert str(Path.home() / "Library" / "Logs" / "mindroom" / "stdout.log") in args
    assert str(Path.home() / "Library" / "Logs" / "mindroom" / "stderr.log") in args


def test_systemd_log_args_follow_user_unit() -> None:
    """The systemd log args follow the MindRoom user unit."""
    assert _get_systemd_log_args() == ["journalctl", "--user", "-u", "mindroom", "-f"]


def test_resolve_service_environment_captures_active_runtime(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    """Installed services capture the same config and storage context as the invoking CLI."""
    config_path = tmp_path / "local config.yaml"
    storage_path = tmp_path / "local storage"
    uv_path = tmp_path / "custom uv bin" / "uv"
    uv_path.parent.mkdir()
    uv_path.touch()
    config_path.write_text("agents: {}\n", encoding="utf-8")
    monkeypatch.setenv("MINDROOM_CONFIG_PATH", str(config_path))
    monkeypatch.setenv("MINDROOM_STORAGE_PATH", str(storage_path))
    monkeypatch.setenv("PATH", "/existing/bin")

    service_environment = resolve_service_environment(uv_path)

    assert service_environment["MINDROOM_CONFIG_PATH"] == str(config_path.resolve())
    assert service_environment["MINDROOM_STORAGE_PATH"] == str(storage_path.resolve())
    path_entries = service_environment["PATH"].split(":")
    assert path_entries[0] == str(uv_path.parent)
    assert str(Path.home() / ".local" / "bin") in path_entries
    assert "/opt/homebrew/bin" in path_entries
    assert "/usr/local/bin" in path_entries
    assert "/existing/bin" in path_entries


def test_resolve_service_environment_requires_existing_config(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
) -> None:
    """Service install fails before writing a service when no active config exists."""
    config_path = tmp_path / "missing.yaml"
    monkeypatch.setenv("MINDROOM_CONFIG_PATH", str(config_path))

    with pytest.raises(ServiceConfigMissingError, match="mindroom config init"):
        resolve_service_environment(tmp_path / "uv")


@patch("mindroom.services.systemd.subprocess.run")
def test_systemd_lifecycle_actions_use_systemctl(mock_run: MagicMock, tmp_path: Path) -> None:
    """Systemd lifecycle actions preserve the unit and call systemctl."""
    unit_path = tmp_path / "mindroom.service"
    unit_path.touch()
    mock_run.return_value = MagicMock(returncode=0, stderr="")

    with patch("mindroom.services.systemd._get_unit_path", return_value=unit_path):
        assert _start_systemd_service().success is True
        assert _stop_systemd_service().success is True
        assert _restart_systemd_service().success is True

    assert [call.args[0] for call in mock_run.call_args_list] == [
        ["systemctl", "--user", "start", "mindroom.service"],
        ["systemctl", "--user", "stop", "mindroom.service"],
        ["systemctl", "--user", "restart", "mindroom.service"],
    ]


def test_systemd_lifecycle_action_requires_installed_unit(tmp_path: Path) -> None:
    """Missing systemd lifecycle actions should tell users to install first."""
    with patch("mindroom.services.systemd._get_unit_path", return_value=tmp_path / "missing.service"):
        start_result = _start_systemd_service()
        stop_result = _stop_systemd_service()
        restart_result = _restart_systemd_service()

    expected_message = "Service is not installed. Run `mindroom service install` first."
    assert start_result == ServiceActionResult(success=False, message=expected_message)
    assert stop_result == ServiceActionResult(success=False, message=expected_message)
    assert restart_result == ServiceActionResult(success=False, message=expected_message)


@patch("mindroom.services.systemd.subprocess.run")
def test_systemd_lifecycle_actions_propagate_systemctl_errors(mock_run: MagicMock, tmp_path: Path) -> None:
    """Systemd lifecycle actions surface non-zero systemctl results."""
    unit_path = tmp_path / "mindroom.service"
    unit_path.touch()
    mock_run.return_value = MagicMock(returncode=1, stderr="unit failed")

    with patch("mindroom.services.systemd._get_unit_path", return_value=unit_path):
        start_result = _start_systemd_service()
        stop_result = _stop_systemd_service()
        restart_result = _restart_systemd_service()

    assert start_result.success is False
    assert start_result.message == "Failed to start service: unit failed"
    assert stop_result.success is False
    assert stop_result.message == "Failed to stop service: unit failed"
    assert restart_result.success is False
    assert restart_result.message == "Failed to restart service: unit failed"


@patch("mindroom.services.launchd.os.getuid", return_value=501)
@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_lifecycle_actions_use_launchctl(
    mock_run: MagicMock,
    mock_getuid: MagicMock,
    tmp_path: Path,
) -> None:
    """Launchd lifecycle actions load and unload the existing plist."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    mock_run.return_value = MagicMock(returncode=0, stderr="")

    status = ServiceStatus(installed=True, running=True, pid=123)
    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        assert _start_launchd_service().success is True
        assert _stop_launchd_service().success is True
        assert _restart_launchd_service().success is True

    assert mock_getuid.call_count == 2
    assert [call.args[0] for call in mock_run.call_args_list] == [
        ["launchctl", "bootout", "gui/501", str(plist_path)],
        ["launchctl", "bootout", "gui/501", str(plist_path)],
        ["launchctl", "bootstrap", "gui/501", str(plist_path)],
    ]


@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_start_is_idempotent_when_already_running(mock_run: MagicMock, tmp_path: Path) -> None:
    """Starting an already running launchd service should not re-bootstrap it."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    status = ServiceStatus(installed=True, running=True, pid=123)

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        result = _start_launchd_service()

    assert result.success is True
    assert result.message == "Service already running"
    mock_run.assert_not_called()


@patch("mindroom.services.launchd.os.getuid", return_value=501)
@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_restart_starts_stopped_service(
    mock_run: MagicMock,
    mock_getuid: MagicMock,
    tmp_path: Path,
) -> None:
    """Restarting a stopped launchd service should still clear any loaded job."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    mock_run.return_value = MagicMock(returncode=0, stderr="")
    status = ServiceStatus(installed=True, running=False)

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        result = _restart_launchd_service()

    assert result.success is True
    mock_getuid.assert_called_once_with()
    assert [call.args[0] for call in mock_run.call_args_list] == [
        ["launchctl", "bootout", "gui/501", str(plist_path)],
        ["launchctl", "bootstrap", "gui/501", str(plist_path)],
    ]


def test_launchd_lifecycle_action_requires_installed_plist(tmp_path: Path) -> None:
    """Missing launchd lifecycle actions should tell users to install first."""
    with patch("mindroom.services.launchd._get_plist_path", return_value=tmp_path / "missing.plist"):
        start_result = _start_launchd_service()
        stop_result = _stop_launchd_service()
        restart_result = _restart_launchd_service()

    expected_message = "Service is not installed. Run `mindroom service install` first."
    assert start_result == ServiceActionResult(success=False, message=expected_message)
    assert stop_result == ServiceActionResult(success=False, message=expected_message)
    assert restart_result == ServiceActionResult(success=False, message=expected_message)


@patch("mindroom.services.launchd.os.getuid", return_value=501)
@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_lifecycle_actions_propagate_launchctl_errors(
    mock_run: MagicMock,
    mock_getuid: MagicMock,
    tmp_path: Path,
) -> None:
    """Launchd lifecycle actions surface non-zero launchctl results."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    status = ServiceStatus(installed=True, running=False)
    mock_run.return_value = MagicMock(returncode=1, stderr="bootstrap failed")

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        start_result = _start_launchd_service()

    assert start_result.success is False
    assert start_result.message == "Failed to start service: bootstrap failed"
    mock_getuid.assert_called_once_with()


@patch("mindroom.services.launchd.os.getuid", return_value=501)
@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_stop_service_propagates_launchctl_errors(
    mock_run: MagicMock,
    mock_getuid: MagicMock,
    tmp_path: Path,
) -> None:
    """Launchd stop surfaces bootout failures."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    status = ServiceStatus(installed=True, running=True, pid=123)
    mock_run.return_value = MagicMock(returncode=1, stderr="bootout failed")

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        stop_result = _stop_launchd_service()

    assert stop_result.success is False
    assert stop_result.message == "Failed to stop service: bootout failed"
    mock_getuid.assert_called_once_with()


@patch("mindroom.services.launchd.os.getuid", return_value=501)
@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_restart_service_propagates_bootstrap_errors(
    mock_run: MagicMock,
    mock_getuid: MagicMock,
    tmp_path: Path,
) -> None:
    """Launchd restart ignores bootout failures but surfaces bootstrap failures."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    status = ServiceStatus(installed=True, running=True, pid=123)
    mock_run.side_effect = [
        MagicMock(returncode=1, stderr="ignored bootout failure"),
        MagicMock(returncode=1, stderr="bootstrap failed"),
    ]

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        restart_result = _restart_launchd_service()

    assert restart_result.success is False
    assert restart_result.message == "Failed to restart service: bootstrap failed"
    mock_getuid.assert_called_once_with()


@patch("mindroom.services.launchd.subprocess.run")
def test_launchd_stop_service_already_stopped(mock_run: MagicMock, tmp_path: Path) -> None:
    """Stopping an installed but stopped launchd service should be a no-op."""
    plist_path = tmp_path / "chat.mindroom.local.plist"
    plist_path.touch()
    status = ServiceStatus(installed=True, running=False)

    with (
        patch("mindroom.services.launchd._get_plist_path", return_value=plist_path),
        patch("mindroom.services.launchd._get_service_status", return_value=status),
    ):
        result = _stop_launchd_service()

    assert result == ServiceActionResult(success=True, message="Service already stopped")
    mock_run.assert_not_called()


def test_service_help_is_registered() -> None:
    """The top-level CLI exposes the service command group."""
    result = runner.invoke(app, ["service", "--help"])

    assert result.exit_code == 0
    assert "install" in result.output
    assert "start" in result.output
    assert "stop" in result.output
    assert "restart" in result.output
    assert "uninstall" in result.output
    assert "status" in result.output
    assert "logs" in result.output


@patch("mindroom.cli.service._get_service_manager")
def test_service_status_not_installed(mock_get_manager: MagicMock) -> None:
    """Service status renders a not-installed service without logs."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_service_status.return_value = ServiceStatus(installed=False, running=False)
    mock_manager.get_log_command.return_value = "tail logs"
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "status"])

    assert result.exit_code == 0
    assert "not installed" in result.output


@patch("mindroom.cli.service._get_service_manager")
def test_service_install_no_confirm(mock_get_manager: MagicMock) -> None:
    """Service install -y installs without interactive prompts."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.check_uv_installed.return_value = (True, Path("/usr/bin/uv"))
    mock_manager.install_service.return_value = InstallResult(success=True, message="Installed and started")
    mock_manager.get_log_command.return_value = "journalctl --user -u mindroom -f"
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "install", "-y"])

    assert result.exit_code == 0
    assert "Installed and started" in result.output
    assert "After upgrading, rerun mindroom service install" in result.output
    mock_manager.install_service.assert_called_once_with()


@patch("mindroom.cli.service._get_service_manager")
def test_service_start_command_succeeds(mock_get_manager: MagicMock) -> None:
    """Service start calls the platform manager start action."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.start_service.return_value = ServiceActionResult(success=True, message="Service started")
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "start"])

    assert result.exit_code == 0
    assert "Service started" in result.output
    mock_manager.start_service.assert_called_once_with()


@patch("mindroom.cli.service._get_service_manager")
def test_service_stop_command_succeeds(mock_get_manager: MagicMock) -> None:
    """Service stop calls the platform manager stop action."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.stop_service.return_value = ServiceActionResult(success=True, message="Service stopped")
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "stop"])

    assert result.exit_code == 0
    assert "Service stopped" in result.output
    mock_manager.stop_service.assert_called_once_with()


@patch("mindroom.cli.service._get_service_manager")
def test_service_restart_command_succeeds(mock_get_manager: MagicMock) -> None:
    """Service restart calls the platform manager restart action."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.restart_service.return_value = ServiceActionResult(success=True, message="Service restarted")
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "restart"])

    assert result.exit_code == 0
    assert "Service restarted" in result.output
    mock_manager.restart_service.assert_called_once_with()


@patch("mindroom.cli.service.subprocess.run")
@patch("mindroom.cli.service._get_service_manager")
def test_service_logs_command_follows_platform_logs(mock_get_manager: MagicMock, mock_run: MagicMock) -> None:
    """Service logs follows the platform-specific log stream command."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_log_args.return_value = [
        "tail",
        "-f",
        "/var/log/mindroom/stdout.log",
        "/var/log/mindroom/stderr.log",
    ]
    mock_get_manager.return_value = mock_manager
    mock_run.return_value = MagicMock(returncode=0)

    result = runner.invoke(app, ["service", "logs"])

    assert result.exit_code == 0
    mock_manager.get_log_args.assert_called_once_with()
    mock_run.assert_called_once_with(
        ["tail", "-f", "/var/log/mindroom/stdout.log", "/var/log/mindroom/stderr.log"],
        check=False,
    )


@patch("mindroom.cli.service.subprocess.run")
@patch("mindroom.cli.service._get_service_manager")
def test_service_logs_command_propagates_nonzero_exit_code(
    mock_get_manager: MagicMock,
    mock_run: MagicMock,
) -> None:
    """Service logs propagates non-zero exits from the log command."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_log_args.return_value = ["journalctl", "--user", "-u", "mindroom", "-f"]
    mock_get_manager.return_value = mock_manager
    mock_run.return_value = MagicMock(returncode=2)

    result = runner.invoke(app, ["service", "logs"])

    assert result.exit_code == 2
    mock_manager.get_log_args.assert_called_once_with()
    mock_run.assert_called_once_with(["journalctl", "--user", "-u", "mindroom", "-f"], check=False)


@patch("mindroom.cli.service.subprocess.run")
@patch("mindroom.cli.service._get_service_manager")
def test_service_logs_command_handles_keyboard_interrupt(
    mock_get_manager: MagicMock,
    mock_run: MagicMock,
) -> None:
    """Service logs exits cleanly when the user interrupts log following."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_log_args.return_value = ["tail", "-f", "/var/log/mindroom/stdout.log"]
    mock_get_manager.return_value = mock_manager
    mock_run.side_effect = KeyboardInterrupt

    result = runner.invoke(app, ["service", "logs"])

    assert result.exit_code == 0
    assert "Aborted" not in result.output


@patch("mindroom.cli.service.subprocess.run")
@patch("mindroom.cli.service._get_service_manager")
def test_service_logs_command_treats_sigint_return_code_as_clean_exit(
    mock_get_manager: MagicMock,
    mock_run: MagicMock,
) -> None:
    """Service logs exits cleanly when the child log process is interrupted."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_log_args.return_value = ["tail", "-f", "/var/log/mindroom/stdout.log"]
    mock_get_manager.return_value = mock_manager
    mock_run.return_value = MagicMock(returncode=-2)

    result = runner.invoke(app, ["service", "logs"])

    assert result.exit_code == 0
    assert "Aborted" not in result.output


@patch("mindroom.cli.service.subprocess.run")
@patch("mindroom.cli.service._get_service_manager")
def test_service_logs_command_reports_spawn_failure(
    mock_get_manager: MagicMock,
    mock_run: MagicMock,
) -> None:
    """Service logs reports missing log executables without a traceback."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.get_log_args.return_value = ["missing-tail", "-f", "/var/log/mindroom/stdout.log"]
    mock_get_manager.return_value = mock_manager
    mock_run.side_effect = FileNotFoundError("missing-tail")

    result = runner.invoke(app, ["service", "logs"])

    assert result.exit_code == 1
    assert "Failed to run log command" in result.output


@patch("mindroom.cli.service._get_service_manager")
def test_service_start_failure_exits_with_message(mock_get_manager: MagicMock) -> None:
    """Service lifecycle failures should print a concise error and exit non-zero."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.start_service.return_value = ServiceActionResult(
        success=False,
        message="Service is not installed. Run `mindroom service install` first.",
    )
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "start"])

    assert result.exit_code == 1
    assert "Service is not installed" in result.output


@patch("mindroom.cli.service._get_service_manager")
def test_service_stop_failure_exits_with_message(mock_get_manager: MagicMock) -> None:
    """Service stop failures should print a concise error and exit non-zero."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.stop_service.return_value = ServiceActionResult(success=False, message="stop failed")
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "stop"])

    assert result.exit_code == 1
    assert "stop failed" in result.output
    mock_manager.stop_service.assert_called_once_with()


@patch("mindroom.cli.service._get_service_manager")
def test_service_restart_failure_exits_with_message(mock_get_manager: MagicMock) -> None:
    """Service restart failures should print a concise error and exit non-zero."""
    mock_manager = MagicMock(spec=ServiceManager)
    mock_manager.restart_service.return_value = ServiceActionResult(success=False, message="restart failed")
    mock_get_manager.return_value = mock_manager

    result = runner.invoke(app, ["service", "restart"])

    assert result.exit_code == 1
    assert "restart failed" in result.output
    mock_manager.restart_service.assert_called_once_with()
