#!/usr/bin/env bash

# What this machine is.
#
# These predicates decide whether a battery indicator appears, whether closing
# the lid suspends or keeps working, and which idle timings apply. Every one of
# them is asked on machines that have none of the hardware involved, so the
# property that matters most is that a desktop gets a calm "no" rather than an
# error or a wrong yes.
#
# The laptop answers cannot be tested on a desktop, so they are driven against
# fixture sysfs trees through PANAMA_HW_SYS and PANAMA_HW_ACPI. The real
# machine is only asked whether every predicate runs and answers.
#
# The one definition worth pinning hardest: clamshell is lid-closed AND an
# external monitor. Get that backwards and a laptop on a train stays awake with
# its lid shut, or a docked one suspends mid-work.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
hw="$repo_dir/bin/panama-hw"

findings=()
note() { findings+=("$1"); }

[[ -x "$hw" ]] || { printf 'hardware predicates contract: %s is not executable\n' "$hw" >&2; exit 1; }

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT

# Builds a fixture machine. Every argument is optional; what is absent is
# absent on the machine too, which is the case worth testing.
#   fixture <name> chassis=<n> battery=<pct|-> mains=<0|1|-> lid=<open|closed|->
#           external=<yes|no> internal=<yes|no>
fixture() {
    local name="$1"; shift
    local root="$work/$name"
    local chassis="" battery="" mains="" lid="" external="no" internal="no"
    local arg
    for arg in "$@"; do
        case "$arg" in
            chassis=*)  chassis="${arg#*=}" ;;
            battery=*)  battery="${arg#*=}" ;;
            mains=*)    mains="${arg#*=}" ;;
            lid=*)      lid="${arg#*=}" ;;
            external=*) external="${arg#*=}" ;;
            internal=*) internal="${arg#*=}" ;;
        esac
    done

    mkdir -p "$root/sys/class/dmi/id" "$root/sys/class/power_supply" \
             "$root/sys/class/drm" "$root/sys/class/input" \
             "$root/sys/bus/pci/devices" "$root/acpi/button/lid"

    [[ -n "$chassis" ]] && printf '%s\n' "$chassis" >"$root/sys/class/dmi/id/chassis_type"

    if [[ -n "$battery" && "$battery" != "-" ]]; then
        mkdir -p "$root/sys/class/power_supply/BAT0"
        printf 'Battery\n' >"$root/sys/class/power_supply/BAT0/type"
        printf '%s\n' "$battery" >"$root/sys/class/power_supply/BAT0/capacity"
    fi
    if [[ -n "$mains" && "$mains" != "-" ]]; then
        mkdir -p "$root/sys/class/power_supply/AC0"
        printf 'Mains\n' >"$root/sys/class/power_supply/AC0/type"
        printf '%s\n' "$mains" >"$root/sys/class/power_supply/AC0/online"
    fi
    if [[ -n "$lid" && "$lid" != "-" ]]; then
        mkdir -p "$root/acpi/button/lid/LID0"
        printf 'state:      %s\n' "$lid" >"$root/acpi/button/lid/LID0/state"
    fi
    if [[ "$internal" == "yes" ]]; then
        mkdir -p "$root/sys/class/drm/card0-eDP-1"
        printf 'connected\n' >"$root/sys/class/drm/card0-eDP-1/status"
    fi
    if [[ "$external" == "yes" ]]; then
        mkdir -p "$root/sys/class/drm/card0-DP-1"
        printf 'connected\n' >"$root/sys/class/drm/card0-DP-1/status"
    else
        mkdir -p "$root/sys/class/drm/card0-DP-1"
        printf 'disconnected\n' >"$root/sys/class/drm/card0-DP-1/status"
    fi
    printf '%s\n' "$root"
}

ask() {
    local root="$1" predicate="$2"
    PANAMA_HW_SYS="$root/sys" PANAMA_HW_ACPI="$root/acpi" "$hw" "$predicate"
}

# Asserts a predicate's answer. `expect yes|no <root> <predicate> <why>`
expect() {
    local want="$1" root="$2" predicate="$3" why="$4"
    if ask "$root" "$predicate"; then
        [[ "$want" == "yes" ]] || note "$why (answered yes, expected no)"
    else
        [[ "$want" == "no" ]] || note "$why (answered no, expected yes)"
    fi
}

