"""``hermes desktop`` from inside a BUNDLED desktop payload.

A bundled artifact ships the CLI inside ``<app>/resources/agent-payload``
and the desktop app is the artifact itself — there is no source tree to
build and the app resources are signed and read-only. These tests pin the
two halves of that: where the launcher is (hermes_cli.bundled_app) and
that cmd_gui starts it instead of running the checkout build ladder.

The layout resolver is pure path arithmetic over real directories, so
every platform's shape is exercised from any host.
"""

import json
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import patch

import pytest

import hermes_cli.main as cli_main
from hermes_cli.bundled_app import (
    NotBundledApp,
    launch_detached,
    resolve_bundle_layout,
)
from hermes_cli.steward import is_bundled_payload

STAMP = {
    "schemaVersion": 2,
    "commit": "a" * 40,
    "commitDate": 1750000000,
    "branch": None,
    "builtAt": "2026-08-21T00:00:00+00:00",
    "dirty": False,
    "source": "ci",
    "distribution": "desktop-app",
    "updateMechanism": "electron-updater",
    "baseVersion": "0.27.0",
    "displayVersion": "0.27.0",
    "distance": 0,
    "payload": "bundled",
    "tag": "v0.27.0",
}


def _exe(path: Path) -> Path:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_bytes(b"MZ")
    path.chmod(0o755)
    return path


def _payload(app_root: Path, *, resources_name: str = "resources") -> Path:
    """Stage a payload repo under *app_root*; returns the repo root."""
    repo = app_root / resources_name / "agent-payload" / "repo"
    # repo/apps/desktop survives prunePayload(), which is exactly why a
    # package.json probe cannot tell a bundle from a checkout.
    (repo / "apps" / "desktop").mkdir(parents=True)
    (repo / "apps" / "desktop" / "package.json").write_text('{"name":"desktop"}\n')
    (repo / "install-stamp.json").write_text(json.dumps(STAMP) + "\n", encoding="utf-8")
    return repo


def _linux_bundle(tmp_path: Path) -> Path:
    app = tmp_path / "linux-unpacked"
    repo = _payload(app)
    _exe(app / "Hermes")
    # The helpers a Linux Electron tree ships beside its launcher.
    _exe(app / "chrome-sandbox")
    _exe(app / "chrome_crashpad_handler")
    _exe(app / "libffmpeg.so")
    (app / "version").write_text("40.9.3\n")
    return repo


def _windows_bundle(tmp_path: Path) -> Path:
    app = tmp_path / "Hermes"
    repo = _payload(app)
    _exe(app / "Hermes.exe")
    _exe(app / "Uninstall Hermes.exe")
    return repo


def _macos_bundle(tmp_path: Path) -> Path:
    app = tmp_path / "Hermes.app"
    repo = _payload(app / "Contents", resources_name="Resources")
    _exe(app / "Contents" / "MacOS" / "Hermes")
    return repo


def _host_bundle(tmp_path: Path) -> tuple[Path, Path]:
    """A bundle in THIS host's shape, plus its expected launcher.

    cmd_gui resolves the launcher with the real sys.platform, and the
    Linux rule depends on POSIX execute bits that do not exist on Windows
    — so the cmd_gui tests must stage the host's own shape.
    """
    if sys.platform == "win32":
        repo = _windows_bundle(tmp_path)
        return repo, tmp_path / "Hermes" / "Hermes.exe"
    if sys.platform == "darwin":
        return _macos_bundle(tmp_path), tmp_path / "Hermes.app/Contents/MacOS/Hermes"
    repo = _linux_bundle(tmp_path)
    return repo, tmp_path / "linux-unpacked" / "Hermes"


class TestResolveBundleLayout:
    @pytest.mark.platforms("posix")  # POSIX-only: the Linux launcher rule reads execute bits, which os.access reports as always-set on Windows
    def test_linux_launcher_is_the_one_non_helper_executable(self, tmp_path):
        repo = _linux_bundle(tmp_path)
        layout = resolve_bundle_layout(repo, platform="linux")
        assert layout.app_root == tmp_path / "linux-unpacked"
        assert layout.payload.name == "agent-payload"
        assert layout.launcher == tmp_path / "linux-unpacked" / "Hermes"

    def test_windows_launcher_is_never_the_uninstaller(self, tmp_path):
        repo = _windows_bundle(tmp_path)
        layout = resolve_bundle_layout(repo, platform="win32")
        assert layout.launcher == tmp_path / "Hermes" / "Hermes.exe"

    def test_macos_app_root_climbs_out_of_contents_resources(self, tmp_path):
        repo = _macos_bundle(tmp_path)
        layout = resolve_bundle_layout(repo, platform="darwin")
        assert layout.app_root == tmp_path / "Hermes.app"
        assert layout.resources == tmp_path / "Hermes.app" / "Contents" / "Resources"
        assert layout.launcher == tmp_path / "Hermes.app" / "Contents" / "MacOS" / "Hermes"

    def test_ambiguous_launcher_reports_none_rather_than_guessing(self, tmp_path):
        repo = _windows_bundle(tmp_path)
        _exe(repo.parent.parent.parent / "Other.exe")
        assert resolve_bundle_layout(repo, platform="win32").launcher is None

    def test_a_checkout_is_not_a_bundle(self, tmp_path):
        checkout = tmp_path / "hermes-agent"
        (checkout / "apps" / "desktop").mkdir(parents=True)
        with pytest.raises(NotBundledApp):
            resolve_bundle_layout(checkout)

    def test_a_sealed_tree_with_no_app_is_not_a_bundle(self, tmp_path):
        # docker/nix shape: sealed repo, no agent-payload parent.
        repo = tmp_path / "opt" / "hermes" / "repo"
        repo.mkdir(parents=True)
        with pytest.raises(NotBundledApp):
            resolve_bundle_layout(repo)


