"""Tests for the launchd manager service."""

from unittest.mock import MagicMock, patch

import pytest

from mlx_manager.config import DEFAULT_PORT
from mlx_manager.models import ExecutionProfile
from mlx_manager.services.launchd import LaunchdManager


@pytest.fixture
def launchd_manager(tmp_path):
    """Create a LaunchdManager instance with temp directory."""
    manager = LaunchdManager()
    manager.launch_agents_dir = tmp_path
    return manager


@pytest.fixture
def sample_profile():
    """Create a sample ExecutionProfile for testing.

    With the embedded MLX server, profiles no longer have port/host fields.
    """
    return ExecutionProfile(
        id=1,
        name="Test Profile",
        model_id=1,
        auto_start=True,
        profile_type="inference",
    )


class TestLaunchdManagerGetLabel:
    """Tests for the get_label method."""

    def test_basic_label(self, launchd_manager, sample_profile):
        """Test basic label generation."""
        label = launchd_manager.get_label(sample_profile)
        assert label == "com.mlx-manager.test-profile"

    def test_label_with_spaces(self, launchd_manager, sample_profile):
        """Test label generation with spaces in name."""
        sample_profile.name = "My Test Profile"
        label = launchd_manager.get_label(sample_profile)
        assert label == "com.mlx-manager.my-test-profile"

    def test_label_with_underscores(self, launchd_manager, sample_profile):
        """Test label generation with underscores."""
        sample_profile.name = "my_test_profile"
        label = launchd_manager.get_label(sample_profile)
        assert label == "com.mlx-manager.my-test-profile"

    def test_label_with_special_chars(self, launchd_manager, sample_profile):
        """Test label generation removes special characters."""
        sample_profile.name = "Test@Profile#123!"
        label = launchd_manager.get_label(sample_profile)
        assert label == "com.mlx-manager.testprofile123"


class TestLaunchdManagerGetPlistPath:
    """Tests for the get_plist_path method."""

    def test_plist_path(self, launchd_manager, sample_profile, tmp_path):
        """Test plist path generation."""
        path = launchd_manager.get_plist_path(sample_profile)
        expected = tmp_path / "com.mlx-manager.test-profile.plist"
        assert path == expected


class TestLaunchdManagerGeneratePlist:
    """Tests for the generate_plist method.

    Note: With the embedded MLX Server, launchd now runs mlx-manager serve.
    The server uses a default port (10242) since profiles no longer have ports.
    """

    def test_basic_plist(self, launchd_manager, sample_profile):
        """Test basic plist generation for mlx-manager serve."""
        plist = launchd_manager.generate_plist(sample_profile)

        assert plist["Label"] == "com.mlx-manager.test-profile"
        assert plist["RunAtLoad"] is True  # auto_start is True
        assert "ProgramArguments" in plist
        # With embedded server, we use mlx-manager serve
        assert "serve" in plist["ProgramArguments"]
        assert "--port" in plist["ProgramArguments"]

    def test_plist_program_arguments(self, launchd_manager, sample_profile):
        """Test plist program arguments for embedded server."""
        plist = launchd_manager.generate_plist(sample_profile)
        args = plist["ProgramArguments"]

        # With embedded server, we just launch mlx-manager serve with default port
        assert "serve" in args
        assert "--port" in args
        assert str(DEFAULT_PORT) in args  # Default port for embedded server

    def test_plist_keepalive_settings(self, launchd_manager, sample_profile):
        """Test plist has correct KeepAlive settings."""
        plist = launchd_manager.generate_plist(sample_profile)

        assert "KeepAlive" in plist
        assert plist["KeepAlive"]["SuccessfulExit"] is False
        assert plist["KeepAlive"]["Crashed"] is True

    def test_plist_environment_variables(self, launchd_manager, sample_profile):
        """Test plist has required environment variables."""
        plist = launchd_manager.generate_plist(sample_profile)

        assert "EnvironmentVariables" in plist
        env = plist["EnvironmentVariables"]
        assert "PATH" in env
        assert "HOME" in env
        assert "PYTHONUNBUFFERED" in env


class TestLaunchdManagerIsInstalled:
    """Tests for the is_installed method."""

    def test_not_installed(self, launchd_manager, sample_profile):
        """Test returns False when plist doesn't exist."""
        result = launchd_manager.is_installed(sample_profile)
        assert result is False

    def test_installed(self, launchd_manager, sample_profile, tmp_path):
        """Test returns True when plist exists."""
        plist_path = tmp_path / "com.mlx-manager.test-profile.plist"
        plist_path.write_text("test")

        result = launchd_manager.is_installed(sample_profile)
        assert result is True


