#!/usr/bin/env bash

# Display configuration.
#
# This is the only setting in Panama that can leave the user unable to SEE the
# screen well enough to undo it: a mode the panel cannot show, or a scale that
# makes everything unreadable, is not recoverable through the UI that caused it.
#
# So the property under test is not "can it change the resolution" but "does an
# unconfirmed change always come back". A regression here is not a broken
# feature, it is a user staring at a blank monitor.
#
#   * an unconfirmed change reverts on its own, and stores nothing
#   * a confirmed change is what writes to the settings store
#   * a mode, scale, rotation, or output the compositor did not offer is refused
#     before anything is applied
#
# The compositor is the live one -- there is no way to test this otherwise --
# but preferences are isolated, and every path restores the display it started
# from.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
harness="$repo_dir/config/dot/quickshell/displays-harness.qml"
service="$repo_dir/config/dot/quickshell/services/Displays.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/DisplaysPage.qml"
settings_page="$repo_dir/config/dot/quickshell/modules/settings/SettingsPage.qml"
monitors_lua="$repo_dir/config/dot/hypr/monitors.lua"

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

# Keep is unavailable until compositor readback exactly matches the request.
for contract in \
    'property var pendingRequestedLayout:' \
    'property var revertExpectedLayout:' \
    'property bool revertVerificationActive:' \
    'property int revertGeneration:' \
    'readonly property bool canConfirm:' \
    'function matchesLayout(' \
    'function scalesForMode(' \
    'function isScaleClean(' \
    'x: Number.isInteger(monitor.x)' \
    'y: Number.isInteger(monitor.y)' \
    'primary: monitor.name === primaryName'; do
    rg -Fq "$contract" "$service" || fail "display service contract is missing: $contract"
done
rg -Fq 'enabled: Displays.canConfirm' "$page" \
    || fail 'Keep is enabled before the display change is verified'
rg -Fq 'options: Displays.scalesForMode(' "$page" \
    || fail 'scale choices are not filtered for the active resolution'
rg -Fq 'property string selectedOutput:' "$page" \
    || fail 'connected outputs cannot be selected'
# primaryFirstMonitors is monitors sorted with the primary first, so the
# selector is still populated from what is connected -- which is what this
# protects. Naming the sorted list rather than the raw one is the point: the
# picker should open on the display somebody is most likely to mean.
rg -Fq 'options: Displays.primaryFirstMonitors.map(' "$page" \
    || fail 'the output selector is not populated from connected displays'
rg -Fq 'id: revertVerifyTimer' "$service" \
    || fail 'automatic restoration has no bounded readback verification'
rg -Fq 'if (root.busy)' "$service" \
    || fail 'the display service accepts a new apply while another operation is busy'

# Stored JSON is untyped at field level, so the Lua startup consumer is the
# final validation boundary and must support every named output it accepts.
for contract in 'valid_mode' 'valid_scale' 'valid_transform' 'valid_position' 'valid_primary' 'pairs(displays)'; do
    rg -Fq "$contract" "$monitors_lua" || fail "monitor startup validation is missing: $contract"
done

# SettingsPage headers are genuinely pinned outside its scrolling surface.
python3 - "$settings_page" <<'PY' || fail 'SettingsPage header is not pinned outside the Flickable'
import sys
text = open(sys.argv[1], encoding="utf-8").read()
loader = text.find("id: pinnedHeader")
flickable = text.find("id: pageScroll")
if loader < 0 or flickable < 0 or loader > flickable:
    raise SystemExit(1)
PY

