//! DPMS control of the box's live KDE desktop (`org_kde_kwin_dpms`) — how a bare-spawn gamescope
//! session honors [`Topology::Exclusive`](crate::policy::Topology::Exclusive).
//!
//! A bare spawn is its OWN headless compositor: nothing on that route touches the desktop the box
//! is showing, so on a KDE machine the physical panel keeps displaying the (idle) desktop for the
//! whole stream — while the same `exclusive` policy on the KWin route turns the physicals off
//! outright. The KWin route's mechanism is closed to us here: KWin refuses an output configuration
//! with ZERO enabled outputs, and a gamescope session has no KWin output of its own to leave
//! enabled. DPMS is the honest translation of `exclusive` for this route — the desktop stays
//! exactly where it is (no topology churn, no window re-homing), the panels go dark, and any
//! LOCAL input wakes them, which is the right answer for a desktop someone can walk up to.
//! Stream input never wakes them: it is injected into the nested gamescope's own EIS socket and
//! does not pass through KWin.
//!
//! Driven in-process over the compositor's own Wayland (`Connection::connect_to_env`, the same
//! stack as [`crate::kwin_output_mgmt`] and for the same reason: `kscreen-doctor` rides a separate
//! libkscreen/KDED layer that can be wedged while KWin itself answers fine), with a
//! `kscreen-doctor --dpms` shell-out fallback. Best-effort everywhere — a box with no Wayland
//! session, or a non-KDE desktop, declines quietly and the stream proceeds with the panel lit,
//! exactly as before this module existed.
//!
//! **The hold is refcounted here, NOT floated through the registry's per-group restore.** Every
//! gamescope spawn is its own display group (`registry::group_key` — deliberately, they are
//! independent nested sessions), so the §6.1 group machinery alone would run the FIRST session's
//! restore at that session's teardown and re-light the panel under a second, still-streaming
//! session. Instead each exclusive spawn takes one [`acquire_stream_darken`] hold (the 0→1 edge
//! darkens) and registers [`release_stream_darken`] as its per-display topology restore (the 1→0
//! edge re-lights) — the same shape as `sleep_inhibit`'s refcount, riding the registry only for
//! the *timing* of each release.
//!
//! Crash safety comes free: DPMS is non-persistent, so a host that dies holding the panel dark
//! leaves nothing to journal — the screen re-lights on the next local input or compositor
//! restart. (Contrast the Windows `pnp_disable_monitors` path, which needs a recovery journal
//! precisely because its disable survives everything.)

use std::collections::HashMap;
use std::os::fd::{AsFd, AsRawFd};
use std::sync::Mutex;
use std::time::{Duration, Instant};
use wayland_client::protocol::wl_callback::{self, WlCallback};
use wayland_client::protocol::wl_output::{self, WlOutput};
use wayland_client::protocol::wl_registry::{self, WlRegistry};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle};

// Client bindings for the vendored KDE dpms protocol (`protocols/dpms.xml`), generated inline like
// the two in `kwin_output_mgmt`. Self-contained: its only foreign object type is the core
// `wl_output`, which `wayland_client::protocol` already provides.
#[allow(clippy::all, dead_code, non_camel_case_types, non_snake_case, unused)]
pub mod protocol {
    use wayland_client;
    use wayland_client::protocol::*;

    pub mod __interfaces {
        use wayland_client::protocol::__interfaces::*;
        wayland_scanner::generate_interfaces!("protocols/dpms.xml");
    }
    use self::__interfaces::*;

    wayland_scanner::generate_client_code!("protocols/dpms.xml");
}

use protocol::org_kde_kwin_dpms::{Event as DpmsEvent, OrgKdeKwinDpms as Dpms};
use protocol::org_kde_kwin_dpms_manager::OrgKdeKwinDpmsManager as DpmsManager;

