#!/usr/bin/env bash

# Desktop style stays split across three boundaries that can all be checked
# without starting the shell: the read-only icon catalog, the preference/schema
# wiring, and the QML surfaces that consume it. The catalog runs only against
# disposable XDG roots so this contract never reads or changes the live desktop.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-desktop-style"
service="$repo_dir/config/dot/quickshell/services/DesktopStyle.qml"
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
appearance="$repo_dir/config/dot/quickshell/modules/settings/AppearancePage.qml"
mouse="$repo_dir/config/dot/quickshell/modules/settings/MousePage.qml"
accessibility="$repo_dir/config/dot/quickshell/services/Accessibility.qml"
search="$repo_dir/config/dot/quickshell/services/SettingsSearch.qml"
env_lua="$repo_dir/config/dot/hypr/env.lua"
looks_lua="$repo_dir/config/dot/hypr/looks.lua"

fail() {
    printf 'desktop style contract: %s\n' "$1" >&2
    exit 1
}

for file in "$helper" "$service" "$schema" "$appearance" "$mouse" \
    "$accessibility" "$search" "$env_lua" "$looks_lua"; do
    [[ -f "$file" ]] || fail "missing ${file#"$repo_dir/"}"
done
[[ -x "$helper" ]] || fail 'desktop-style catalog is not executable'

# ── Read-only catalog semantics ─────────────────────────────────────────────
fixture="$(mktemp -d /tmp/panama-desktop-style.XXXXXX)"
trap 'rm -rf "$fixture"' EXIT

home="$fixture/home"
data_home="$fixture/data-home"
data_one="$fixture/data-one"
data_two="$fixture/data-two"
mkdir -p "$home" \
    "$data_home/icons/CursorOnly/cursors" \
    "$data_one/icons/IconOnly/16x16/apps" \
    "$data_one/icons/Both/cursors" \
    "$data_one/icons/Both/scalable/apps" \
    "$data_one/icons/EmptyDirectories" \
    "$data_two/icons/Both/cursors" \
    "$data_two/icons/Both/scalable/apps" \
    "$data_two/icons/NoIndex/16x16/apps"

cat >"$data_home/icons/CursorOnly/index.theme" <<'EOF'
[Icon Theme]
Name=Cursor only
Directories=
EOF
cat >"$data_one/icons/IconOnly/index.theme" <<'EOF'
[Icon Theme]
Name=Icon only
Directories=16x16/apps
EOF
cat >"$data_one/icons/Both/index.theme" <<'EOF'
[Icon Theme]
Name=Both
Directories=scalable/apps
EOF
cat >"$data_one/icons/EmptyDirectories/index.theme" <<'EOF'
[Icon Theme]
Name=Not an icon catalog entry
Directories=
EOF
cp "$data_one/icons/Both/index.theme" "$data_two/icons/Both/index.theme"

catalog="$(
    HOME="$home" \
    XDG_DATA_HOME="$data_home" \
    XDG_DATA_DIRS="$data_one:$data_two" \
        "$helper"
)" || fail 'catalog helper failed against disposable XDG roots'

jq -e '
    type == "object"
    and (keys | sort) == ["cursorThemes", "iconThemes"]
    and .cursorThemes == ["Both", "CursorOnly"]
    and .iconThemes == ["Both", "IconOnly"]
    and (all(.cursorThemes[]; type == "string" and length > 0))
    and (all(.iconThemes[]; type == "string" and length > 0))
' <<<"$catalog" >/dev/null \
    || fail "catalog JSON shape or theme classification is wrong: $catalog"

if HOME="$home" XDG_DATA_HOME="$data_home" XDG_DATA_DIRS="$data_one:$data_two" \
    "$helper" "$fixture/not-an-xdg-root" >/dev/null 2>&1; then
    fail 'catalog accepted a caller-supplied path'
fi

# ── Schema, compositor replay, service, and UI wiring ───────────────────────
python3 - "$schema" <<'PY' || fail 'desktop-style schema entries are missing or malformed'
import re
import sys

text = open(sys.argv[1], encoding="utf-8").read()

