#!/usr/bin/env bash

# Project sessions: the windows you open together, opened together.
#
# Three things here are easy to get wrong and were, each caught by running it
# rather than reading it:
#
#   * A terminal's directory is not the terminal's own working directory. That
#     is where the terminal was launched from; the shell inside it is what has
#     been cd'd. Reading the wrong one appeared correct for exactly as long as
#     the test terminals happened to have been started from the right place.
#   * gtk-launch cannot place a window. It activates over D-Bus, so the process
#     Hyprland started exits and a [workspace N silent] rule has no child to
#     apply to -- the window lands wherever you were looking.
#   * DBusActivatable applications do the same thing even when launched
#     directly, so the window has to be found and moved afterwards.
#
# The save/open round trip is exercised for real, against a fake HOME and a
# stubbed hyprctl, so no window is opened on the machine running the tests.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-project"
desktop_page="$repo_dir/config/dot/quickshell/modules/settings/DesktopPage.qml"
service="$repo_dir/config/dot/quickshell/services/Projects.qml"

findings=()
note() { findings+=("$1"); }

[[ -x "$helper" ]] || { printf 'projects contract: helper is missing or not executable\n' >&2; exit 1; }

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin" "$work/home"

# A compositor that reports two windows on two workspaces and accepts every
# dispatch, recording what it was asked to do.
cat >"$work/bin/hyprctl" <<'STUB'
#!/usr/bin/env bash
if [[ "$1" == "-j" && "$2" == "clients" ]]; then
    cat "$HYPRCTL_CLIENTS"
    exit 0
fi
if [[ "$1" == "eval" ]]; then
    printf '%s\n' "$2" >>"$HYPRCTL_LOG"
    echo ok
    exit 0
fi
echo "{}"
STUB
chmod +x "$work/bin/hyprctl"

cat >"$work/clients.json" <<'JSON'
[
  {"address":"0x1","class":"kitty","workspace":{"name":"2"},"floating":false,"pid":1},
  {"address":"0x2","class":"org.gnome.Nautilus","workspace":{"name":"3"},"floating":false,"pid":2},
  {"address":"0x3","class":"bitwarden","workspace":{"name":"special:scratch"},"floating":true,"pid":3}
]
JSON

export HYPRCTL_CLIENTS="$work/clients.json"
export HYPRCTL_LOG="$work/dispatch.log"
: >"$HYPRCTL_LOG"

run() { PATH="$work/bin:$PATH" HOME="$work/home" XDG_DATA_HOME="$work/home/.local/share" "$helper" "$@"; }

# ── Saving ───────────────────────────────────────────────────────────────────

saved="$(run save demo)"
[[ "$(jq -r '.ok' <<<"$saved")" == "true" ]] || note "saving a layout failed: $saved"

# Two workspaces, not three: the scratchpad is somewhere things are put aside,
# not part of a layout, and a floating window is not a tiled arrangement.
[[ "$(jq -r '.workspaces' <<<"$saved")" == "2" ]] \
    || note "a layout with 2 tiled workspaces and a scratchpad saved $(jq -r '.workspaces' <<<"$saved") workspaces"
[[ "$(jq -r '.windows' <<<"$saved")" == "2" ]] \
    || note 'the scratchpad or a floating window was recorded as part of the layout'

stored="$work/home/.local/share/panama/projects/demo.json"
[[ -f "$stored" ]] || note 'saving produced no project file'

if [[ -f "$stored" ]]; then
    # Positions, not numbers. A project saved from 2 and 3 must not insist on 2
    # and 3, or opening it lands on top of whatever is there now.
    jq -e '.workspaces | type == "array"' "$stored" >/dev/null 2>&1 \
        || note 'workspaces are not stored as an ordered list of positions'
    jq -e '[.workspaces[].windows[] | has("index")] | any' "$stored" >/dev/null 2>&1 \
        && note 'a workspace number was recorded, so the project would demand specific workspaces'

    # gtk-launch cannot place a window; the Exec line can.
    jq -r '.workspaces[].windows[].exec // ""' "$stored" | grep -q 'gtk-launch' \
        && note 'projects launch through gtk-launch, which activates over D-Bus and cannot be placed on a workspace'
