#!/usr/bin/env bash

# The theme editor's colour controls.
#
# The editor used to be a curated swatch strip (AccentPicker), a profile
# dropdown (ThemeProfilePicker) and six HSV sliders. It is now four colour
# wells -- primary, secondary, background, foreground -- with the HSV rows
# demoted to a fine-tune behind a disclosure. Both deleted components are
# pinned as gone here, because a file left behind and still registered in the
# qmldir is a component the next person wires back up.
#
# What must hold:
#
#   1. Every colour route ends in ONE function. Typed hex, the colour wheel and
#      the eyedropper all call ThemeEditorWells.apply(), so validation cannot
#      differ between them -- and every write ends in ThemeProfiles.commitActive,
#      which recomputes accentName. That is what stops GNOME's accent enum,
#      kitty's border and the lock screen going stale after a custom edit.
#   2. Colour is never the only signal. Each well has a hex field, each button
#      a spoken name, each HSV row a numeric readout, and all of them take
#      keyboard focus. A hue ring alone tells someone with a colour vision
#      deficiency nothing.
#   3. Writes are debounced. Committing per slider move meant a single drag
#      spent the whole gesture in apply-and-verify round trips with the desktop
#      repainting behind the pointer.
#   4. Nothing here animates continuously. These are settings controls on a
#      high-refresh display; a shimmer costs real frame time forever.

set -euo pipefail

repo_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)"
settings="$repo_dir/config/dot/quickshell/modules/settings"
theme="$repo_dir/config/dot/quickshell/config/Theme.qml"
profiles="$repo_dir/config/dot/quickshell/services/ThemeProfiles.qml"
scheme="$repo_dir/config/dot/quickshell/services/ColorScheme.qml"
search="$repo_dir/config/dot/quickshell/services/SettingsSearch.qml"

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

# ── The components that exist, and the two that must not ────────────────────
for component in AccentEditor ColorWell ThemeEditorWells ThemeStartChips \
    ThemeSaturationRow ThemeSaveRow ThemeCard ThemeGallery ThemeModeHero; do
    [[ -f "$settings/$component.qml" ]] || fail "$component is missing"
    rg -Fq "$component 1.0 $component.qml" "$settings/qmldir" \
        || fail "$component is not registered in the settings module"
done

for retired in AccentPicker ThemeProfilePicker; do
    if [[ -f "$settings/$retired.qml" ]]; then
        fail "$retired was deleted in the redesign but the file is back"
    fi
    if rg -Fq "$retired" "$settings/qmldir"; then
        fail "$retired is still registered in the settings qmldir"
    fi
done

# ── The four wells are the fast path ────────────────────────────────────────
for role in '"primary", role: "Primary"' '"secondary", role: "Secondary"' \
    '"background", role: "Background"' '"foreground", role: "Foreground"'; do
    rg -Fq "$role" "$settings/ThemeEditorWells.qml" \
        || fail "the editor is missing the $role well"
done
rg -Fq 'function apply(which: string, hex: string)' "$settings/ThemeEditorWells.qml" \
    || fail 'the wells no longer share one apply path'
rg -Fq 'ThemeProfiles.setGroundColors(' "$settings/ThemeEditorWells.qml" \
    || fail 'background and foreground do not derive the surface and text families'
rg -Fq 'ThemeProfiles.setAccentPair(' "$settings/ThemeEditorWells.qml" \
    || fail 'the accent wells do not write the accent pair'
rg -Fq 'ThemeProfiles.activePalette' "$settings/ThemeEditorWells.qml" \
    || fail 'the ground wells do not show the palette actually in effect'

# ── One eyedropper, one wheel, both aimed by a remembered target ────────────
rg -Fq '["hyprpicker", "--format=hex", "--lowercase-hex", "--quiet", "--no-fancy"]' \
    "$settings/ThemeEditorWells.qml" \
    || fail 'screen picking does not use the validated hyprpicker hex invocation'
[[ "$(rg -c 'hyprpicker' "$settings/ThemeEditorWells.qml")" == "1" ]] \
    || fail 'the eyedropper is invoked from more than one place'
rg -Fq 'root.target = which' "$settings/ThemeEditorWells.qml" \
    || fail 'the eyedropper and wheel do not remember which well they answer'
rg -Fq 'onStreamFinished: root.apply(root.target, this.text)' "$settings/ThemeEditorWells.qml" \
    || fail 'the picked colour does not return through the shared apply path'
rg -Fq 'root.apply(root.target, Qt.rgba(' "$settings/ThemeEditorWells.qml" \
    || fail 'the colour wheel does not return through the shared apply path'
for target in 'Open the color wheel for " + root.role' 'Pick " + root.role + " from the screen'; do
    rg -Fq "$target" "$settings/ColorWell.qml" \
        || fail "a glyph-only button has no spoken name: $target"
done

# ── Hex validation lives in the well, and commits on intent ─────────────────
rg -Fq '/^#[0-9a-f]{6}$/.test(prefixed)' "$settings/ColorWell.qml" \
    || fail 'ColorWell does not validate typed hex'