class TestLaunchdManagerInstall:
    """Tests for the install method."""

    def test_install_creates_plist(self, launchd_manager, sample_profile, tmp_path):
        """Test install creates plist file."""
        with patch("mlx_manager.services.launchd.bootstrap"):
            result = launchd_manager.install(sample_profile)

        plist_path = tmp_path / "com.mlx-manager.test-profile.plist"
        assert plist_path.exists()
        assert result == str(plist_path)

    def test_install_calls_bootstrap(self, launchd_manager, sample_profile, tmp_path):
        """Test install calls bootstrap with correct args."""
        with patch("mlx_manager.services.launchd.bootstrap") as mock_bootstrap:
            launchd_manager.install(sample_profile)

        plist_path = tmp_path / "com.mlx-manager.test-profile.plist"
        mock_bootstrap.assert_called_once_with(str(plist_path), "com.mlx-manager.test-profile")


class TestLaunchdManagerUninstall:
    """Tests for the uninstall method."""

    def test_uninstall_nonexistent(self, launchd_manager, sample_profile):
        """Test uninstall returns False when plist doesn't exist."""
        result = launchd_manager.uninstall(sample_profile)
        assert result is False

    def test_uninstall_removes_plist(self, launchd_manager, sample_profile, tmp_path):
        """Test uninstall removes plist file."""
        plist_path = tmp_path / "com.mlx-manager.test-profile.plist"
        plist_path.write_text("test")

        with patch("mlx_manager.services.launchd.bootout"):
            result = launchd_manager.uninstall(sample_profile)

        assert result is True
        assert not plist_path.exists()

    def test_uninstall_calls_bootout(self, launchd_manager, sample_profile, tmp_path):
        """Test uninstall calls bootout with correct label."""
        plist_path = tmp_path / "com.mlx-manager.test-profile.plist"
        plist_path.write_text("test")

        with patch("mlx_manager.services.launchd.bootout") as mock_bootout:
            launchd_manager.uninstall(sample_profile)

        mock_bootout.assert_called_once_with("com.mlx-manager.test-profile")


class TestLaunchdManagerIsRunning:
    """Tests for the is_running method."""

    def test_is_running_true(self, launchd_manager, sample_profile):
        """Test returns True when service is running."""
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=0)

            result = launchd_manager.is_running(sample_profile)

        assert result is True

    def test_is_running_false(self, launchd_manager, sample_profile):
        """Test returns False when service is not running."""
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1)

            result = launchd_manager.is_running(sample_profile)

        assert result is False


class TestLaunchdManagerStartStop:
    """Tests for the start and stop methods."""

    def test_start_service(self, launchd_manager, sample_profile):
        """Test starting a service calls kickstart."""
        with patch("mlx_manager.services.launchd.kickstart") as mock_kickstart:
            mock_kickstart.return_value = True

            result = launchd_manager.start(sample_profile)

        assert result is True
        mock_kickstart.assert_called_once_with("com.mlx-manager.test-profile")

    def test_stop_service(self, launchd_manager, sample_profile):
        """Test stopping a service calls kill_service."""
        with patch("mlx_manager.services.launchd.kill_service") as mock_kill:
            mock_kill.return_value = True

            result = launchd_manager.stop(sample_profile)

        assert result is True
        mock_kill.assert_called_once_with("com.mlx-manager.test-profile")


class TestLaunchdManagerGetStatus:
    """Tests for the get_status method."""

    def test_get_status_not_running(self, launchd_manager, sample_profile):
        """Test status when service is not running."""
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(returncode=1, stdout="")

            result = launchd_manager.get_status(sample_profile)

        assert result.running is False
        assert result.label == "com.mlx-manager.test-profile"

    def test_get_status_running(self, launchd_manager, sample_profile):
        """Test status when service is running."""
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="12345\t0\tcom.mlx-manager.test-profile"
            )

            result = launchd_manager.get_status(sample_profile)

        assert result.installed is True
        assert result.running is True
        assert result.pid == 12345

    def test_get_status_installed_not_running(self, launchd_manager, sample_profile):
        """Test status when service is installed but not running."""
        with patch("subprocess.run") as mock_run:
            mock_run.return_value = MagicMock(
                returncode=0, stdout="-\t0\tcom.mlx-manager.test-profile"
            )

            result = launchd_manager.get_status(sample_profile)

        assert result.installed is True
        assert result.running is False
        assert result.pid is None