fi

# ── Opening ──────────────────────────────────────────────────────────────────

: >"$HYPRCTL_LOG"
opened="$(run open demo)"
[[ "$(jq -r '.ok' <<<"$opened")" == "true" ]] || note "opening a project failed: $opened"

# Free workspaces. 2 and 3 are occupied by the stub's own windows, so a project
# saved from them must be laid out somewhere else entirely.
targets="$(jq -r '.workspaces | join(",")' <<<"$opened")"
[[ "$targets" == "1,4" ]] \
    || note "a project opened onto [$targets] when 2 and 3 were occupied; it should claim free workspaces"

grep -q 'workspace 1 silent' "$HYPRCTL_LOG" \
    || note 'windows are not launched with a workspace rule'
grep -q 'exec_cmd' "$HYPRCTL_LOG" \
    || note 'the Lua dispatch form is not used, and the plain hyprctl dispatch form is a syntax error on this config'

# ── Terminal directories ─────────────────────────────────────────────────────

# Exercised against a real process tree rather than grepped for. Checking that
# the source mentions a shell name proves only that somebody wrote the word: an
# earlier version of this contract passed against a helper that had been changed
# back to reading the wrong process, because the constant was still there.
#
# The shape below is the shape that matters -- a parent sitting in one directory
# with a shell child in another, which is exactly a terminal you have cd'd
# inside. The answer must be the child's directory.
mkdir -p "$work/parent" "$work/child"
# exec, so the subshell is replaced by python and $! is python's own pid rather
# than a shell that has already gone.
( cd "$work/parent" && exec python3 -c "
import subprocess, time
subprocess.Popen(['bash', '-c', 'sleep 30; :'], cwd='$work/child')
time.sleep(30)
" ) >/dev/null 2>&1 &
parent_pid=$!
sleep 2
if [[ -n "$parent_pid" ]]; then
    answer="$(PATH="$work/bin:$PATH" HOME="$work/home" python3 - "$helper" "$parent_pid" <<'PROBE'
import importlib.machinery, importlib.util, sys
spec = importlib.util.spec_from_loader("pp",
    importlib.machinery.SourceFileLoader("pp", sys.argv[1]))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
print(module.working_directory(int(sys.argv[2])) or "")
PROBE
)"
    [[ "$answer" == "$work/child" ]] \
        || note "a terminal's directory is read as '$answer' rather than the shell's '$work/child' -- the terminal own working directory is where it was launched from, not where you have changed to"
    kill "$parent_pid" 2>/dev/null || true
fi

# -d, not --directory with a space, which kitty accepts and silently ignores.
grep -qE 'TERMINAL_DIRECTORY_FLAG = "-d"' "$helper" \
    || note 'the terminal directory flag is not -d; the space-separated long form is ignored without an error'

# ── Placement has to be verified, not assumed ────────────────────────────────

grep -q 'hl.dsp.window.move' "$helper" \
    || note 'a window that lands on the wrong workspace is never moved, so DBusActivatable applications stay where they appeared'

# ── The page ─────────────────────────────────────────────────────────────────

grep -q 'Projects.projects' "$desktop_page" \
    || note 'the Desktop page does not list saved projects'
grep -q 'confirmingProject' "$desktop_page" \
    || note 'a project can be deleted without confirming'
grep -q '"list"' "$service" \
    || note 'the settings service never reads what is saved'

if (( ${#findings[@]} > 0 )); then
    mapfile -t findings < <(printf '%s\n' "${findings[@]}" | sort -u)
    printf 'projects contract: %d finding(s)\n' "${#findings[@]}" >&2
    printf '  - %s\n' "${findings[@]}" >&2
    exit 1
fi

printf 'projects contract: PASS\n'