# ── A desktop ────────────────────────────────────────────────────────────────
# No battery, no lid, no mains supply at all. Every laptop answer must be no,
# and AC must be yes: a machine with no mains reporting is on wall power, and
# answering otherwise would apply battery timings to something that cannot run
# out of power.

desktop="$(fixture desktop chassis=3 external=yes)"
expect no  "$desktop" laptop           'a desktop chassis reports as a laptop'
expect no  "$desktop" battery          'a desktop reports a battery'
expect yes "$desktop" ac               'a desktop with no mains supply is not treated as on wall power'
expect no  "$desktop" lid-closed       'a machine with no lid reports its lid closed'
expect no  "$desktop" clamshell        'a desktop reports clamshell'
expect yes "$desktop" external-monitor 'a connected DisplayPort output is not seen as external'

# ── A laptop, lid open, undocked ─────────────────────────────────────────────

open_undocked="$(fixture open-undocked chassis=10 battery=64 mains=1 lid=open internal=yes)"
expect yes "$open_undocked" laptop     'a notebook chassis does not report as a laptop'
expect yes "$open_undocked" battery    'a battery is not detected'
expect yes "$open_undocked" ac         'a plugged-in laptop is not seen as on AC'
expect no  "$open_undocked" lid-closed 'an open lid reports closed'
expect no  "$open_undocked" clamshell  'an open lid reports clamshell'
expect no  "$open_undocked" external-monitor \
    'the built-in panel is counted as an external monitor'

# ── The same laptop, on battery ──────────────────────────────────────────────

unplugged="$(fixture unplugged chassis=10 battery=41 mains=0 lid=open internal=yes)"
expect no "$unplugged" ac 'a laptop with mains offline is still reported as on AC'

# ── Lid shut, no external monitor: this must suspend ─────────────────────────

closed_alone="$(fixture closed-alone chassis=10 battery=30 mains=0 lid=closed internal=yes)"
expect yes "$closed_alone" lid-closed 'a closed lid reports open'
expect no  "$closed_alone" clamshell \
    'a closed lid with no external monitor reports clamshell, which would keep a laptop awake in a bag'

# ── Lid shut with an external monitor: this must keep working ────────────────

docked="$(fixture docked chassis=10 battery=88 mains=1 lid=closed internal=yes external=yes)"
expect yes "$docked" lid-closed       'a docked closed lid reports open'
expect yes "$docked" external-monitor 'a docked external monitor is not detected'
expect yes "$docked" clamshell \
    'a closed lid with an external monitor does not report clamshell, which would suspend a docked machine mid-work'

# ── An empty machine answers rather than erroring ────────────────────────────
# Nothing present at all: no DMI, no power supplies, no lid, no outputs. Every
# predicate must still exit cleanly, because a missing sysfs is what a
# container, a VM, or an unusual kernel looks like.

empty="$(fixture empty)"
for predicate in laptop battery ac lid-closed external-monitor clamshell touchpad nvidia; do
    output="$(ask "$empty" "$predicate" 2>&1)"
    status=$?
    (( status == 0 || status == 1 )) \
        || note "$predicate exits $status on a machine with no hardware; it must answer, not fail"
    [[ -n "$output" ]] \
        && note "$predicate printed '$output' instead of answering silently"
done

# ── The real machine answers every question ──────────────────────────────────

for predicate in laptop battery ac lid-closed external-monitor clamshell touchpad nvidia; do
    "$hw" "$predicate" >/dev/null 2>&1
    status=$?
    (( status == 0 || status == 1 )) \
        || note "$predicate exits $status on this machine"
done

json="$("$hw" --json 2>/dev/null)"
if command -v jq >/dev/null 2>&1; then
    jq -e . >/dev/null 2>&1 <<<"$json" || note '--json does not emit valid JSON'
    for key in laptop battery ac lidClosed externalMonitor clamshell touchpad nvidia; do
        jq -e "has(\"$key\")" >/dev/null 2>&1 <<<"$json" \
            || note "--json omits $key"
    done
fi

# An unknown predicate is a caller's mistake and must be loud, not a silent no.
"$hw" not-a-real-predicate >/dev/null 2>&1
(( $? == 2 )) || note 'an unknown predicate does not exit 2, so a typo reads as a no'

if (( ${#findings[@]} > 0 )); then
    printf 'hardware predicates contract: %d finding(s)\n' "${#findings[@]}" >&2
    printf '  - %s\n' "${findings[@]}" >&2
    exit 1
fi

printf 'hardware predicates contract: PASS\n'
