#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (C) 2026 Altera Corporation
# SPDX-License-Identifier: MIT-0
# Customer-facing Linux runner for the packaged HPS test-suite.
# Do not use set -u: manifest tag names may collide with unset shell variables
# during parsing of customer-edited config/manifest files.
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ASSETS_ROOT="$SCRIPT_DIR/assets"
LOG_DIR="$SCRIPT_DIR/logs"
MANIFEST_FILE="$SCRIPT_DIR/manifest.tsv"
METADATA_FILE="$SCRIPT_DIR/metadata.env"
CONFIG_FILE_DEFAULT="$SCRIPT_DIR/config/testsuite.conf"
CONFIG_FILE="${TESTSUITE_CONFIG:-$CONFIG_FILE_DEFAULT}"

PACKAGE_PROJECT="test-suite"
PACKAGE_PLATFORM="unknown"
PACKAGE_BUILD_TYPE="release"
PACKAGE_BUILD_TIME_UTC="unknown"
PACKAGE_VERSION="unknown"
PACKAGE_SOURCE_COMMIT="unknown"
PACKAGE_MANIFEST_SHA256="unknown"
PACKAGE_CONFIG_SHA256="unknown"

if [[ -f "$METADATA_FILE" ]]; then
  # shellcheck disable=SC1090
  source "$METADATA_FILE"
fi

C_RESET="\033[0m"
C_TITLE="\033[1;34m"
C_MENU="\033[1;36m"
C_OK="\033[1;32m"
C_FAIL="\033[1;31m"
C_WARN="\033[1;33m"

if [[ "${TERM:-}" == "dumb" || "${NO_COLOR:-}" == "1" ]]; then
  C_RESET=""; C_TITLE=""; C_MENU=""; C_OK=""; C_FAIL=""; C_WARN=""
fi

REMAINING_ARGS=()
MANIFEST_FIELD_COUNT=12
TAG_OPT_IN="opt-in"
TAG_OPT_IN_LEGACY="manual"
TAG_DESTRUCTIVE="destructive-op"
TAG_DESTRUCTIVE_LEGACY="destructive"

trim_field() {
  local s="$1"
  s="${s#"${s%%[![:space:]]*}"}"
  s="${s%"${s##*[![:space:]]}"}"
  printf '%s' "$s"
}

parse_global_options() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --config)
        [[ $# -ge 2 ]] || { echo "--config requires a file" >&2; exit 1; }
        CONFIG_FILE="$2"; shift 2 ;;
      --no-config) CONFIG_FILE=""; shift ;;
      *) break ;;
    esac
  done
  REMAINING_ARGS=("$@")
}

load_runtime_config() {
  [[ -z "${CONFIG_FILE:-}" ]] && return 0

  if [[ ! -f "$CONFIG_FILE" ]]; then
    echo -e "${C_WARN}WARN:${C_RESET} config file not found: $CONFIG_FILE" >&2
    return 0
  fi

  set +u
  set -a
  # shellcheck disable=SC1090
  source "$CONFIG_FILE"
  set +a

  if [[ -f "$SCRIPT_DIR/lib/lab_config.sh" ]]; then
    # shellcheck disable=SC1091
    source "$SCRIPT_DIR/lib/lab_config.sh"
    lab_load_config_from_file "$CONFIG_FILE"
  fi
}

need_layout() {
  [[ -f "$MANIFEST_FILE" ]] || { echo "Missing manifest: $MANIFEST_FILE" >&2; exit 1; }
  mkdir -p "$LOG_DIR"
}

manifest_entries() {
  grep -vE '^[[:space:]]*($|#)' "$MANIFEST_FILE"
}