rg -Fq 'property bool invalid' "$settings/ColorWell.qml" \
    || fail 'a rejected hex is not marked in the field'
rg -Fq 'onAccepted: root.commit()' "$settings/ColorWell.qml" \
    || fail 'a typed hex does not commit on Enter'
rg -Fq 'onActiveFocusChanged:' "$settings/ColorWell.qml" \
    || fail 'a typed hex does not commit when focus leaves'
# Per keystroke would repaint the desktop on the way to the colour you meant:
# "#82a" is a real colour.
if rg -q 'onTextChanged:.*commit|onTextEdited:.*commit' "$settings/ColorWell.qml"; then
    fail 'the hex field commits per keystroke'
fi
rg -Fq 'Accessible.name: root.role + " color, hex"' "$settings/ColorWell.qml" \
    || fail 'the hex field is not programmatically labelled'
rg -Fq 'activeFocusOnTab: true' "$settings/ColorWell.qml" \
    || fail 'the hex field cannot receive keyboard focus'

# ── The six HSV rows, still labelled, still keyboard-operable ───────────────
for label in \
    'Primary hue' 'Primary saturation' 'Primary value' \
    'Secondary hue' 'Secondary saturation' 'Secondary value'; do
    rg -Fq "$label" "$settings/AccentEditor.qml" \
        || fail "HSV control is missing the visible label $label"
done

rg -Fq 'ValueSlider {' "$settings/AccentEditor.qml" \
    || fail 'the fine-tune does not reuse ValueSlider'
rg -Fq 'Accessible.role: Accessible.Slider' "$settings/AccentEditor.qml" \
    || fail 'HSV controls do not expose slider semantics'
rg -Fq 'Accessible.name: row.label' "$settings/AccentEditor.qml" \
    || fail 'HSV controls are not programmatically labelled'
rg -Fq 'Accessible.description:' "$settings/AccentEditor.qml" \
    || fail 'HSV controls do not expose their numeric value as a non-hue signal'
rg -Fq 'activeFocusOnTab: true' "$settings/AccentEditor.qml" \
    || fail 'HSV controls cannot receive keyboard focus'
rg -Fq 'Keys.onPressed:' "$settings/AccentEditor.qml" \
    || fail 'HSV controls cannot be adjusted from the keyboard'
rg -Fq 'border.width: keyboardSlider.activeFocus ? 2 : 1' "$settings/AccentEditor.qml" \
    || fail 'HSV controls have no visible keyboard focus treatment'

# ── Debounced, not per-move ─────────────────────────────────────────────────
rg -Fq 'property var pending' "$settings/AccentEditor.qml" \
    || fail 'the sliders no longer track the drag locally'
rg -Fq 'commitTimer.restart();' "$settings/AccentEditor.qml" \
    || fail 'a slider move does not restart the debounce'
python3 - "$settings/AccentEditor.qml" <<'PY' || fail 'the HSV commit is no longer debounced through a timer'
import re
import sys

text = open(sys.argv[1], encoding="utf-8").read()
# The write must happen inside a Timer's onTriggered, never in changeChannel.
commit = re.search(r'Timer\s*\{\s*id: commitTimer(.*?)\n    \}', text, re.S)
if not commit or 'ThemeProfiles.setAccentPair(' not in commit.group(1):
    raise SystemExit('the commit timer does not write the accent pair')
change = re.search(r'function changeChannel\(.*?\n    \}', text, re.S)
if not change or 'ThemeProfiles.' in change.group(0):
    raise SystemExit('changeChannel writes directly instead of scheduling a commit')
# A refused write must snap the sliders back to what is really in effect.
if not re.search(r'Timer\s*\{\s*id: releaseTimer.*?editor\.pending = null', text, re.S):
    raise SystemExit('nothing hands the sliders back to the stored pair')
PY

# ── One commit path, and it keeps accentName in sync ────────────────────────
rg -Fq 'ThemeProfileModel.nearestCuratedName(profile.scheme, profile.accent)' "$profiles" \
    || fail 'commitActive does not recompute accentName from the chosen colour'
python3 - "$profiles" <<'PY' || fail 'a write path bypasses commitActive'
import re
import sys

text = re.sub(r"//.*", "", open(sys.argv[1], encoding="utf-8").read())
commit = re.search(r'function commitActive\(.*?\n    \}', text, re.S)
if not commit:
    raise SystemExit('commitActive is gone')
for fragment in (
    'SystemSettings.commitPreference("colorScheme"',
    'SystemSettings.commitPreference("themeProfileId"',
    'SystemSettings.commitPreference("accentName"',
    'root.applyProfileEffects(profile)',
):
    if fragment not in commit.group(0):
        raise SystemExit(f'commitActive no longer does: {fragment}')
# Everything that changes the active theme routes through it.
for func in ('selectProfile', 'updateActive'):
    block = re.search(r'function ' + func + r'\(.*?\n    \}', text, re.S)
    if not block or 'root.commitActive(' not in block.group(0):
        raise SystemExit(f'{func} does not end in commitActive')