// The wire enum `org_kde_kwin_dpms.mode`. The XML types the `mode` request/event args as plain
// `uint` (no `enum=` attribute), so the generated signatures take/deliver `u32` — these constants
// are the protocol's values, kept in sync with the vendored `dpms.xml`.
const DPMS_MODE_ON: u32 = 0;
const DPMS_MODE_OFF: u32 = 3;

/// `org_kde_kwin_dpms_manager` is a frozen v1 protocol (its own header warns it may change
/// without a version bump, but no v2 has appeared since 2015); bind `min(advertised, 1)`.
const MANAGER_MAX: u32 = 1;
/// `wl_output.name` — the connector name used for logging — arrived in v4. Everything else we do
/// works at v1, so a lower advert just costs the log its names.
const WL_OUTPUT_MAX: u32 = 4;

/// Overall budget for one darken/re-light operation (mirrors `kwin_output_mgmt::OP_BUDGET`):
/// generous next to a healthy roundtrip, and only there so a wedged compositor can't pin the
/// session-create (or group-teardown) thread.
const OP_BUDGET: Duration = Duration::from_secs(3);

/// Poll slice while waiting on the Wayland fd (matches `kwin_output_mgmt`).
const POLL_MS: i32 = 100;

/// One output's accumulated state on this connection, keyed by its `wl_output` global name.
#[derive(Default)]
struct OutputState {
    proxy: Option<WlOutput>,
    /// Connector name (`DP-1`) from `wl_output.name` (v4) — logging only; the global number is
    /// the address everything operates on.
    connector: Option<String>,
    dpms: Option<Dpms>,
    /// `org_kde_kwin_dpms.supported` — `None` until the bind burst arrives.
    supported: Option<bool>,
    /// The last `org_kde_kwin_dpms.mode` seen — kept current, so the post-`set` wait can watch it
    /// flip.
    mode: Option<u32>,
}

/// Everything one connection's queue accumulates.
#[derive(Default)]
struct State {
    manager: Option<DpmsManager>,
    /// Keyed by the `wl_output` GLOBAL NAME — a stable address for the compositor's lifetime, and
    /// the identity the darken records so the re-light (a separate, later connection) can find the
    /// same outputs again.
    outputs: HashMap<u32, OutputState>,
    /// Highest `wl_callback` serial whose `done` has arrived — the barrier the pump waits on.
    sync_done: u32,
}

impl Dispatch<WlRegistry, ()> for State {
    fn event(
        state: &mut Self,
        registry: &WlRegistry,
        event: wl_registry::Event,
        _: &(),
        _: &Connection,
        qh: &QueueHandle<Self>,
    ) {
        match event {
            wl_registry::Event::Global {
                name,
                interface,
                version,
            } => {
                if interface == DpmsManager::interface().name {
                    let v = version.min(MANAGER_MAX);
                    state.manager = Some(registry.bind::<DpmsManager, _, _>(name, v, qh, ()));
                } else if interface == WlOutput::interface().name {
                    let v = version.min(WL_OUTPUT_MAX);
                    // The global name rides in the UserData so the output's own events (and the
                    // dpms object's, which gets the same stamp) can find this entry.
                    let out = registry.bind::<WlOutput, _, _>(name, v, qh, name);
                    state.outputs.entry(name).or_default().proxy = Some(out);
                }
            }
            // An output unplugged mid-operation: drop the entry so we never `set` on its corpse.
            wl_registry::Event::GlobalRemove { name } => {
                state.outputs.remove(&name);
            }
            _ => {}
        }
    }
}

impl Dispatch<WlOutput, u32> for State {
    fn event(
        state: &mut Self,
        _: &WlOutput,
        event: wl_output::Event,
        global: &u32,
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
        if let wl_output::Event::Name { name } = event {
            if let Some(o) = state.outputs.get_mut(global) {
                o.connector = Some(name);
            }
        }
    }
}