valid_manifest_id() {
  [[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]
}

manifest_load_fields_from_line() {
  local line="$1"
  local nf

  category=""; name=""; entry_rel=""; description=""; required_bins=""
  needs_root=""; board_count=""; timeout_sec=""; default_args=""
  tags=""; assets=""; env_hints=""

  line="$(trim_field "$line")"
  [[ -z "$line" ]] && return 1
  [[ "$line" =~ ^# ]] && return 1

  nf="$(awk -F'|' '{print NF}' <<< "$line")"
  [[ "$nf" -eq "$MANIFEST_FIELD_COUNT" ]] || return 2

  IFS='|' read -r category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints <<< "$line"

  category="$(trim_field "$category")"
  name="$(trim_field "$name")"
  entry_rel="$(trim_field "$entry_rel")"
  needs_root="$(trim_field "${needs_root:-0}")"
  board_count="$(trim_field "${board_count:-1}")"
  timeout_sec="$(trim_field "${timeout_sec:-0}")"
  tags="$(trim_field "${tags:-}")"

  valid_manifest_id "$category" && valid_manifest_id "$name" || return 2
  return 0
}

manifest_warn_malformed_line() {
  local line="$1" preview="${line:0:120}"
  [[ "${#line}" -gt 120 ]] && preview="${preview}..."
  echo -e "${C_WARN}WARN:${C_RESET} skipping malformed manifest line: $preview" >&2
}

has_tag() {
  local tags_csv="${1:-}" want="${2:-}" tag
  [[ -n "$tags_csv" && -n "$want" ]] || return 1
  IFS=',' read -ra tag_list <<< "$tags_csv"
  for tag in "${tag_list[@]}"; do
    tag="$(trim_field "$tag")"
    [[ -n "$tag" && "$tag" == "$want" ]] && return 0
  done
  return 1
}

is_opt_in_test() {
  has_tag "$1" "$TAG_OPT_IN" || has_tag "$1" "$TAG_OPT_IN_LEGACY"
}

is_destructive_test() {
  has_tag "$1" "$TAG_DESTRUCTIVE" || has_tag "$1" "$TAG_DESTRUCTIVE_LEGACY"
}

raw_tag_list() {
  local tags_csv="$1" out="" tag
  IFS=',' read -ra tag_list <<< "$tags_csv"
  for tag in "${tag_list[@]}"; do
    tag="$(trim_field "$tag")"
    [[ -n "$tag" ]] || continue
    out="${out:+$out, }$tag"
  done
  printf '%s' "${out:-none}"
}

normalized_tags() {
  local tags_csv="$1" needs_root_in="${2:-0}" board_count_in="${3:-1}"
  local out=() tag normalized key

  add_tag() {
    local candidate="$1" existing
    [[ -n "$candidate" ]] || return 0
    for existing in "${out[@]:-}"; do
      [[ "$existing" == "$candidate" ]] && return 0
    done
    out+=("$candidate")
  }

  [[ "$needs_root_in" == "1" ]] && add_tag "access:root"
  [[ "${board_count_in:-1}" -gt 1 ]] && add_tag "setup:requires-peer"

  IFS=',' read -ra tag_list <<< "$tags_csv"
  for tag in "${tag_list[@]}"; do
    tag="$(trim_field "$tag")"
    [[ -z "$tag" ]] && continue
    case "$tag" in
      smoke) normalized="profile:smoke" ;;
      oobe) normalized="profile:oobe" ;;
      opt-in|manual) normalized="safety:manual" ;;
      destructive|destructive-op) normalized="safety:destructive" ;;
      multiboard) normalized="setup:requires-peer" ;;
      requires-*) normalized="setup:${tag}" ;;
      network) normalized="setup:requires-network" ;;
      board-state) normalized="safety:board-state" ;;
      generated) normalized="quality:generated" ;;
      performance) normalized="profile:advanced" ;;
      storage|flash|qspi|usb|gpio|i2c|i3c|spi|sdmmc|sdram|memory|timer|uart|crypto|fpga|bridge|dma|kernel|hwmon|tsn|qbu|sdm|emmc|clocksource)
        normalized="area:${tag}" ;;
      *) normalized="raw:${tag}" ;;
    esac
    add_tag "$normalized"
  done

  ((${#out[@]} == 0)) && { printf 'none'; return 0; }
  printf '%s' "${out[0]}"
  for ((key=1; key<${#out[@]}; key++)); do
    printf ', %s' "${out[$key]}"
  done
}

tag_badges() {
  local tags_csv="$1" needs_root_in="${2:-0}" board_count_in="${3:-1}"
  local badges=()
  [[ "$needs_root_in" == "1" ]] && badges+=("[root]")
  [[ "${board_count_in:-1}" -gt 1 ]] && badges+=("[${board_count_in}-board]")
  is_destructive_test "$tags_csv" && badges+=("[destructive]")
  is_opt_in_test "$tags_csv" && badges+=("[manual]")
  has_tag "$tags_csv" smoke && badges+=("[smoke]")
  has_tag "$tags_csv" oobe && badges+=("[oobe]")
  has_tag "$tags_csv" requires-usb3 && badges+=("[requires-usb3]")
  has_tag "$tags_csv" requires-usb2 && badges+=("[requires-usb2]")
  has_tag "$tags_csv" requires-i2c && badges+=("[requires-i2c]")
  has_tag "$tags_csv" requires-i3c && badges+=("[requires-i3c]")
  has_tag "$tags_csv" requires-spi && badges+=("[requires-spi]")
  has_tag "$tags_csv" requires-fcs && badges+=("[requires-fcs]")
  has_tag "$tags_csv" requires-dma && badges+=("[requires-dma]")
  has_tag "$tags_csv" requires-usb_device_mode && badges+=("[requires-usb-device]")
  ((${#badges[@]} == 0)) && { printf '[safe]'; return 0; }
  printf '%s' "${badges[*]}"
}

pass_criteria_for() {
  local category="$1" name="$2" description_in="$3" tags_csv="$4"
  : "$name" "$description_in"
  if is_destructive_test "$tags_csv"; then
    printf 'Command completes, data/state verification passes, and no destructive-operation error is reported.'
  elif has_tag "$tags_csv" network; then
    printf 'Network command completes and packet loss/error checks stay within the script criteria.'
  elif has_tag "$tags_csv" storage || [[ "$category" =~ ^(sdmmc|qspi|usb)$ ]]; then
    printf 'Target device is enumerated and the script-specific read/write or information check succeeds.'
  elif [[ "$category" =~ ^(i2c|i3c|spi|gpio|uart)$ ]]; then
    printf 'Configured peripheral is reachable and the script-specific transaction/check succeeds.'
  elif [[ "$category" =~ ^(timer|sdram|dma|hwmon|crypto)$ ]]; then
    printf 'Kernel/user-space command output satisfies the script assertions and returns success.'
  else
    printf 'Script exits with code 0 and prints a PASS line after completing its built-in checks.'
  fi
}

setup_note_for() {
  local tags_csv="$1" board_count_in="$2" note=""
  [[ "${board_count_in:-1}" -gt 1 ]] && note+="Requires ${board_count_in} boards/devkits. "
  has_tag "$tags_csv" network && note+="Configure NET_IFACE/PEER_IP and network connectivity. "
  has_tag "$tags_csv" requires-usb3 && note+="Attach required USB3 device/storage. "
  has_tag "$tags_csv" requires-usb2 && note+="Attach required USB2 device/storage. "
  has_tag "$tags_csv" requires-i2c && note+="Enable i2c=y and set I2C_BUS/I2C_ADDR if a real slave is used. "
  has_tag "$tags_csv" requires-i3c && note+="Enable i3c=y and ensure the I3C target is present. "
  has_tag "$tags_csv" requires-spi && note+="Enable spi=y and verify SPI_DEV/loopback setup. "
  has_tag "$tags_csv" requires-fcs && note+="Enable fcs=y and ensure FCS driver/client assets are present. "
  has_tag "$tags_csv" requires-dma && note+="Enable dma=y and ensure dmatest support is available. "
  is_opt_in_test "$tags_csv" && note+="Manual/advanced test: review ./ts info and script help before running. "
  is_destructive_test "$tags_csv" && note+="Destructive test: review recovery plan and run only with TESTSUITE_ALLOW_DESTRUCTIVE=1 plus --yes. "
  printf '%s' "${note:-No special setup beyond the packaged default config.}"
}

recovery_note_for() {
  local category="$1" tags_csv="$2"
  if ! is_destructive_test "$tags_csv"; then
    printf 'Not destructive by manifest tag.'
    return 0
  fi
  case "$category" in
    qspi|rsu) printf 'Have a known-good boot/recovery image and board reprogramming method ready before running.' ;;
    sdmmc|usb) printf 'Back up target media and confirm the selected block device/partition is disposable.' ;;
    reset) printf 'Expect the board to reboot; keep serial console and power-cycle access available.' ;;
    fpga|fpga_bridge) printf 'Be ready to reload the reference FPGA design or reboot to restore expected state.' ;;
    *) printf 'Confirm the operation is acceptable and that board recovery access is available.' ;;
  esac
}

fail_reason_from_log() {
  local log_file="$1"
  awk '
    /FAIL:|ERROR:|command not found|No such file|not found|required|missing|Permission denied|timed out|TIMEOUT/ {
      sub(/^[[:space:]]+/, ""); print; exit
    }
  ' "$log_file" 2>/dev/null || true
}

pass_evidence_from_log() {
  local log_file="$1"
  awk '
    /PASS-EVIDENCE:|PASS:/ { last=$0 }
    END { if (last != "") { sub(/^[[:space:]]+/, "", last); print last } }
  ' "$log_file" 2>/dev/null || true
}

lab_entry_disabled() {
  local tags_csv="$1" entry_rel_in="$2"
  declare -F lab_entry_runnable >/dev/null 2>&1 || return 1
  ! lab_entry_runnable "$tags_csv" "$entry_rel_in"
}

lab_print_disabled_skip() {
  local category_in="$1" name_in="$2" tags_csv="$3" entry_rel_in="$4" reason
  reason="$(lab_skip_reason "$tags_csv" "$entry_rel_in")"
  echo -e "${C_WARN}SKIP:${C_RESET} ${category_in}/${name_in} — ${reason} in config/testsuite.conf"
  echo "       Enable the peripheral in config/testsuite.conf, then run again."
}

usage() {
  cat <<USAGE_EOF
${PACKAGE_PROJECT} (${PACKAGE_PLATFORM}, ${PACKAGE_BUILD_TYPE})
Version: ${PACKAGE_VERSION}
Built  : ${PACKAGE_BUILD_TIME_UTC}

Customer quick flow:
  ./ts help
  ./ts doctor
  ./ts list
  ./ts quick                 # same as: ./ts all --profile oobe
  ./ts info <category/test>
  ./ts run <category/test>

Global options:
  ./ts --config <file> <command>
  ./ts --no-config <command>

Commands:
  help | -h | --help
  version
  config show                # show active config file
  config check               # validate customer-safe config basics
  config                     # alias for config show
  list | ls [category] [--verbose]
  info | show <test-or-category/test>
  doctor | check [test-or-category/test]
  run | r <test-or-category/test> [-- args...]
  quick | oobe               # run safe OOBE/smoke tests only
  all | a [category] [--include-manual] [--include-destructive --yes] [--include-multiboard]
  menu

Destructive tests:
  Destructive tests require BOTH an environment opt-in and explicit --yes:
    TESTSUITE_ALLOW_DESTRUCTIVE=1 ./ts run qspi/qspi-rw --yes
    TESTSUITE_ALLOW_DESTRUCTIVE=1 ./ts all --include-destructive --yes
USAGE_EOF
}

show_config() {
  echo "Config file: ${CONFIG_FILE:-disabled}"
  echo
  if [[ -n "${CONFIG_FILE:-}" && -f "$CONFIG_FILE" ]]; then
    sed -n '1,260p' "$CONFIG_FILE"
  else
    echo "No config file loaded."
  fi
  if declare -F lab_show_profile >/dev/null 2>&1; then
    echo
    lab_show_profile
  fi
  cat <<'SHOWCFG_EOF'

Override examples:
  ./ts --config ./config/examples/agilex5_network.conf doctor
  NET_IFACE=end0 PEER_IP=192.168.1.50 ./ts run ethernet/ping
  GPIO_PIN=512 ./ts run gpio/gpio-smoke
SHOWCFG_EOF
}

config_check() {
  local failures=0 var
  echo "Config file: ${CONFIG_FILE:-disabled}"
  if [[ -z "${CONFIG_FILE:-}" || ! -f "$CONFIG_FILE" ]]; then
    echo -e "${C_WARN}WARN:${C_RESET} no config file loaded"
    return 0
  fi
  local required=(NET_IFACE PEER_IP LOCAL_IP GPIO_PIN QSPI_MTD_DEV QSPI_RW_OFFSET SDMMC_DEV MMC_DEV USB_PARTITION SPI_DEV I2C_BUS UART_DEV)
  for var in "${required[@]}"; do
    if [[ -z "${!var:-}" ]]; then
      echo -e "${C_WARN}[WARN]${C_RESET} $var is empty"
      failures=$((failures + 1))
    else
      echo "[OK] $var=${!var}"
    fi
  done
  if [[ "${QSPI_RW_OFFSET:-}" =~ ^0[xX]0+$|^0$ ]]; then
    echo -e "${C_WARN}[WARN]${C_RESET} QSPI_RW_OFFSET is zero; this may target boot-critical flash"
    failures=$((failures + 1))
  fi
  [[ "${USB_MOUNT_RW:-0}" == "1" ]] && echo -e "${C_WARN}[WARN]${C_RESET} USB_MOUNT_RW=1; write tests may modify USB media"
  [[ "${SDMMC_MOUNT_RW:-0}" == "1" ]] && echo -e "${C_WARN}[WARN]${C_RESET} SDMMC_MOUNT_RW=1; write tests may modify storage"
  if [[ "$failures" -gt 0 ]]; then
    echo
    echo "Config check completed with $failures warning(s). Review before running advanced tests."
  else
    echo
    echo -e "${C_OK}Config check passed.${C_RESET}"
  fi
  return 0
}

find_entry_by_full() {
  local want_category="$1" want_name="$2" line rc
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  while IFS= read -r line || [[ -n "$line" ]]; do
    rc=0
    manifest_load_fields_from_line "$line" || rc=$?
    [[ "$rc" -eq 1 ]] && continue
    [[ "$rc" -eq 2 ]] && continue
    if [[ "$category" == "$want_category" && "$name" == "$want_name" ]]; then
      printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' "$category" "$name" "$entry_rel" "$description" "$required_bins" "$needs_root" "$board_count" "$timeout_sec" "$default_args" "$tags" "$assets" "$env_hints"
      return 0
    fi
  done < <(manifest_entries)
  return 1
}

find_entry_by_name() {
  local want_name="$1" line rc
  local matches=()
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  while IFS= read -r line || [[ -n "$line" ]]; do
    rc=0
    manifest_load_fields_from_line "$line" || rc=$?
    [[ "$rc" -eq 1 ]] && continue
    [[ "$rc" -eq 2 ]] && continue
    if [[ "$name" == "$want_name" ]]; then
      matches+=("$category|$name|$entry_rel|$description|$required_bins|$needs_root|$board_count|$timeout_sec|$default_args|$tags|$assets|$env_hints")
    fi
  done < <(manifest_entries)
  if [[ "${#matches[@]}" -eq 1 ]]; then printf '%s\n' "${matches[0]}"; return 0; fi
  if [[ "${#matches[@]}" -gt 1 ]]; then echo "Ambiguous test name '$want_name'. Use category/test instead." >&2; return 2; fi
  return 1
}

resolve_test_ref() {
  local ref="$1"
  if [[ "$ref" == */* ]]; then
    find_entry_by_full "${ref%%/*}" "${ref##*/}"
  else
    find_entry_by_name "$ref"
  fi
}

print_compact_test_line() {
  local category_in="$1" name_in="$2" description_in="$3" needs_root_in="$4" board_count_in="$5" tags_in="$6"
  local badges
  badges="$(tag_badges "$tags_in" "$needs_root_in" "$board_count_in")"
  printf '%-12s %-24s %-42s %s\n' "$category_in" "$name_in" "$badges" "$description_in"
}

print_verbose_test_line() {
  local category_in="$1" name_in="$2" description_in="$3" needs_root_in="$4" board_count_in="$5" tags_in="$6"
  printf '%-12s %-24s %s\n' "$category_in" "$name_in" "$(tag_badges "$tags_in" "$needs_root_in" "$board_count_in")"
  printf '  Description    : %s\n' "$description_in"
  printf '  Raw tags       : %s\n' "$(raw_tag_list "$tags_in")"
  printf '  Normalized tags: %s\n' "$(normalized_tags "$tags_in" "$needs_root_in" "$board_count_in")"
}

list_tests() {
  local want_category="" verbose=0 line rc
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --verbose|-v) verbose=1; shift ;;
      *) want_category="$1"; shift ;;
    esac
  done
  if [[ "$verbose" -eq 0 ]]; then
    printf '%-12s %-24s %-42s %s\n' "Category" "Test" "Safety/setup" "Description"
    printf '%-12s %-24s %-42s %s\n' "--------" "----" "------------" "-----------"
  fi
  while IFS= read -r line || [[ -n "$line" ]]; do
    rc=0
    manifest_load_fields_from_line "$line" || rc=$?
    [[ "$rc" -eq 1 ]] && continue
    [[ "$rc" -eq 2 ]] && { manifest_warn_malformed_line "$line"; continue; }
    [[ -n "$want_category" && "$category" != "$want_category" ]] && continue
    if [[ "$verbose" -eq 1 ]]; then
      print_verbose_test_line "$category" "$name" "$description" "$needs_root" "$board_count" "$tags"
      if lab_entry_disabled "$tags" "$entry_rel"; then
        printf '  Config status  : disabled (set %s in config/testsuite.conf)\n' "$(lab_skip_reason "$tags" "$entry_rel")"
      fi
    else
      print_compact_test_line "$category" "$name" "$description" "$needs_root" "$board_count" "$tags"
    fi
  done < <(manifest_entries)
}

show_help_snippet() {
  local script="$1"
  if [[ -x "$script" ]]; then
    echo
    echo "Script help:"
    "$script" --help 2>/dev/null || true
  fi
}

info_test() {
  local entry
  entry="$(resolve_test_ref "$1")" || { echo -e "${C_FAIL}Unknown test:${C_RESET} $1" >&2; exit 1; }
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  if ! manifest_load_fields_from_line "$entry"; then
    echo -e "${C_FAIL}CORRUPT manifest entry for:${C_RESET} $1" >&2
    exit 1
  fi
  local script="$SCRIPT_DIR/$entry_rel"
  cat <<INFO_EOF
Test          : ${category}/${name}
Description   : $description
Script        : $entry_rel
Need root     : $needs_root
Boards needed : $board_count
Timeout (sec) : $timeout_sec
Default args  : ${default_args:-none}
Requirements  : ${required_bins:-none}
Assets        : ${assets:-none}
Raw tags      : $(raw_tag_list "$tags")
Safety/setup  : $(tag_badges "$tags" "$needs_root" "$board_count")
Normalized    : $(normalized_tags "$tags" "$needs_root" "$board_count")
Setup note    : $(setup_note_for "$tags" "$board_count")
Pass criteria : $(pass_criteria_for "$category" "$name" "$description" "$tags")
Recovery note : $(recovery_note_for "$category" "$tags")
Env hints     : ${env_hints:-none}
INFO_EOF
  if lab_entry_disabled "$tags" "$entry_rel"; then
    echo "Lab config    : DISABLED (set $(lab_skip_reason "$tags" "$entry_rel") in config/testsuite.conf)"
  elif [[ -n "$(lab_required_peripherals "$tags" "$entry_rel" | head -1 || true)" ]]; then
    echo "Lab config    : ENABLED"
  else
    echo "Lab config    : enabled (no special peripheral required)"
  fi
  show_help_snippet "$script"
}

check_one_bin_expr() {
  local expr="$1" alt found=1
  IFS=':' read -ra alternatives <<< "$expr"
  for alt in "${alternatives[@]}"; do
    alt="$(trim_field "$alt")"
    [[ -z "$alt" ]] && continue
    if command -v "$alt" >/dev/null 2>&1; then echo "  [OK] $alt"; found=0; break; fi
  done
  [[ "$found" -ne 0 ]] && echo -e "  ${C_WARN}[MISSING]${C_RESET} ${expr}"
  return "$found"
}

check_bins() {
  local reqs="$1" missing=0 bin
  IFS=',' read -ra bins <<< "$reqs"
  for bin in "${bins[@]}"; do
    bin="$(trim_field "$bin")"
    [[ -z "$bin" ]] && continue
    if ! check_one_bin_expr "$bin"; then missing=1; fi
  done
  return "$missing"
}

check_assets() {
  local assets_csv="$1" missing=0 asset
  [[ -z "${assets_csv// }" ]] && return 0
  IFS=',' read -ra list <<< "$assets_csv"
  for asset in "${list[@]}"; do
    asset="$(trim_field "$asset")"
    [[ -z "$asset" ]] && continue
    if [[ -f "$SCRIPT_DIR/$asset" ]]; then echo "  [OK] $asset"; else echo -e "  ${C_WARN}[MISSING]${C_RESET} $asset"; missing=1; fi
  done
  return "$missing"
}

doctor_one() {
  local entry
  entry="$(resolve_test_ref "$1")" || { echo -e "${C_FAIL}Unknown test:${C_RESET} $1" >&2; return 1; }
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  if ! manifest_load_fields_from_line "$entry"; then echo -e "${C_FAIL}CORRUPT manifest entry for:${C_RESET} $1" >&2; return 1; fi
  local script="$SCRIPT_DIR/$entry_rel" failures=0
  echo "${category}/${name}"
  echo "  description  : $description"
  echo "  safety/setup : $(tag_badges "$tags" "$needs_root" "$board_count")"
  echo "  setup note   : $(setup_note_for "$tags" "$board_count")"
  echo "  pass criteria: $(pass_criteria_for "$category" "$name" "$description" "$tags")"
  echo "  timeout sec  : $timeout_sec"
  if [[ ! -x "$script" ]]; then echo -e "  ${C_WARN}[MISSING]${C_RESET} script executable: $entry_rel"; failures=1; else echo "  [OK] script executable"; fi
  echo "  binary checks:"
  if ! check_bins "$required_bins"; then failures=1; fi
  if [[ -n "${assets// }" ]]; then echo "  asset checks:"; if ! check_assets "$assets"; then failures=1; fi; fi
  [[ "$needs_root" == "1" && "${EUID:-$(id -u)}" -ne 0 ]] && echo -e "  ${C_WARN}[INFO]${C_RESET} this test requires root at runtime"
  is_destructive_test "$tags" && echo -e "  ${C_WARN}[INFO]${C_RESET} destructive guard: requires TESTSUITE_ALLOW_DESTRUCTIVE=1 and --yes"
  return "$failures"
}

doctor_all() {
  local failures=0 line rc
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  while IFS= read -r line || [[ -n "$line" ]]; do
    rc=0; manifest_load_fields_from_line "$line" || rc=$?
    [[ "$rc" -eq 1 ]] && continue
    [[ "$rc" -eq 2 ]] && { manifest_warn_malformed_line "$line"; continue; }
    if ! doctor_one "${category}/${name}"; then failures=1; fi
    echo
  done < <(manifest_entries)
  return "$failures"
}

run_with_optional_timeout() {
  local timeout_sec_in="$1"; shift
  if [[ "$timeout_sec_in" -gt 0 ]] && command -v timeout >/dev/null 2>&1; then
    timeout --preserve-status "${timeout_sec_in}s" "$@"
  else
    "$@"
  fi
}

print_destructive_banner() {
  local category_in="$1" name_in="$2" tags_in="$3"
  cat <<BANNER_EOF
${C_WARN}WARNING: destructive test requested${C_RESET}
  Test         : ${category_in}/${name_in}
  Risk         : may modify board state, storage, flash, reset state, RSU state, or FPGA state.
  Recovery note: $(recovery_note_for "$category_in" "$tags_in")
  Required     : TESTSUITE_ALLOW_DESTRUCTIVE=1 and --yes
BANNER_EOF
}

run_one() {
  local ref="$1"; shift || true
  local destructive_yes=0 arg
  for arg in "$@"; do [[ "$arg" == "--yes" ]] && destructive_yes=1; done
  local entry
  entry="$(resolve_test_ref "$ref")" || { echo -e "${C_FAIL}Unknown test:${C_RESET} $ref" >&2; return 1; }
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  if ! manifest_load_fields_from_line "$entry"; then echo -e "${C_FAIL}CORRUPT manifest entry for:${C_RESET} $ref" >&2; return 1; fi
  local script="$SCRIPT_DIR/$entry_rel"
  [[ -x "$script" ]] || chmod +x "$script"
  if lab_entry_disabled "$tags" "$entry_rel"; then lab_print_disabled_skip "$category" "$name" "$tags" "$entry_rel"; return 0; fi
  if [[ "$needs_root" == "1" && "${EUID:-$(id -u)}" -ne 0 ]]; then echo -e "${C_FAIL}FAIL:${C_RESET} ${category}/${name} requires root. Re-run as root or with sudo." >&2; return 1; fi
  if is_destructive_test "$tags"; then
    print_destructive_banner "$category" "$name" "$tags"
    if [[ "${TESTSUITE_ALLOW_DESTRUCTIVE:-0}" != "1" || "$destructive_yes" -ne 1 ]]; then
      echo -e "${C_FAIL}FAIL:${C_RESET} destructive test is not armed." >&2
      echo "       Example: TESTSUITE_ALLOW_DESTRUCTIVE=1 ./ts run ${category}/${name} --yes" >&2
      return 1
    fi
  fi
  echo -e "${C_MENU}==> Running:${C_RESET} ${C_TITLE}${category}/${name}${C_RESET}"
  echo "    Description : $description"
  echo "    Safety/setup: $(tag_badges "$tags" "$needs_root" "$board_count")"
  echo "    Pass criteria: $(pass_criteria_for "$category" "$name" "$description" "$tags")"
  echo "    Config      : ${CONFIG_FILE:-disabled}"
  echo "    Log dir     : $LOG_DIR"
  [[ "$board_count" -gt 1 ]] && echo "    Setup       : requires ${board_count} boards/devkits"
  [[ "$needs_root" == "1" ]] && echo "    Privilege   : root required"
  echo "    Pre-flight command check:"
  if ! check_bins "$required_bins"; then echo -e "${C_FAIL}FAIL:${C_RESET} missing required commands" >&2; return 1; fi
  if [[ -n "${assets// }" ]]; then echo "    Pre-flight asset check:"; if ! check_assets "$assets"; then echo -e "${C_FAIL}FAIL:${C_RESET} missing required assets" >&2; return 1; fi; fi
  local ts log_file rc reason evidence
  ts="$(date +%Y%m%d_%H%M%S)"
  log_file="$LOG_DIR/${category}_${name}_${ts}.log"
  echo "    Log file    : $log_file"
  local -a preset_args=()
  if [[ -n "${default_args// }" ]]; then read -r -a preset_args <<< "$default_args"; fi
  set +e
  (
    export TESTSUITE_ROOT="$SCRIPT_DIR"
    export TESTSUITE_ASSETS_DIR="$ASSETS_ROOT"
    export TESTSUITE_LOG_DIR="$LOG_DIR"
    export TESTSUITE_PLATFORM="$PACKAGE_PLATFORM"
    export TESTSUITE_TEST_REF="${category}/${name}"
    export TESTSUITE_PASS_CRITERIA="$(pass_criteria_for "$category" "$name" "$description" "$tags")"
    cd "$SCRIPT_DIR" || exit 1
    run_with_optional_timeout "$timeout_sec" "$script" "${preset_args[@]}" "$@"
  ) 2>&1 | tee "$log_file"
  rc="${PIPESTATUS[0]}"
  set -e
  if [[ "$rc" -eq 0 ]]; then
    evidence="$(pass_evidence_from_log "$log_file")"
    echo -e "${C_OK}PASS:${C_RESET} ${category}/${name}"
    echo "    Evidence    : ${evidence:-script exited 0 after built-in checks}"
    echo "    Log         : $log_file"
  else
    reason="$(fail_reason_from_log "$log_file")"
    echo -e "${C_FAIL}FAIL:${C_RESET} ${category}/${name} (rc=$rc)"
    echo "    Reason      : ${reason:-see log for first failing command or assertion}"
    echo "    Hint        : run './ts info ${category}/${name}' and review setup/config requirements"
    echo "    Log         : $log_file"
  fi
  return "$rc"
}

print_run_all_summary() {
  local filter_category="${1:-}" elapsed_sec="$2" count_run="$3" count_pass="$4" count_fail="$5"
  local count_skip_multiboard="$6" count_skip_lab="$7" count_skip_opt_in="$8" count_skip_destructive="$9" count_skip_malformed="${10}"
  shift 10
  local -a failed_tests=("$@")
  local count_skip_total=$((count_skip_multiboard + count_skip_lab + count_skip_opt_in + count_skip_destructive + count_skip_malformed))
  local count_listed=$((count_run + count_skip_total)) idx shown
  echo
  echo "========== Test Run Summary =========="
  [[ -n "$filter_category" ]] && echo "Category      : ${filter_category}"
  echo "Duration      : ${elapsed_sec}s"
  echo "Listed tests  : ${count_listed}"
  echo "Executed      : ${count_run}"
  echo "  PASS        : ${count_pass}"
  if [[ "$count_fail" -gt 0 ]]; then echo -e "  ${C_FAIL}FAIL${C_RESET}        : ${count_fail}"; else echo "  FAIL        : 0"; fi
  echo "Skipped       : ${count_skip_total}"
  [[ "$count_skip_lab" -gt 0 ]] && echo "  lab config  : ${count_skip_lab} (enable peripheral in config/testsuite.conf)"
  [[ "$count_skip_opt_in" -gt 0 ]] && echo "  manual      : ${count_skip_opt_in} (use: ts all --include-manual)"
  [[ "$count_skip_destructive" -gt 0 ]] && echo "  destructive : ${count_skip_destructive} (use: TESTSUITE_ALLOW_DESTRUCTIVE=1 ts all --include-destructive --yes)"
  [[ "$count_skip_multiboard" -gt 0 ]] && echo "  multiboard  : ${count_skip_multiboard} (use: ts all --include-multiboard)"
  [[ "$count_skip_malformed" -gt 0 ]] && echo "  malformed   : ${count_skip_malformed} (fix manifest.tsv / redeploy zip)"
  if [[ "$count_fail" -gt 0 ]]; then
    echo
    echo "Failed tests:"
    shown=0
    for idx in "${!failed_tests[@]}"; do
      if [[ "$shown" -ge 25 ]]; then echo "  ... and $((count_fail - shown)) more"; break; fi
      echo "  - ${failed_tests[$idx]}"
      shown=$((shown + 1))
    done
    echo
    echo "Tip: re-run one failed test and inspect its log:"
    echo "  ./ts run ${failed_tests[0]}"
    echo "  ls -t logs/*${failed_tests[0]##*/}* 2>/dev/null | head -1"
  fi
  echo "======================================"
  if [[ "$count_fail" -eq 0 ]]; then echo -e "${C_OK}ALL PASS${C_RESET} — ${count_pass}/${count_run} executed tests passed"; return 0; fi
  echo -e "${C_FAIL}COMPLETED WITH FAILURES${C_RESET} — ${count_fail} failed, ${count_pass} passed, ${count_skip_total} skipped"
  return 1
}

run_all() {
  local want_category="" include_multiboard=0 include_manual=0 include_destructive=0 destructive_yes=0 profile_filter=""
  local count_run=0 count_pass=0 count_fail=0 count_skip_multiboard=0 count_skip_lab=0 count_skip_opt_in=0 count_skip_destructive=0 count_skip_malformed=0
  local -a failed_tests=() extra_run_args=()
  local line rc start_ts end_ts elapsed_sec
  local category name entry_rel description required_bins needs_root board_count timeout_sec default_args tags assets env_hints
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --include-multiboard) include_multiboard=1; shift ;;
      --include-manual) include_manual=1; shift ;;
      --include-destructive) include_destructive=1; shift ;;
      --yes) destructive_yes=1; extra_run_args+=("--yes"); shift ;;
      --profile) [[ $# -ge 2 ]] || { echo "--profile requires a value" >&2; exit 1; }; profile_filter="$2"; shift 2 ;;
      *) if [[ -z "$want_category" ]]; then want_category="$1"; shift; else echo "Unexpected argument to all: $1" >&2; exit 1; fi ;;
    esac
  done
  if [[ "$include_destructive" -eq 1 && ( "${TESTSUITE_ALLOW_DESTRUCTIVE:-0}" != "1" || "$destructive_yes" -ne 1 ) ]]; then
    echo -e "${C_FAIL}FAIL:${C_RESET} --include-destructive requires TESTSUITE_ALLOW_DESTRUCTIVE=1 and --yes" >&2
    return 1
  fi
  start_ts="$(date +%s)"
  while IFS= read -r line || [[ -n "$line" ]]; do
    rc=0; manifest_load_fields_from_line "$line" || rc=$?
    [[ "$rc" -eq 1 ]] && continue
    if [[ "$rc" -eq 2 ]]; then manifest_warn_malformed_line "$line"; count_skip_malformed=$((count_skip_malformed + 1)); continue; fi
    [[ -n "$want_category" && "$category" != "$want_category" ]] && continue
    if [[ -n "$profile_filter" ]]; then
      case "$profile_filter" in
        oobe) { has_tag "$tags" oobe || has_tag "$tags" smoke; } || continue ;;
        smoke) has_tag "$tags" smoke || continue ;;
        *) echo "Unknown profile: $profile_filter" >&2; return 1 ;;
      esac
    fi
    board_count="${board_count:-1}"; needs_root="${needs_root:-0}"; timeout_sec="${timeout_sec:-0}"; tags="${tags:-}"; default_args="${default_args:-}"; entry_rel="${entry_rel:-}"
    if [[ "${board_count:-1}" -gt 1 && "$include_multiboard" -ne 1 ]]; then echo -e "${C_WARN}SKIP:${C_RESET} ${category}/${name} requires ${board_count} boards (use --include-multiboard)"; count_skip_multiboard=$((count_skip_multiboard + 1)); continue; fi
    if lab_entry_disabled "$tags" "$entry_rel"; then lab_print_disabled_skip "$category" "$name" "$tags" "$entry_rel"; count_skip_lab=$((count_skip_lab + 1)); continue; fi
    if is_opt_in_test "$tags" && [[ "$include_manual" -ne 1 ]]; then echo -e "${C_WARN}SKIP:${C_RESET} ${category}/${name} is manual/opt-in (use: ts run ${category}/${name} or ts all --include-manual)"; count_skip_opt_in=$((count_skip_opt_in + 1)); continue; fi
    if is_destructive_test "$tags" && [[ "$include_destructive" -ne 1 ]]; then echo -e "${C_WARN}SKIP:${C_RESET} ${category}/${name} is destructive (use: TESTSUITE_ALLOW_DESTRUCTIVE=1 ts all --include-destructive --yes)"; count_skip_destructive=$((count_skip_destructive + 1)); continue; fi
    count_run=$((count_run + 1))
    if run_one "${category}/${name}" "${extra_run_args[@]}"; then count_pass=$((count_pass + 1)); else count_fail=$((count_fail + 1)); failed_tests+=("${category}/${name}"); fi
    echo
  done < <(manifest_entries)
  end_ts="$(date +%s)"; elapsed_sec=$((end_ts - start_ts))
  print_run_all_summary "$want_category" "$elapsed_sec" "$count_run" "$count_pass" "$count_fail" "$count_skip_multiboard" "$count_skip_lab" "$count_skip_opt_in" "$count_skip_destructive" "$count_skip_malformed" "${failed_tests[@]}"
}

