//! INSTALL-TREE — the backend-neutral service-install abstraction.
//!
//! `src/systemd.rs` is the *first* install backend: a `SystemdServiceSpec` that
//! renders straight to a systemd `.service` unit (system + `--user` scope). That
//! is excellent for systemd hosts, but a 2015+ install tree must also target
//! **OpenRC · runit · s6-rc · dinit · SysV** (and OCI/quadlet/Windows for
//! completeness). Hard-coupling every product to systemd would re-create the
//! per-init bespoke install scripts Skíðblaðnir set out to delete.
//!
//! This module introduces the **tree**: ONE declarative [`ServiceSpec`] (exec,
//! user, scope, env, deps, restart, sockets) that renders to ALL of them through
//! a [`ServiceRenderer`] trait — one renderer per init/service backend. The spike
//! lands **two real renderers**:
//!
//! 1. [`SystemdRenderer`] — bridges to the existing, tested
//!    [`crate::systemd::systemd_unit_scoped`] (system + user scope), so the tree
//!    *reuses* the install limb that already ships, it does not fork it.
//! 2. [`OpenRcRenderer`] — an `#!/sbin/openrc-run` service script + a
//!    `/etc/conf.d/<name>` env file. A deliberately *non-systemd* second backend:
//!    it proves the abstraction is init-neutral, not just "systemd with a coat of
//!    paint".
//!
//! Everything here is **pure** (no root, no IO, no subprocess): a renderer turns a
//! [`ServiceSpec`] into a [`RenderedService`] — the file bodies to write plus the
//! enable/start/status/stop **verbs as data** (argv vectors). A thin privileged
//! `apply()` layer (writing files + invoking the verbs) is a follow-up; keeping
//! render pure is what makes the whole tree unit-testable with zero privilege,
//! exactly like the systemd generators.
//!
//! Design + the full multi-init landscape survey + the KVM test-matrix/probe plan:
//! `.nornir/install-tree-design.md`.

use serde::{Deserialize, Serialize};

// The tree is a *generalization* of the systemd limb, so it reuses that limb's
// vocabulary rather than cloning it: scope (system vs per-user) and the restart
// policy mean the same thing in every backend.
pub use crate::systemd::{RestartPolicy, Scope};

/// The init / service-manager backends a 2015+ install tree targets. The spike
/// implements [`Backend::SystemdSystem`], [`Backend::SystemdUser`] and
/// [`Backend::OpenRc`]; the rest are declared (so the tree's shape is complete and
/// [`Backend::all`] can drive a coverage matrix) and resolve to `None` in
/// [`renderer_for`] until their renderer lands — an honest "not yet", never a fake.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Backend {
    /// systemd, system scope — root unit in `/etc/systemd/system`, runs as a
    /// dedicated system user. **Implemented** (bridges [`crate::systemd`]).
    SystemdSystem,
    /// systemd, `--user` scope — unit in `~/.config/systemd/user`, runs as the
    /// caller. **Implemented** (bridges [`crate::systemd`], `Scope::User`).
    SystemdUser,
    /// OpenRC — `#!/sbin/openrc-run` script in `/etc/init.d` + `/etc/conf.d`
    /// env. **Implemented**.
    OpenRc,
    /// runit — a `run` (+ optional `finish`/`log/run`) executable service dir
    /// under `/etc/sv/<name>`, symlinked into `/var/service`. *Declared.*
    Runit,
    /// s6-rc — a compiled source definition dir (`type`, `run`, `dependencies`).
    /// *Declared.*
    S6Rc,
    /// dinit — a textual service description in `/etc/dinit.d`. *Declared.*
    Dinit,
    /// SysV init — an LSB `/etc/init.d/<name>` script with an init-info header.
    /// The universal fallback. *Declared.*
    SysV,
    /// OCI / Docker — a container the runtime supervises (restart policy + labels
    /// stand in for unit fields). *Declared.*
    Oci,
    /// Kubernetes-adjacent: a Podman **quadlet** (`.container` rendered by
    /// `podman-systemd` into a transient unit) / `systemd-nspawn`. *Declared.*
    Quadlet,
    /// Windows service (SCM) — for completeness; rendered via the existing MSI
    /// `WindowsServiceSpec` seam. *Declared.*
    WindowsService,
}

