# tests/test_systemd.py
"""Tests for systemd module - systemctl wrapper functions."""

import subprocess

import pytest


@pytest.fixture(autouse=True)
def mock_systemctl_available(mocker):
    """Mock shutil.which to report systemctl as available for all tests."""
    mocker.patch("shutil.which", return_value="/usr/bin/systemctl")


class TestUnitName:
    """Test unit_name helper function."""

    def test_web_unit_name(self):
        """Should generate correct web unit name."""
        from ots_containers import systemd

        assert systemd.unit_name("web", "7043") == "onetime-web@7043"

    def test_worker_unit_name(self):
        """Should generate correct worker unit name."""
        from ots_containers import systemd

        assert systemd.unit_name("worker", "1") == "onetime-worker@1"
        assert systemd.unit_name("worker", "billing") == "onetime-worker@billing"

    def test_scheduler_unit_name(self):
        """Should generate correct scheduler unit name."""
        from ots_containers import systemd

        assert systemd.unit_name("scheduler", "main") == "onetime-scheduler@main"
        assert systemd.unit_name("scheduler", "1") == "onetime-scheduler@1"


class TestDiscoverWebInstances:
    """Test discover_web_instances function for web containers."""

    def test_discover_web_instances_returns_sorted_ports(self, mocker):
        """Should parse systemctl output and return sorted port list."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-web@7043.service loaded active running OTS 7043\n"
            "onetime-web@7044.service loaded active running OTS 7044\n"
            "onetime-web@7042.service loaded active running OTS 7042\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances()

        assert ports == [7042, 7043, 7044]

    def test_discover_web_instances_empty_output(self, mocker):
        """Should return empty list when no instances running."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances()

        assert ports == []

    def test_discover_web_instances_ignores_malformed_lines(self, mocker):
        """Should skip lines that don't match expected format."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-web@7043.service loaded active running OTS\n"
            "some-other.service loaded active running Other\n"
            "onetime-web@abc.service loaded active running Bad port\n"
            "onetime-web@7044.service loaded active running OTS\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances()

        assert ports == [7043, 7044]

    def test_discover_web_instances_returns_all_loaded_by_default(self, mocker):
        """Should return all loaded units regardless of state by default."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-web@7042.service loaded active running OneTimeSecret Container 7042\n"
            "onetime-web@7043.service loaded failed failed OneTimeSecret Container 7043\n"
            "onetime-web@7044.service loaded inactive dead OneTimeSecret Container 7044\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances()

        assert ports == [7042, 7043, 7044]

    def test_discover_web_instances_running_only_filters_failed(self, mocker):
        """With running_only=True, should exclude failed units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-web@7043.service loaded failed failed OneTimeSecret Container 7043\n"
            "onetime-web@7044.service loaded failed failed OneTimeSecret Container 7044\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances(running_only=True)

        assert ports == []

    def test_discover_web_instances_running_only_mixed(self, mocker):
        """With running_only=True, should return only running units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-web@7042.service loaded active running OneTimeSecret Container 7042\n"
            "onetime-web@7043.service loaded failed failed OneTimeSecret Container 7043\n"
            "onetime-web@7044.service loaded active running OneTimeSecret Container 7044\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ports = systemd.discover_web_instances(running_only=True)

        assert ports == [7042, 7044]

    def test_discover_web_instances_calls_systemctl_correctly(self, mocker):
        """Should call systemctl with --all flag to show all units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.discover_web_instances()

        mock_run.assert_called_once_with(
            ["systemctl", "list-units", "onetime-web@*", "--plain", "--no-legend", "--all"],
            capture_output=True,
            text=True,
        )


class TestDaemonReload:
    """Test daemon_reload function."""

    def test_daemon_reload_calls_systemctl(self, mocker):
        """Should call sudo systemctl daemon-reload."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.daemon_reload()

        mock_run.assert_called_once_with(["sudo", "systemctl", "daemon-reload"], check=True)

    def test_daemon_reload_raises_on_failure(self, mocker):
        """Should propagate CalledProcessError on failure."""
        from ots_containers import systemd

        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(1, "cmd"),
        )

        with pytest.raises(subprocess.CalledProcessError):
            systemd.daemon_reload()