menu_cli() {
  while true; do
    echo
    echo "========== Test Suite Menu =========="
    echo "1) List tests"
    echo "2) Run a test"
    echo "3) Show test info"
    echo "4) Doctor check all"
    echo "5) Run quick OOBE/smoke tests"
    echo "6) Run all enabled single-board tests"
    echo "7) Config check"
    echo "8) Exit"
    echo "====================================="
    read -r -p "Choose [1-8]: " choice
    case "$choice" in
      1) echo; list_tests ;;
      2) echo; list_tests; echo; read -r -p "Enter test name (example: ethernet/ping): " test_ref; run_one "$test_ref" ;;
      3) echo; list_tests; echo; read -r -p "Enter test name: " test_ref; info_test "$test_ref" ;;
      4) doctor_all ;;
      5) run_all --profile oobe ;;
      6) run_all ;;
      7) config_check ;;
      8) exit 0 ;;
      *) echo "Invalid choice" ;;
    esac
  done
}

need_layout
parse_global_options "$@"
set -- "${REMAINING_ARGS[@]}"
load_runtime_config
cmd="${1:-help}"
shift || true
case "$cmd" in
  help|-h|--help) usage ;;
  version)
    echo "PROJECT=$PACKAGE_PROJECT"
    echo "VERSION=$PACKAGE_VERSION"
    echo "PLATFORM=$PACKAGE_PLATFORM"
    echo "BUILD_TYPE=$PACKAGE_BUILD_TYPE"
    echo "BUILD_TIME_UTC=$PACKAGE_BUILD_TIME_UTC"
    echo "SOURCE_COMMIT=$PACKAGE_SOURCE_COMMIT"
    echo "MANIFEST_SHA256=$PACKAGE_MANIFEST_SHA256"
    echo "CONFIG_SHA256=$PACKAGE_CONFIG_SHA256"
    echo "CONFIG_FILE=${CONFIG_FILE:-disabled}" ;;
  config|cfg)
    sub="${1:-show}"; shift || true
    case "$sub" in
      show) show_config ;;
      check) config_check ;;
      *) echo "Usage: $0 config [show|check]" >&2; exit 1 ;;
    esac ;;
  list|ls) list_tests "$@" ;;
  info|show) [[ $# -ge 1 ]] || { echo "Usage: $0 info <test-or-category/test>" >&2; exit 1; }; info_test "$1" ;;
  doctor|check) if [[ $# -eq 0 ]]; then doctor_all; else doctor_one "$1"; fi ;;
  run|r) [[ $# -ge 1 ]] || { echo "Usage: $0 run <test-or-category/test> [-- args...]" >&2; exit 1; }; ref="$1"; shift; [[ "${1:-}" == "--" ]] && shift; run_one "$ref" "$@" ;;
  quick|oobe) run_all --profile oobe ;;
  all|a) run_all "$@" ;;
  menu) menu_cli ;;
  *) run_one "$cmd" "$@"; exit $? ;;
esac