MONITORS_LUA="$monitors_lua" lua - <<'LUA' || fail 'monitor startup accepted invalid persisted geometry or ignored a named output'
package.preload["prefs"] = function()
    return {
        get = function()
            return {
                ["DP-2"] = {
                    mode = "4500x3000@60", scale = 1.5, transform = 0,
                    x = 0, y = 0, primary = true,
                },
                ["HDMI-A-1"] = {
                    mode = "2560x1440@60", scale = 1, transform = 1,
                    x = 3000, y = 0, primary = false,
                },
                ["LEGACY-1"] = { mode = "1920x1080@60", scale = 1.5, transform = 0 },
                ["PARTIAL-1"] = {
                    mode = "1920x1080@60", scale = 1, transform = 0,
                    x = 4440,
                },
                ["BAD-PRIMARY"] = {
                    mode = "1920x1080@60", scale = 1, transform = 0,
                    x = 4440, y = 0, primary = "yes",
                },
                ["BAD OUTPUT"] = {
                    mode = "1920x1080@60", scale = 1, transform = 0,
                    x = 4440, y = 0, primary = false,
                },
            }
        end,
    }
end

local calls = {}
hl = { monitor = function(value) table.insert(calls, value) end }
assert(loadfile(os.getenv("MONITORS_LUA")))()

local by_output = {}
for _, value in ipairs(calls) do by_output[value.output] = value end
assert(by_output["DP-2"].mode == "4500x3000@60")
assert(by_output["DP-2"].scale == 1.5)
assert(by_output["DP-2"].transform == 0)
assert(by_output["DP-2"].position == "0x0")
-- The panel-specific color policy rides the description-matched rule, not the
-- connector: a stranger's monitor on DP-2 must not inherit the Kuycon's mode
-- or its 10-bit request.
local kuycon = by_output["desc:GVT Kuycon P20"]
assert(kuycon ~= nil)
assert(kuycon.mode == "4500x3000@60")
assert(kuycon.bitdepth == 10)
assert(kuycon.cm == "auto")
assert(by_output["HDMI-A-1"].mode == "2560x1440@60")
assert(by_output["HDMI-A-1"].scale == 1)
assert(by_output["HDMI-A-1"].transform == 1)
assert(by_output["HDMI-A-1"].position == "3000x0")
assert(by_output["LEGACY-1"].position == "auto")
assert(by_output["PARTIAL-1"] == nil)
assert(by_output["BAD-PRIMARY"] == nil)
assert(by_output["BAD OUTPUT"] == nil)
assert(by_output[""] ~= nil)
LUA

rg -Fq 'Resolution, scale, rotation, position, and primary display' \
    "$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml" \
    || fail 'the display preference does not document complete layout persistence'

if [[ "${PANAMA_DISPLAYS_STATIC_ONLY:-0}" == "1" ]]; then
    printf 'displays contract: PASS (static)\n'
    exit 0
fi

config_home="$(mktemp -d /tmp/panama-displays-config.XXXXXX)"

run() { XDG_CONFIG_HOME="$config_home" qs -p "$harness" "$@"; }
status() { run ipc call displays-test status; }

# The shipped geometry, read from the Hyprland config rather than from the
# running compositor.
#
# Everything below captures "original" from what it observes at start, which is
# correct only if the display is already in a good state. A previous run that
# failed mid-revert leaves the display changed, and the next run then captures
# THAT as the original and faithfully restores the desktop to a broken value.
# One flake becomes permanent. So refuse to run from a state that does not match
# what the config says, rather than laundering it.
shipped_scale="$(sed -n 's/^local shipped_scale *= *\([0-9.]*\).*/\1/p' \
    "$repo_dir/config/dot/hypr/monitors.lua" | head -1)"
[[ -n "$shipped_scale" ]] || fail 'could not read the shipped scale from monitors.lua -- the guard below depends on it, and skipping it silently is how a dirty baseline gets laundered'
if [[ -n "$shipped_scale" ]]; then
    live_scale="$(hyprctl -j monitors | jq -r '.[0].scale')"
    if ! awk -v a="$live_scale" -v b="$shipped_scale" 'BEGIN { exit !(a == b) }'; then
        fail "the display is at scale $live_scale but the config ships $shipped_scale -- refusing to capture a dirty state as the baseline. Restore it first: hyprctl eval 'hl.monitor({ output = \"DP-2\", mode = \"4500x3000@60\", scale = $shipped_scale, transform = 0 })'"
    fi
fi

original_mode=""
original_scale=""
original_transform=""
original_width=""
original_height=""
original_refresh=""
original_x=""
original_y=""
monitor_name=""

