#!/usr/bin/env bash

# Device profiles are the last thing the Sound page handed to GNOME Settings,
# and the reason it had to was that reading them means shelling out to pactl.
# SoundCards does that, so the page can show the profile dropdown itself.
#
# What is worth pinning is the parsing, because pactl's JSON is not the shape
# the UI wants and every mistake in the conversion is invisible until someone
# with a headset and a surround card opens the page:
#
#   * profiles arrive keyed by name in an object, and come out as an ordered
#     list, because a dropdown has an order and an object does not;
#   * a profile pactl marked unavailable is kept and flagged, not dropped --
#     "Headset" missing entirely is a bug report, "Headset (unavailable)" is an
#     explanation;
#   * the port hint says what is physically plugged in, which is the one thing
#     the profile name never tells you;
#   * pactl failing produces an error string, not an empty list that reads as
#     "this machine has no sound card".
#
# Runs against canned pactl output. The real audio graph is never touched:
# pactl is replaced on PATH and through the service's own helper seam.

set -euo pipefail

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

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

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

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

# Two cards, chosen for the two things that go wrong. The built-in card has a
# connected line out and an available analog profile. The headset has no port
# connected at all and a profile pactl reports as unavailable, which is what a
# Bluetooth device looks like between "paired" and "actually here".
cat >"$fixture/cards.good.json" <<'JSON'
[
  {
    "index": 47,
    "name": "alsa_card.pci-0000_00_1f.3",
    "driver": "module-alsa-card.c",
    "properties": {
      "device.description": "Built-in Audio",
      "device.api": "alsa"
    },
    "profiles": {
      "off": {
        "description": "Off",
        "sinks": 0,
        "sources": 0,
        "priority": 0,
        "available": true
      },
      "output:hdmi-stereo": {
        "description": "Digital Stereo (HDMI) Output",
        "sinks": 1,
        "sources": 0,
        "priority": 5900,
        "available": false
      },
      "output:analog-stereo+input:analog-stereo": {
        "description": "Analog Stereo Duplex",
        "sinks": 1,
        "sources": 1,
        "priority": 6565,
        "available": true
      }
    },
    "active_profile": "output:analog-stereo+input:analog-stereo",
    "ports": {
      "analog-output-lineout": {
        "description": "Line Out",
        "type": "Line",
        "priority": 9900,
        "availability_group": "Legacy 1",
        "availability": "available"
      },
      "analog-output-headphones": {
        "description": "Headphones",
        "type": "Headphones",
        "priority": 9000,
        "availability_group": "Legacy 2",
        "availability": "not available"
      }
    }
  },
  {
    "index": 51,
    "name": "bluez_card.74_15_F5_13_A4_28",
    "driver": "module-bluez5-device.c",
    "properties": {
      "device.description": "WH-1000XM4",
      "device.api": "bluez5"
    },
    "profiles": {
      "a2dp-sink": {
        "description": "High Fidelity Playback (A2DP Sink)",
        "sinks": 1,
        "sources": 0,
        "priority": 40,
        "available": true
      },
      "headset-head-unit": {
        "description": "Headset Head Unit (HSP/HFP)",
        "sinks": 1,
        "sources": 1,
        "priority": 30,
        "available": false
      }
    },
    "active_profile": "a2dp-sink",
    "ports": {
      "bluez-output": {
        "description": "Headphone",
        "type": "Headphones",
        "priority": 0,
        "availability_group": "",
        "availability": "not available"
      }
    }
  }
]
JSON

# The read side never runs pactl: SoundCards' own seam,
# PANAMA_SOUND_CARDS_FIXTURE, points it at a file it `cat`s instead. So this
# stub exists for the *write* side -- `pactl set-card-profile` -- and to prove
# the read side did not quietly fall back to the live daemon.
cat >"$fixture/bin/pactl" <<'STUB'
#!/usr/bin/env bash
printf 'pactl' >>"$PANAMA_SOUND_PACTL_LOG"
printf ' <%s>' "$@" >>"$PANAMA_SOUND_PACTL_LOG"
printf '\n' >>"$PANAMA_SOUND_PACTL_LOG"
for arg in "$@"; do
    [[ "$arg" == "cards" ]] && { printf '[]\n'; exit 0; }
done
exit 0
STUB
chmod +x "$fixture/bin/pactl"

# SoundDefaults shares this harness and reads on construction. Give it a
# fixture of its own so it cannot reach the session's real metadata.
: >"$fixture/defaults"