expected = {
    "cursorTheme": ("string", '"oreo_blue_cursors"', "themes"),
    "iconTheme": ("string", '"Adwaita"', "themes"),
    "applicationFont": ("string", '"Adwaita Sans"', "typography"),
    "applicationFontSize": ("int", "11", "typography"),
    "documentFont": ("string", '"Adwaita Sans"', "typography"),
    "documentFontSize": ("int", "12", "typography"),
    "monospaceFont": ("string", '"VictorMono Nerd Font"', "typography"),
    "monospaceFontSize": ("int", "10", "typography"),
    "fontHinting": ("enum", '"slight"', "typography"),
    "fontAntialiasing": ("enum", '"rgba"', "typography"),
    "titlebarButtonSide": ("enum", '"right"', "titlebar"),
    "panamaTitlebar": ("bool", "true", "titlebar"),
    "middleClickPaste": ("bool", "true", "pointer"),
}

# Deleted on purpose. Hyprland has no minimize and maximize is noise in a
# tiler, so neither button is offered -- and neither is a setting for it. A
# schema entry that comes back is a control that writes a preference nothing
# reads, which is worse than no control at all.
for gone in ("titlebarMaximizeButton", "titlebarDoubleClick"):
    if re.search(r'key:\s*"' + gone + r'"', text):
        raise SystemExit(f"{gone} is back in the schema; it was removed on purpose")

def block_for(key: str) -> str:
    match = re.search(r"\{\s*\n\s*key:\s*\"" + re.escape(key) + r"\".*?\n\s{8}\}", text, re.S)
    if not match:
        raise SystemExit(f"missing {key}")
    return match.group(0)

for key, (kind, default, group) in expected.items():
    block = block_for(key)
    if not re.search(rf'type:\s*"{re.escape(kind)}"', block):
        raise SystemExit(f"{key} has wrong type")
    if not re.search(rf'def:\s*{re.escape(default)}', block):
        raise SystemExit(f"{key} has wrong default")
    if not re.search(rf'group:\s*"{re.escape(group)}"', block):
        raise SystemExit(f"{key} has wrong group")

enum_values = {
    "fontHinting": ["none", "slight", "medium", "full"],
    "fontAntialiasing": ["none", "grayscale", "rgba"],
    "titlebarButtonSide": ["left", "right"],
}
for key, values in enum_values.items():
    block = block_for(key)
    actual = re.findall(r'value:\s*"([^"]+)"', block)
    if actual != values:
        raise SystemExit(f"{key} options are {actual}, expected {values}")

middle = block_for("middleClickPaste")
for fragment in (
    'path: ["misc", "middle_click_paste"]',
    'option: "misc:middle_click_paste"',
    'readAs: "bool"',
):
    if fragment not in middle:
        raise SystemExit(f"middleClickPaste is missing {fragment}")
PY

rg -q 'prefs\.get\("cursorTheme", "oreo_blue_cursors"\)' "$env_lua" \
    || fail 'Hyprland environment does not replay cursorTheme'
rg -q 'middle_click_paste\s*=\s*prefs\.get\("middleClickPaste", true\)' "$looks_lua" \
    || fail 'Hyprland misc does not replay middleClickPaste'

rg -q 'DesktopPreferences\.get\("cursorTheme"\)' "$accessibility" \
    || fail 'Accessibility does not use the stored cursor theme'
! rg -q 'gsettings.*cursor-theme|themeQuery' "$accessibility" \
    || fail 'Accessibility still queries cursor-theme from gsettings'

for needle in \
    'property var cursorThemes' \
    'property var iconThemes' \
    'DesktopPreferences.revision' \
    'Fonts.interfaceFonts' \
    'Fonts.monospaceFonts' \
    'gtk-enable-primary-paste' \
    'button-layout'; do
    rg -Fq "$needle" "$service" || fail "DesktopStyle is missing $needle"
done
! rg -q 'sh -c|bash -c' "$service" \
    || fail 'DesktopStyle routes gsettings through a shell'

