#!/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"),
    "titlebarMaximizeButton": ("bool", "false", "titlebar"),
    "titlebarDoubleClick": ("enum", '"toggle-maximize"', "titlebar"),
    "middleClickPaste": ("bool", "true", "pointer"),
}

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"],
    "titlebarDoubleClick": ["toggle-maximize", "none"],
}
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' \
    'action-double-click-titlebar'; 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'

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'

for needle in \
    'title: "Application typography"' \
    'setting: "applicationFontSize"' \
    'setting: "documentFontSize"' \
    'setting: "monospaceFontSize"' \
    'setting: "fontHinting"' \
    'setting: "fontAntialiasing"' \
    'title: "Icons & pointer"' \
    'DesktopStyle.cursorThemes' \
    'DesktopStyle.iconThemes' \
    'title: "Titlebars"' \
    'setting: "titlebarButtonSide"' \
    'setting: "titlebarMaximizeButton"' \
    'setting: "titlebarDoubleClick"'; do
    rg -Fq "$needle" "$appearance" || fail "Appearance is missing $needle"
done
! rg -q 'setting: "titlebarMinimize|GTK theme|Shell theme' "$appearance" \
    || fail 'Appearance exposes an inert or separately-owned theme control'

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'
