"""Tests for the Linux XDG desktop entry installed by ``hermes desktop``."""

from __future__ import annotations

import io
import os
import stat
import struct
import sys
import time
from pathlib import Path

import pytest

from hermes_cli import linux_desktop_entry as lde


@pytest.fixture
def xdg_home(tmp_path, monkeypatch) -> Path:
    data_home = tmp_path / "xdg-data"
    monkeypatch.setenv("XDG_DATA_HOME", str(data_home))
    # Isolate the known-wrapper probe too: tests must never see the real
    # ~/.local/bin/hermes on the dev machine.
    monkeypatch.setenv("HOME", str(tmp_path))
    monkeypatch.setattr(lde.sys, "platform", "linux")
    return data_home


def _make_project(tmp_path: Path) -> Path:
    root = tmp_path / "hermes-agent"
    icon = root / "apps" / "desktop" / "assets" / "icon.png"
    icon.parent.mkdir(parents=True)
    icon.write_bytes(b"\x89PNG fake")
    return root


def _png_ihdr(width: int, height: int) -> bytes:
    """Minimal PNG prefix whose IHDR the installer can parse (no pixels)."""
    return (
        b"\x89PNG\r\n\x1a\n"
        + b"\x00\x00\x00\r"
        + b"IHDR"
        + struct.pack(">II", width, height)
    )


def _stub_install(tmp_path, monkeypatch) -> None:
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir(exist_ok=True)
    hermes_bin.write_text("", encoding="utf-8")
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])


def _parse(entry_text: str) -> dict:
    values = {}
    for line in entry_text.splitlines():
        if "=" in line and not line.startswith("["):
            key, val = line.split("=", 1)
            values[key] = val
    return values


def test_install_writes_entry_with_absolute_exec_and_icon(
    tmp_path, xdg_home, monkeypatch
):
    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text("", encoding="utf-8")
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])
    # Keep the icon install out of the way: this test pins the
    # absolute-path FALLBACK (copy impossible / not attempted here).
    monkeypatch.setattr(lde, "_install_icon_to_hicolor", lambda _icon: False)

    entry = lde.install_desktop_entry(root)

    assert entry == xdg_home / "applications" / lde.DESKTOP_ENTRY_NAME
    values = _parse(entry.read_text(encoding="utf-8"))

    # Exec must be the absolute path of the resolved binary. The launcher
    # runs with a minimal PATH, so a bare `hermes` would not resolve.
    assert values["Exec"] == f"{hermes_bin} desktop"
    assert Path(values["Exec"].split(" ")[0]).is_absolute()

    # Icon must be an absolute path to the real icon in the checkout.
    icon_path = Path(values["Icon"])
    assert icon_path.is_absolute()
    assert icon_path == lde.icon_path(root)


def test_install_prefers_themed_icon_from_hicolor(tmp_path, xdg_home, monkeypatch):
    """When the icon installs into hicolor, the entry uses the themed name.

    The themed name survives a moved/archived checkout; an absolute
    Icon= path does not (the same durability class the Exec line was
    fixed for).
    """
    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text("", encoding="utf-8")
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)

    values = _parse(entry.read_text(encoding="utf-8"))
    assert values["Icon"] == "hermes"

    # And the icon really landed in the hicolor tree: the fixture icon is
    # a fake PNG (no valid IHDR), so the size is unknown and the icon
    # lands under 256x256/ (indexed; never scalable, which is SVG-only).
    dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
    assert dest.is_file()
    assert dest.read_bytes() == lde.icon_path(root).read_bytes()


def test_install_icon_copy_failure_falls_back_to_absolute(
    tmp_path, xdg_home, monkeypatch
):
    """An impossible icon copy keeps the absolute path (never breaks)."""
    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text("", encoding="utf-8")
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    def _boom(src, dst):
        raise OSError("read-only tree")

    monkeypatch.setattr(lde.shutil, "copyfile", _boom)

    entry = lde.install_desktop_entry(root)
    values = _parse(entry.read_text(encoding="utf-8"))
    # The real helper catches the copy OSError and returns False, so the
    # caller falls back to the absolute path without raising.
    assert values["Icon"] == str(lde.icon_path(root))

    assert values["Type"] == "Application"
    assert values["Name"] == "Hermes"
    assert values["Terminal"] == "false"


def test_installed_entry_is_executable(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)

    assert entry.stat().st_mode & stat.S_IXUSR


