#!/usr/bin/env bash

# The device list used to show only what is present, which meant that unplugging
# a headset made the row that owned the sound vanish and left no trace of why
# the laptop speakers had taken over. PipeWire remembers the choice; the page
# did not show it.
#
# SoundDefaults is what makes the ghost row possible. Its whole job is the
# difference between two keys that look the same in a log:
#
#   default.audio.sink             what is playing right now
#   default.configured.audio.sink  what was chosen, present or not
#
# Reading the first one is the bug, and it is a quiet one: it always names a
# device that exists, so the ghost row never appears and the feature does
# nothing on every machine where the configured device happens to be plugged
# in -- which is most machines, most of the time. This pins the second.
#
# It also pins the label, because an absent device has no description to borrow
# and the stored name is all there is to work with.
#
# Runs against canned metadata through the service's own fixture seam. The
# session's real defaults are never read and never written.

set -euo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
service="$repo_dir/config/dot/quickshell/services/SoundDefaults.qml"
harness="$repo_dir/config/dot/quickshell/sound-services-harness.qml"
fixture="$(mktemp -d /tmp/panama-sound-defaults.XXXXXX)"
state_home="$fixture/state"
shell_log="$fixture/quickshell.log"
harness_pid=""

# A Bluetooth address no adapter in this house has ever seen, so the "not
# present" assertions cannot be flipped by what happens to be paired during the
# sweep. The effective sink is a plausible built-in card, and the point of the
# fixture is that the two disagree.
configured_sink="bluez_output.AA_BB_CC_11_22_33.1"
effective_sink="alsa_output.pci-0000_00_1f.3.analog-stereo"
configured_source="alsa_input.usb-Blue_Microphones_Yeti-00.analog-stereo"

fail() {
    printf 'sound defaults contract: %s\n' "$1" >&2
    [[ -s "$shell_log" ]] && sed -n '1,80p' "$shell_log" >&2
    exit 1
}

[[ -f "$service" ]] || fail 'SoundDefaults.qml is missing'
[[ -f "$harness" ]] || fail 'sound-services-harness.qml is missing'

mkdir -p "$fixture/bin" "$state_home"

# The headset is the configured sink and is not here; the built-in card is what
# is actually playing. A service that read the effective key would report the
# built-in card and be wrong in the one case this exists for. The configured
# line is written *first* so that "last key wins" is not what makes it pass.
cat >"$fixture/metadata-absent" <<JSON
Found "default" metadata 30
update: id:0 key:'default.configured.audio.sink' value:'{"name":"$configured_sink"}' type:'Spa:String:JSON'
update: id:0 key:'default.audio.sink' value:'{"name":"$effective_sink"}' type:'Spa:String:JSON'
update: id:0 key:'default.configured.audio.source' value:'{"name":"$configured_source"}' type:'Spa:String:JSON'
update: id:0 key:'default.audio.source' value:'{"name":"$configured_source"}' type:'Spa:String:JSON'
JSON

# Nothing was ever configured: PipeWire is picking by priority. There is no
# ghost to draw, and an empty string is how that has to read -- not the name of
# whatever happens to be playing, which would render a ghost row for a device
# that is present, sitting underneath its own real row.
cat >"$fixture/metadata-unconfigured" <<JSON
Found "default" metadata 30
update: id:0 key:'default.audio.sink' value:'{"name":"$effective_sink"}' type:'Spa:String:JSON'
update: id:0 key:'default.audio.source' value:'{"name":"$configured_source"}' type:'Spa:String:JSON'
JSON

cat >"$fixture/metadata-empty" <<'JSON'
Found "default" metadata 30
JSON

# pw-metadata prints keys it does not own alongside the ones it does, and some
# of them are bare strings rather than JSON. One of those must not take the
# whole read down with it.
cat >"$fixture/metadata-noise" <<JSON
Found "default" metadata 30
update: id:0 key:'log.level' value:'2' type:'Spa:String:JSON'
update: id:0 key:'clock.force-quantum' value:'not json at all' type:'Spa:String:JSON'
update: id:0 key:'default.configured.audio.sink' value:'{"name":"$configured_sink"}' type:'Spa:String:JSON'
JSON

# SoundCards shares this harness. An inert fixture keeps it off the live daemon.
printf '[]\n' >"$fixture/cards.json"

