#!/usr/bin/env python3
"""Display-free single-instance regression. Never uses personal settings or a microphone.

One Hex process owns a data root. A second launch forwards to it and exits;
`hex` commands forward over the same socket and fail fast without it.
"""
import json
import os
from pathlib import Path
import socket
import stat
import struct
import subprocess
import sys
import tempfile
import time

binary = str(Path(sys.argv[1]).resolve())


def wait_for(check, timeout=8):
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        result = check()
        if result:
            return result
        time.sleep(0.025)
    raise AssertionError("timed out waiting for Hex")


with tempfile.TemporaryDirectory(prefix="hex-instance-", dir=os.environ.get("TMPDIR")) as directory:
    root = Path(directory)
    support = root / "data"
    support.mkdir(mode=0o700)
    runtime = root / "runtime"
    runtime.mkdir(mode=0o700)
    # A custom root must not discover a display it wasn't explicitly given.
    wayland = socket.socket(socket.AF_UNIX)
    wayland.bind(str(runtime / "wayland-test"))
    wayland.listen(8)
    # Never listen: no microphone, no shortcuts portal registration.
    (support / "settings.json").write_text(json.dumps({"enable_on_launch": False}))
    alsa = root / "alsa.conf"
    alsa.write_text("# No physical audio devices in this test.\n")
    # Custom data roots must never start, stop, or restart the user's real unit.
    fakebin = root / "bin"
    fakebin.mkdir()
    marker = root / "unexpected-systemctl"
    fake = fakebin / "systemctl"
    fake.write_text(f'#!/bin/sh\ntouch "{marker}"\necho loaded\nexit 0\n')
    fake.chmod(0o755)
    env = os.environ | {
        "HEX_APPLICATION_SUPPORT_DIR": str(support),
        # Hex tidies its launcher and autostart entries at startup.
        "XDG_DATA_HOME": str(root / "xdg-data"),
        "XDG_CONFIG_HOME": str(root / "xdg-config"),
        "XDG_RUNTIME_DIR": str(runtime),
        # A stale Xauthority must not query the user's manager either.
        "XAUTHORITY": str(root / "missing-xauthority"),
        "ALSA_CONFIG_PATH": str(alsa),
        "PATH": f'{fakebin}:{os.environ["PATH"]}',
    }
    # No display: a window request fails the window, never Hex.
    for key in ("DISPLAY", "WAYLAND_DISPLAY", "HEX_SYSTEMD_UNIT", "NOTIFY_SOCKET"):
        env.pop(key, None)
    path = support / "service.sock"
    children = []

    def spawn(*arguments):
        child = subprocess.Popen([binary, *arguments], env=env, stdin=subprocess.DEVNULL,
                                 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        children.append(child)
        return child

    def run(*arguments, timeout=10):
        return subprocess.run([binary, *arguments], env=env, capture_output=True, text=True,
                              timeout=timeout)

    def connection():
        client = socket.socket(socket.AF_UNIX)
        client.settimeout(4)
        client.connect(str(path))
        return client

    def exact(client, size):
        data = b""
        while len(data) < size:
            chunk = client.recv(size - len(data))
            assert chunk, "Hex disconnected"
            data += chunk
        return data

    def call(client, request="Status", version=2):
        body = json.dumps({"version": version, "request": request}).encode()
        client.sendall(struct.pack(">I", len(body)) + body)
        size, = struct.unpack(">I", exact(client, 4))
        return json.loads(exact(client, size))

    def status():
        try:
            with connection() as client:
                return call(client)["status"]
        except (OSError, AssertionError):
            return None

    try:
        # Without an instance every forwarded command fails fast.
        for command in (["status"], ["dictation", "start"], ["paste-last"]):
            started = time.monotonic()
            missing = run(*command)
            assert missing.returncode != 0 and "not running" in missing.stderr, missing.stderr
            assert time.monotonic() - started < 2
        assert run("quit").returncode == 3, "quitting nothing says Hex isn't running"

        # `hex app --hidden` with no unit becomes the instance in this process.
        instance = spawn("app", "--hidden")
        state = wait_for(status)
        assert state["pid"] == instance.pid
        assert state["window_open"] is False
        assert state["desktop"]["listener"]["status"] == "Disabled"
        assert stat.S_IMODE(path.stat().st_mode) == 0o600
        assert stat.S_IMODE(support.stat().st_mode) == 0o700

        # A second launch forwards and exits 0, leaving the same instance;
        # `hex start` finds it running.
        for command in (["start"], ["app", "--hidden"], ["service"]):
            forwarded = run(*command)
            assert forwarded.returncode == 0, (command, forwarded.stderr)
            assert status()["pid"] == instance.pid

        # Activate reaches the window seam; without a display only the window fails.
        token = run("app")
        assert token.returncode == 0, token.stderr
        time.sleep(0.5)
        assert instance.poll() is None
        assert status()["pid"] == instance.pid

        with connection() as client:
            reply = call(client, {"Activate": {
                "activation": {"token": "test-token", "startup_id": None},
                "show": False,
                "session": state["session"],
            }})
            assert reply["error"] is None and reply["status"]["pid"] == instance.pid

        printed = run("status", "--json")
        assert printed.returncode == 0, printed.stderr
        printed = json.loads(printed.stdout)
        assert printed["status"] == "status" and printed["pid"] == instance.pid
        assert printed["listener"]["running"] is False
        # Control commands ride the same socket.
        assert run("settings", "set", "sound_effects", "off").returncode == 0
        assert run("settings", "get", "sound_effects").stdout.strip() == "false"
        refused = run("window", "story", "HUD")
        assert refused.returncode == 1 and "macOS-only" in refused.stderr, refused.stderr

        # Keybinding commands need a listening Hex; they fail fast without one,
        # with one line rather than an error report.
        refused = run("dictation", "toggle")
        assert refused.returncode != 0 and "not listening" in refused.stderr, refused.stderr
        assert refused.stderr.startswith("hex: ") and refused.stderr.count("\n") == 1, refused.stderr

        # Version 1 (service/Settings) clients get a version error, not a hang.
        with connection() as client:
            reply = call(client, "Snapshot", version=1)
            assert reply["version"] == 2 and reply["error"]
        with connection() as client:
            client.sendall(struct.pack(">I", 128 * 1024 + 1))
            assert client.recv(1) == b""
        # A stalled request cannot stall other clients.
        with connection() as stalled:
            stalled.sendall(b"\x00")
            assert status()["pid"] == instance.pid
            assert stalled.recv(1) == b""

        # A crash leaves a socket, not a permanent lock.
        instance.kill()
        instance.wait(timeout=5)
        assert path.exists()
        missing = run("paste-last")
        assert missing.returncode != 0 and "not running" in missing.stderr, missing.stderr
        assert missing.stderr.count("\n") == 1, missing.stderr

        # Two racing launches produce one instance; the other forwards and exits 0.
        first, second = spawn("app", "--hidden"), spawn("app", "--hidden")
        state = wait_for(status)
        owners = {first.pid, second.pid}
        assert state["pid"] in owners
        loser = second if state["pid"] == first.pid else first
        assert loser.wait(timeout=8) == 0
        instance = first if loser is second else second

        # Quit drains and exits 0, removing the socket and keeping settings.
        assert run("quit").returncode == 0
        assert instance.wait(timeout=8) == 0
        assert not path.exists()
        assert (support / "settings.json").exists()

        # The service entry point also stays headless with an answering
        # Wayland socket nearby, rather than adopting the user's desktop.
        instance = spawn("service")
        state = wait_for(status)
        assert state["pid"] == instance.pid
        assert state["session"]["display"] == "", state["session"]
        assert run("quit").returncode == 0
        assert instance.wait(timeout=8) == 0

        for command in (["quit"], ["dictation", "start"], ["paste-last"]):
            assert run(*command).returncode == 3, command
        assert not marker.exists(), "a custom data root ran systemctl"
    finally:
        wayland.close()
        for child in children:
            if child.poll() is None:
                child.kill()
            child.wait(timeout=5)

print("Linux single-instance checks passed.")