def test_exec_falls_back_to_interpreter_module(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line.endswith("-m hermes_cli.main desktop")
    assert Path(exec_line.split(" ")[0]).is_absolute()


# #90292: the shell installer's bash wrapper makes argv[0] the repo `hermes`
# python script whose `#!/usr/bin/env python3` shebang resolves to the SYSTEM
# interpreter when the DE spawns the .desktop entry → ModuleNotFoundError,
# silent (Terminal=false). The Exec line must prefix sys.executable for any
# resolved bin that is a python script escaping the running venv.
def test_exec_prefixes_interpreter_for_env_shebang_python_script(
    tmp_path, xdg_home, monkeypatch
):
    import sys

    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    hermes_bin.chmod(0o755)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    interpreter = os.path.abspath(sys.executable)
    assert exec_line.split(" ")[0].strip('"') == interpreter
    assert str(hermes_bin) in exec_line
    assert exec_line.endswith("desktop")


def test_exec_leaves_shell_wrapper_launchers_alone(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text(
        '#!/usr/bin/env bash\nexec /opt/hermes/venv/bin/python "$@"\n', encoding="utf-8"
    )
    hermes_bin.chmod(0o755)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # A bash wrapper execs the venv python itself — no interpreter prefix.
    assert exec_line == f"{hermes_bin} desktop"


def test_exec_leaves_venv_shebang_scripts_alone(tmp_path, xdg_home, monkeypatch):
    import sys

    root = _make_project(tmp_path)
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    interpreter = os.path.abspath(sys.executable)
    hermes_bin.write_text(f"#!{interpreter}\nimport hermes_cli\n", encoding="utf-8")
    hermes_bin.chmod(0o755)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # Console-script with the venv's own interpreter in the shebang: correct
    # as-is, prefixing would only add noise.
    assert exec_line == f"{hermes_bin} desktop"


# The persisted entry must be launch-context independent: whatever process
# writes it, the next launch reads and rewrites the same bytes. argv[0]
# differs per launch path (wrapper / repo script / python -m), so a
# checkout-internal argv[0] must not be persisted — the resolver falls
# through to PATH, where the installer's durable wrapper lives.
def _argv0_context(monkeypatch, argv0: str) -> None:
    import sys

    monkeypatch.setattr(sys, "argv", [argv0, "desktop"])


def test_exec_converges_from_repo_script_argv0_to_installed_wrapper(
    tmp_path, xdg_home, monkeypatch
):
    """A broken interpreter-form entry must self-heal to the wrapper form.

    Launching with argv[0] = <checkout>/hermes (what the broken entry
    itself spawns) previously re-persisted the same broken form forever —
    the bootstrap loop that kept #90492 from repairing existing installs.
    """
    import sys

    root = _make_project(tmp_path)
    repo_script = root / "hermes"  # checkout-internal launcher candidate
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)
    wrapper = tmp_path / "installed" / "bin" / "hermes"
    wrapper.parent.mkdir(parents=True)
    wrapper.write_text(f'#!/usr/bin/env bash\nexec {sys.executable} "$@"\n', encoding="utf-8")
    wrapper.chmod(0o755)

    # argv[0] = repo script; PATH lookup finds the installed wrapper.
    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr(
        "shutil.which", lambda name: str(wrapper) if name == "hermes" else None
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # Converged on the durable wrapper — NOT the repo script, and NOT an
    # interpreter-prefixed form pinning sys.executable.
    assert exec_line == f"{wrapper} desktop"


def test_exec_never_persists_a_bare_interpreter_command(
    tmp_path, xdg_home, monkeypatch
):
    """The `python -m hermes_cli.main` relaunch context must not write
    `Exec=<python> desktop` — a command line no DE can run."""

    root = _make_project(tmp_path)
    wrapper = tmp_path / "installed" / "bin" / "hermes"
    wrapper.parent.mkdir(parents=True)
    wrapper.write_text("#!/usr/bin/env bash\nexit 0\n", encoding="utf-8")
    wrapper.chmod(0o755)

    interpreter = tmp_path / "uv" / "cpython-3.11.15" / "bin" / "python3.11"
    interpreter.parent.mkdir(parents=True)
    interpreter.write_bytes(b"\x7fELF fake")
    interpreter.chmod(0o755)

    # argv[0] IS the interpreter (python -m context); PATH has the wrapper.
    _argv0_context(monkeypatch, str(interpreter))
    monkeypatch.setattr(
        "shutil.which", lambda name: str(wrapper) if name == "hermes" else None
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    first_token = exec_line.split(" ")[0].strip('"')
    assert Path(first_token) != interpreter
    assert not (
        Path(first_token).name.startswith("python")
        and "desktop" in exec_line.split(" ", 1)[1]
    ), f"persisted an unrunnable bare-interpreter Exec: {exec_line}"
    assert exec_line == f"{wrapper} desktop"


def test_exec_keeps_resolver_fallback_when_no_wrapper_on_path(
    tmp_path, xdg_home, monkeypatch
):
    """No wrapper anywhere → #90492's runnable fallback, never a dead Exec.

    With argv[0] checkout-internal and PATH + known locations both empty,
    the resolver returns None and resolve_exec_command emits the runnable
    `sys.executable -m hermes_cli.main desktop` fallback. Persisting the
    interpreter itself (`<python> desktop`) would be unrunnable by any DE;
    persisting the repo script alone dies on its env shebang.
    """
    import sys

    root = _make_project(tmp_path)
    repo_script = root / "hermes"
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)

    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr("shutil.which", lambda name: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    def fake_resolve():
        # Mirror resolve_hermes_bin's chain: argv[0] → relative → PATH → None.
        return sys.argv[0] if sys.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # The runnable module fallback — NOT the bare repo script (its env
    # shebang would escape the venv under a DE) and NOT `<python> desktop`.
    assert exec_line.endswith("-m hermes_cli.main desktop")
    assert Path(exec_line.split(" ")[0].strip('"')).is_absolute()
    assert str(repo_script) not in exec_line


def test_exec_uses_known_wrapper_when_path_lookup_misses(
    tmp_path, xdg_home, monkeypatch
):
    """Stripped-PATH session + wrapper at the known installer location.

    systemd user sessions and autostart relaunches often run without
    ~/.local/bin on PATH. When shutil.which finds nothing, the resolver
    must probe the known durable locations directly instead of silently
    persisting a checkout-internal Exec line.
    """
    import sys

    root = _make_project(tmp_path)
    repo_script = root / "hermes"
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)

    # The wrapper exists at the known location but is NOT on PATH.
    # Realistic installer shim: execs this checkout's venv python on the
    # checkout's hermes script (the aidiyet check requires it to target
    # the writing checkout).
    known_wrapper = tmp_path / "known-home" / ".local" / "bin" / "hermes"
    known_wrapper.parent.mkdir(parents=True)
    known_wrapper.write_text(
        f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    known_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path / "known-home"))

    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr("shutil.which", lambda name: None)

    def fake_resolve():
        return sys.argv[0] if sys.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # The probe found the wrapper despite the PATH miss.
    assert exec_line == f"{known_wrapper} desktop"


def test_exec_never_persists_a_checkout_internal_path_hit(tmp_path, xdg_home, monkeypatch):
    """A PATH hit inside THIS checkout is a launch-context artifact, like argv[0].

    The desktop-update hand-off hands the updater <checkout>/venv/bin at the
    FRONT of PATH (apps/desktop/electron/main.ts), so argv[0] is the venv
    console script — checkout-internal, correctly skipped as a durable
    answer — and the reroute that hides argv[0] re-resolves over PATH and
    hits THE SAME SCRIPT. The rerouted branch returned that hit outright,
    persisting the venv form; the next DE launch re-resolves to the durable
    wrapper and flips the bytes back. Alternating writers alternate the
    file content (captured: wrapper -> venv -> wrapper inside one update
    cycle), and every flip rewrites the entry. A rewrite landing
    inside a grid launch's STARTING window is the arm for the gnome-shell
    50.x crash this module already guards against. A PATH hit inside the
    checkout must fall through to the durable probe.
    """
    root = _make_project(tmp_path)
    venv_script = root / "venv" / "bin" / "hermes"
    venv_script.parent.mkdir(parents=True)
    venv_script.write_text("#!/usr/bin/env bash\nexec true\n", encoding="utf-8")
    venv_script.chmod(0o755)

    known_wrapper = tmp_path / "path-home" / ".local" / "bin" / "hermes"
    known_wrapper.parent.mkdir(parents=True)
    known_wrapper.write_text(
        f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    known_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path / "path-home"))

    # The hand-off's resolver chain: argv[0] = venv console script; with
    # argv[0] hidden, the PATH rerun yields the SAME script.
    def fake_resolve():
        return sys.argv[0] or str(venv_script)

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    _argv0_context(monkeypatch, str(venv_script))
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    assert entry is not None
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line == f"{known_wrapper} desktop"
    assert str(venv_script) not in exec_line

    # …and the same context a second time re-renders byte-identical content:
    # the no-op guard then skips the rewrite entirely (no write, no rescan).
    lde._probe_cache.clear()
    entry2 = lde.install_desktop_entry(root)
    assert entry2 is not None
    assert entry2.read_text(encoding="utf-8") == entry.read_text(encoding="utf-8")