# Nothing here should reach pipewire, but if the fixture seam ever regressed,
# these stubs are what stands between the contract and the session's real
# defaults. They answer nothing, which fails the assertions loudly rather than
# passing them against real hardware.
for tool in pw-metadata pw-dump pactl wpctl; do
    printf '%s\n' '#!/usr/bin/env bash' 'exit 1' >"$fixture/bin/$tool"
    chmod +x "$fixture/bin/$tool"
done

export PANAMA_SOUND_DEFAULTS_FIXTURE="$fixture/defaults"
export PANAMA_SOUND_CARDS_FIXTURE="$fixture/cards.json"

# The seam is a path read once at construction, so a case changes what the file
# says rather than where it points. `absent` deletes it, which is what a read
# that fails looks like from the service's side.
defaults_fixture() {
    if [[ "$1" == "failing" ]]; then
        rm -f "$PANAMA_SOUND_DEFAULTS_FIXTURE"
    else
        cp "$fixture/metadata-$1" "$PANAMA_SOUND_DEFAULTS_FIXTURE"
    fi
}
defaults_fixture absent

instances_for_harness() {
    qs list --all 2>/dev/null | awk -v expected="$harness" '
        /^Instance / { pid = "" }
        /^[[:space:]]*Process ID:/ { pid = $3 }
        /^[[:space:]]*Config path:/ {
            path = $0
            sub(/^[[:space:]]*Config path: /, "", path)
            if (path == expected && pid ~ /^[0-9]+$/) print pid
        }
    '
}

cleanup() {
    if [[ "$harness_pid" =~ ^[0-9]+$ ]] && kill -0 "$harness_pid" 2>/dev/null; then
        kill "$harness_pid" 2>/dev/null || true
        for _ in $(seq 1 40); do
            kill -0 "$harness_pid" 2>/dev/null || break
            sleep 0.05
        done
    fi
    rm -rf "$fixture"
}
trap cleanup EXIT

run() {
    PATH="$fixture/bin:$PATH" \
    XDG_STATE_HOME="$state_home" \
    PANAMA_SOUND_DEFAULTS_FIXTURE="$PANAMA_SOUND_DEFAULTS_FIXTURE" \
    PANAMA_SOUND_CARDS_FIXTURE="$PANAMA_SOUND_CARDS_FIXTURE" \
        qs -p "$harness" "$@"
}

ipc() {
    if [[ "$harness_pid" =~ ^[0-9]+$ ]]; then
        run ipc --pid "$harness_pid" "$@"
    else
        run ipc "$@"
    fi
}

# Refresh, then wait for the parsed value to settle into the shape the filter
# describes. Everything passed here goes to jq, so a case can bring its own
# `--arg`.
await_defaults() {
    local state=""
    ipc call sound-services-test refreshDefaults >/dev/null
    for _ in $(seq 1 60); do
        state="$(ipc call sound-services-test defaults)"
        jq -e "$@" >/dev/null <<<"$state" && { printf '%s' "$state"; return 0; }
        sleep 0.1
    done
    printf '%s' "$state"
    return 1
}

run --daemonize >"$shell_log" 2>&1 || fail 'sound services harness did not launch'
for _ in $(seq 1 60); do
    harness_pid="$(instances_for_harness | head -1)"
    if [[ "$harness_pid" =~ ^[0-9]+$ ]] \
        && ipc show 2>/dev/null | rg -q '^target sound-services-test$'; then
        break
    fi
    sleep 0.1
done
[[ "$harness_pid" =~ ^[0-9]+$ ]] || fail 'sound services harness process did not start'

# ── Configured, and not here ─────────────────────────────────────────────────
state="$(await_defaults --arg sink "$configured_sink" '.sink == $sink')" \
    || fail "the configured sink was not read from the configured key: $state"
[[ "$(jq -r .sink <<<"$state")" != "$effective_sink" ]] \
    || fail 'the effective sink was reported as the configured one, so no ghost row can ever appear'
[[ "$(jq -r .source <<<"$state")" == "$configured_source" ]] \
    || fail "the configured source was not read: $state"

# ── The ghost row ────────────────────────────────────────────────────────────
# Configured, absent from the graph, and labelled from the stored name -- which
# is all there is, because the node that carried the description is gone.
ghost="$(ipc call sound-services-test absent output)"
jq -e --arg sink "$configured_sink" '.present == false and .name == $sink' \
    >/dev/null <<<"$ghost" || fail "a configured device that is not present did not become a ghost row: $ghost"