# ── The button layout is close-only, on either side ─────────────────────────
# A minimize button on Hyprland is a button that does nothing, and maximize is
# noise in a tiler. Neither token may reappear in the layout DesktopStyle
# pushes, and the double-click action is left at GNOME's default rather than
# being driven from a preference that no longer exists.
python3 - "$service" <<'PY' || fail 'the titlebar button layout is no longer close-only'
import re
import sys

text = open(sys.argv[1], encoding="utf-8").read()
block = re.search(r'function buttonLayout\(\).*?\n    \}', text, re.S)
if not block:
    raise SystemExit("DesktopStyle no longer builds a button layout")
body = block.group(0)
if '"close:appmenu"' not in body or '"appmenu:close"' not in body:
    raise SystemExit("the layout is not close-only on both sides")
for token in ("minimize", "maximize"):
    if token in body:
        raise SystemExit(f"the button layout offers {token}, which Hyprland cannot honour")
if 'DesktopPreferences.get("titlebarButtonSide")' not in body:
    raise SystemExit("the button side is no longer read from the schema")

for gone in ("titlebarMaximizeButton", "titlebarDoubleClick", "action-double-click-titlebar"):
    if gone in text:
        raise SystemExit(f"DesktopStyle still reads or writes {gone}")
PY

for group in typography themes titlebar; do
    rg -q "\"$group\": \"appearance\"" "$search" \
        || fail "settings search does not route $group to Appearance"
done
rg -q '"pointer": "mouse"' "$search" \
    || fail 'settings search no longer routes pointer controls to Mouse'

# Typography is one Fonts card of five dropdowns plus Sizes and Rendering.
# The old split into "Shell typography" and "Application typography" is gone:
# it never said which font was the shell's and which was the applications',
# which was the one question the page existed to answer.
for needle in \
    'title: "Fonts"' \
    'title: "Sizes"' \
    'title: "Rendering"' \
    'setting: "interfaceFontSize"' \
    'setting: "applicationFontSize"' \
    'setting: "documentFontSize"' \
    'setting: "monospaceFontSize"' \
    'setting: "fontHinting"' \
    'setting: "fontAntialiasing"' \
    'title: "Icons & pointer"' \
    'DesktopStyle.cursorThemes' \
    'DesktopStyle.iconThemes' \
    'DesktopStyle.setApplicationFont(' \
    'DesktopStyle.setDocumentFont(' \
    'DesktopStyle.setMonospaceFont(' \
    'title: "Titlebars"' \
    'setting: "panamaTitlebar"' \
    'setting: "titlebarButtonSide"'; do
    rg -Fq "$needle" "$appearance" || fail "Appearance is missing $needle"
done

# Five font rows, each naming what it controls, each a closed dropdown.
[[ "$(rg -c 'FontPicker \{' "$appearance")" == "5" ]] \
    || fail 'Appearance no longer offers exactly one FontPicker per font role'
for role in 'label: "Interface"' 'label: "Icons"' 'label: "Application"' \
    'label: "Document"' 'label: "Monospace"'; do
    rg -Fq "$role" "$appearance" || fail "the Fonts card is missing $role"
done

# The titlebar card must say why there is no minimize or maximize toggle,
# rather than leaving the absence looking like an oversight.
rg -Fq 'no minimize or maximize toggle' "$appearance" \
    || fail 'the Titlebars card does not explain why it offers only close'
for gone in 'setting: "titlebarMaximizeButton"' 'setting: "titlebarDoubleClick"' \
    'setting: "titlebarMinimize' 'GTK theme' 'Shell theme' \
    'title: "Shell typography"' 'title: "Application typography"'; do
    if rg -Fq "$gone" "$appearance"; then
        fail "Appearance exposes a retired, inert, or separately-owned control: $gone"
    fi
done

rg -q 'setting: "middleClickPaste"' "$mouse" \
    || fail 'Mouse does not expose middle-click paste'
rg -q 'GTK.*Wayland|Wayland.*GTK' "$mouse" \
    || fail 'Mouse does not explain the GTK and Wayland scope honestly'

printf 'desktop style contract: PASS\n'