impl Dispatch<Dpms, u32> for State {
    fn event(
        state: &mut Self,
        _: &Dpms,
        event: DpmsEvent,
        global: &u32,
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
        let Some(o) = state.outputs.get_mut(global) else {
            return;
        };
        match event {
            DpmsEvent::Supported { supported } => o.supported = Some(supported != 0),
            DpmsEvent::Mode { mode } => o.mode = Some(mode),
            DpmsEvent::Done => {}
        }
    }
}

// The manager has no events; the impl exists because `WlRegistry::bind` demands one.
impl Dispatch<DpmsManager, ()> for State {
    fn event(
        _: &mut Self,
        _: &DpmsManager,
        _: protocol::org_kde_kwin_dpms_manager::Event,
        _: &(),
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
    }
}

impl Dispatch<WlCallback, u32> for State {
    fn event(
        state: &mut Self,
        _: &WlCallback,
        event: wl_callback::Event,
        serial: &u32,
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
        if let wl_callback::Event::Done { .. } = event {
            state.sync_done = state.sync_done.max(*serial);
        }
    }
}

/// Why [`Session::open`] declined — the same honest-decline discipline as
/// `kwin_output_mgmt::OpenFailure`: which rung said no decides both the log level and whether the
/// `kscreen-doctor` fallback is worth attempting.
enum OpenFailure {
    /// No Wayland connection at all (`WAYLAND_DISPLAY` unset/stale). The common case for the bare
    /// spawn's natural habitat — a headless plain-distro box with no desktop to darken.
    Connect(String),
    /// The compositor accepted the connection but did not answer the registry barrier in budget:
    /// a live but wedged session — the case the shell-out fallback exists for.
    RegistryBarrier,
    /// Connected and answering, but `org_kde_kwin_dpms_manager` is not advertised — not KWin. A
    /// definitive answer: no fallback can succeed here either (`kscreen-doctor` drives the same
    /// KDE-only machinery), so this rung declines without one.
    NoDpmsGlobal,
    /// The manager is there but the per-output DPMS state bursts never completed in budget.
    StateBarrier,
}

impl std::fmt::Display for OpenFailure {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            OpenFailure::Connect(e) => write!(f, "no Wayland connection ({e})"),
            OpenFailure::RegistryBarrier => {
                write!(
                    f,
                    "the compositor did not answer the registry roundtrip in budget"
                )
            }
            OpenFailure::NoDpmsGlobal => {
                write!(f, "org_kde_kwin_dpms_manager is not advertised (not KWin)")
            }
            OpenFailure::StateBarrier => {
                write!(
                    f,
                    "the outputs' DPMS state never finished announcing in budget"
                )
            }
        }
    }
}

/// A connected session with the manager bound and every output's DPMS state read.
struct Session {
    conn: Connection,
    queue: wayland_client::EventQueue<State>,
    state: State,
    next_sync: u32,
}

impl Session {
    /// [`Session::connect`] for the operation named by `op`, logging the decline at a level that
    /// matches what it means: `Connect`/`NoDpmsGlobal` are the everyday non-KDE answers (most
    /// bare-spawn boxes have no desktop at all) and log at debug; the two barrier failures mean a
    /// LIVE session stopped answering — on a KDE box that is a panel left lit, so they warn.
    fn open(op: &'static str) -> Result<Session, OpenFailure> {
        let opened = Session::connect();
        if let Err(reason) = &opened {
            match reason {
                OpenFailure::Connect(_) | OpenFailure::NoDpmsGlobal => {
                    tracing::debug!(op, %reason, "KWin DPMS unavailable");
                }
                OpenFailure::RegistryBarrier | OpenFailure::StateBarrier => {
                    tracing::warn!(
                        op,
                        %reason,
                        "KWin DPMS: in-process path unavailable — falling back to kscreen-doctor"
                    );
                }
            }
        }
        opened
    }