monitor_state() {
    hyprctl -j monitors | jq -c --arg output "$monitor_name" '.[] | select(.name == $output)'
}

display_is_restored() {
    local current
    current="$(monitor_state)"
    [[ -n "$current" ]] || return 1
    jq -e \
        --argjson width "$original_width" \
        --argjson height "$original_height" \
        --argjson refresh "$original_refresh" \
        --argjson scale "$original_scale" \
        --argjson transform "$original_transform" \
        --argjson x "$original_x" \
        --argjson y "$original_y" \
        '.width == $width and .height == $height
            and ((.refreshRate - $refresh) | fabs) < 0.01
            and ((.scale - $scale) | fabs) < 0.001
            and .transform == $transform
            and .x == $x and .y == $y' <<<"$current" >/dev/null
}

restore_display() {
    [[ -n "$original_mode" ]] || return 0
    hyprctl eval "hl.monitor({ output = \"$monitor_name\", mode = \"$original_mode\", position = \"${original_x}x${original_y}\", scale = $original_scale, transform = $original_transform })" >/dev/null \
        || return 1
    for _ in $(seq 1 50); do
        display_is_restored && return 0
        sleep 0.2
    done
    return 1
}

stop_harness() {
    # Kill by PID, never `pkill -f displays-harness`: that pattern also matches
    # any shell whose command line contains this script's text, which includes
    # the invoking shell itself.
    [[ -n "${harness_pid:-}" ]] && kill "$harness_pid" >/dev/null 2>&1 || true
    rm -rf "$config_home"
}

cleanup() {
    local status=$?
    trap - EXIT
    if ! restore_display; then
        printf 'displays contract: FAILED to restore %s to %s at %sx%s scale %s transform %s\n' \
            "$monitor_name" "$original_mode" "$original_x" "$original_y" \
            "$original_scale" "$original_transform" >&2
        status=1
    fi
    stop_harness
    exit "$status"
}
trap cleanup EXIT

XDG_CONFIG_HOME="$config_home" qs -p "$harness" --daemonize >/dev/null
harness_pid=""
for _ in $(seq 1 40); do
    run ipc show 2>/dev/null | rg -q '^target displays-test$' && break
    sleep 0.1
done
run ipc show 2>/dev/null | rg -q '^target displays-test$' || fail 'test IPC target did not start'
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"

refresh_fixture="$(run ipc call displays-test refreshIdentityFixture)"
jq -e '
    .count == 2
    and .modes == ["1920x1080@60.00", "1920x1080@59.94"]
    and .selected == ["1920x1080@59.94"]
' <<<"$refresh_fixture" >/dev/null \
    || fail "59.94 Hz and 60.00 Hz lost their distinct selection identity: $refresh_fixture"

position_fixture="$(run ipc call displays-test positionFixture)"
jq -e '. == [
    {"name":"DP-2","x":140,"y":80,"primary":true},
    {"name":"HDMI-A-1","x":3140,"y":80,"primary":false}
]' <<<"$position_fixture" >/dev/null \
    || fail "monitor positions or primary selection were parsed incorrectly: $position_fixture"

for _ in $(seq 1 50); do
    [[ "$(status | jq -r .count)" != "0" ]] && break
    sleep 0.1
done

state="$(status)"
monitor_name="$(jq -r .name <<<"$state")"
[[ -n "$monitor_name" ]] || fail "no display was detected: $state"
original_mode="$(jq -r .mode <<<"$state")"
original_width="$(jq -r .width <<<"$state")"
original_height="$(jq -r .height <<<"$state")"
original_refresh="$(jq -r .refresh <<<"$state")"
original_scale="$(jq -r .scale <<<"$state")"
original_transform="$(jq -r .transform <<<"$state")"
original_x="$(jq -r .x <<<"$state")"
original_y="$(jq -r .y <<<"$state")"

[[ "$(jq -r .modes <<<"$state")" -gt 0 ]] || fail 'the display reported no usable modes'