export PANAMA_SOUND_PACTL_LOG="$fixture/pactl.log"
export PANAMA_SOUND_CARDS_FIXTURE="$fixture/cards.json"
export PANAMA_SOUND_DEFAULTS_FIXTURE="$fixture/defaults"
: >"$PANAMA_SOUND_PACTL_LOG"

# 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 how a
# read that fails looks from the service's side.
cards_fixture() {
    case "$1" in
        good)      cp "$fixture/cards.good.json" "$PANAMA_SOUND_CARDS_FIXTURE" ;;
        malformed) printf 'Failure: Module initialization failed\n' >"$PANAMA_SOUND_CARDS_FIXTURE" ;;
        absent)    rm -f "$PANAMA_SOUND_CARDS_FIXTURE" ;;
    esac
}

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_PACTL_LOG="$PANAMA_SOUND_PACTL_LOG" \
    PANAMA_SOUND_CARDS_FIXTURE="$PANAMA_SOUND_CARDS_FIXTURE" \
    PANAMA_SOUND_DEFAULTS_FIXTURE="$PANAMA_SOUND_DEFAULTS_FIXTURE" \
        qs -p "$harness" "$@"
}

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

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'
ipc show 2>/dev/null | rg -q '^target sound-services-test$' \
    || fail 'sound-services-test IPC target did not register'

# ── The good parse ───────────────────────────────────────────────────────────
cards_fixture good
ipc call sound-services-test refreshCards >/dev/null
state=""
for _ in $(seq 1 60); do
    state="$(ipc call sound-services-test cards)"
    [[ "$(jq -r '.cards | length' <<<"$state")" == "2" ]] && break
    sleep 0.1
done
jq -e '.cards | length == 2' >/dev/null <<<"$state" \
    || fail "canned pactl output did not become two cards: $state"
jq -e '.lastError == ""' >/dev/null <<<"$state" \
    || fail "a successful listing carried an error: $state"

# The fixture is the whole read. A service that fell back to the live daemon
# when handed one would pass every assertion below on the developer's machine
# and none of them on anyone else's.
if grep -Fq 'list' "$PANAMA_SOUND_PACTL_LOG"; then
    fail "the fixture was ignored and the live daemon was read: $(cat "$PANAMA_SOUND_PACTL_LOG")"
fi

# Which leaves the live command itself unexercised, so pin it where it is
# written. `-f json` is the load-bearing half: without it pactl prints a
# human-readable block that JSON.parse rejects, and every card disappears.
rg -Fq '"pactl", "-f", "json", "list", "cards"' "$service" \
    || fail 'the live card listing is not the JSON one'

jq -e '.cards[0].name == "alsa_card.pci-0000_00_1f.3"
    and .cards[0].description == "Built-in Audio"
    and .cards[0].activeProfile == "output:analog-stereo+input:analog-stereo"' \
    >/dev/null <<<"$state" || fail "the built-in card lost its identity: $state"

# Profiles become an ordered list. An object has no order, and a dropdown does.
jq -e '.cards[0].profiles | type == "array"' >/dev/null <<<"$state" \
    || fail "profiles are still keyed by name, so the dropdown has no order: $state"
jq -e '(.cards[0].profiles | map(.name)) as $names
    | ($names | index("output:analog-stereo+input:analog-stereo")) != null
    and ($names | index("output:hdmi-stereo")) != null' \
    >/dev/null <<<"$state" || fail "a profile pactl reported went missing: $state"
jq -e '(.cards[0].profiles[] | select(.name == "output:analog-stereo+input:analog-stereo") | .description)
        == "Analog Stereo Duplex"' >/dev/null <<<"$state" \
    || fail "profiles are labelled by their internal name rather than their description: $state"

# The order is pactl's priority, descending -- the order PulseAudio and GNOME
# both present, and the one that puts "Off" at the bottom where nobody clicks
# it by accident. The fixture's priorities (6565, 5900, 0) are deliberately not
# the order the JSON lists them in, so a service that kept insertion order
# fails here.
jq -e '(.cards[0].profiles | map(.name))
        == ["output:analog-stereo+input:analog-stereo", "output:hdmi-stereo", "off"]' \
    >/dev/null <<<"$state" || fail "profiles are not ordered by priority: $state"

# Unavailable profiles are kept and flagged. Dropping them is how a card ends
# up silently missing the mode someone is looking for.
jq -e '(.cards[0].profiles[] | select(.name == "output:hdmi-stereo") | .available) == false
    and (.cards[0].profiles[] | select(.name == "output:analog-stereo+input:analog-stereo") | .available) == true' \
    >/dev/null <<<"$state" || fail "profile availability was not carried through: $state"