PY

rg -Fq 'ThemeProfiles.activePalette' "$theme" \
    || fail 'Theme roles do not read the resolved palette'
rg -Fq 'root.hyprColor(Theme.accent, ' "$scheme" \
    || fail 'the focused border start does not follow Theme.accent'
rg -Fq 'root.hyprColor(Theme.accentSecondary, ' "$scheme" \
    || fail 'the focused border end does not follow Theme.accentSecondary'

# ── The editor is on the page, under its own tab ────────────────────────────
for component in ThemeStartChips ThemeEditorWells ThemeSaturationRow \
    AccentEditor ThemeSaveRow ThemeModeHero ThemeGallery; do
    rg -Fq "$component {" "$settings/AppearancePage.qml" \
        || fail "Appearance does not include $component"
done
for tab in '{ value: "themes", label: "Themes" }' '{ value: "editor", label: "Theme editor" }'; do
    rg -Fq "$tab" "$settings/AppearancePage.qml" \
        || fail "Appearance is missing the tab $tab"
done

# ── Findable by name ────────────────────────────────────────────────────────
for term in 'Themes' 'Theme editor' 'Theme profiles' 'Advanced accent' \
    'Pick color from screen'; do
    rg -Fq "\"$term\"" "$search" || fail "Settings search is missing $term"
done

# ── No decorative or continuously repainting animation ──────────────────────
if rg -n 'NumberAnimation|ColorAnimation|SequentialAnimation|ParallelAnimation|loops:[[:space:]]*Animation\.Infinite' \
        "$settings/AccentEditor.qml" "$settings/ColorWell.qml" \
        "$settings/ThemeEditorWells.qml" "$settings/ThemeStartChips.qml" \
        "$settings/ThemeSaturationRow.qml" "$settings/ThemeSaveRow.qml" \
        "$settings/ThemeCard.qml" "$settings/ThemeGallery.qml" \
        "$settings/ThemeModeHero.qml"; then
    fail 'theme controls introduce continuously repainting or decorative animation'
fi

# ── Live: the editor, headless ──────────────────────────────────────────────
state_home="$(mktemp -d /tmp/panama-accent-controls.XXXXXX)"
harness="$repo_dir/config/dot/quickshell/accent-controls-harness.qml"

qs_for_test() {
    XDG_CONFIG_HOME="$state_home/config" XDG_STATE_HOME="$state_home/state" \
        QS_DISABLE_CRASH_HANDLER=1 qs -p "$harness" "$@"
}

cleanup() {
    qs_for_test kill >/dev/null 2>&1 || true
    rm -rf "$state_home"
}
trap cleanup EXIT

qs_for_test --daemonize >/dev/null
for _ in $(seq 1 40); do
    qs_for_test ipc show 2>/dev/null | rg -q '^target accent-controls-test$' && break
    sleep 0.1
done
qs_for_test ipc show 2>/dev/null | rg -q '^target accent-controls-test$' \
    || fail 'headless theme editor harness did not start'

status() { qs_for_test ipc call accent-controls-test status; }

jq -e '.id == "moon" and .shipped == true and .accent == "#82aaff"
    and .accentName == "blue"' <<<"$(status)" >/dev/null \
    || fail 'the headless editor did not begin on Moon'

# A slider move shows immediately and writes on a beat, not per move.
moved="$(qs_for_test ipc call accent-controls-test adjust primary h 0)"
jq -e '.pending == true and .shown.accent != "#82aaff"
    and .stored.accent == "#82aaff"' <<<"$moved" >/dev/null \
    || fail 'an HSV move either did not show or wrote before the debounce elapsed'

sleep 0.8
settled="$(status)"
jq -e '.shipped == false and .accent != "#82aaff" and .secondary == "#b172b0"
    and .pending == false' <<<"$settled" >/dev/null \
    || fail 'the debounced commit did not fork a custom profile with the unchanged secondary'

# The wells: a valid hex lands, and accentName follows it without being asked.
jq -e '.accent == "#86e1fc" and .accentName == "teal" and .wellError == ""' \
    <<<"$(qs_for_test ipc call accent-controls-test well primary '#86e1fc')" >/dev/null \
    || fail 'a typed primary hex did not land, or accentName did not follow it'

jq -e '.accent == "#86e1fc" and (.wellError | length) > 0' \
    <<<"$(qs_for_test ipc call accent-controls-test well primary 'not-a-colour')" >/dev/null \
    || fail 'an unusable hex was accepted, or was refused without saying so'

# The ground wells move the whole surface and text family, not one token.
ground="$(qs_for_test ipc call accent-controls-test well background '#101020')"
jq -e '.bg == "#101020" and .fg == "#c8d3f5" and .fgDim != "#828bb8"' \
    <<<"$ground" >/dev/null \
    || fail 'changing the background did not re-derive the surface and text family'

trap - EXIT
cleanup
printf 'accent controls contract: PASS\n'