class TestShapePredicate:
    """cmd_gui and the bundle build must agree about what a bundle is."""

    def test_a_stamped_payload_is_bundled(self, tmp_path):
        assert is_bundled_payload(_linux_bundle(tmp_path))

    def test_a_surviving_desktop_package_json_does_not_make_a_bundle(self, tmp_path):
        """repo/apps/desktop survives the payload prune, so a filesystem
        probe cannot be the authority — only the stamp can."""
        checkout = tmp_path / "hermes-agent"
        (checkout / "apps" / "desktop").mkdir(parents=True)
        (checkout / "apps" / "desktop" / "package.json").write_text("{}\n")
        assert not is_bundled_payload(checkout)

    def test_a_bootstrap_artifact_is_not_bundled(self, tmp_path):
        root = tmp_path / "hermes-agent"
        root.mkdir()
        stamp = dict(STAMP, payload="bootstrap", tag=None)
        (root / "install-stamp.json").write_text(json.dumps(stamp) + "\n")
        assert not is_bundled_payload(root)

    def test_an_unreadable_stamp_is_not_bundled(self, tmp_path):
        root = tmp_path / "hermes-agent"
        root.mkdir()
        (root / "install-stamp.json").write_text("{ not json\n")
        assert not is_bundled_payload(root)


class TestLaunchDetached:
    def test_detach_flags_and_unowned_stdio_are_forwarded(self):
        seen = {}

        def fake_popen(argv, **kwargs):
            seen["argv"] = argv
            seen["kwargs"] = kwargs
            return SimpleNamespace(pid=1234)

        with patch("hermes_cli.bundled_app.subprocess.Popen", side_effect=fake_popen):
            pid = launch_detached(["/app/Hermes", "--no-sandbox"], cwd="/app")

        assert pid == 1234
        assert seen["argv"] == ["/app/Hermes", "--no-sandbox"]
        assert seen["kwargs"]["cwd"] == "/app"
        assert seen["kwargs"]["stdout"] is subprocess.DEVNULL
        assert seen["kwargs"]["stderr"] is subprocess.DEVNULL
        assert seen["kwargs"]["stdin"] is subprocess.DEVNULL
        # POSIX detaches with a new session; Windows with creation flags.
        detach_key = "creationflags" if sys.platform == "win32" else "start_new_session"
        assert seen["kwargs"][detach_key]

    def test_a_real_detached_child_runs(self, tmp_path):
        """The one E2E rung: spawn a real process and prove it ran."""
        marker = tmp_path / "ran.txt"
        pid = launch_detached(
            [sys.executable, "-c", f"open({str(marker)!r}, 'w').write('yes')"]
        )
        assert pid > 0
        deadline = 10.0
        import time

        while deadline > 0 and not marker.exists():
            time.sleep(0.1)
            deadline -= 0.1
        assert marker.read_text() == "yes"