impl Backend {
    /// Every backend in the tree (drives a render-coverage matrix: which backends
    /// a given [`ServiceSpec`] can be expressed on today).
    pub fn all() -> &'static [Backend] {
        use Backend::*;
        &[
            SystemdSystem,
            SystemdUser,
            OpenRc,
            Runit,
            S6Rc,
            Dinit,
            SysV,
            Oci,
            Quadlet,
            WindowsService,
        ]
    }

    /// Stable slug (filenames, telemetry, the test matrix' backend axis).
    pub fn slug(self) -> &'static str {
        use Backend::*;
        match self {
            SystemdSystem => "systemd-system",
            SystemdUser => "systemd-user",
            OpenRc => "openrc",
            Runit => "runit",
            S6Rc => "s6-rc",
            Dinit => "dinit",
            SysV => "sysv",
            Oci => "oci",
            Quadlet => "quadlet",
            WindowsService => "windows-service",
        }
    }
}

/// A socket the service listens on — the backend-neutral form of systemd socket
/// activation. Carried in the spec so a renderer that supports it (systemd `.socket`)
/// can wire it, and one that does not (OpenRC) records it as a documented port the
/// service binds itself. `port` is the TCP port; `stream` distinguishes
/// stream(TCP)/dgram(UDP) for the future `.socket` render.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Socket {
    pub port: u16,
    pub stream: bool,
}

impl Socket {
    pub fn tcp(port: u16) -> Self {
        Socket { port, stream: true }
    }
}

/// The ONE declarative service spec at the top of the install tree. Backend-neutral
/// superset of [`crate::systemd::SystemdServiceSpec`] and
/// [`crate::product::ComponentSpec`]; a [`ServiceRenderer`] projects it onto a
/// concrete init system. `serde`, so a product embeds it in its airgap bundle.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceSpec {
    /// Service name without any suffix (e.g. `"holger"`).
    pub name: String,
    /// Human description (unit `Description=`, OpenRC `description=`).
    pub description: String,
    /// Absolute path of the installed binary to exec.
    pub exec: String,
    /// Args appended to `exec` (space-joined for the rendered command line).
    pub args: Vec<String>,
    /// Run-as user. `None` ⇒ run as the caller (the only valid choice for
    /// [`Scope::User`]); a renderer omits the user directive in that case.
    pub user: Option<String>,
    /// Install scope — system service vs per-user service.
    pub scope: Scope,
    /// Working directory (`WorkingDirectory=` / OpenRC `directory=`).
    pub working_dir: Option<String>,
    /// Environment, rendered inline AND/OR to the backend's env file.
    pub env: Vec<(String, String)>,
    /// Services this one must start *after* (ordering edges).
    pub after: Vec<String>,
    /// Services this one *hard-needs* (start fails if they are down).
    pub needs: Vec<String>,
    /// Restart policy (maps to each backend's supervision knob).
    pub restart: RestartPolicy,
    /// Restart backoff seconds.
    pub restart_sec: u32,
    /// Sockets the service listens on (socket activation / declared ports).
    pub sockets: Vec<Socket>,
}

impl ServiceSpec {
    /// A spec with sane defaults: system scope, restart on-failure (2s), no deps.
    pub fn new(
        name: impl Into<String>,
        description: impl Into<String>,
        exec: impl Into<String>,
    ) -> Self {
        ServiceSpec {
            name: name.into(),
            description: description.into(),
            exec: exec.into(),
            args: Vec::new(),
            user: None,
            scope: Scope::System,
            working_dir: None,
            env: Vec::new(),
            after: Vec::new(),
            needs: Vec::new(),
            restart: RestartPolicy::OnFailure,
            restart_sec: 2,
            sockets: Vec::new(),
        }
    }

    pub fn arg(mut self, a: impl Into<String>) -> Self {
        self.args.push(a.into());
        self
    }
    pub fn user(mut self, u: impl Into<String>) -> Self {
        self.user = Some(u.into());
        self
    }
    pub fn scope(mut self, s: Scope) -> Self {
        self.scope = s;
        self
    }
    pub fn working_dir(mut self, d: impl Into<String>) -> Self {
        self.working_dir = Some(d.into());
        self
    }
    pub fn env(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
        self.env.push((k.into(), v.into()));
        self
    }
    pub fn after(mut self, dep: impl Into<String>) -> Self {
        self.after.push(dep.into());
        self
    }
    pub fn needs(mut self, dep: impl Into<String>) -> Self {
        self.needs.push(dep.into());
        self
    }
    pub fn restart(mut self, p: RestartPolicy) -> Self {
        self.restart = p;
        self
    }
    pub fn socket(mut self, s: Socket) -> Self {
        self.sockets.push(s);
        self
    }