def test_exec_skips_managed_environment_cli_without_desktop(
    tmp_path, xdg_home, monkeypatch
):
    """A PM-managed console script is outside the checkout but cannot launch Desktop.

    Its generated workspace has no ``apps/desktop``. Treating it as the durable
    external primary persists ``Exec=`` at that script, and the grid launcher
    exits with "Desktop GUI source not found". The entry must keep the
    checkout's installed wrapper instead.
    """
    from pm.environments import install_key

    root = _make_project(tmp_path)
    installs = tmp_path / "installs"
    monkeypatch.setattr("pm.environments.installs_root", lambda: installs)
    generation = installs / install_key(root) / "environments" / "gen"
    workspace = generation / "workspace"
    workspace.mkdir(parents=True)
    managed = generation / "venv" / "bin" / "hermes"
    managed.parent.mkdir(parents=True)
    managed.write_text("#!/usr/bin/env bash\nexec true\n", encoding="utf-8")
    managed.chmod(0o755)

    known_wrapper = tmp_path / ".local" / "bin" / "hermes"
    known_wrapper.parent.mkdir(parents=True)
    known_wrapper.write_text(
        f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    known_wrapper.chmod(0o755)

    def fake_resolve():
        return str(managed)

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    _argv0_context(monkeypatch, str(managed))
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    assert entry is not None
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line == f"{known_wrapper} desktop"
    assert str(managed) not in exec_line


def test_exec_finds_known_wrapper_when_resolver_has_no_candidate(
    tmp_path, xdg_home, monkeypatch
):
    """`None` from the resolver must still probe known wrapper locations.

    A cold relaunch (argv[0] is not an executable file, e.g. `-c` under
    `python -m`, and PATH has no `hermes`) makes resolve_hermes_bin return
    None outright. The early `return primary` that used to fire here skipped
    the durable-wrapper probe, so the persisted Exec flipped to the bare
    `<python> -m hermes_cli.main desktop` module form. Each flip between the
    wrapper and module forms rewrites the entry on the next launch; any
    rewrite that lands while gnome-shell's ShellApp for the entry is still
    STARTING crashes the shell (shell_app_dispose `state == STOPPED`
    assertion, gnome-shell 50.4). The entry must converge on the durable
    wrapper wherever it exists.
    """
    root = _make_project(tmp_path)

    known_wrapper = tmp_path / "cold-home" / ".local" / "bin" / "hermes"
    known_wrapper.parent.mkdir(parents=True)
    known_wrapper.write_text(
        f'#!/usr/bin/env bash\nexec {root / "venv" / "bin" / "python"} {root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    known_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path / "cold-home"))

    # argv[0] is not an executable path at all — the resolver's own chain
    # yields None with or without argv[0].
    _argv0_context(monkeypatch, "-c")
    monkeypatch.setattr("shutil.which", lambda name: None)
    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    assert entry is not None
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line == f"{known_wrapper} desktop"

    # …and the SAME context a second time re-renders byte-identical content:
    # the no-op guard in install_desktop_entry then skips the rewrite.
    lde._probe_cache.clear()
    entry2 = lde.install_desktop_entry(root)
    assert entry2 is not None
    assert entry2.read_text(encoding="utf-8") == entry.read_text(encoding="utf-8")


def test_exec_rejects_known_wrapper_from_another_checkout(
    tmp_path, xdg_home, monkeypatch
):
    """A known-location wrapper that targets a DIFFERENT checkout is skipped.

    On machines with multiple installs over time, ~/.local/bin/hermes may
    belong to another checkout. Persisting it would make the entry stable
    but silently point at that other installation — the failure class the
    aidiyet check exists to prevent. The runnable module fallback must win
    instead.
    """
    import sys

    root = _make_project(tmp_path)
    repo_script = root / "hermes"
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)

    # A shim belonging to a DIFFERENT checkout.
    other_root = tmp_path / "other-install"
    other_root.mkdir()
    foreign_wrapper = tmp_path / "known-home" / ".local" / "bin" / "hermes"
    foreign_wrapper.parent.mkdir(parents=True)
    foreign_wrapper.write_text(
        f"#!/usr/bin/env bash\nexec {other_root / 'venv' / 'bin' / 'python'} "
        f'{other_root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    foreign_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(tmp_path / "known-home"))

    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr("shutil.which", lambda name: None)

    def fake_resolve():
        return sys.argv[0] if sys.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # The foreign wrapper was rejected; the runnable module fallback won.
    assert str(foreign_wrapper) not in exec_line
    assert exec_line.endswith("-m hermes_cli.main desktop")


@pytest.mark.parametrize(
    ("layout", "env_overrides", "expected"),
    [
        pytest.param(
            "user",
            {},
            "HOME-SET-BY-TEST/.local/bin/hermes",
            id="user-layout",
        ),
        pytest.param(
            "termux",
            {"PREFIX": "PREFIX-SET-BY-TEST"},
            "PREFIX-SET-BY-TEST/bin/hermes",
            id="termux-prefix-first",
        ),
        pytest.param(
            "root-fhs",
            {"__EUID0__": "1"},
            "/usr/local/bin/hermes",
            id="root-fhs",
        ),
        pytest.param(
            "non-root-no-fhs",
            {"__EUID0__": "0"},
            "HOME-SET-BY-TEST/.local/bin/hermes",
            id="non-root-excludes-fhs",
        ),
    ],
)
def test_known_wrapper_candidates_cover_installer_layouts(
    layout, env_overrides, expected, monkeypatch
):
    """_known_wrapper_candidates mirrors get_command_link_dir() layouts.

    Termux ($PREFIX/bin) outranks everything; root FHS (/usr/local/bin)
    applies only to euid 0; the user layout (~/.local/bin) is always a
    candidate. Locking these in protects against silent regressions in
    the stripped-PATH probe path.
    """

    sentinel_home = "/home/__sentinel_home__"
    monkeypatch.setenv("HOME", sentinel_home)
    for key, value in env_overrides.items():
        if key == "__EUID0__":
            monkeypatch.setattr(lde.os, "geteuid", lambda: 0 if value == "1" else 1000)
        else:
            monkeypatch.setenv(key, value)

    candidates = [str(c) for c in lde._known_wrapper_candidates()]

    expected_resolved = expected.replace("HOME-SET-BY-TEST", sentinel_home)
    assert expected_resolved in candidates
    if layout == "termux":
        # PREFIX outranks the user layout.
        assert candidates[0] == expected_resolved
    if layout == "root-fhs":
        # Root FHS outranks the user layout.
        assert candidates.index("/usr/local/bin/hermes") < candidates.index(
            f"{sentinel_home}/.local/bin/hermes"
        )
    if layout == "non-root-no-fhs":
        # Non-root euid: /usr/local/bin must be excluded outright.
        assert "/usr/local/bin/hermes" not in candidates


def test_installed_entry_carries_the_window_app_id(tmp_path, xdg_home, monkeypatch):
    """The window's app_id is what GNOME matches the entry against — not the old "Hermes"."""
    _stub_install(tmp_path, monkeypatch)
    root = _make_project(tmp_path)

    entry = lde.install_desktop_entry(root)

    assert entry is not None
    assert entry.name == f"{lde.APP_ID}.desktop"
    values = _parse(entry.read_text(encoding="utf-8"))
    assert values["StartupWMClass"] == lde.APP_ID
    assert values["Name"] == "Hermes"  # the menu label is not part of the identity


def test_install_keeps_the_legacy_entry_as_a_hidden_alias(tmp_path, xdg_home, monkeypatch):
    """A pin resolves by the entry file name it was pinned against (#124492).

    Deleting ``hermes.desktop`` silently kills existing taskbar pins (GNOME drops
    the favourite, Plasma leaves an inert item) and the shell has no mechanism to
    re-point the association for the user. The pre-rename entry must survive as a
    ``NoDisplay=true`` alias of the app-id entry: out of the app grid, still
    launchable, and window-matched through the same ``StartupWMClass`` and
    ``Exec`` as the app-id entry.
    """
    _stub_install(tmp_path, monkeypatch)
    root = _make_project(tmp_path)
    legacy = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
    legacy.parent.mkdir(parents=True)
    legacy.write_text(
        "[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n",
        encoding="utf-8",
    )

    entry = lde.install_desktop_entry(root)

    assert entry is not None and entry.is_file()
    assert legacy.is_file(), "an existing pin resolves through this file — it must survive"
    alias = _parse(legacy.read_text(encoding="utf-8"))
    assert alias["NoDisplay"] == "true"  # no second Hermes in the app grid
    assert alias["StartupWMClass"] == lde.APP_ID  # still groups with the window
    entry_values = _parse(entry.read_text(encoding="utf-8"))
    assert alias["Exec"] == entry_values["Exec"]  # launches the same command
    assert alias["Icon"] == entry_values["Icon"]


def test_unchanged_entry_still_aliases_a_legacy_entry(tmp_path, xdg_home, monkeypatch):
    """An up-to-date app-id entry must not skip converting a legacy file found beside it."""
    _stub_install(tmp_path, monkeypatch)
    root = _make_project(tmp_path)
    assert lde.install_desktop_entry(root) is not None
    legacy = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
    legacy.write_text("[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n", encoding="utf-8")

    lde.install_desktop_entry(root)

    assert _parse(legacy.read_text(encoding="utf-8"))["NoDisplay"] == "true"


def test_install_keeps_foreign_files_at_the_legacy_path(tmp_path, xdg_home, monkeypatch):
    """Only our own entry is converted to an alias; another app's file is not ours to rewrite."""
    _stub_install(tmp_path, monkeypatch)
    root = _make_project(tmp_path)
    foreign = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
    foreign.parent.mkdir(parents=True)
    foreign.write_text(
        "[Desktop Entry]\nType=Application\nName=Someone else\nExec=other-app\n",
        encoding="utf-8",
    )

    lde.install_desktop_entry(root)

    assert foreign.is_file()
    assert "Name=Someone else" in foreign.read_text(encoding="utf-8")


def test_install_opt_out_preserves_the_legacy_entry(tmp_path, xdg_home, monkeypatch):
    """The opt-out protects user edits, so it also stops the legacy alias conversion.

    The missing-entry path still creates the app-id entry; the rewrite is
    management too and must not run when the user asked to be left alone.
    """
    hermes_home = tmp_path / "hermes-home"
    hermes_home.mkdir()
    monkeypatch.setenv("HERMES_HOME", str(hermes_home))
    (hermes_home / "config.yaml").write_text(
        "desktop:\n  manage_launcher_entry: false\n", encoding="utf-8"
    )
    _stub_install(tmp_path, monkeypatch)
    root = _make_project(tmp_path)
    legacy = xdg_home / "applications" / lde.LEGACY_DESKTOP_ENTRY_NAME
    legacy.parent.mkdir(parents=True)
    legacy.write_text(
        "[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n",
        encoding="utf-8",
    )

    entry = lde.install_desktop_entry(root)

    assert entry == xdg_home / "applications" / lde.DESKTOP_ENTRY_NAME
    assert legacy.read_text(encoding="utf-8") == (
        "[Desktop Entry]\nType=Application\nName=Hermes\nExec=hermes desktop\n"
    ), "the opt-out must leave the legacy entry byte-for-byte untouched"


def test_app_id_matches_the_desktop_build_identity():
    """APP_ID mirrors apps/desktop/product-identity.cjs; the two must not drift apart."""
    import shutil
    import subprocess

    node = shutil.which("node")
    if node is None:
        pytest.skip("node is not available")
    repo = Path(__file__).resolve().parents[2]
    probe = "console.log(require('./apps/desktop/product-identity.cjs').appId)"
    result = subprocess.run(
        [node, "-e", probe], cwd=repo, capture_output=True, text=True, timeout=60
    )
    if result.returncode != 0:
        pytest.skip(f"product-identity.cjs did not evaluate: {result.stderr.strip()[:200]}")
    assert result.stdout.strip() == lde.APP_ID


def test_install_is_idempotent_and_skips_cache_refresh(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
    )
    calls: list[Path] = []
    monkeypatch.setattr(
        lde, "refresh_desktop_databases", lambda d: calls.append(d) or []
    )

    lde.install_desktop_entry(root)
    assert len(calls) == 1

    # Unchanged content → no rewrite, no menu-cache churn on every launch.
    lde.install_desktop_entry(root)
    assert len(calls) == 1


def test_install_without_source_icon_uses_themed_name(tmp_path, xdg_home, monkeypatch):
    root = tmp_path / "hermes-agent"
    root.mkdir()
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: "/usr/bin/hermes"
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)

    # A broken absolute path renders as no icon. The themed name resolves
    # when Hermes is installed some other way.
    assert _parse(entry.read_text(encoding="utf-8"))["Icon"] == "hermes"


@pytest.mark.platforms("macos")
def test_install_is_a_noop_on_macos(tmp_path):
    """Faking darwin only renamed the host — the real macOS runner is the
    only place the `sys.platform` guard is exercised against a real host."""
    assert lde.install_desktop_entry(_make_project(tmp_path)) is None


@pytest.mark.platforms("windows")
def test_install_is_a_noop_on_windows(tmp_path):
    """As above for Windows: a fake left POSIX paths and a POSIX XDG layout
    in place, so the no-op was never proven against a real one."""
    assert lde.install_desktop_entry(_make_project(tmp_path)) is None


# ---------------------------------------------------------------------------
# Cache refresh tool gating
# ---------------------------------------------------------------------------


def _stub_tools(monkeypatch, available: "set[str]") -> "list[list[str]]":
    ran: list[list[str]] = []
    monkeypatch.setattr(
        lde.shutil,
        "which",
        lambda name: f"/usr/bin/{name}" if name in available else None,
    )
    monkeypatch.setattr(lde, "_run_quiet", lambda cmd: ran.append(cmd) or True)
    return ran




def test_refresh_falls_back_to_kbuildsycoca5(monkeypatch, tmp_path):
    ran = _stub_tools(monkeypatch, {"kbuildsycoca5"})

    tools = lde.refresh_desktop_databases(tmp_path)

    assert tools == ["kbuildsycoca5"]
    assert ran == [["/usr/bin/kbuildsycoca5", "--noincremental"]]


def test_refresh_prefers_kbuildsycoca6_over_5(monkeypatch, tmp_path):
    ran = _stub_tools(monkeypatch, {"kbuildsycoca6", "kbuildsycoca5"})

    lde.refresh_desktop_databases(tmp_path)

    assert [cmd[0] for cmd in ran] == ["/usr/bin/kbuildsycoca6"]


def test_refresh_skips_missing_tools(monkeypatch, tmp_path):
    ran = _stub_tools(monkeypatch, set())

    assert lde.refresh_desktop_databases(tmp_path) == []
    assert ran == []


def test_refresh_reports_only_tools_that_succeeded(monkeypatch, tmp_path):
    monkeypatch.setattr(lde.shutil, "which", lambda name: f"/usr/bin/{name}")
    # update-desktop-database fails (exit != 0). kbuildsycoca6 succeeds.
    monkeypatch.setattr(lde, "_run_quiet", lambda cmd: "kbuildsycoca" in cmd[0])

    assert lde.refresh_desktop_databases(tmp_path) == ["kbuildsycoca6"]


def test_run_quiet_swallows_missing_binary(tmp_path):
    assert lde._run_quiet([str(tmp_path / "definitely-not-a-binary")]) is False




@pytest.mark.skipif(
    sys.platform == "win32", reason="Symlinks require elevated privileges on Windows"
)
def test_running_interpreter_keeps_venv_semantic_path(tmp_path, monkeypatch):
    """Lexical preserved only when pyvenv.cfg marks the path as a venv."""
    # venv layout: bin/python symlink -> base, pyvenv.cfg at venv root
    base = tmp_path / "base" / "python3.11"
    base.parent.mkdir(parents=True)
    base.write_text("", encoding="utf-8")
    venv_root = tmp_path / "venv"
    venv_bin = venv_root / "bin"
    venv_bin.mkdir(parents=True)
    (venv_root / "pyvenv.cfg").write_text("home = /base\n", encoding="utf-8")
    venv_python = venv_bin / "python"
    venv_python.symlink_to(base)

    monkeypatch.setattr(lde.sys, "executable", str(venv_python))
    assert lde._running_interpreter() == str(venv_python)

    # non-venv symlink: resolve instead (durability over lexical)
    plain_root = tmp_path / "plain" / "bin"
    plain_root.mkdir(parents=True)
    plain_link = plain_root / "python3"
    plain_link.symlink_to(base)
    monkeypatch.setattr(lde.sys, "executable", str(plain_link))
    assert lde._running_interpreter() == str(base)


def test_running_interpreter_resolves_plain_interpreter(monkeypatch):
    """A non-symlinked, non-venv executable resolves to itself."""
    monkeypatch.setattr(lde.sys, "executable", "/usr/bin/python3")
    out = lde._running_interpreter()
    assert Path(out).is_absolute()




def test_exec_falls_back_to_running_interpreter_when_probe_fails(
    tmp_path, xdg_home, monkeypatch
):
    """A candidate interpreter that fails the import probe is not persisted."""
    import sys as _s

    root = _make_project(tmp_path)
    interpreter = tmp_path / "uv" / "bin" / "python3.11"
    interpreter.parent.mkdir(parents=True)
    interpreter.write_bytes(b"\x7fELF fake")
    interpreter.chmod(0o755)

    _argv0_context(monkeypatch, str(interpreter))
    monkeypatch.setattr("shutil.which", lambda name: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    def fake_resolve():
        return _s.argv[0] if _s.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    # Force the probe to fail for whatever interpreter gets chosen first.
    monkeypatch.setattr(lde, "_can_import_hermes_cli", lambda p: False)
    # And the fallback interpreter must itself pass (it always should).
    monkeypatch.setattr(
        lde,
        "_running_interpreter_fallback",
        lambda: os.path.abspath(sys.executable),
    )

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    # Runnable module form under the RUNNING interpreter - never the
    # unprobeable ELF fake, never a bare "<python> desktop".
    assert exec_line.endswith("-m hermes_cli.main desktop")
    first = exec_line.split(" ")[0].strip('"')
    assert first == os.path.abspath(sys.executable)
    assert str(interpreter) not in exec_line


@pytest.mark.parametrize(
    "suffix",
    ["-old", ".bak", "-copy"],
)
def test_wrapper_ownership_rejects_sibling_extensions(suffix, tmp_path):
    """A shim execing `<checkout><suffix>/...` must NOT pass ownership.

    Bare substring matching accepted these; the boundary-aware matcher
    must reject them (stable-but-wrong entry pointing at the renamed
    old install).
    """
    checkout = tmp_path / "hermes-agent"
    checkout.mkdir()
    evil = tmp_path / "evil-shim"
    evil.write_text(
        f"#!/usr/bin/env bash\n"
        f"exec {checkout}{suffix}/venv/bin/python "
        f'{checkout}{suffix}/hermes "$@"\n',
        encoding="utf-8",
    )
    assert lde._wrapper_targets_checkout(evil, checkout) is False


@pytest.mark.skipif(
    sys.platform == "win32", reason="Symlinks require elevated privileges on Windows"
)
def test_wrapper_ownership_accepts_shim_via_symlinked_home(tmp_path, monkeypatch):
    """Installer writes $INSTALL_DIR lexically; the root stays lexical too.

    With /home -> /real-home, a shim that references the lexical
    checkout path must match the lexical checkout root (the resolved
    root alone would never match the shim's text).
    """
    home_link = tmp_path / "home-link"
    home_real = tmp_path / "home-real"
    home_real.mkdir()
    home_link.symlink_to(home_real)
    lexical_checkout = home_link / "hermes-agent"
    (home_real / "hermes-agent").mkdir()

    shim = home_link / ".local" / "bin" / "hermes"
    shim.parent.mkdir(parents=True)
    shim.write_text(
        f"#!/usr/bin/env bash\n"
        f"exec {lexical_checkout}/venv/bin/python "
        f'{lexical_checkout}/hermes "$@"\n',
        encoding="utf-8",
    )
    assert lde._wrapper_targets_checkout(shim, lexical_checkout) is True
    # And the end-to-end probe finds it via the lexical root: make the
    # shim executable, point HOME at the symlinked home, and give the
    # resolver a checkout-internal primary (the repo script) so the
    # probe leg actually engages.
    shim.chmod(0o755)
    monkeypatch.setenv("HOME", str(home_link))
    monkeypatch.setattr("shutil.which", lambda name: None)
    repo_script = lexical_checkout / "hermes"
    repo_script.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
    monkeypatch.setattr(sys, "argv", [str(repo_script), "desktop"])
    assert lde._resolve_hermes_bin_for_desktop_entry(
        resolve_fn=lambda: sys.argv[0] if sys.argv[0] else None,
        checkout_root=lexical_checkout,
    ) == str(shim)


def test_needs_interpreter_case_insensitive_match(tmp_path, monkeypatch):
    """Interpreter paths with uppercase must not flag own venv scripts.

    The shebang is lowercased for comparison; the interpreter dir must
    be too (conda env names, usernames, uv's ephemeral .tmpXXX dirs all
    carry uppercase - an asymmetric compare would prefix the venv's own
    console script spuriously).
    """
    venv_bin = tmp_path / "MyEnv" / "bin"
    venv_bin.mkdir(parents=True)
    interpreter = venv_bin / "python"
    interpreter.write_text("", encoding="utf-8")

    console_script = venv_bin / "hermes"
    console_script.write_text(f"#!{interpreter}\nimport hermes_cli\n", encoding="utf-8")
    monkeypatch.setattr(lde.sys, "executable", str(interpreter))

    assert lde._needs_interpreter(console_script) is False


def test_needs_interpreter_rejects_sibling_directory(tmp_path, monkeypatch):
    """``<venv>/bin-extra/python`` is NOT inside ``<venv>/bin``.

    Substring matching accepted it (the parent dir appears verbatim inside
    the sibling path), skipping the interpreter prefix for a script whose
    shebang actually points OUTSIDE the venv. Path-component comparison
    rejects it.
    """
    venv_bin = tmp_path / "venv" / "bin"
    venv_bin.mkdir(parents=True)
    interp = venv_bin / "python"
    interp.write_text("", encoding="utf-8")
    monkeypatch.setattr(lde.sys, "executable", str(interp))

    sibling_script = tmp_path / "sibling"
    sibling_script.write_text(
        f"#!{tmp_path}/venv/bin-extra/python\nimport hermes_cli\n",
        encoding="utf-8",
    )
    assert lde._needs_interpreter(sibling_script) is True


def test_needs_interpreter_strips_flags_before_comparing(tmp_path, monkeypatch):
    """A flagged own-venv shebang is not misclassified by the flag token."""
    venv_bin = tmp_path / "venv" / "bin"
    venv_bin.mkdir(parents=True)
    interp = venv_bin / "python"
    interp.write_text("", encoding="utf-8")
    monkeypatch.setattr(lde.sys, "executable", str(interp))

    flagged = tmp_path / "flagged"
    flagged.write_text(f"#!{interp} -S\nimport hermes_cli\n", encoding="utf-8")
    assert lde._needs_interpreter(flagged) is False


def test_needs_interpreter_env_shebang_always_escapes(tmp_path, monkeypatch):
    """``env`` resolves through the DE's PATH - not the installer's."""
    venv_bin = tmp_path / "venv" / "bin"
    venv_bin.mkdir(parents=True)
    interp = venv_bin / "python"
    interp.write_text("", encoding="utf-8")
    monkeypatch.setattr(lde.sys, "executable", str(interp))

    # Even when env itself sits in the venv bin (parent equality would
    # pass), the PATH resolution semantics mean the shebang escapes.
    env_script = tmp_path / "envscript"
    env_script.write_text(
        f"#!{venv_bin}/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    assert lde._needs_interpreter(env_script) is True

    # ...unless env carries an absolute venv interpreter after -S.
    env_abs = tmp_path / "envabs"
    env_abs.write_text(
        f"#!/usr/bin/env -S {interp}\nimport hermes_cli\n", encoding="utf-8"
    )
    assert lde._needs_interpreter(env_abs) is False


def test_probe_skips_wrapper_with_escaping_python_shebang(
    tmp_path, xdg_home, monkeypatch
):
    """A checkout-referencing wrapper with an env shebang is skipped.

    Ownership alone would accept it (the body references this checkout),
    but its `#!/usr/bin/env python3` shebang dies in the DE context.
    The shebang-safety gate skips it; the module fallback wins. Idea
    credited to autumn8's #92122 rung-2 check.
    """

    root = _make_project(tmp_path)
    repo_script = root / "hermes"
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)

    # A wrapper that targets this checkout but cannot run itself.
    broken_wrapper = xdg_home / ".local" / "bin" / "hermes"
    broken_wrapper.parent.mkdir(parents=True)
    broken_wrapper.write_text(
        f"#!/usr/bin/env python3\n# launcher for {root}\nimport hermes_cli\n",
        encoding="utf-8",
    )
    broken_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(xdg_home))
    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr("shutil.which", lambda name: None)

    def fake_resolve():
        return sys.argv[0] if sys.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert str(broken_wrapper) not in exec_line
    assert exec_line.endswith("-m hermes_cli.main desktop")


def test_probe_accepts_shell_launcher_wrapper(tmp_path, xdg_home, monkeypatch):
    """A bash launcher is safe by construction and still wins the probe."""
    root = _make_project(tmp_path)
    repo_script = root / "hermes"
    repo_script.write_text(
        "#!/usr/bin/env python3\nimport hermes_cli\n", encoding="utf-8"
    )
    repo_script.chmod(0o755)

    good_wrapper = xdg_home / ".local" / "bin" / "hermes"
    good_wrapper.parent.mkdir(parents=True)
    good_wrapper.write_text(
        f"#!/usr/bin/env bash\nexec {root / 'venv' / 'bin' / 'python'} "
        f'{root / "hermes"} "$@"\n',
        encoding="utf-8",
    )
    good_wrapper.chmod(0o755)
    monkeypatch.setenv("HOME", str(xdg_home))
    _argv0_context(monkeypatch, str(repo_script))
    monkeypatch.setattr("shutil.which", lambda name: None)

    def fake_resolve():
        return sys.argv[0] if sys.argv[0] else None

    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", fake_resolve)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]
    assert exec_line == f"{good_wrapper} desktop"


def test_install_icon_handles_truncated_png_header(tmp_path, xdg_home, monkeypatch):
    """A truncated PNG (valid signature + IHDR tag, <24 bytes) must not
    raise struct.error out of the fail-safe: it lands in 256x256/ like
    any other unknown-size raster."""
    root = _make_project(tmp_path)
    icon = lde.icon_path(root)
    icon.write_bytes(
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\x0dIHDR\x00\x00"  # 22 bytes
    )
    hermes_bin = tmp_path / "bin" / "hermes"
    hermes_bin.parent.mkdir()
    hermes_bin.write_text("", encoding="utf-8")
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin", lambda: str(hermes_bin)
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)

    values = _parse(entry.read_text(encoding="utf-8"))
    assert values["Icon"] == "hermes"
    dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
    assert dest.is_file()