# ── Anything the compositor did not offer is refused before applying ─────────
while IFS= read -r kind; do
    [[ "$(run ipc call displays-test applyBad "$kind")" == "false" ]] \
        || fail "an invalid $kind was accepted"
    [[ "$(status | jq -r .awaiting)" == "false" ]] \
        || fail "an invalid $kind left a change pending"
done <<'KINDS'
mode
scale
transform
output
dirtyScale
KINDS

# The display must not have moved for any of those.
now="$(status)"
[[ "$(jq -r .scale <<<"$now")" == "$original_scale" ]] || fail 'a refused change still altered the scale'

# An immediate Revert may race both the apply process and its first readback.
# It must queue until both are clear, then verify the original generation.
target_scale=$(awk -v s="$original_scale" 'BEGIN { print (s == 1.25) ? 1.5 : 1.25 }')
[[ "$(run ipc call displays-test applyScale "$target_scale")" == "true" ]] \
    || fail 'the immediate-revert fixture could not apply'
run ipc call displays-test revertChange >/dev/null
immediate_reverted=false
for _ in $(seq 1 60); do
    if display_is_restored && [[ "$(status | jq -r .awaiting)" == "false" ]]; then
        immediate_reverted=true
        break
    fi
    sleep 0.2
done
[[ "$immediate_reverted" == true ]] \
    || fail 'an immediate Revert raced the apply/readback and did not restore the display'

# ── An unconfirmed change reverts on its own and stores nothing ──────────────
[[ "$(run ipc call displays-test applyScale "$target_scale")" == "true" ]] \
    || fail 'a valid scale change was refused'

applied=false
for _ in $(seq 1 30); do
    [[ "$(monitor_state | jq -r '.scale')" == "$target_scale" ]] && { applied=true; break; }
    sleep 0.2
done
[[ "$applied" == true ]] || fail 'the scale change never reached the compositor'
[[ "$(status | jq -r .awaiting)" == "true" ]] || fail 'an applied change is not awaiting confirmation'
[[ "$(status | jq -r .canConfirm)" == "true" ]] || fail 'an applied change was never verified by compositor readback'

# Wait out the countdown. This is the whole point of the contract.
reverted=false
for _ in $(seq 1 120); do
    if [[ "$(monitor_state | jq -r '.scale')" == "$original_scale" ]]; then
        reverted=true
        break
    fi
    sleep 0.5
done
[[ "$reverted" == true ]] || fail 'an unconfirmed change did NOT revert -- this would strand a user on an unreadable display'
[[ "$(status | jq -r .awaiting)" == "false" ]] || fail 'the pending state survived the revert'
[[ "$(status | jq -r .overridden)" == "false" ]] || fail 'an unconfirmed change was written to the settings store'

# ── A confirmed change is what writes ────────────────────────────────────────
[[ "$(run ipc call displays-test applyScale "$target_scale")" == "true" ]] \
    || fail 'the confirmed-change fixture could not apply'

# Hyprland can apply and read back a change faster than two IPC round trips, so
# this live test cannot reliably observe the pre-readback state. The
# fake-compositor contract deterministically holds that boundary open and
# proves Keep refuses it; this path proves a real readback eventually enables
# and persists Keep on the physical display.
verified=false
for _ in $(seq 1 30); do
    [[ "$(status | jq -r .canConfirm)" == "true" ]] && { verified=true; break; }
    sleep 0.2
done
[[ "$verified" == true ]] || fail 'the confirmed change never became safe to keep'
[[ "$(run ipc call displays-test confirmChange)" == "true" ]] \
    || fail 'Keep refused a verified display change'
sleep 0.6
[[ "$(status | jq -r .awaiting)" == "false" ]] || fail 'confirming did not clear the pending state'
[[ "$(status | jq -r .overridden)" == "true" ]] || fail 'confirming did not store the change'

store="$config_home/panama/settings.json"
jq -e --arg m "$monitor_name" '.displays[$m].scale != null' "$store" >/dev/null \
    || fail 'the confirmed change is not in the settings store'

# ── Forgetting clears it ─────────────────────────────────────────────────────
run ipc call displays-test forget >/dev/null
sleep 0.6
[[ "$(status | jq -r .overridden)" == "false" ]] || fail 'forget did not clear the stored display setting'