# The port hint says what is plugged in. The spec's own example is "Line out
# connected"; this pins the two halves rather than the exact casing, so a
# sentence tweak does not read as a regression -- but a hint that names the
# wrong port, or none, does.
hint="$(jq -r '.cards[0].portHint' <<<"$state")"
[[ "$(tr '[:upper:]' '[:lower:]' <<<"$hint")" == *"line out"* ]] \
    || fail "the built-in card's port hint does not name its connected port: $hint"
[[ "$(tr '[:upper:]' '[:lower:]' <<<"$hint")" == *"connected"* ]] \
    || fail "the port hint does not say the port is connected: $hint"

# Nothing plugged in says so, in the spec's words.
[[ "$(jq -r '.cards[1].portHint' <<<"$state")" == "No port connected" ]] \
    || fail "a card with no available port did not say so: $(jq -r '.cards[1].portHint' <<<"$state")"

jq -e '.cards[1].name == "bluez_card.74_15_F5_13_A4_28"
    and .cards[1].description == "WH-1000XM4"
    and .cards[1].activeProfile == "a2dp-sink"
    and ((.cards[1].profiles[] | select(.name == "headset-head-unit") | .available) == false)' \
    >/dev/null <<<"$state" || fail "the Bluetooth card did not survive the parse: $state"

# ── Switching a profile ──────────────────────────────────────────────────────
: >"$PANAMA_SOUND_PACTL_LOG"
ipc call sound-services-test setProfile \
    alsa_card.pci-0000_00_1f.3 output:hdmi-stereo >/dev/null
for _ in $(seq 1 60); do
    grep -Fq 'set-card-profile' "$PANAMA_SOUND_PACTL_LOG" && break
    sleep 0.1
done
grep -Fq 'pactl <set-card-profile> <alsa_card.pci-0000_00_1f.3> <output:hdmi-stereo>' \
    "$PANAMA_SOUND_PACTL_LOG" \
    || fail "profile switch did not reach pactl: $(cat "$PANAMA_SOUND_PACTL_LOG")"

# A card or profile nobody named is not a reason to run pactl with an empty
# argument and let it decide.
: >"$PANAMA_SOUND_PACTL_LOG"
ipc call sound-services-test setProfile "" output:hdmi-stereo >/dev/null || true
ipc call sound-services-test setProfile alsa_card.pci-0000_00_1f.3 "" >/dev/null || true
sleep 0.3
if grep -Fq 'set-card-profile' "$PANAMA_SOUND_PACTL_LOG"; then
    fail "an empty card or profile name still started a pactl write: $(cat "$PANAMA_SOUND_PACTL_LOG")"
fi

# ── pactl answering with something that is not JSON ──────────────────────────
cards_fixture malformed
ipc call sound-services-test refreshCards >/dev/null
for _ in $(seq 1 60); do
    state="$(ipc call sound-services-test cards)"
    [[ -n "$(jq -r '.lastError' <<<"$state")" ]] && break
    sleep 0.1
done
jq -e '.lastError != "" and (.cards | type == "array")' >/dev/null <<<"$state" \
    || fail "unparseable pactl output did not degrade into an error: $state"

# ── pactl not answering at all ───────────────────────────────────────────────
cards_fixture absent
ipc call sound-services-test refreshCards >/dev/null
for _ in $(seq 1 60); do
    state="$(ipc call sound-services-test cards)"
    [[ -n "$(jq -r '.lastError' <<<"$state")" ]] && break
    sleep 0.1
done
jq -e '.lastError != "" and .busy == false' >/dev/null <<<"$state" \
    || fail "a failing pactl left the service busy or silent: $state"

# ── Recovery ─────────────────────────────────────────────────────────────────
# An error is a state, not a terminal one. The card comes back on the next
# refresh, and the error goes away with it.
cards_fixture good
ipc call sound-services-test refreshCards >/dev/null
for _ in $(seq 1 60); do
    state="$(ipc call sound-services-test cards)"
    jq -e '.lastError == "" and (.cards | length) == 2' >/dev/null <<<"$state" && break
    sleep 0.1
done
jq -e '.lastError == "" and (.cards | length) == 2' >/dev/null <<<"$state" \
    || fail "the service never recovered from a failed listing: $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

# The Sound page is a PipeWire surface. SoundCards is allowed to shell out --
# it is the sibling singleton that exists so the page does not have to -- but
# it must be the one place that speaks pactl about cards.
rg -Fq 'pragma Singleton' "$service" || fail 'SoundCards is not a singleton'

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