    /// Connect to the desktop's Wayland socket, bind the dpms manager + every `wl_output`, create
    /// a dpms status object per output and drain their state bursts — all bounded by [`OP_BUDGET`].
    fn connect() -> Result<Session, OpenFailure> {
        let conn = Connection::connect_to_env().map_err(|e| OpenFailure::Connect(e.to_string()))?;
        let queue = conn.new_event_queue();
        let qh = queue.handle();
        let _registry = conn.display().get_registry(&qh, ());
        let mut s = Session {
            conn,
            queue,
            state: State::default(),
            next_sync: 0,
        };
        let deadline = Instant::now() + OP_BUDGET;
        // Phase 1: process the registry globals (binds the manager + every wl_output).
        if !s.sync_barrier(deadline) {
            return Err(OpenFailure::RegistryBarrier);
        }
        let Some(mgr) = s.state.manager.clone() else {
            return Err(OpenFailure::NoDpmsGlobal);
        };
        // Phase 2: one dpms status object per output (stamped with the output's global name so its
        // events land on the right entry), then a barrier that drains both the outputs' `name`
        // events and the dpms objects' supported/mode/done bursts.
        let qh = s.queue.handle();
        let bound: Vec<(u32, WlOutput)> = s
            .state
            .outputs
            .iter()
            .filter_map(|(g, o)| o.proxy.clone().map(|p| (*g, p)))
            .collect();
        for (global, out) in bound {
            let d = mgr.get(&out, &qh, global);
            if let Some(o) = s.state.outputs.get_mut(&global) {
                o.dpms = Some(d);
            }
        }
        if !s.sync_barrier(deadline) {
            return Err(OpenFailure::StateBarrier);
        }
        Ok(s)
    }

    /// Send a `wl_display.sync` and pump the queue until its `done` arrives or `deadline` passes.
    fn sync_barrier(&mut self, deadline: Instant) -> bool {
        self.next_sync += 1;
        let serial = self.next_sync;
        let qh = self.queue.handle();
        let _cb = self.conn.display().sync(&qh, serial);
        self.pump_until(deadline, |st| st.sync_done >= serial)
    }

    /// Bounded manual event loop — flush, dispatch, poll the fd. Mirrors
    /// `kwin_output_mgmt::Session::pump_until` (same rationale: `blocking_dispatch` can't be
    /// interrupted, so the fd is polled in [`POLL_MS`] slices against `deadline`).
    fn pump_until(&mut self, deadline: Instant, done: impl Fn(&State) -> bool) -> bool {
        loop {
            if done(&self.state) {
                return true;
            }
            if self.queue.dispatch_pending(&mut self.state).is_err() {
                return false;
            }
            if done(&self.state) {
                return true;
            }
            if Instant::now() >= deadline {
                return false;
            }
            if self.conn.flush().is_err() {
                return false;
            }
            let Some(guard) = self.conn.prepare_read() else {
                continue; // events already queued — loop dispatches them
            };
            let mut pfd = libc::pollfd {
                fd: self.conn.as_fd().as_raw_fd(),
                events: libc::POLLIN,
                revents: 0,
            };
            let remaining = deadline.saturating_duration_since(Instant::now());
            let timeout = (remaining.as_millis() as i32).clamp(0, POLL_MS);
            // SAFETY: `&mut pfd` points at one live, fully-initialized `libc::pollfd` on the stack
            // and the count `1` matches that single element, so `poll` reads `fd`/`events` and
            // writes `revents` strictly within `pfd`. `pfd.fd` is the Wayland connection's fd,
            // valid because `self.conn` (and the `prepare_read` guard) outlive the call. `poll`
            // blocks up to `timeout` ms and writes only `revents`; `pfd` is a fresh local that
            // aliases nothing.
            let r = unsafe { libc::poll(&mut pfd, 1, timeout) };
            if r > 0 && (pfd.revents & libc::POLLIN) != 0 {
                let _ = guard.read();
            } // else: timeout/signal — drop the guard, re-check the deadline
        }
    }