    /// The full command line (`exec` + space-joined `args`).
    pub fn command_line(&self) -> String {
        if self.args.is_empty() {
            self.exec.clone()
        } else {
            format!("{} {}", self.exec, self.args.join(" "))
        }
    }
}

/// One file a renderer emits, with its install path (relative to the backend's
/// root) and unix mode. Pure data — the privileged `apply()` writes it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenderedFile {
    /// Install path. Absolute for system scope (e.g.
    /// `/etc/systemd/system/holger.service`); a renderer may also emit a path
    /// relative to `$HOME` for user scope — documented per renderer.
    pub path: String,
    /// File body.
    pub contents: String,
    /// Unix mode (e.g. `0o644` units, `0o755` scripts, `0o600` env files).
    pub mode: u32,
}

/// The full output of rendering a [`ServiceSpec`] for one backend: the file(s) to
/// write **plus the lifecycle verbs as data** (argv vectors). Capturing the verbs
/// as data — not hidden inside an `apply()` — is what lets a test assert "OpenRC
/// enables with `rc-update add holger default`" without running anything, and lets
/// a single privileged driver invoke them uniformly across backends.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RenderedService {
    pub backend: Backend,
    pub files: Vec<RenderedFile>,
    /// argv to enable (start-at-boot) the service.
    pub enable: Vec<String>,
    /// argv to start it now.
    pub start: Vec<String>,
    /// argv to query status.
    pub status: Vec<String>,
    /// argv to stop it.
    pub stop: Vec<String>,
}

impl RenderedService {
    /// The body of the first file whose path ends with `suffix` (test helper).
    pub fn file_ending(&self, suffix: &str) -> Option<&str> {
        self.files
            .iter()
            .find(|f| f.path.ends_with(suffix))
            .map(|f| f.contents.as_str())
    }
}

/// A backend renderer — projects the neutral [`ServiceSpec`] onto one init system.
/// Pure: no root, no IO. The privileged side (write files, run the verbs) is a
/// separate driver, exactly like [`crate::systemd::install_systemd`] sits behind
/// the pure [`crate::systemd::systemd_unit`].
pub trait ServiceRenderer {
    fn backend(&self) -> Backend;
    fn render(&self, spec: &ServiceSpec) -> RenderedService;
}

/// Resolve a [`Backend`] to its renderer. `Some` for the implemented backends,
/// `None` for the declared-but-unrendered ones — honest, never a fake success.
pub fn renderer_for(backend: Backend) -> Option<Box<dyn ServiceRenderer>> {
    match backend {
        Backend::SystemdSystem => Some(Box::new(SystemdRenderer { scope: Scope::System })),
        Backend::SystemdUser => Some(Box::new(SystemdRenderer { scope: Scope::User })),
        Backend::OpenRc => Some(Box::new(OpenRcRenderer)),
        _ => None,
    }
}

// ───────────────────────────── systemd renderer ─────────────────────────────

/// systemd renderer — bridges the neutral [`ServiceSpec`] to the existing,
/// tested [`crate::systemd::systemd_unit_scoped`]. The tree REUSES the install
/// limb that already ships rather than forking a second systemd code path.
pub struct SystemdRenderer {
    pub scope: Scope,
}

impl SystemdRenderer {
    /// Project the neutral spec onto the systemd-specific spec the existing
    /// renderer consumes.
    fn to_systemd_spec(&self, spec: &ServiceSpec) -> crate::systemd::SystemdServiceSpec {
        // user scope runs as the caller → no User=; system scope needs a user
        // (fall back to the service name if none was set).
        let user = spec
            .user
            .clone()
            .unwrap_or_else(|| spec.name.clone());
        let mut s = crate::systemd::SystemdServiceSpec::new(
            &spec.name,
            &spec.description,
            &spec.exec,
            user,
            spec.working_dir.clone().unwrap_or_else(|| "/".to_string()),
        )
        .with_restart(spec.restart);
        s.restart_sec = spec.restart_sec;
        for a in &spec.args {
            s = s.with_arg(a);
        }
        for (k, v) in &spec.env {
            s = s.with_env(k, v);
        }
        // ordering + hard requires both become After=; needs additionally Wants=.
        for dep in &spec.after {
            s = s.after(unit_name(dep));
        }
        for dep in &spec.needs {
            s = s.after(unit_name(dep));
            s = s.wants(unit_name(dep));
        }
        s
    }
}

