#!/bin/sh
# func-test - Unit Test Helper Functions
#
# Copyright (c) 2026 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

# shellcheck disable=SC2034

# ----------------------------------------------------------------------------
# Constants

# ansi codes
readonly ANSI_RED="\033[31m"
readonly ANSI_GREEN="\033[32m"
readonly ANSI_BLACK="\033[m"

# power supplies
readonly PS_AC=0
readonly PS_BAT=1
readonly PS_UNKNOWN=128

# power profiles
readonly PP_PRF=0
readonly PP_BAL=1
readonly PP_SAV=2
readonly PP_SUS=3
readonly PP_USER="0 1 2"

# manual/persistent mode exceptional cases
readonly PP_NONE=255

# statefile
readonly LASTPWR='/run/tlp/last_pwr'
readonly MANUALMODE='/run/tlp/manual_mode'

# workaround: mitigate void threshold writes
export VWRITE_SLEEP=2

# ----------------------------------------------------------------------------
# Functions

# --- Checks

is_uint () { # check for unsigned integer -- $1: string
    printf "%s" "$1" | grep -E -q "^[0-9]+$" 2> /dev/null
}

toupper () { # print string in uppercase -- $1: string
    printf "%s" "$1" | tr "[:lower:]" "[:upper:]"
}

wordinlist () { # test if word in list
                # $1: word, $2: whitespace-separated list of words
    local word

    if [ -n "${1-}" ]; then
        for word in ${2-}; do
            [ "${word}" != "${1}" ] || return 0 # exact match
        done
    fi

    return 1 # no match
}

get_listitem () { # print the active list item, which is the one enclosed in []
                  # $1: list
    printf "%s" "$(echo "$1" | sed -r 's/.*\[([A-Za-z0-9_]+)\].*/\1/')"
}

test_root () {
    # test root privilege -- rc: 0=root, 1=not root
    [ "$(id -u)" = "0" ]
}

check_tlp () {
    # test if tlp installed
    if [ ! -f /usr/sbin/tlp ]; then
        printf_msg "Error: %s not installed." "$TLP" 1>&2
        exit 254
    fi
}

on_ac () {
    # Detect AC power
    # rc: 0=AC/1=BAT
    # Note: compared to get_sys_power_supply() this is primitive. but it will do.
    upower -i /org/freedesktop/UPower/devices/line_power_AC 2> /dev/null | grep -qE 'online:\s+yes'
}

bat_present () {
    # Check for battery
    # $1: battery name
    # rc: 0=present/1=absent

    [ "$(read_sysf "/sys/class/power_supply/$1/present")" = "1" ]
}

pp2str () {
    # Convert profile code to string
    # $1: profile: PP_PRF=0/PP_BAL=1/PP_SAV=2

    case "$1" in
        "$PP_PRF")  printf "performance" ;;
        "$PP_BAL")  printf "balanced" ;;
        "$PP_SAV")  printf "power-saver" ;;
        *)          printf "unknown" ;;
    esac
}

id2pp () { # convert power profile id to code
    # $1: id: PRF/AC/BAL/BAT/SAV

    case "$(toupper "$1")" in
        PRF|AC)  printf "%s" "$PP_PRF"  ;;
        BAL|BAT) printf "%s" "$PP_BAL"  ;;
        SAV)     printf "%s" "$PP_SAV"  ;;
        *)       printf "%s" "$PP_NONE" ;;
    esac
}

# --- sysfs
read_sysf () {
    # read and print contents of a sysfile
    # return 1 and print default if read fails
    # $1: sysfile
    # $2: default
    # rc: 0=ok/1=error
    if cat "$1" 2> /dev/null; then
        return 0
    else
        printf "%s" "$2"
        return 1
    fi
}

write_sysf () { # write string to a sysfile
    # $1: string
    # $2: sysfile
    # rc: 0=ok/1=error
    { printf '%s\n' "$1" > "$2"; } 2> /dev/null
}

compare_sysf () {
    # Compare a string to the contents of a sysfile
    # expression
    # $1: string
    # $2: file

    local cmp_str="$1"
    local sys_str

    if [ -f "$2" ]; then
        sys_str="$(read_sysf "$2")"
        if [ "$sys_str" != "$cmp_str" ]; then
            printf_msg "\n*** Deviation at %s: %s (act) != %s (exp)\n" "$2" "$sys_str" "$cmp_str"
            return 1
        fi
    else
        # file is missing
        if [ -n "$cmp_str" ]; then
            printf_msg "\n*** Deviation for %s: sysfile does not exist.\n" "$2"
            return 2
        fi
    fi

    return 0
}

compare_sysf_list () {
    # Compare a string to the contents of a sysfile
    # expression
    # $1: string
    # $2: file

    local cmp_str="$1"
    local sys_str

    if [ -f "$2" ]; then
        sys_str="$(get_listitem "$(read_sysf "$2")")"
        if [ "$sys_str" != "$cmp_str" ]; then
            printf_msg "\n*** Deviation at %s: %s (act) != %s (exp)\n" "$2" "$sys_str" "$cmp_str"
            return 1
        fi
    else
        # file is missing
        if [ -n "$cmp_str" ]; then
            printf_msg "\n*** Deviation for %s: sysfile does not exist.\n" "$2"
            return 2
        fi
    fi

    return 0
}