    /// Request `target` on every DPMS-supporting output not already there — restricted to the
    /// globals in `only` when given (the re-light path, which must touch ONLY what the darken
    /// touched: a panel the USER had put to sleep before the stream is theirs to keep dark).
    /// Returns the outputs actually asked to change, `(global, connector)`, then waits (within
    /// budget) for each one's `mode` event to confirm — the protocol is explicit that `set` is a
    /// request the compositor may decline, so the confirmation is watched and its absence logged
    /// rather than assumed.
    fn set_mode(&mut self, target: u32, only: Option<&[u32]>) -> Vec<(u32, Option<String>)> {
        let deadline = Instant::now() + OP_BUDGET;
        let mut touched: Vec<(u32, Option<String>)> = Vec::new();
        for (global, o) in &self.state.outputs {
            if only.is_some_and(|list| !list.contains(global)) {
                continue;
            }
            if o.supported != Some(true) || o.mode == Some(target) {
                continue;
            }
            if let Some(dpms) = &o.dpms {
                dpms.set(target);
                touched.push((*global, o.connector.clone()));
            }
        }
        if touched.is_empty() {
            return touched;
        }
        let want: Vec<u32> = touched.iter().map(|(g, _)| *g).collect();
        // An output that vanished mid-wait (GlobalRemove pruned it) counts as settled — there is
        // nothing left to flip.
        let confirmed = self.pump_until(deadline, |st| {
            want.iter()
                .all(|g| st.outputs.get(g).is_none_or(|o| o.mode == Some(target)))
        });
        if !confirmed {
            tracing::warn!(
                outputs = ?touched,
                target,
                "KWin DPMS: the compositor did not confirm the mode change in budget (the \
                 requests are flushed; it may still land, or KWin may have declined)"
            );
        }
        touched
    }
}

/// What the 0→1 darken actually achieved — the record the 1→0 re-light undoes. Which arm did the
/// work matters: the two are undone through different doors.
enum Darkened {
    /// The in-process path turned these outputs off — `(wl_output global, connector)`. Global
    /// names are stable for the compositor's lifetime, so a later connection re-lights exactly
    /// these. If KWin restarted in between the names match nothing — and that is the CORRECT
    /// no-op, because a fresh KWin brings its outputs up lit anyway.
    Wayland(Vec<(u32, Option<String>)>),
    /// The `kscreen-doctor --dpms off` fallback ran (it takes no per-output address, so the
    /// re-light is the symmetric `--dpms on`).
    Kscreen,
}

/// The host-wide darken hold — refcounted like `sleep_inhibit`: the 0→1 edge darkens, the 1→0
/// edge re-lights, and everything between is bookkeeping. See the module docs for why the
/// registry's per-group restore float can't provide this (every gamescope spawn is its own group).
struct Holds {
    count: u32,
    /// What the 0→1 darken achieved, held until the 1→0 release undoes it. `None` while count > 0
    /// means the darken found nothing to do (no KDE, panels already dark) — the release then has
    /// nothing to undo, which is exactly right.
    darkened: Option<Darkened>,
}

impl Holds {
    /// Take a hold; `true` on the 0→1 edge — the caller darkens and [`record`](Self::record)s.
    fn acquire_edge(&mut self) -> bool {
        self.count += 1;
        self.count == 1
    }

    /// Store the 0→1 darken's outcome.
    fn record(&mut self, d: Option<Darkened>) {
        self.darkened = d;
    }

    /// Drop a hold; `Some` on the 1→0 edge hands the caller the record to undo. A release with no
    /// hold outstanding is a caller bug (an unbalanced restore) — logged, never underflowed.
    fn release_edge(&mut self) -> Option<Darkened> {
        if self.count == 0 {
            tracing::warn!("KWin DPMS: release without a matching acquire (unbalanced restore)");
            return None;
        }
        self.count -= 1;
        if self.count == 0 {
            self.darkened.take()
        } else {
            None
        }
    }
}