impl ServiceRenderer for SystemdRenderer {
    fn backend(&self) -> Backend {
        match self.scope {
            Scope::User => Backend::SystemdUser,
            Scope::System => Backend::SystemdSystem,
        }
    }

    fn render(&self, spec: &ServiceSpec) -> RenderedService {
        let sys = self.to_systemd_spec(spec);
        let unit = crate::systemd::systemd_unit_scoped(&sys, self.scope);
        let unit_path = match self.scope {
            Scope::System => format!("/etc/systemd/system/{}.service", spec.name),
            // user scope: relative to $HOME (the driver joins it).
            Scope::User => format!(".config/systemd/user/{}.service", spec.name),
        };
        let user_flag: &[&str] = if matches!(self.scope, Scope::User) {
            &["--user"]
        } else {
            &[]
        };
        let argv = |verb: &str| -> Vec<String> {
            let mut v: Vec<String> = vec!["systemctl".into()];
            v.extend(user_flag.iter().map(|s| s.to_string()));
            v.push(verb.into());
            v.push(format!("{}.service", spec.name));
            v
        };
        RenderedService {
            backend: self.backend(),
            files: vec![RenderedFile {
                path: unit_path,
                contents: unit,
                mode: 0o644,
            }],
            enable: argv("enable"),
            start: argv("start"),
            status: argv("is-active"),
            stop: argv("stop"),
        }
    }
}

// ───────────────────────────── OpenRC renderer ──────────────────────────────

/// OpenRC renderer — emits an `#!/sbin/openrc-run` service script for
/// `/etc/init.d/<name>` plus, when the spec carries env, an `/etc/conf.d/<name>`
/// file (OpenRC auto-sources it). Uses `supervise-daemon` for real restart
/// supervision (the modern OpenRC equivalent of `Restart=`), `command_user` for
/// the run-as user, and a `depend()` block for ordering/needs.
pub struct OpenRcRenderer;

impl ServiceRenderer for OpenRcRenderer {
    fn backend(&self) -> Backend {
        Backend::OpenRc
    }

    fn render(&self, spec: &ServiceSpec) -> RenderedService {
        let mut s = String::new();
        s.push_str("#!/sbin/openrc-run\n");
        s.push_str(&format!("description=\"{}\"\n", spec.description));
        s.push_str(&format!("command=\"{}\"\n", spec.exec));
        if !spec.args.is_empty() {
            s.push_str(&format!("command_args=\"{}\"\n", spec.args.join(" ")));
        }
        // user scope runs as the caller → no command_user.
        if !matches!(spec.scope, Scope::User) {
            if let Some(u) = &spec.user {
                s.push_str(&format!("command_user=\"{u}\"\n"));
            }
        }
        if let Some(d) = &spec.working_dir {
            s.push_str(&format!("directory=\"{d}\"\n"));
        }
        s.push_str("command_background=true\n");
        s.push_str(&format!("pidfile=\"/run/{}.pid\"\n", spec.name));
        // Restart supervision: supervise-daemon respawns; backoff = restart_sec.
        // RestartPolicy::No ⇒ no supervisor (a plain foreground command).
        if !matches!(spec.restart, RestartPolicy::No) {
            s.push_str("supervisor=\"supervise-daemon\"\n");
            s.push_str(&format!("respawn_delay={}\n", spec.restart_sec));
        }
        // depend(): `need net` is the OpenRC twin of network-online.target (always
        // on, matching the systemd renderer); then `need`/`after` from the spec.
        s.push_str("\ndepend() {\n");
        s.push_str("\tneed net\n");
        for dep in &spec.needs {
            s.push_str(&format!("\tneed {dep}\n"));
        }
        for dep in &spec.after {
            s.push_str(&format!("\tafter {dep}\n"));
        }
        s.push_str("}\n");

        let mut files = vec![RenderedFile {
            path: format!("/etc/init.d/{}", spec.name),
            contents: s,
            mode: 0o755,
        }];
        // Env → /etc/conf.d/<name> (auto-sourced by OpenRC). mode 0600 (it may hold
        // secrets), matching the systemd env-file posture.
        if !spec.env.is_empty() {
            let mut conf = String::new();
            for (k, v) in &spec.env {
                conf.push_str(&format!("export {k}=\"{v}\"\n"));
            }
            files.push(RenderedFile {
                path: format!("/etc/conf.d/{}", spec.name),
                contents: conf,
                mode: 0o600,
            });
        }

        let n = &spec.name;
        RenderedService {
            backend: Backend::OpenRc,
            files,
            // enable at boot = add to the `default` runlevel.
            enable: vec!["rc-update".into(), "add".into(), n.clone(), "default".into()],
            start: vec!["rc-service".into(), n.clone(), "start".into()],
            status: vec!["rc-service".into(), n.clone(), "status".into()],
            stop: vec!["rc-service".into(), n.clone(), "stop".into()],
        }
    }
}

