"""Tests for system service registration (service.py)."""

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

import pytest

from skop_runner.service import (
    get_service_status,
    get_skop_runner_path,
    install_service,
    uninstall_service,
    _install_windows,
    _uninstall_windows,
    _status_windows,
)


# ============================================
# get_skop_runner_path
# ============================================


class TestGetSkopRunnerPath:
    def test_resolves_symlink(self, tmp_path: Path):
        real = tmp_path / "real-skop-runner"
        real.write_text("#!/bin/sh\n")
        link = tmp_path / "skop-runner"
        link.symlink_to(real)

        with patch("shutil.which", return_value=str(link)):
            result = get_skop_runner_path()
        assert result == str(real)

    def test_not_found_raises(self):
        with patch("shutil.which", return_value=None):
            with pytest.raises(FileNotFoundError, match="Could not find"):
                get_skop_runner_path()


# ============================================
# install_service — macOS
# ============================================


class TestInstallMacOS:
    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.get_skop_runner_path", return_value="/usr/local/bin/skop-runner")
    @patch("skop_runner.service.sys")
    def test_creates_plist(self, mock_sys, mock_path, mock_run, tmp_path: Path):
        mock_sys.platform = "darwin"
        mock_run.return_value = MagicMock(returncode=0)

        plist_dir = tmp_path / "Library" / "LaunchAgents"
        log_dir = tmp_path / "Library" / "Logs"

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            msg = install_service()

        plist_path = plist_dir / "com.skoplabs.runner.plist"
        assert plist_path.exists()
        content = plist_path.read_text()
        assert "/usr/local/bin/skop-runner" in content
        assert "<string>start</string>" in content
        assert "<key>KeepAlive</key>" in content
        assert "installed and started" in msg

        # Verify launchctl load was called
        load_calls = [
            c for c in mock_run.call_args_list
            if "load" in str(c)
        ]
        assert len(load_calls) == 1

    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.get_skop_runner_path", return_value="/usr/local/bin/skop-runner")
    @patch("skop_runner.service.sys")
    def test_reinstall_unloads_first(self, mock_sys, mock_path, mock_run, tmp_path: Path):
        mock_sys.platform = "darwin"
        mock_run.return_value = MagicMock(returncode=0)

        plist_dir = tmp_path / "Library" / "LaunchAgents"
        plist_dir.mkdir(parents=True)
        plist_path = plist_dir / "com.skoplabs.runner.plist"
        plist_path.write_text("old")

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            install_service()

        # Should have unload call before load call
        calls = mock_run.call_args_list
        unload_idx = next(
            i for i, c in enumerate(calls) if "unload" in str(c.args[0])
        )
        load_idx = next(
            i for i, c in enumerate(calls) if c.args[0][-1] == str(plist_path) and "load" == c.args[0][-2]
        )
        assert unload_idx < load_idx


# ============================================
# install_service — Linux
# ============================================


class TestInstallLinux:
    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.get_skop_runner_path", return_value="/home/user/.local/bin/skop-runner")
    @patch("skop_runner.service.sys")
    def test_creates_unit_file(self, mock_sys, mock_path, mock_run, tmp_path: Path):
        mock_sys.platform = "linux"
        mock_run.return_value = MagicMock(returncode=0)

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            msg = install_service()

        unit_path = tmp_path / ".config" / "systemd" / "user" / "skop-runner.service"
        assert unit_path.exists()
        content = unit_path.read_text()
        assert "/home/user/.local/bin/skop-runner" in content
        assert "Restart=always" in content
        assert "RestartSec=5" in content
        assert f"Environment=HOME={tmp_path}" in content
        assert "installed and started" in msg

        # Verify daemon-reload + enable were called
        daemon_calls = [c for c in mock_run.call_args_list if "daemon-reload" in str(c)]
        enable_calls = [c for c in mock_run.call_args_list if "enable" in str(c)]
        assert len(daemon_calls) == 1
        assert len(enable_calls) == 1


# ============================================
# uninstall_service
# ============================================


class TestUninstallMacOS:
    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.sys")
    def test_removes_plist(self, mock_sys, mock_run, tmp_path: Path):
        mock_sys.platform = "darwin"
        mock_run.return_value = MagicMock(returncode=0)

        plist_dir = tmp_path / "Library" / "LaunchAgents"
        plist_dir.mkdir(parents=True)
        plist_path = plist_dir / "com.skoplabs.runner.plist"
        plist_path.write_text("plist content")

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            msg = uninstall_service()

        assert not plist_path.exists()
        assert "stopped and removed" in msg

    @patch("skop_runner.service.sys")
    def test_not_installed(self, mock_sys, tmp_path: Path):
        mock_sys.platform = "darwin"

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            msg = uninstall_service()

        assert "not installed" in msg


class TestUninstallLinux:
    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.sys")
    def test_removes_unit_file(self, mock_sys, mock_run, tmp_path: Path):
        mock_sys.platform = "linux"
        mock_run.return_value = MagicMock(returncode=0)

        unit_dir = tmp_path / ".config" / "systemd" / "user"
        unit_dir.mkdir(parents=True)
        unit_path = unit_dir / "skop-runner.service"
        unit_path.write_text("unit content")

        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            msg = uninstall_service()

        assert not unit_path.exists()
        assert "stopped and removed" in msg


# ============================================
# get_service_status
# ============================================