class TestStart:
    """Test start function."""

    def test_start_calls_systemctl_start(self, mocker):
        """Should call sudo systemctl start with unit name."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.start("onetime-web@7043")

        mock_run.assert_called_once_with(
            ["sudo", "systemctl", "start", "onetime-web@7043"], check=True
        )

    def test_start_raises_on_failure(self, mocker):
        """Should propagate CalledProcessError on failure."""
        from ots_containers import systemd

        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(1, "cmd"),
        )

        with pytest.raises(subprocess.CalledProcessError):
            systemd.start("onetime-web@7043")


class TestStop:
    """Test stop function."""

    def test_stop_calls_systemctl_stop(self, mocker):
        """Should call sudo systemctl stop with unit name."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.stop("onetime-web@7043")

        mock_run.assert_called_once_with(
            ["sudo", "systemctl", "stop", "onetime-web@7043"], check=True
        )

    def test_stop_raises_on_failure(self, mocker):
        """Should propagate CalledProcessError on failure."""
        from ots_containers import systemd

        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(1, "cmd"),
        )

        with pytest.raises(subprocess.CalledProcessError):
            systemd.stop("onetime-web@7043")


class TestRestart:
    """Test restart function."""

    def test_restart_calls_systemctl_restart(self, mocker):
        """Should call sudo systemctl restart with unit name."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.restart("onetime-web@7043")

        mock_run.assert_called_once_with(
            ["sudo", "systemctl", "restart", "onetime-web@7043"], check=True
        )

    def test_restart_raises_on_failure(self, mocker):
        """Should propagate CalledProcessError on failure."""
        from ots_containers import systemd

        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(1, "cmd"),
        )

        with pytest.raises(subprocess.CalledProcessError):
            systemd.restart("onetime-web@7043")


class TestStatus:
    """Test status function."""

    def test_status_calls_systemctl_status(self, mocker):
        """Should call sudo systemctl status with unit name."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.status("onetime-web@7043")

        mock_run.assert_called_once_with(
            [
                "sudo",
                "systemctl",
                "--no-pager",
                "-n25",
                "status",
                "onetime-web@7043",
            ],
            check=False,
        )

    def test_status_custom_lines(self, mocker):
        """Should use custom line count when specified."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.status("onetime-web@7043", lines=50)

        mock_run.assert_called_once_with(
            [
                "sudo",
                "systemctl",
                "--no-pager",
                "-n50",
                "status",
                "onetime-web@7043",
            ],
            check=False,
        )

    def test_status_does_not_raise_on_nonzero_exit(self, mocker):
        """Should not raise when unit is not running (non-zero exit)."""
        from ots_containers import systemd

        # status returns non-zero when unit is not active
        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(3, "cmd"),
        )

        # Should not raise because check=False
        # But since we're mocking with side_effect, it will raise
        # Let's fix the mock to just return normally
        mock_run = mocker.patch("subprocess.run")
        mock_run.return_value.returncode = 3

        systemd.status("onetime-web@7043")  # Should not raise

        mock_run.assert_called_once()


class TestUnitToContainerName:
    """Test unit_to_container_name function."""

    def test_converts_web_template_instance_unit(self):
        """Should convert onetime-web@7044 to systemd-onetime-web_7044."""
        from ots_containers import systemd

        assert systemd.unit_to_container_name("onetime-web@7044") == "systemd-onetime-web_7044"

    def test_handles_service_suffix(self):
        """Should strip .service suffix before converting."""
        from ots_containers import systemd

        assert (
            systemd.unit_to_container_name("onetime-web@7043.service") == "systemd-onetime-web_7043"
        )

    def test_handles_different_ports(self):
        """Should work with various port numbers."""
        from ots_containers import systemd

        assert systemd.unit_to_container_name("onetime-web@3000") == "systemd-onetime-web_3000"
        assert systemd.unit_to_container_name("onetime-web@8080") == "systemd-onetime-web_8080"


class TestRecreate:
    """Test recreate function."""

    def test_recreate_stops_removes_and_starts(self, mocker):
        """Should stop unit, remove container, then start unit."""
        from ots_containers import systemd

        mock_run = mocker.patch("subprocess.run")

        systemd.recreate("onetime-web@7044")

        assert mock_run.call_count == 3
        calls = mock_run.call_args_list
        assert calls[0][0][0] == ["sudo", "systemctl", "stop", "onetime-web@7044"]
        assert calls[1][0][0] == ["sudo", "podman", "rm", "--ignore", "systemd-onetime-web_7044"]
        assert calls[2][0][0] == ["sudo", "systemctl", "start", "onetime-web@7044"]

    def test_recreate_raises_on_stop_failure(self, mocker):
        """Should propagate error if stop fails."""
        from ots_containers import systemd

        mocker.patch(
            "subprocess.run",
            side_effect=subprocess.CalledProcessError(1, "cmd"),
        )

        with pytest.raises(subprocess.CalledProcessError):
            systemd.recreate("onetime-web@7044")


class TestContainerExists:
    """Test container_exists function."""

    def test_container_exists_returns_true_when_found(self, mocker):
        """Should return True when container exists."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.returncode = 0
        mocker.patch("subprocess.run", return_value=mock_result)

        assert systemd.container_exists("onetime-web@7044") is True

    def test_container_exists_returns_false_when_not_found(self, mocker):
        """Should return False when container doesn't exist."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.returncode = 1
        mocker.patch("subprocess.run", return_value=mock_result)

        assert systemd.container_exists("onetime-web@7044") is False

    def test_container_exists_uses_correct_container_name(self, mocker):
        """Should convert unit name to container name for podman check."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.returncode = 0
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.container_exists("onetime-web@7044")

        mock_run.assert_called_once_with(
            ["podman", "container", "exists", "systemd-onetime-web_7044"],
            capture_output=True,
        )


