#!/usr/bin/env bash

# config/dot/hypr/prefs.lua is read at config time by looks.lua, input.lua, and
# monitors.lua. It is therefore the one piece of Panama that can cost the user a
# working compositor rather than merely a working feature.
#
# This contract pins the only behaviour that matters: whatever is in the
# settings file -- including nothing, garbage, or values of the wrong type --
# the config still parses, and every setting either takes the stored value or
# falls back to the value shipped in the Lua.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
hypr_dir="$repo_dir/config/dot/hypr"
work="$(mktemp -d /tmp/panama-prefs-contract.XXXXXX)"

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

cleanup() {
    rm -rf "$work"
}
trap cleanup EXIT

# Exercise prefs.lua directly rather than through a full compositor launch: the
# question is what the module returns, and Hyprland's own parse is covered by
# the --verify-config case at the end.
probe() {
    local settings="$1"
    mkdir -p "$work/config/panama"
    if [[ "$settings" == "__absent__" ]]; then
        rm -f "$work/config/panama/settings.json"
    else
        printf '%s' "$settings" >"$work/config/panama/settings.json"
    fi

    XDG_CONFIG_HOME="$work/config" lua -e "
        package.path = '$hypr_dir/?.lua;' .. package.path
        local ok, prefs = pcall(require, 'prefs')
        if not ok then
            print('LOAD_ERROR ' .. tostring(prefs))
            os.exit(0)
        end
        print(string.format(
            'gapsOut=%s rounding=%s blur=%s layout=%s hdr=%s',
            tostring(prefs.get('gapsOut', 10)),
            tostring(prefs.get('windowRounding', 18)),
            tostring(prefs.get('blurEnabled', true)),
            tostring(prefs.get('keyboardLayout', 'us')),
            tostring(prefs.getInt('autoHdr', 1))))
    " 2>&1
}

shipped='gapsOut=10 rounding=18 blur=true layout=us hdr=1'

# ── No file at all: the normal first run ─────────────────────────────────────
result="$(probe '__absent__')"
[[ "$result" == "$shipped" ]] || fail "an absent settings file did not yield shipped defaults: $result"

# ── Empty file ───────────────────────────────────────────────────────────────
result="$(probe '')"
[[ "$result" == "$shipped" ]] || fail "an empty settings file did not yield shipped defaults: $result"

# ── Whitespace only ──────────────────────────────────────────────────────────
result="$(probe '
	')"
[[ "$result" == "$shipped" ]] || fail "a whitespace-only file did not yield shipped defaults: $result"

# ── Truncated mid-object, the shape a crashed write leaves behind ────────────
result="$(probe '{ "gapsOut": 24, "windowRounding":')"
[[ "$result" == "$shipped" ]] || fail "a truncated file did not yield shipped defaults: $result"

# ── Not JSON at all ──────────────────────────────────────────────────────────
result="$(probe 'gaps_out = 24')"
[[ "$result" == "$shipped" ]] || fail "a non-JSON file did not yield shipped defaults: $result"

# ── A JSON array rather than an object ───────────────────────────────────────
result="$(probe '[1, 2, 3]')"
[[ "$result" == "$shipped" ]] || fail "a top-level array did not yield shipped defaults: $result"

# ── Wrong types: each bad value falls back on its own ────────────────────────
result="$(probe '{"gapsOut": "wide", "windowRounding": 24, "blurEnabled": 3, "keyboardLayout": 7}')"
[[ "$result" == 'gapsOut=10 rounding=24 blur=true layout=us hdr=1' ]] \
    || fail "wrong-typed values did not fall back per key: $result"

# ── Good values are actually used ────────────────────────────────────────────
result="$(probe '{"gapsOut": 24, "windowRounding": 6, "blurEnabled": false, "keyboardLayout": "us,de", "autoHdr": false}')"
[[ "$result" == 'gapsOut=24 rounding=6 blur=false layout=us,de hdr=0' ]] \
    || fail "stored values were not applied: $result"

# ── A boolean maps onto an integer option, matching SystemSettings.hyprValue ─
result="$(probe '{"autoHdr": true}')"
[[ "$result" == 'gapsOut=10 rounding=18 blur=true layout=us hdr=1' ]] \
    || fail "getInt did not convert a boolean: $result"

# ── Escapes and nesting do not break the reader ──────────────────────────────
result="$(probe '{"note": "a \"quoted\" value\nwith escapes", "nested": {"a": [1, 2, {"b": null}]}, "gapsOut": 12}')"
[[ "$result" == 'gapsOut=12 rounding=18 blur=true layout=us hdr=1' ]] \
    || fail "a file with escapes and nesting was not parsed: $result"

# ── The real config parses in every one of those states ─────────────────────
# This is the case that actually protects the desktop: prefs.lua returning
# defaults is only useful if Hyprland still accepts the config around it.
#
# Hyprland resolves its own config from $HOME/.config/hypr regardless of
# XDG_CONFIG_HOME, while prefs.lua honours XDG_CONFIG_HOME. That asymmetry is
# what makes this loop useful rather than vacuous: the *real* looks.lua and
# input.lua are parsed against a *fixture* settings file. The probe cases above
# already establish that prefs.lua reads the fixture and not the live file.
for settings in '__absent__' '' '{ "gapsOut": 24, "windowRounding":' 'gaps_out = 24' '{"gapsOut": "wide"}' \
                '{"gapsOut": 24, "windowRounding": 6, "blurEnabled": false, "borderSize": 0, "keyboardLayout": "us,de"}'; do
    if [[ "$settings" == "__absent__" ]]; then
        rm -f "$work/config/panama/settings.json"
    else
        mkdir -p "$work/config/panama"
        printf '%s' "$settings" >"$work/config/panama/settings.json"
    fi
    output="$(XDG_CONFIG_HOME="$work/config" Hyprland --verify-config 2>&1 || true)"
    grep -q 'config ok' <<<"$output" \
        || fail "Hyprland rejected the config with settings=<$settings>: $(tail -5 <<<"$output")"
done

trap - EXIT
cleanup
printf 'prefs fallback contract: PASS\n'