@pytest.mark.platforms("windows", "linux", "macos")
class TestCmdGuiOnABundle:
    """The behavior the checkout ladder got wrong: never build in a bundle."""

    @staticmethod
    def _args(**overrides):
        base = dict(
            source=False, build_only=False, fake_boot=False, ignore_existing=False,
            hermes_root=None, cwd=None, skip_build=False, force_build=False,
        )
        base.update(overrides)
        return SimpleNamespace(**base)

    @staticmethod
    def _run(monkeypatch, repo: Path, args, *, launcher_ok: bool = True):
        """Run cmd_gui against *repo*, recording builds and launches."""
        builds: list[list[str]] = []
        launches: list[list[str]] = []

        def record_run(cmd, *a, **kw):
            builds.append([str(c) for c in cmd])
            return subprocess.CompletedProcess(cmd, 0)

        def record_popen(argv, **kw):
            launches.append([str(a) for a in argv])
            return SimpleNamespace(pid=4242)

        from hermes_cli import main_desktop, source_build
        import pm
        monkeypatch.setattr(cli_main, "PROJECT_ROOT", repo)
        monkeypatch.setattr(source_build, "source_build_env", lambda env, **kwargs: dict(env))
        monkeypatch.setattr(pm, "ensure", lambda name, *, base_env: SimpleNamespace(env=base_env))
        monkeypatch.setattr(main_desktop, "_desktop_build_needed", lambda *a, **k: True)
        monkeypatch.setattr(main_desktop, "_stop_desktop_processes_locking_build", lambda *a, **k: [])
        monkeypatch.setattr(main_desktop, "_desktop_linux_sandbox_fixup", lambda *a, **k: launcher_ok)
        monkeypatch.setattr(main_desktop, "_desktop_linux_needs_no_sandbox", lambda: not launcher_ok)
        monkeypatch.setattr(main_desktop, "_desktop_linux_sandbox_helper_is_regular_file", lambda *a, **k: True)
        monkeypatch.setattr(main_desktop, "_detect_linux_password_store", lambda: None)

        with patch("hermes_cli.bundled_app.subprocess.Popen", side_effect=record_popen), \
             patch.object(cli_main.subprocess, "run", side_effect=record_run), \
             pytest.raises(SystemExit) as exit_info:
            cli_main.cmd_gui(args)

        return exit_info.value.code, builds, launches

    def test_launches_the_bundle_and_builds_nothing(self, tmp_path, monkeypatch):
        repo, launcher = _host_bundle(tmp_path)
        code, builds, launches = self._run(monkeypatch, repo, self._args())

        assert code == 0
        # THE regression: repo/apps/desktop/package.json exists in a bundle,
        # so the old probe fell through to npm ci + npm run pack inside the
        # app's own signed resources.
        assert builds == []
        assert launches == [[str(launcher)]]

    def test_explicit_profile_reaches_the_bundled_launcher(self, tmp_path, monkeypatch):
        """`hermes -p work desktop` on a bundle hands Electron --profile work."""
        repo, launcher = _host_bundle(tmp_path / "app")
        # HERMES_HOME outside the platform home is the root itself on every OS.
        root = tmp_path / "hermes-root"
        profile = root / "profiles" / "work"
        profile.mkdir(parents=True)
        (profile / "config.yaml").write_text("{}\n", encoding="utf-8")
        monkeypatch.setenv("HERMES_HOME", str(root))
        monkeypatch.setattr(sys, "argv", ["hermes", "-p", "work", "desktop"])
        monkeypatch.setattr(cli_main, "_explicit_cli_profile", None)
        cli_main._apply_profile_override()

        code, builds, launches = self._run(monkeypatch, repo, self._args())

        assert code == 0
        assert builds == []
        assert launches == [[str(launcher), "--profile", "work"]]

    def test_a_checkout_still_takes_the_build_ladder(self, tmp_path, monkeypatch):
        checkout = tmp_path / "hermes-agent"
        (checkout / "apps" / "desktop").mkdir(parents=True)
        (checkout / "apps" / "desktop" / "package.json").write_text("{}\n")
        code, builds, launches = self._run(monkeypatch, checkout, self._args())

        assert launches == []
        assert any("npm" in " ".join(cmd) for cmd in builds), builds

    @pytest.mark.parametrize("flag", ["source", "build_only", "force_build"])
    def test_build_flags_are_refused_rather_than_ignored(self, tmp_path, monkeypatch, flag):
        repo, _launcher = _host_bundle(tmp_path)
        code, builds, launches = self._run(
            monkeypatch, repo, self._args(**{flag: True})
        )

        assert code == 2
        assert builds == []
        assert launches == []

    def test_a_stamped_bundle_with_no_app_reports_damage(self, tmp_path, monkeypatch):
        """A bundled stamp over a non-bundle tree must never fall back to
        the build ladder — that would npm-build inside app resources."""
        orphan = tmp_path / "somewhere" / "repo"
        orphan.mkdir(parents=True)
        (orphan / "install-stamp.json").write_text(json.dumps(STAMP) + "\n")
        code, builds, launches = self._run(monkeypatch, orphan, self._args())

        assert code == 1
        assert builds == []
        assert launches == []

    def test_a_bundle_with_no_resolvable_launcher_reports_damage(self, tmp_path, monkeypatch):
        app = tmp_path / "linux-unpacked"
        repo = _payload(app)
        _exe(app / "Hermes")
        _exe(app / "Hermes-Other")
        code, builds, launches = self._run(monkeypatch, repo, self._args())

        assert code == 1
        assert builds == []
        assert launches == []