class TestUnitExists:
    """Test unit_exists function."""

    def test_unit_exists_returns_true_when_found(self, mocker):
        """Should return True when unit file exists."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = "onetime-web@.container enabled enabled"
        mocker.patch("subprocess.run", return_value=mock_result)

        assert systemd.unit_exists("onetime-web@7043") is True

    def test_unit_exists_returns_false_when_not_found(self, mocker):
        """Should return False when unit file doesn't exist."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mocker.patch("subprocess.run", return_value=mock_result)

        assert systemd.unit_exists("onetime-web@7043") is False

    def test_unit_exists_calls_systemctl_correctly(self, mocker):
        """Should call systemctl list-unit-files with correct args."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.unit_exists("onetime-web@7043")

        mock_run.assert_called_once_with(
            [
                "systemctl",
                "list-unit-files",
                "onetime-web@7043",
                "--plain",
                "--no-legend",
            ],
            capture_output=True,
            text=True,
        )


class TestDiscoverWorkerInstances:
    """Test discover_worker_instances function for background worker containers."""

    def test_discover_worker_instances_returns_sorted_ids(self, mocker):
        """Should parse systemctl output and return sorted worker ID list."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@1.service loaded active running OTS Worker 1\n"
            "onetime-worker@3.service loaded active running OTS Worker 3\n"
            "onetime-worker@2.service loaded active running OTS Worker 2\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        # Numeric IDs should be sorted numerically
        assert ids == ["1", "2", "3"]

    def test_discover_worker_instances_with_string_ids(self, mocker):
        """Should correctly parse worker instances with string IDs."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@billing.service loaded active running OTS Worker billing\n"
            "onetime-worker@emails.service loaded active running OTS Worker emails\n"
            "onetime-worker@cleanup.service loaded active running OTS Worker cleanup\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        # String IDs should be sorted alphabetically
        assert ids == ["billing", "cleanup", "emails"]

    def test_discover_worker_instances_mixed_ids(self, mocker):
        """Should handle mix of numeric and string worker IDs."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@1.service loaded active running OTS Worker 1\n"
            "onetime-worker@billing.service loaded active running OTS Worker billing\n"
            "onetime-worker@2.service loaded active running OTS Worker 2\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        # Should return all IDs as strings, sorted
        assert "1" in ids
        assert "2" in ids
        assert "billing" in ids

    def test_discover_worker_instances_empty_output(self, mocker):
        """Should return empty list when no worker instances running."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        assert ids == []

    def test_discover_worker_instances_ignores_web_instances(self, mocker):
        """Should ignore onetime-web@* (web) units, only return onetime-worker@* units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@1.service loaded active running OTS Worker 1\n"
            "onetime-web@7043.service loaded active running OTS Web 7043\n"
            "onetime-worker@2.service loaded active running OTS Worker 2\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        # Should only include worker instances, not web instances
        assert ids == ["1", "2"]
        assert "7043" not in ids

    def test_discover_worker_instances_calls_systemctl_correctly(self, mocker):
        """Should call systemctl with correct pattern for worker units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.discover_worker_instances()

        mock_run.assert_called_once_with(
            ["systemctl", "list-units", "onetime-worker@*", "--plain", "--no-legend", "--all"],
            capture_output=True,
            text=True,
        )

    def test_discover_worker_instances_running_only(self, mocker):
        """With running_only=True, should only return running worker units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@1.service loaded active running OTS Worker 1\n"
            "onetime-worker@2.service loaded failed failed OTS Worker 2\n"
            "onetime-worker@3.service loaded inactive dead OTS Worker 3\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances(running_only=True)

        assert ids == ["1"]

    def test_discover_worker_instances_returns_all_by_default(self, mocker):
        """Without running_only, should return all loaded worker units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-worker@1.service loaded active running OTS Worker 1\n"
            "onetime-worker@2.service loaded failed failed OTS Worker 2\n"
            "onetime-worker@3.service loaded inactive dead OTS Worker 3\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_worker_instances()

        assert ids == ["1", "2", "3"]


class TestDiscoverSchedulerInstances:
    """Test discover_scheduler_instances function for scheduler containers."""

    def test_discover_scheduler_instances_returns_sorted_ids(self, mocker):
        """Should parse systemctl output and return sorted scheduler ID list."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-scheduler@main.service loaded active running OTS Scheduler main\n"
            "onetime-scheduler@cron.service loaded active running OTS Scheduler cron\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_scheduler_instances()

        assert ids == ["cron", "main"]

    def test_discover_scheduler_instances_empty_output(self, mocker):
        """Should return empty list when no scheduler instances running."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_scheduler_instances()

        assert ids == []

    def test_discover_scheduler_instances_calls_systemctl_correctly(self, mocker):
        """Should call systemctl with correct pattern for scheduler units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = ""
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.discover_scheduler_instances()

        mock_run.assert_called_once_with(
            ["systemctl", "list-units", "onetime-scheduler@*", "--plain", "--no-legend", "--all"],
            capture_output=True,
            text=True,
        )

    def test_discover_scheduler_instances_running_only(self, mocker):
        """With running_only=True, should only return running scheduler units."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.stdout = (
            "onetime-scheduler@main.service loaded active running OTS Scheduler main\n"
            "onetime-scheduler@cron.service loaded failed failed OTS Scheduler cron\n"
        )
        mocker.patch("subprocess.run", return_value=mock_result)

        ids = systemd.discover_scheduler_instances(running_only=True)

        assert ids == ["main"]


class TestWorkerUnitToContainerName:
    """Test unit_to_container_name for worker units."""

    def test_converts_worker_unit_with_numeric_id(self):
        """Should convert onetime-worker@1 to systemd-onetime-worker_1."""
        from ots_containers import systemd

        assert systemd.unit_to_container_name("onetime-worker@1") == "systemd-onetime-worker_1"

    def test_converts_worker_unit_with_string_id(self):
        """Should convert onetime-worker@billing to systemd-onetime-worker_billing."""
        from ots_containers import systemd

        assert (
            systemd.unit_to_container_name("onetime-worker@billing")
            == "systemd-onetime-worker_billing"
        )

    def test_handles_worker_service_suffix(self):
        """Should strip .service suffix from worker units."""
        from ots_containers import systemd

        assert (
            systemd.unit_to_container_name("onetime-worker@emails.service")
            == "systemd-onetime-worker_emails"
        )


class TestSchedulerUnitToContainerName:
    """Test unit_to_container_name for scheduler units."""

    def test_converts_scheduler_unit(self):
        """Should convert onetime-scheduler@main to systemd-onetime-scheduler_main."""
        from ots_containers import systemd

        assert (
            systemd.unit_to_container_name("onetime-scheduler@main")
            == "systemd-onetime-scheduler_main"
        )


class TestWorkerContainerExists:
    """Test container_exists for worker containers."""

    def test_worker_container_exists_checks_correct_name(self, mocker):
        """Should check for worker container with correct naming convention."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.returncode = 0
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.container_exists("onetime-worker@billing")

        mock_run.assert_called_once_with(
            ["podman", "container", "exists", "systemd-onetime-worker_billing"],
            capture_output=True,
        )

    def test_worker_container_exists_with_numeric_id(self, mocker):
        """Should check for worker container with numeric ID."""
        from ots_containers import systemd

        mock_result = mocker.Mock()
        mock_result.returncode = 0
        mock_run = mocker.patch("subprocess.run", return_value=mock_result)

        systemd.container_exists("onetime-worker@1")

        mock_run.assert_called_once_with(
            ["podman", "container", "exists", "systemd-onetime-worker_1"],
            capture_output=True,
        )