class TestServiceStatus:
    @patch("skop_runner.service.sys")
    def test_not_installed_macos(self, mock_sys, tmp_path: Path):
        mock_sys.platform = "darwin"
        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            assert get_service_status() == "not installed"

    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.sys")
    def test_running_macos(self, mock_sys, mock_run, tmp_path: Path):
        mock_sys.platform = "darwin"
        plist_dir = tmp_path / "Library" / "LaunchAgents"
        plist_dir.mkdir(parents=True)
        (plist_dir / "com.skoplabs.runner.plist").write_text("plist")

        mock_run.return_value = MagicMock(
            returncode=0,
            stdout='{\n\t"LimitLoadToSessionType" = "Aqua";\n\t"Label" = "com.skoplabs.runner";\n\t"OnDemand" = true;\n\t"LastExitStatus" = 0;\n\t"PID" = 1234;\n\t"Program" = "/usr/local/bin/skop-runner";\n};\n',
        )
        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            assert get_service_status() == "running"

    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.sys")
    def test_stopped_macos(self, mock_sys, mock_run, tmp_path: Path):
        mock_sys.platform = "darwin"
        plist_dir = tmp_path / "Library" / "LaunchAgents"
        plist_dir.mkdir(parents=True)
        (plist_dir / "com.skoplabs.runner.plist").write_text("plist")

        mock_run.return_value = MagicMock(
            returncode=0,
            stdout='{\n\t"LimitLoadToSessionType" = "Aqua";\n\t"Label" = "com.skoplabs.runner";\n\t"OnDemand" = true;\n\t"LastExitStatus" = 0;\n\t"Program" = "/usr/local/bin/skop-runner";\n};\n',
        )
        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            assert get_service_status() == "stopped"

    @patch("skop_runner.service.subprocess.run")
    @patch("skop_runner.service.sys")
    def test_running_linux(self, mock_sys, mock_run, tmp_path: Path):
        mock_sys.platform = "linux"
        unit_dir = tmp_path / ".config" / "systemd" / "user"
        unit_dir.mkdir(parents=True)
        (unit_dir / "skop-runner.service").write_text("unit")

        mock_run.return_value = MagicMock(returncode=0, stdout="active\n")
        with patch("skop_runner.service.Path.home", return_value=tmp_path):
            assert get_service_status() == "running"

    @patch("skop_runner.service.sys")
    def test_unsupported_platform(self, mock_sys):
        mock_sys.platform = "freebsd"
        assert get_service_status() == "not supported"


# ============================================
# unsupported platform
# ============================================


class TestUnsupportedPlatform:
    @patch("skop_runner.service.sys")
    @patch("skop_runner.service.get_skop_runner_path", return_value="/usr/bin/skop-runner")
    def test_install_raises(self, mock_path, mock_sys):
        mock_sys.platform = "freebsd"
        with pytest.raises(RuntimeError, match="not supported"):
            install_service()

    @patch("skop_runner.service.sys")
    def test_uninstall_raises(self, mock_sys):
        mock_sys.platform = "freebsd"
        with pytest.raises(RuntimeError, match="not supported"):
            uninstall_service()


# ============================================
# Windows (Task Scheduler)
# ============================================


class TestWindowsService:
    @patch("skop_runner.service.subprocess")
    @patch("skop_runner.service.os")
    @patch("skop_runner.service.tempfile")
    @patch("skop_runner.service.sys")
    @patch("skop_runner.service.get_skop_runner_path", return_value="C:\\Python\\skop-runner.exe")
    def test_install_creates_task(self, mock_path, mock_sys, mock_tempfile, mock_os, mock_subprocess):
        mock_sys.platform = "win32"
        mock_tempfile.mkstemp.return_value = (99, "C:\\tmp\\skop-task.xml")
        mock_os.fdopen.return_value.__enter__ = lambda s: s
        mock_os.fdopen.return_value.__exit__ = lambda s, *a: None
        mock_subprocess.run.return_value.returncode = 0

        result = _install_windows("C:\\Python\\skop-runner.exe")

        assert "Task Scheduler" in result
        # Should have called schtasks /Create and schtasks /Run
        assert mock_subprocess.run.call_count == 2

    @patch("skop_runner.service.subprocess")
    @patch("skop_runner.service.sys")
    def test_uninstall_deletes_task(self, mock_sys, mock_subprocess):
        mock_sys.platform = "win32"
        mock_subprocess.run.return_value.returncode = 0
        mock_subprocess.run.return_value.stderr = ""

        result = _uninstall_windows()

        assert "removed" in result.lower()
        args = mock_subprocess.run.call_args[0][0]
        assert args[0] == "schtasks"
        assert "/Delete" in args

    @patch("skop_runner.service.subprocess")
    @patch("skop_runner.service.sys")
    def test_status_running(self, mock_sys, mock_subprocess):
        mock_sys.platform = "win32"
        mock_subprocess.run.return_value.returncode = 0
        mock_subprocess.run.return_value.stdout = '"SkopRunner","Running"'

        assert _status_windows() == "running"

    @patch("skop_runner.service.subprocess")
    @patch("skop_runner.service.sys")
    def test_status_not_installed(self, mock_sys, mock_subprocess):
        mock_sys.platform = "win32"
        mock_subprocess.run.return_value.returncode = 1

        assert _status_windows() == "not installed"