def test_hicolor_subdir_puts_rasters_in_indexed_dirs_never_scalable():
    """Panel lookup uses fixed sizes. scalable/ is SVG-only."""
    assert lde._hicolor_subdir(None) == "256x256"
    assert lde._hicolor_subdir((1024, 1024)) == "256x256"
    assert lde._hicolor_subdir((512, 512)) == "512x512"
    assert lde._hicolor_subdir((256, 256)) == "256x256"
    assert lde._hicolor_subdir((48, 48)) == "48x48"
    assert lde._hicolor_subdir((24, 24)) == "24x24"
    assert lde._hicolor_subdir((64, 32)) == "256x256"


def test_install_places_1024_png_in_256x256_not_scalable(
    tmp_path, xdg_home, monkeypatch
):
    """The shipped desktop asset is 1024×1024. A PNG in scalable/ is what
    Cinnamon's panel rasterizes as a mangled low-res icon."""
    root = _make_project(tmp_path)
    lde.icon_path(root).write_bytes(_png_ihdr(1024, 1024))
    _stub_install(tmp_path, monkeypatch)

    entry = lde.install_desktop_entry(root)
    values = _parse(entry.read_text(encoding="utf-8"))

    dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
    stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
    assert values["Icon"] == "hermes"
    assert dest.is_file()
    assert dest.read_bytes() == lde.icon_path(root).read_bytes()
    assert not stale.exists()