static HOLDS: Mutex<Holds> = Mutex::new(Holds {
    count: 0,
    darkened: None,
});

/// Take one darken hold for an exclusive-topology stream. The first hold turns the live KDE
/// desktop's panels off (best-effort, bounded); later holds just count. Callers MUST balance each
/// call with [`release_stream_darken`] — the gamescope backend does it by registering the release
/// as the display's topology restore, so the registry runs it exactly once per display at
/// teardown (§6.1).
///
/// The lock is deliberately held across the darken itself: a racing second acquire must queue
/// behind it (and then see the recorded outcome), not observe a count of 2 with nothing darkened.
/// Same discipline on the release side, which keeps a teardown-overlapping-connect sequence
/// strictly ordered: re-light completes, then the new stream's darken runs.
pub fn acquire_stream_darken() {
    let mut h = HOLDS.lock().unwrap_or_else(|e| e.into_inner());
    if h.acquire_edge() {
        let d = darken();
        h.record(d);
    }
}

/// Drop one darken hold; the last one out re-lights whatever the first hold's darken achieved.
pub fn release_stream_darken() {
    let mut h = HOLDS.lock().unwrap_or_else(|e| e.into_inner());
    if let Some(d) = h.release_edge() {
        relight(d);
    }
}

/// The 0→1 darken: in-process over `org_kde_kwin_dpms` first, `kscreen-doctor --dpms off` as the
/// wedged-compositor fallback. `None` = nothing was darkened (no desktop, not KDE, panels already
/// off, or every arm declined) — and therefore nothing to restore.
fn darken() -> Option<Darkened> {
    match Session::open("darken") {
        Ok(mut s) => {
            let touched = s.set_mode(DPMS_MODE_OFF, None);
            if touched.is_empty() {
                tracing::debug!(
                    "KWin DPMS: no output to darken (none supported, or all already off)"
                );
                None
            } else {
                tracing::info!(
                    outputs = ?touched,
                    "KWin DPMS: desktop outputs off for the exclusive gamescope stream"
                );
                Some(Darkened::Wayland(touched))
            }
        }
        // Definitive "not KDE" / "no desktop": no fallback can do better (kscreen-doctor drives
        // the same KDE-only machinery), so decline quietly — already logged by `open`.
        Err(OpenFailure::NoDpmsGlobal) | Err(OpenFailure::Connect(_)) => None,
        // A live session that stopped answering: the standalone tool rides a different stack
        // (libkscreen/KDED) and may still get through — the same rationale as `kwin.rs`'s
        // kscreen fallbacks, honest-verdict discipline included.
        Err(_) => match kscreen_dpms("off") {
            Some(true) => {
                tracing::info!(
                    "KWin DPMS: desktop outputs off for the exclusive gamescope stream \
                     (kscreen-doctor fallback)"
                );
                Some(Darkened::Kscreen)
            }
            // Killed at its budget — NOT a refusal: kscreen-doctor applies first and then waits
            // on the compositor, so a loaded KWin routinely lands the change and still gets
            // killed. Record the darken so the teardown re-light runs either way; a `--dpms on`
            // against a lit panel is a no-op.
            None => Some(Darkened::Kscreen),
            Some(false) => {
                tracing::warn!(
                    "KWin DPMS: could not darken the desktop outputs for the exclusive topology \
                     (in-process path and kscreen-doctor both declined) — the panel stays lit"
                );
                None
            }
        },
    }
}