[[ "$(jq -r .label <<<"$ghost")" == "Bluetooth device (AA:BB:CC:11:22:33)" ]] \
    || fail "the ghost row's label is the raw node name: $(jq -r .label <<<"$ghost")"

# The label is the only part of a ghost row anyone reads, and the names it has
# to work with are structured differently per transport.
[[ "$(ipc call sound-services-test labelFor raop_sink.Living-Room.local.192.168.1.162.7000)" \
    == "Living Room" ]] \
    || fail 'an AirPlay speaker is not named by its mDNS hostname'
[[ "$(ipc call sound-services-test labelFor alsa_output.usb-Generic_USB_Audio-00.analog-stereo)" \
    == "Generic USB Audio" ]] \
    || fail 'a USB device is not named by its product string'
[[ "$(ipc call sound-services-test labelFor "")" == "" ]] \
    || fail 'an empty name produced a label out of nothing'

# ── Keys that are not ours ───────────────────────────────────────────────────
# pw-metadata prints the whole store, and some of it is not JSON. One bad line
# must not take the read down with it.
defaults_fixture noise
state="$(await_defaults --arg sink "$configured_sink" '.sink == $sink')" \
    || fail "a non-JSON value on an unrelated key discarded the whole read: $state"

# ── Nothing configured ───────────────────────────────────────────────────────
defaults_fixture unconfigured
state="$(await_defaults '.sink == ""')" \
    || fail "an unconfigured default did not read as empty: $state"
[[ "$(jq -r .source <<<"$state")" == "" ]] \
    || fail "an unconfigured source did not read as empty: $state"
jq -e '.present == true' >/dev/null <<<"$(ipc call sound-services-test absent output)" \
    || fail 'a session with no configured default still drew a ghost row'

# ── An empty metadata store ──────────────────────────────────────────────────
defaults_fixture empty
state="$(await_defaults '.sink == "" and .source == ""')" \
    || fail "an empty metadata store did not read as empty: $state"

# ── The read failing ─────────────────────────────────────────────────────────
# A session where this cannot be read is a session with no ghost rows, which is
# the pre-redesign behaviour and perfectly usable. It is not a session where the
# Sound page reports a device nobody configured.
defaults_fixture failing
state="$(await_defaults '.sink == "" and .source == ""')" \
    || fail "a failed read left a stale or invented configured device: $state"

# ── Recovery ─────────────────────────────────────────────────────────────────
defaults_fixture absent
state="$(await_defaults --arg sink "$configured_sink" '.sink == $sink')" \
    || fail "the service never recovered after a failed read: $state"

if rg -n 'ReferenceError|TypeError|Binding loop|Unable to assign|Cannot assign' "$shell_log"; then
    fail 'sound services harness emitted a QML runtime warning'
fi

# ── Static ───────────────────────────────────────────────────────────────────
rg -Fq 'pragma Singleton' "$service" || fail 'SoundDefaults is not a singleton'

# The two greps that say which keys are being read. A regression here is
# invisible at runtime on any machine whose configured device is plugged in.
rg -Fq 'default.configured.audio.sink' "$service" \
    || fail 'SoundDefaults does not name the configured sink key'
rg -Fq 'default.configured.audio.source' "$service" \
    || fail 'SoundDefaults does not name the configured source key'

# The live command, which the fixture seam means nothing above exercises.
rg -Fq '"pw-metadata", "-n", "default", "0"' "$service" \
    || fail 'the live read is not pw-metadata against the default metadata store'

# `Pipewire.preferredDefaultAudioSink` is the same configured value typed as a
# node pointer, so it reads null in exactly the case this service exists for.
# Binding the ghost row to it would make the ghost row impossible. The file's
# own comment is allowed to say so; the code is not allowed to do it.
python3 - "$service" <<'PY' || fail 'the configured device was read as a node pointer, which is null precisely when the device is absent'
import sys

for line in open(sys.argv[1], encoding="utf-8"):
    if line.strip().startswith("//"):
        continue
    if "preferredDefaultAudio" in line:
        raise SystemExit(1)
PY

trap - EXIT
cleanup
printf 'sound defaults contract: PASS\n'