def test_install_removes_stale_scalable_png(tmp_path, xdg_home, monkeypatch):
    """v2026.8.31 wrote the PNG into scalable/. A later hermes desktop
    must delete that leftover so Cinnamon does not keep using it."""
    root = _make_project(tmp_path)
    lde.icon_path(root).write_bytes(_png_ihdr(1024, 1024))
    _stub_install(tmp_path, monkeypatch)

    stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
    stale.parent.mkdir(parents=True)
    stale.write_bytes(b"old scalable png")

    lde.install_desktop_entry(root)

    dest = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
    assert dest.is_file()
    assert not stale.exists()


def test_install_exact_48_png_uses_48x48_dir(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    lde.icon_path(root).write_bytes(_png_ihdr(48, 48))
    _stub_install(tmp_path, monkeypatch)

    lde.install_desktop_entry(root)

    dest = xdg_home / "icons" / "hicolor" / "48x48" / "apps" / "hermes.png"
    assert dest.is_file()
    assert not (
        xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
    ).exists()


def test_install_resizes_decodable_png_to_panel_sizes(
    tmp_path, xdg_home, monkeypatch
):
    """A decodeable PNG is Lanczos-resized so the 24px slot is actually 24px."""
    from PIL import Image

    root = _make_project(tmp_path)
    im = Image.new("RGBA", (64, 64), (255, 255, 255, 255))
    for x in range(16, 48):
        for y in range(16, 48):
            im.putpixel((x, y), (0, 0, 0, 255))
    buf = io.BytesIO()
    im.save(buf, format="PNG")
    lde.icon_path(root).write_bytes(buf.getvalue())
    _stub_install(tmp_path, monkeypatch)

    lde.install_desktop_entry(root)

    dest_24 = xdg_home / "icons" / "hicolor" / "24x24" / "apps" / "hermes.png"
    dest_256 = xdg_home / "icons" / "hicolor" / "256x256" / "apps" / "hermes.png"
    stale = xdg_home / "icons" / "hicolor" / "scalable" / "apps" / "hermes.png"
    assert dest_24.is_file()
    assert dest_256.is_file()
    assert not stale.exists()
    assert struct.unpack(">II", dest_24.read_bytes()[16:24]) == (24, 24)
    assert struct.unpack(">II", dest_256.read_bytes()[16:24]) == (256, 256)


def test_deferred_install_skips_heal_after_exit_without_reveal():
    """Electron exiting without ever revealing a window (boot crash, --version, early quit) must
    NOT heal the entry: gnome-shell keeps the ShellApp in STARTING until the startup-notification
    sequence completes or times out, not until the process dies, so a write right after the exit
    is exactly the #111906 arming condition. The next terminal/updater or revealed launch heals."""
    calls: list[Path] = []
    deferred = lde.DeferredDesktopEntryInstall(
        Path("/proj"), install=lambda root: calls.append(root) or Path("/entry"), settle_seconds=0
    )
    deferred.start()
    time.sleep(0.05)
    assert calls == []  # nothing is written while the app may still be STARTING

    deferred.finish()
    assert calls == []
    assert not deferred._thread.is_alive()


# --- desktop-capability of the persisted Exec (managed runtime env launchers) ------------------


def _write_script(path: Path, text: str) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    path.chmod(0o755)
    return path


def _env_shaped_tree(tmp_path: Path, name: str = "managed-env") -> Path:
    """The runtime env layout: a code tree (workspace/hermes_cli) with no desktop app in it."""
    tree = tmp_path / name
    (tree / "workspace" / "hermes_cli").mkdir(parents=True)
    (tree / "workspace" / "hermes_cli" / "main.py").write_text("", encoding="utf-8")
    _write_script(tree / "venv" / "bin" / "hermes", "#!/bin/sh\nexit 0\n")
    python_bin = tree / "venv" / "bin" / "python"
    python_bin.write_bytes(b"\x7fELF fake")  # real interpreters are binaries, never scripts
    python_bin.chmod(0o755)
    return tree


def _full_tree(tmp_path: Path, name: str = "install") -> Path:
    """A checkout that carries the desktop app beside hermes_cli."""
    tree = tmp_path / name
    (tree / "hermes_cli").mkdir(parents=True)
    (tree / "apps" / "desktop" / "assets").mkdir(parents=True)
    (tree / "apps" / "desktop" / "package.json").write_text("{}", encoding="utf-8")
    _write_script(tree / "venv" / "bin" / "hermes", "#!/bin/sh\nexit 0\n")
    python_bin = tree / "venv" / "bin" / "python"
    python_bin.write_bytes(b"\x7fELF fake")  # real interpreters are binaries, never scripts
    python_bin.chmod(0o755)
    return tree


def test_capability_rejects_env_shaped_launcher_and_accepts_full_tree(tmp_path):
    env_tree = _env_shaped_tree(tmp_path)
    full_tree = _full_tree(tmp_path)
    assert lde._can_serve_desktop(str(env_tree / "venv" / "bin" / "hermes")) is False
    assert lde._can_serve_desktop(str(full_tree / "venv" / "bin" / "hermes")) is True
    # Interpreters are not judged: a launcher execing one is followed to its own tree instead.
    assert lde._can_serve_desktop(str(env_tree / "venv" / "bin" / "python")) is None
    assert lde._can_serve_desktop(str(full_tree / "venv" / "bin" / "python")) is None


def test_capability_unknown_shapes_pass(tmp_path):
    missing = tmp_path / "nowhere" / "bin" / "hermes"
    assert lde._can_serve_desktop(str(missing)) is None
    native = _write_script(tmp_path / "opt" / "bin" / "hermes", "")
    assert lde._can_serve_desktop(str(native)) is None


def test_capability_follows_wrapper_to_its_target(tmp_path):
    env_tree = _env_shaped_tree(tmp_path)
    full_tree = _full_tree(tmp_path)
    bad_wrapper = _write_script(
        tmp_path / "shims" / "hermes",
        f'#!/usr/bin/env bash\nexec "{env_tree}/venv/bin/hermes" "$@"\n',
    )
    good_wrapper = _write_script(
        tmp_path / "shims" / "hermes-good",
        f'#!/usr/bin/env bash\nexec "{full_tree}/venv/bin/hermes" "$@"\n',
    )
    assert lde._can_serve_desktop(str(bad_wrapper)) is False
    assert lde._can_serve_desktop(str(good_wrapper)) is True


def test_resolver_skips_incapable_primary_for_wrapper(tmp_path, xdg_home):
    """A PATH-first launcher from an env-shaped tree must not win over the durable wrapper."""
    env_tree = _env_shaped_tree(tmp_path)
    checkout = _full_tree(tmp_path, "checkout")
    wrapper = _write_script(
        tmp_path / ".local" / "bin" / "hermes",
        f'#!/usr/bin/env bash\nexec "{checkout}/venv/bin/hermes" "$@"\n',
    )
    resolution = lde._resolve_hermes_bin_for_desktop_entry(
        resolve_fn=lambda: str(env_tree / "venv" / "bin" / "hermes"),
        checkout_root=checkout,
    )
    assert resolution == str(wrapper)


def test_install_skips_write_when_exec_provably_cannot_serve_desktop(tmp_path, xdg_home, monkeypatch):
    """The fallback must not create a dead entry: no entry on disk stays that way."""
    root = _make_project(tmp_path)
    env_tree = _env_shaped_tree(tmp_path)
    monkeypatch.setattr(lde, "_launcher_entry_management_enabled", lambda: True)
    monkeypatch.setattr(
        lde,
        "resolve_exec_command",
        lambda project_root=None: f"{env_tree / 'venv' / 'bin' / 'hermes'} desktop",
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    assert lde.install_desktop_entry(root) is None
    assert not lde.desktop_entry_path().exists()


def test_install_leaves_existing_entry_untouched_when_exec_incapable(tmp_path, xdg_home, monkeypatch):
    """A provably dead Exec skips instead of churning an entry that is already on disk."""
    root = _make_project(tmp_path)
    env_tree = _env_shaped_tree(tmp_path)
    entry_path = lde.desktop_entry_path()
    entry_path.parent.mkdir(parents=True, exist_ok=True)
    entry_path.write_text("hand-tuned\n", encoding="utf-8")
    monkeypatch.setattr(lde, "_launcher_entry_management_enabled", lambda: True)
    monkeypatch.setattr(
        lde,
        "resolve_exec_command",
        lambda project_root=None: f"{env_tree / 'venv' / 'bin' / 'hermes'} desktop",
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    assert lde.install_desktop_entry(root) is None
    assert entry_path.read_text(encoding="utf-8") == "hand-tuned\n"


def test_module_form_passes_the_gate(tmp_path):
    """The module fallback is not second-guessed: only a process that already passed the desktop
    launch checks (or the bundled payload) ever writes it."""
    env_tree = _env_shaped_tree(tmp_path)
    module_form = f'"{env_tree / "venv" / "bin" / "python"}" -m hermes_cli.main desktop'
    assert lde._persisted_exec_serves_desktop(module_form) is None


def test_install_through_wrapper_when_primary_is_incapable(tmp_path, xdg_home, monkeypatch):
    """End to end: the PATH-first env launcher is skipped and the wrapper's Exec is persisted."""
    root = _full_tree(tmp_path, "checkout")
    (root / "apps" / "desktop" / "assets" / "icon.png").write_bytes(b"\x89PNG fake")
    env_tree = _env_shaped_tree(tmp_path)
    wrapper = _write_script(
        tmp_path / ".local" / "bin" / "hermes",
        f'#!/usr/bin/env bash\nexec "{root}/venv/bin/hermes" "$@"\n',
    )
    monkeypatch.setattr(lde, "_launcher_entry_management_enabled", lambda: True)
    monkeypatch.setattr(
        "hermes_cli.relaunch.resolve_hermes_bin",
        lambda: str(env_tree / "venv" / "bin" / "hermes"),
    )
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    assert entry == lde.desktop_entry_path()
    assert _parse(entry.read_text(encoding="utf-8"))["Exec"] == f"{wrapper} desktop"


# #126009: the persisted launcher is a menu/taskbar click — a launch, not a
# build request. With a packaged Electron app already present, the Exec line
# must carry --skip-build (start the packaged app in seconds) instead of the
# build-then-launch default, whose source-hash freshness check reports
# "stale" on any locally modified tree and pays a 60s+ rebuild per click.
def test_exec_appends_skip_build_when_packaged_app_exists(tmp_path, xdg_home, monkeypatch):
    root = _make_project(tmp_path)
    unpacked = root / "apps" / "desktop" / "release" / "linux-unpacked"
    unpacked.mkdir(parents=True)
    (unpacked / "hermes").write_text("", encoding="utf-8")
    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line.endswith("desktop --skip-build")


def test_exec_keeps_build_then_launch_when_no_packaged_app(tmp_path, xdg_home, monkeypatch):
    # First install (nothing packaged yet): the click must still build and
    # launch — --skip-build would exit with "no packaged desktop app found".
    root = _make_project(tmp_path)
    monkeypatch.setattr("hermes_cli.relaunch.resolve_hermes_bin", lambda: None)
    monkeypatch.setattr(lde, "refresh_desktop_databases", lambda _dir: [])

    entry = lde.install_desktop_entry(root)
    exec_line = _parse(entry.read_text(encoding="utf-8"))["Exec"]

    assert exec_line.endswith("desktop")
    assert "--skip-build" not in exec_line