/// The 1→0 re-light. **This is the last line of defence for a dark monitor**, so every arm that
/// gives up says so loudly (the same discipline as `kwin.rs::reenable_outputs_kscreen`) — a dark
/// panel with no line in the log is the failure mode this chain exists to prevent. The worst case
/// stays self-healing regardless: DPMS is non-persistent, and any local input wakes the panel.
fn relight(d: Darkened) {
    match d {
        Darkened::Wayland(outputs) => {
            let globals: Vec<u32> = outputs.iter().map(|(g, _)| *g).collect();
            match Session::open("re-light") {
                Ok(mut s) => {
                    s.set_mode(DPMS_MODE_ON, Some(&globals));
                    tracing::info!(outputs = ?outputs, "KWin DPMS: desktop outputs back on");
                }
                Err(_) => match kscreen_dpms("on") {
                    Some(true) | None => {
                        tracing::info!(
                            "KWin DPMS: desktop outputs back on (kscreen-doctor fallback)"
                        );
                    }
                    Some(false) => {
                        tracing::error!(
                            outputs = ?outputs,
                            "KWin DPMS: could NOT re-light the desktop outputs (in-process \
                             restore and kscreen-doctor both declined) — the panel stays dark \
                             until local input wakes it"
                        );
                    }
                },
            }
        }
        Darkened::Kscreen => {
            if kscreen_dpms("on") == Some(false) {
                tracing::error!(
                    "KWin DPMS: could NOT re-light the desktop outputs (kscreen-doctor refused \
                     the --dpms on it earlier accepted the off for) — the panel stays dark until \
                     local input wakes it"
                );
            }
        }
    }
}

/// `kscreen-doctor --dpms <on|off>` for its verdict, on `kwin.rs`'s shared budget and three-state
/// convention (`Some(true)` ran and succeeded, `Some(false)` refused or unrunnable, `None` killed
/// at the budget — which, for a tool that applies first and waits after, usually means it landed).
fn kscreen_dpms(mode: &'static str) -> Option<bool> {
    crate::kwin::kscreen_verdict(&["--dpms".to_string(), mode.to_string()])
}

#[cfg(test)]
mod tests {
    use super::{Darkened, Holds};

    fn fresh() -> Holds {
        Holds {
            count: 0,
            darkened: None,
        }
    }

    #[test]
    fn first_acquire_darkens_later_ones_count() {
        let mut h = fresh();
        assert!(h.acquire_edge(), "0→1 must darken");
        h.record(Some(Darkened::Kscreen));
        assert!(
            !h.acquire_edge(),
            "a second concurrent stream must not re-darken"
        );
        assert!(!h.acquire_edge());
    }

    #[test]
    fn only_the_last_release_relights() {
        let mut h = fresh();
        assert!(h.acquire_edge());
        h.record(Some(Darkened::Wayland(vec![(7, Some("DP-1".into()))])));
        assert!(!h.acquire_edge());
        // First release: a sibling still streams — the panel must stay dark.
        assert!(h.release_edge().is_none());
        // Last release hands back the record to undo.
        let d = h.release_edge();
        assert!(matches!(d, Some(Darkened::Wayland(v)) if v == vec![(7, Some("DP-1".into()))]));
    }

    #[test]
    fn a_darken_that_did_nothing_restores_nothing() {
        let mut h = fresh();
        assert!(h.acquire_edge());
        h.record(None); // no KDE / already dark: nothing was changed
        assert!(h.release_edge().is_none(), "nothing to undo");
        assert_eq!(h.count, 0);
    }

    #[test]
    fn unbalanced_release_never_underflows() {
        let mut h = fresh();
        assert!(h.release_edge().is_none());
        assert_eq!(h.count, 0, "count must not wrap");
        // And the state machine still works afterwards.
        assert!(h.acquire_edge());
        h.record(Some(Darkened::Kscreen));
        assert!(matches!(h.release_edge(), Some(Darkened::Kscreen)));
    }

    #[test]
    fn a_full_cycle_rearms_the_darken() {
        let mut h = fresh();
        assert!(h.acquire_edge());
        h.record(Some(Darkened::Kscreen));
        assert!(h.release_edge().is_some());
        // A later stream on the same host lifetime darkens again.
        assert!(
            h.acquire_edge(),
            "the 0→1 edge must re-arm after a full cycle"
        );
    }
}