class TestRequireSystemctl:
    """Test require_systemctl function behavior when systemctl is missing."""

    def test_require_systemctl_exits_when_systemctl_missing(self, mocker):
        """Should raise SystemExit(1) when systemctl is not found."""
        from ots_containers import systemd

        # Override the autouse fixture to simulate missing systemctl
        mocker.patch("shutil.which", return_value=None)

        with pytest.raises(SystemExit) as exc_info:
            systemd.require_systemctl()

        assert exc_info.value.code == 1

    def test_require_systemctl_message_mentions_shell_command(self, mocker, capsys):
        """Should print error message mentioning 'ots instance shell' for macOS."""
        from ots_containers import systemd

        # Override the autouse fixture to simulate missing systemctl
        mocker.patch("shutil.which", return_value=None)

        with pytest.raises(SystemExit):
            systemd.require_systemctl()

        captured = capsys.readouterr()
        assert "ots instance shell" in captured.err
        assert "macOS" in captured.err

    def test_require_systemctl_passes_when_systemctl_available(self, mocker):
        """Should not raise when systemctl is available."""
        from ots_containers import systemd

        # The autouse fixture already mocks this, but be explicit
        mocker.patch("shutil.which", return_value="/usr/bin/systemctl")

        # Should not raise
        systemd.require_systemctl()