glob_compare_sysf () {
    # Compare a string to the contents of sysfiles selected by a glob
    # expression
    # $1: string
    # $2..$n: file, ...

    local cmp_str="$1"
    local file_pat="$*"
    file_pat="${file_pat#* }"
    local sys_str
    local cnt=0

    while shift && [ $# -gt 0 ]; do
        if [ -f "$1" ] && sys_str=$(read_sysf "$1"); then
            cnt=$((cnt + 1))
            if [ "$sys_str" != "$cmp_str" ]; then
                printf_msg "\n*** Deviation at %s: %s (act) != %s (exp)\n" "$1" "$sys_str" "$cmp_str"
                return 1
            fi
        fi
    done

    if [ "$cnt" -eq 0 ]; then
        printf_msg "\n*** Deviation for %s: no matching sysfile(s) exist(s).\n" "$file_pat"
        return 2
    fi

    return 0
}

read_saved_profile () {
    # read initial saved profile and power source
    # retval: $_prof, $_ps

    if [ ! -f  "$LASTPWR" ]; then
        # last_pwr non-existent
        sudo tlp start > /dev/null 2>&1
    fi

    read -r _prof _ps < "$LASTPWR"
    if   [ -z "$_prof" ] || [ "$_prof" = "$PP_NONE" ] \
      || [ -z "$_ps" ] || [ "$_ps" = "$PS_UNKNOWN" ]; then
        # last_pwr invalid
        sudo tlp start > /dev/null 2>&1
        read -r _prof _ps < "$LASTPWR"
    fi
}

remove_saved_profile () {
    # delete saved profile and power source
    sudo rm -f "$LASTPWR"
}

# --- Run
cache_root_cred () {
    # cache user credentials before actual testing
    sudo true
}

run_clitest () {
    # Run clitest script and record result line to file
    # $1: script filepath
    # $2: suffix
    # global param: $_report_file

    if [ -f "$_report_file" ]; then
        printf "%-50s --> " "${1##*/} $2" >> "$_report_file"
        clitest --color always "$1" | tee /dev/fd/2 | grep -E '(OK|FAIL):' >> "$_report_file"
    else
        clitest --color always "$1" 1>&2
    fi
    printf "\n" 1>&2
}

threshold_trap () {
    # trap: called from bc tests
    # Reset test machine to configured thresholds
    printf " Test cancelled. Restoring configured thresholds ...\n" 1>&2
    if cat /sys/class/power_supply/BAT0/charge_control_end_threshold > /dev/null 2>&1; then
        sleep $VWRITE_SLEEP
        sudo tlp setcharge BAT0  > /dev/null 2>&1
    fi
    if cat /sys/class/power_supply/BAT1/charge_control_end_threshold > /dev/null 2>&1; then
        sleep $VWRITE_SLEEP
        sudo tlp setcharge BAT1  > /dev/null 2>&1
    fi
    exit 1
}

set_threshold_trap () {
    # enable ^C hook
    # bc test scripts are nested, do not enable multiple hook instances
    if [ -z "$_nested_trap" ]; then
        trap threshold_trap INT
        export _nested_trap=1
    fi
}

reset_threshold_trap () {
    # disable ^C hook
    trap - INT
}

# --- Messages, Reports
printf_msg () {
    # print message to stderr and logfile
    # $1: format string
    # $2..$n: message string(s)
    local fmt="$1"
    shift
    # shellcheck disable=SC2154,SC2059
    printf "$fmt" "$@" | tee -a "${_logfile:-/dev/null}" 1>&2
}

start_report () {
    # Create report file
    # retval: $_report_file, $_nest_level

    if [ -z "$_report_file" ]; then
        # first call -> create report temp file
        if ! _report_file="$(mktemp --tmpdir "tlp-test-report.XXX")"; then
            printf "Error: failed to create report file.\n" 1>&2
        fi
        export _report_file
        export _nest_level=0
    else
        # increment level
        if is_uint "$_nest_level"; then
            export _nest_level="$((_nest_level + 1))"
        else
            export _nest_level=1
        fi
    fi
}

report_test () {
    # Write test name to report
    if [ -f "$_report_file" ]; then
        printf "%-50s --> " "$1" >> "$_report_file"
    fi
}

report_line () {
    # Write text line to report
    # $1: text
    if [ -f "$_report_file" ]; then
        # note: use output string in format for proper ansi esc sequence interpolation
        # shellcheck disable=SC2059
        printf "$1\n"  >> "$_report_file"
    fi
}

report_result () {
    # Write test result to terminal and report
    # $1: # of tests
    # $2: # of errors
    if [ "$2" -eq 0 ]; then
        printf_msg "${ANSI_GREEN}OK:${ANSI_BLACK} %s of %s tests passed\n\n" "$1" "$1"
        report_line "${ANSI_GREEN}OK:${ANSI_BLACK} $1 of $1 tests passed"
    else
        printf_msg "${ANSI_RED}FAIL:${ANSI_BLACK} %s of %s tests failed\n\n" "$2" "$1"
        report_line "${ANSI_RED}FAIL:${ANSI_BLACK} $2 of $1 tests failed"
    fi
}

print_report () {
    [ "$_nest_level" -gt 0 ] && return

    if [ -f "$_report_file" ]; then
        printf "+++ Test Summary ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\n"
        cat "$_report_file"
        printf "\n"
        rm -f "$_report_file"
    else
        printf "Error: missing report file ''%s''.\n" "$_report_file" 1>&2
    fi
}