# ── Restoring an arrangement when a display comes back ───────────────────────
#
# hypr/monitors.lua applies stored per-output entries when the compositor reads
# its config, and never again. A monitor plugged in an hour later used to get
# the compositor's automatic placement instead of the arrangement this machine
# was told to use, and the only way back was to open Settings and apply it
# again. Docking should not cost you your desk.
#
# Driven through plannedRestore, which is the decision with no side effects --
# applying a real layout here would drive the live compositor.
#
# Three cases, and the last two matter most: a wrong answer is a screen you
# cannot see well enough to fix, so it refuses rather than guesses.

# Single-line and space-free: the IPC call splits its arguments on whitespace,
# so a pretty-printed fixture arrives as several arguments instead of one.
two_screens='[{"name":"DP-2","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":0,"y":0,"availableModes":["1920x1080@60.00Hz","2560x1440@60.00Hz"]},{"name":"HDMI-A-1","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":1920,"y":0,"availableModes":["1920x1080@60.00Hz"]}]'

# base64 because `qs ipc call` splits a JSON array of several objects into one
# argument per object, which makes a two-monitor fixture look like an extra
# argument and the call is refused for arity.
b64() { printf '%s' "$1" | base64 -w0; }
plan() { run ipc call displays-test restorePlan "$(b64 "$1")" "$(b64 "$2")"; }

# Nothing stored: the compositor's placement stands.
result="$(plan "$two_screens" '{}')"
jq -e '.action == "none"' <<<"$result" >/dev/null \
    || fail "with nothing stored, Panama wanted to change the arrangement: $result"

# Stored and already correct: still nothing to do, so a reconnect does not
# push a layout the compositor is already showing.
result="$(plan "$two_screens" '{"DP-2":{"mode":"1920x1080@60.00","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
jq -e '.action == "none"' <<<"$result" >/dev/null \
    || fail "an arrangement that already matches was re-applied anyway: $result"

# Stored and different: restore it, with the stored scale and position.
result="$(plan "$two_screens" '{"DP-2":{"mode":"2560x1440@60.00","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
jq -e '.action == "apply" and (.layout[] | select(.name == "DP-2") | .mode == "2560x1440@60.00")' \
    <<<"$result" >/dev/null \
    || fail "a stored arrangement was not restored when the display reconnected: $result"

# A mode this panel does not offer. Same connector, different hardware -- DP-1
# on one dock is not DP-1 on another. It must refuse rather than ask the
# compositor for a mode that does not exist.
result="$(plan "$two_screens" '{"DP-2":{"mode":"3840x2160@120.00","scale":1,"transform":0,"x":0,"y":0,"primary":true}}')"
jq -e '.action == "refuse"' <<<"$result" >/dev/null \
    || fail "a stored mode the connected panel does not offer was requested anyway: $result"

# Undocked: the stored primary is gone. The layout that survives must still
# name exactly one primary, or DisplayLayout.validate refuses it outright and
# the machine keeps whatever it happens to have.
one_screen='[{"name":"DP-2","width":1920,"height":1080,"refreshRate":60.0,"scale":1.0,"transform":0,"x":0,"y":0,"availableModes":["1920x1080@60.00Hz","2560x1440@60.00Hz"]}]'
result="$(plan "$one_screen" '{"DP-2":{"mode":"2560x1440@60.00","scale":1,"transform":0,"x":0,"y":0,"primary":false},"HDMI-A-1":{"mode":"1920x1080@60.00","scale":1,"transform":0,"x":1920,"y":0,"primary":true}}')"
jq -e '.action == "apply" and ([.layout[] | select(.primary)] | length == 1)' <<<"$result" >/dev/null \
    || fail "after undocking, the restored layout did not name exactly one primary: $result"
jq -e '[.layout[] | .name] == ["DP-2"]' <<<"$result" >/dev/null \
    || fail "the restored layout mentions a display that is not connected: $result"


restore_display || fail 'the final cleanup could not restore and verify the original display'

original_mode=""
stop_harness
trap - EXIT
printf 'displays contract: PASS\n'