/// A dep *service name* → the systemd *unit name* (append `.service` unless it
/// already names a unit type, e.g. a `.target` or an explicit `.service`).
fn unit_name(dep: &str) -> String {
    if dep.contains('.') {
        dep.to_string()
    } else {
        format!("{dep}.service")
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A holger-shaped spec used by the cross-backend tests.
    fn holger() -> ServiceSpec {
        ServiceSpec::new("holger", "Holger artifact repository", "/opt/holger/bin/holger")
            .arg("serve")
            .arg("--config")
            .arg("/etc/holger/holger.toml")
            .user("_holger")
            .working_dir("/var/lib/holger")
            .env("HOLGER_DATA", "/var/lib/holger")
            .after("nornir-server")
            .restart(RestartPolicy::OnFailure)
            .socket(Socket::tcp(7070))
    }

    /// THE spike test: ONE declarative `ServiceSpec` renders to BOTH backends, and
    /// each output carries the injected values in that backend's native shape —
    /// proving the tree projects one spec onto genuinely different init systems
    /// (not just two flavours of systemd).
    #[test]
    fn one_spec_renders_to_systemd_and_openrc() {
        let spec = holger();

        // ── systemd-system (bridges the existing tested renderer) ──
        let sd = renderer_for(Backend::SystemdSystem).unwrap().render(&spec);
        assert_eq!(sd.backend, Backend::SystemdSystem);
        let unit = sd.file_ending("holger.service").expect("a .service file");
        assert!(unit.contains("[Unit]"));
        assert!(unit.contains("Description=Holger artifact repository"));
        assert!(unit.contains("User=_holger"));
        assert!(unit.contains(
            "ExecStart=/opt/holger/bin/holger serve --config /etc/holger/holger.toml"
        ));
        assert!(unit.contains("After=network-online.target nornir-server.service"));
        assert!(unit.contains("Environment=HOLGER_DATA=/var/lib/holger"));
        assert!(unit.contains("Restart=on-failure"));
        assert_eq!(
            sd.files[0].path,
            "/etc/systemd/system/holger.service"
        );
        assert_eq!(sd.enable, vec!["systemctl", "enable", "holger.service"]);
        assert_eq!(sd.start, vec!["systemctl", "start", "holger.service"]);

        // ── OpenRC (a genuinely different format) ──
        let rc = renderer_for(Backend::OpenRc).unwrap().render(&spec);
        assert_eq!(rc.backend, Backend::OpenRc);
        let script = rc.file_ending("/etc/init.d/holger").expect("an init.d script");
        assert!(script.starts_with("#!/sbin/openrc-run"));
        assert!(script.contains("command=\"/opt/holger/bin/holger\""));
        assert!(script.contains("command_args=\"serve --config /etc/holger/holger.toml\""));
        assert!(script.contains("command_user=\"_holger\""));
        assert!(script.contains("directory=\"/var/lib/holger\""));
        assert!(script.contains("supervisor=\"supervise-daemon\""));
        // depend(): always-on `need net`, plus the declared `after` edge.
        assert!(script.contains("need net"));
        assert!(script.contains("after nornir-server"));
        // env → conf.d, mode 0600.
        let conf = rc.file_ending("/etc/conf.d/holger").expect("a conf.d env file");
        assert_eq!(conf, "export HOLGER_DATA=\"/var/lib/holger\"\n");
        assert_eq!(
            rc.files.iter().find(|f| f.path.ends_with("/etc/conf.d/holger")).unwrap().mode,
            0o600
        );
        assert_eq!(rc.enable, vec!["rc-update", "add", "holger", "default"]);
        assert_eq!(rc.start, vec!["rc-service", "holger", "start"]);

        // The SAME spec produced a systemd unit AND an OpenRC script — the tree
        // works. The command line is identical across backends (one source of truth).
        assert!(unit.contains(&spec.command_line()));
        assert!(script.contains(&spec.exec));
    }

    /// user scope drops the run-as user in BOTH backends (a per-user service runs
    /// as the caller) and systemd targets the `--user` manager.
    #[test]
    fn user_scope_drops_run_as_user_in_both_backends() {
        let spec = ServiceSpec::new("holger", "d", "/home/me/.cargo/bin/holger")
            .arg("start")
            .user("ignored-in-user-scope")
            .scope(Scope::User);

        let sd = renderer_for(Backend::SystemdUser).unwrap().render(&spec);
        let unit = &sd.files[0].contents;
        assert!(!unit.contains("User="), "no User= in a --user unit");
        assert!(unit.contains("WantedBy=default.target"));
        assert_eq!(sd.files[0].path, ".config/systemd/user/holger.service");
        assert_eq!(sd.enable, vec!["systemctl", "--user", "enable", "holger.service"]);

        let rc = OpenRcRenderer.render(&spec);
        let script = &rc.files[0].contents;
        assert!(!script.contains("command_user="), "no run-as user in user scope");
    }

    /// `needs` becomes a hard require in both backends: systemd `Wants=`+`After=`,
    /// OpenRC `need`.
    #[test]
    fn needs_is_a_hard_require_in_both_backends() {
        let spec = ServiceSpec::new("svc", "d", "/bin/svc").needs("postgresql");

        let sd = renderer_for(Backend::SystemdSystem).unwrap().render(&spec);
        let unit = &sd.files[0].contents;
        assert!(unit.contains("After=network-online.target postgresql.service"));
        assert!(unit.contains("Wants=network-online.target postgresql.service"));

        let rc = OpenRcRenderer.render(&spec);
        assert!(rc.files[0].contents.contains("need postgresql"));
    }

    /// `RestartPolicy::No` ⇒ OpenRC emits no supervisor (a plain command), the
    /// honest mapping of "don't restart".
    #[test]
    fn restart_no_omits_openrc_supervisor() {
        let spec = ServiceSpec::new("svc", "d", "/bin/svc").restart(RestartPolicy::No);
        let rc = OpenRcRenderer.render(&spec);
        assert!(!rc.files[0].contents.contains("supervisor="));
    }

    /// The backend registry is honest: implemented backends resolve to a renderer,
    /// declared-but-unrendered ones resolve to `None` (never a fake), and the slug
    /// axis covers the whole tree for the coverage matrix.
    #[test]
    fn registry_resolves_only_implemented_backends() {
        assert!(renderer_for(Backend::SystemdSystem).is_some());
        assert!(renderer_for(Backend::SystemdUser).is_some());
        assert!(renderer_for(Backend::OpenRc).is_some());
        // Declared but not yet rendered → honest None.
        assert!(renderer_for(Backend::Runit).is_none());
        assert!(renderer_for(Backend::Dinit).is_none());
        assert!(renderer_for(Backend::S6Rc).is_none());
        // Every backend has a stable, unique slug.
        let slugs: Vec<_> = Backend::all().iter().map(|b| b.slug()).collect();
        let uniq: std::collections::BTreeSet<_> = slugs.iter().collect();
        assert_eq!(slugs.len(), uniq.len(), "slugs are unique");
        assert_eq!(Backend::all().len(), 10);
    }

    /// The neutral spec round-trips through JSON (airgap-bundle embeddable, like
    /// `SystemdServiceSpec` and `ComponentSpec`).
    #[test]
    fn service_spec_json_roundtrips() {
        let spec = holger();
        let back: ServiceSpec =
            serde_json::from_str(&serde_json::to_string(&spec).unwrap()).unwrap();
        assert_eq!(back, spec);
    }
}
