#!/usr/bin/env bash
# Comprehensive integration tests for Linux proc(5) magic symlinks
#
# Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
# SPDX-License-Identifier: GPL-3.0

set -Euo pipefail

# Minimal test harness
PASS=0
FAIL=0
SKIP=0
TOTAL=0

green=$'\e[32m'; red=$'\e[31m'; yellow=$'\e[33m'; reset=$'\e[0m'
ok()    { PASS=$((PASS+1)); TOTAL=$((TOTAL+1)); printf "%b\n"   "${green}[ ok ]${reset} $1"; }
notok() { FAIL=$((FAIL+1)); TOTAL=$((TOTAL+1)); printf "%b\n" "${red}[fail]${reset} $1"; printf "   => %s\n" "$2" >&2; }
skip()  { SKIP=$((SKIP+1)); TOTAL=$((TOTAL+1)); printf "%b\n" "${yellow}[skip]${reset} $1"; }

skip_multi() {
  # $1 label, $2 count
  local _label="$1" _n="$2" i
  for ((i=1;i<=_n;i++)); do
    skip "${_label} (missing ${i}/${_n})"
  done
}

STATUS_FILE=".t_status.$$"
cleanup() { rm -f -- "$STATUS_FILE" a.txt myfifo || true; }
trap cleanup EXIT INT TERM

_run_store() {
  # Print command output to STDOUT; write exit code to $STATUS_FILE.
  { set +e; "$@"; printf "%s" $? >"$STATUS_FILE"; } 2>&1
}

_read_status() {
  cat "$STATUS_FILE" 2>/dev/null || printf "127"
}

expect_success() {
  local name="$1"; shift
  local o s; o="$(_run_store "$@")"; s="$(_read_status)"
  if [ "$s" -ne 0 ]; then notok "$name" "exit $s; out: $o"; else ok "$name"; fi
}

expect_fail() {
  local name="$1"; shift
  local o s; o="$(_run_store "$@")"; s="$(_read_status)"
  if [ "$s" -eq 0 ]; then notok "$name" "expected failure; out: $o"; else ok "$name"; fi
}

expect_match() {
  local name="$1" pat="$2"; shift 2
  local o s; o="$(_run_store "$@")"; s="$(_read_status)"
  if [ "$s" -ne 0 ]; then notok "$name" "exit $s; out: $o"; return; fi
  printf "%s" "$o" | grep -Eq -- "$pat" || { notok "$name" "no match /$pat/ in: $o"; return; }
  ok "$name"
}

expect_readlink_match() {
  local name="$1" p="$2" pat="$3"
  if [[ ! -e "$p" ]]; then skip "$name: missing $p"; return; fi
  local o s; o="$(_run_store readlink "$p")"; s="$(_read_status)"
  if [ "$s" -ne 0 ]; then notok "$name" "exit $s; out: $o"; return; fi
  printf "%s" "$o" | grep -Eq -- "$pat" || { notok "$name" "no match /$pat/ in: $o"; return; }
  ok "$name"
}

expect_is_symlink(){ local name="$1" p="$2"; [[ -e "$p" ]] || { skip "$name: missing $p"; return; }; [[ -L "$p" ]] || { notok "$name" "not symlink: $p"; return; }; ok "$name"; }
expect_is_dir()   { local name="$1" p="$2"; [[ -e "$p" ]] || { skip "$name: missing $p"; return; }; [[ -d "$p" ]] || { notok "$name" "not dir: $p"; return; }; ok "$name"; }
expect_not_dir()  { local name="$1" p="$2"; [[ -e "$p" ]] || { skip "$name: missing $p"; return; }; [[ ! -d "$p" ]] || { notok "$name" "unexpected dir: $p"; return; }; ok "$name"; }
expect_same_str() { local name="$1" a="$2" b="$3"; [[ "$a" == "$b" ]] || { notok "$name" "A='$a' B='$b'"; return; }; ok "$name"; }

# Fixtures
printf "hello" > a.txt
exec {FD_A}< a.txt

printf "bye" > z.tmp && exec {FD_Z}< z.tmp && rm -f z.tmp

rm -f myfifo
mkfifo myfifo
# O_RDWR open of FIFO avoids blocking
exec {FD_F}<> myfifo

PID=$$
THREAD_LINK="$(_run_store readlink /proc/thread-self || true)"; _read_status >/dev/null || true
TID="${THREAD_LINK##*/}"
TGID="$PID"

# Namespace kinds
NS_KINDS=(cgroup ipc mnt net pid pid_for_children time time_for_children user uts)

ns_token_base() {
  case "$1" in
    pid_for_children) echo "pid" ;;
    time_for_children) echo "time" ;;
    *) echo "$1" ;;
  esac
}

ns_token_id() {  # extract numeric id from readlink token, else empty
  local tok="$1" id
  id="${tok##*[}"; id="${id%]*}"
  [[ "$id" =~ ^[0-9]+$ ]] && printf "%s" "$id" || printf ""
}

# Build contexts; include task ctx even if absent so totals remain fixed (missing -> SKIP)
CTX=("/proc/self" "/proc/thread-self" "/proc/$PID" "/proc/$TGID/task/$TID")

# --------------------------- sanity: proc mount & basics ----------------------
expect_is_dir "proc mounted" /proc
expect_readlink_match "/proc/self resolves to PID" /proc/self '^[0-9]+$'
# accept both "self/task/<tid>" and "<pid>/task/<tid>"
expect_readlink_match "/proc/thread-self shape" /proc/thread-self '^([0-9]+|self)/task/[0-9]+$'

# exe/cwd/root robust checks
expect_is_symlink "/proc/self/exe is symlink" /proc/self/exe
expect_readlink_match "/proc/self/exe absolute" /proc/self/exe '^/.*'
# portable zero-byte read using head -c0
expect_success "read 0 bytes from exe" head -c0 /proc/self/exe
expect_fail "trailing slash on exe is not a dir" stat /proc/self/exe/

expect_is_symlink "/proc/self/cwd is symlink" /proc/self/cwd
PWD_ESC="$(printf '%s' "$PWD" | sed 's/[][\.^$*+?()|{}]/\\&/g')"
expect_readlink_match "/proc/self/cwd equals PWD" /proc/self/cwd "^${PWD_ESC}/?$"
expect_is_dir "/proc/self/cwd/ is dir" /proc/self/cwd/

expect_is_symlink "/proc/self/root is symlink" /proc/self/root
expect_readlink_match "/proc/self/root points to /" /proc/self/root '^/$'
expect_is_dir "/proc/self/root/ is dir" /proc/self/root/

# fd indirection
FD_PATH="/proc/self/fd/$FD_A"
expect_is_symlink "$FD_PATH is symlink" "$FD_PATH"
expect_readlink_match "$FD_PATH ends with a.txt" "$FD_PATH" 'a\.txt$'
expect_match "cat via fd returns content" '^hello$' cat "$FD_PATH"

# deleted file fd shows (deleted)
FDZ_PATH="/proc/self/fd/$FD_Z"
expect_is_symlink "$FDZ_PATH is symlink" "$FDZ_PATH"
expect_readlink_match "$FDZ_PATH shows deleted suffix" "$FDZ_PATH" ' \(deleted\)$'
expect_match "cat deleted fd still readable" '^bye$' cat "$FDZ_PATH"

# fifo behavior
FDF_PATH="/proc/self/fd/$FD_F"
expect_is_symlink "$FDF_PATH is symlink" "$FDF_PATH"
expect_readlink_match "$FDF_PATH points to path" "$FDF_PATH" "^${PWD_ESC}/myfifo$"

# stdio descriptors present
for n in 0 1 2; do
  expect_success "/proc/self/fd has $n" bash -c 'ls /proc/self/fd | grep -qx '"$n"
done

# Namespace helpers
ns_exists() { [[ -e "$1/ns/$2" ]]; }
ns_token()  { _run_store readlink "$1/ns/$2"; }

ns_expect_symlink_and_token() {
  local ctx="$1" ns="$2" label="$3" path="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $path" 2; return; fi
  local base; base="$(ns_token_base "$ns")"
  expect_is_symlink     "$label: symlink $path" "$path"
  expect_readlink_match "$label: token $path"   "$path" "^${base}:\[[0-9]+\]$"
}

ns_expect_read_failers() {
  local ctx="$1" ns="$2" label="$3" path="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $path" 6; return; fi
  expect_fail "$label: dd"    dd   if="$path" of=/dev/null bs=1 count=1 status=none
  expect_fail "$label: cat"   cat  "$path" >/dev/null
  expect_fail "$label: head"  head -c1 "$path"
  expect_fail "$label: wc"    bash -c 'wc -c < "'"$path"'" >/dev/null'
  expect_fail "$label: slash" stat "$path/"
  expect_fail "$label: write" bash -c 'echo X > "'"$path"'"'
}

# Kernel behavior: readlink -f yields "/proc/<pid>[/task/<tid>]/ns/<name_base>:[id]"
# and "stat -L -c %s" prints size 0. Treat both as success conditions.
ns_expect_resolve_behavior() {
  local ctx="$1" ns="$2" label="$3" path="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $path" 2; return; fi
  local base; base="$(ns_token_base "$ns")"
  local re="^/proc/[0-9]+(/task/[0-9]+)?/ns/${base}:\[[0-9]+\]$"
  expect_match "$label: readlink -f" "$re" readlink -f "$path"
  expect_match "$label: stat -L size0" '^0$' stat -L -c %s "$path"
}

ns_expect_variants_equal_token() {
  local ctx="$1" ns="$2" label="$3"
  local base="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $base" 6; return; fi
  local tok s; tok="$(ns_token "$ctx" "$ns")"; s="$(_read_status)"
  if [ "$s" -ne 0 ]; then
    # 6 planned checks -> fail all distinctly so totals stay correct
    notok "$label: base token" "exit $s"
    notok "$label: // variant"  "base token missing"
    notok "$label: /ns//"       "base token missing"
    notok "$label: /// variant" "base token missing"
    notok "$label: ./ variant"  "base token missing"
    notok "$label: ../ variant" "base token missing"
    return
  fi
  local variants=(
    "$ctx//ns/$ns"
    "$ctx/ns//$ns"
    "$ctx///ns///$ns"
    "$ctx/./ns/./$ns"
    "$ctx/ns/../ns/$ns"
    "${ctx%/}/ns/${ns%/}"
  )
  local v t
  for v in "${variants[@]}"; do
    t="$(_run_store readlink "$v")"; s="$(_read_status)"
    if [ "$s" -ne 0 ]; then notok "$label: $(basename "$v")" "exit $s; out: $t"; continue; fi
    expect_same_str "$label: $(basename "$v")" "$t" "$tok"
  done
}

ns_expect_dot_variants_fail() {
  local ctx="$1" ns="$2" label="$3" p="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $p" 2; return; fi
  expect_fail "$label: dot"     stat "$p/."
  expect_fail "$label: dotdot"  bash -c ': > "'"$p/../$ns"'"'
}

ns_expect_tools_fail_min() {
  local ctx="$1" ns="$2" label="$3" p="$ctx/ns/$ns"
  if ! ns_exists "$ctx" "$ns"; then skip_multi "$label: $p" 2; return; fi
  expect_fail "$label: sed"  sed -n '1p' "$p"
  expect_fail "$label: tail" tail -c1 "$p"
}

# GROUP A: core symlink+token
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_symlink_and_token "$ctx" "$ns" "A[$ctx][$ns]"
  done
done

# GROUP B: read failers
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_read_failers "$ctx" "$ns" "B[$ctx][$ns]"
  done
done

# GROUP C: resolve behavior
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_resolve_behavior "$ctx" "$ns" "C[$ctx][$ns]"
  done
done

# GROUP D: variant token equality
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_variants_equal_token "$ctx" "$ns" "D[$ctx][$ns]"
  done
done

# GROUP E: dot-variants fail
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_dot_variants_fail "$ctx" "$ns" "E[$ctx][$ns]"
  done
done

# GROUP F: cross-context token-ID equality
pairs=(
  "0 1" "0 2" "0 3"
  "1 2" "1 3" "2 3"
)
for ns in "${NS_KINDS[@]}"; do
  for pr in "${pairs[@]}"; do
    i="${pr% *}"; j="${pr#* }"
    ctxA="${CTX[$i]}"; ctxB="${CTX[$j]}"
    a="$ctxA/ns/$ns"; b="$ctxB/ns/$ns"
    if [[ -e "$a" && -e "$b" ]]; then
      ta="$(ns_token "$ctxA" "$ns")"; sa="$(_read_status)"
      tb="$(ns_token "$ctxB" "$ns")"; sb="$(_read_status)"
      if [ "$sa" -eq 0 ] && [ "$sb" -eq 0 ]; then
        ia="$(ns_token_id "$ta")"; ib="$(ns_token_id "$tb")"
        if [[ -n "$ia" && -n "$ib" ]]; then
          expect_same_str "F[$ns] id ${ctxA##*/}==${ctxB##*/}" "$ia" "$ib"
        else
          skip "F[$ns] missing ids ${ctxA##*/}/${ctxB##*/}"
        fi
      else
        skip "F[$ns] token read failed ${ctxA##*/}/${ctxB##*/}"
      fi
    else
      skip "F[$ns] ${ctxA##*/} vs ${ctxB##*/} missing"
    fi
  done
done

# GROUP G: child==base token-ID eq
for ctx in "${CTX[@]}"; do
  for child in pid_for_children time_for_children; do
    base="$(ns_token_base "$child")"
    pa="$ctx/ns/$child"; pb="$ctx/ns/$base"
    if [[ -e "$pa" && -e "$pb" ]]; then
      ta="$(ns_token "$ctx" "$child")"; sa="$(_read_status)"
      tb="$(ns_token "$ctx" "$base")"; sb="$(_read_status)"
      if [ "$sa" -eq 0 ] && [ "$sb" -eq 0 ]; then
        ia="$(ns_token_id "$ta")"; ib="$(ns_token_id "$tb")"
        if [[ -n "$ia" && -n "$ib" ]]; then
          expect_same_str "G[$ctx][$child==$base] id" "$ia" "$ib"
        else
          skip "G[$ctx][$child] missing id"
        fi
      else
        skip "G[$ctx][$child] token read failed"
      fi
    else
      skip "G[$ctx][$child] missing"
    fi
  done
done

# GROUP H: id positive
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    p="$ctx/ns/$ns"
    if [[ -e "$p" ]]; then
      tok="$(_run_store readlink "$p")"; s="$(_read_status)"
      if [ "$s" -eq 0 ]; then
        id="$(ns_token_id "$tok")"
        [[ -n "$id" && "$id" -gt 0 ]] \
          && ok "H[$ctx][$ns] id>0 ($id)" \
          || notok "H[$ctx][$ns] id>0" "token=$tok"
      else
        notok "H[$ctx][$ns] readlink failed" "exit $s"
      fi
    else
      skip "H[$ctx][$ns] missing"
    fi
  done
done

# GROUP I: trailing-slash existence
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    p="$ctx/ns/$ns"
    if [[ -e "$p" ]]; then
      if [[ -e "$p/" ]]; then
        notok "I[$ctx][$ns] exists with slash" "$p/"
      else
        ok "I[$ctx][$ns] no-exist with slash"
      fi
    else
      skip "I[$ctx][$ns] missing"
    fi
  done
done

# GROUP J: ls -l shows arrow
for ctx in "${CTX[@]}"; do
  nsdir="$ctx/ns"
  if [[ -d "$nsdir" ]]; then
    listing="$(_run_store ls -l "$nsdir")"; _read_status >/dev/null || true
    for ns in "${NS_KINDS[@]}"; do
      p="$nsdir/$ns"
      if [[ -e "$p" ]]; then
        printf "%s" "$listing" | grep -Eq -- "[[:space:]]$ns[[:space:]]->[[:space:]]" \
          && ok "J[$ctx][$ns] ls shows arrow" \
          || notok "J[$ctx][$ns] ls shows arrow" "no '$ns ->' in listing"
      else
        skip "J[$ctx][$ns] missing"
      fi
    done
  else
    for ns in "${NS_KINDS[@]}"; do
      skip "J[$ctx][$ns] ns dir missing"
    done
  fi
done

# GROUP K: tool failers minimal
for ctx in "${CTX[@]}"; do
  for ns in "${NS_KINDS[@]}"; do
    ns_expect_tools_fail_min "$ctx" "$ns" "K[$ctx][$ns]"
  done
done

# GROUP L: core fd/cwd/exe across contexts
FD_PATH_SELF="/proc/self/fd/$FD_A"
FD_PATH_TSELF="/proc/thread-self/fd/$FD_A"
FD_PATH_PID="/proc/$PID/fd/$FD_A"

# exe trailing slash not dir
expect_fail   "L[exe slash] self"        stat /proc/self/exe/
expect_fail   "L[exe slash] thread-self" stat /proc/thread-self/exe/
expect_fail   "L[exe slash] pid"         stat "/proc/$PID/exe/"

# exe open-only zero bytes ok
expect_success "L[exe head0] self"        head -c0 /proc/self/exe
expect_success "L[exe head0] thread-self" head -c0 /proc/thread-self/exe
expect_success "L[exe head0] pid"         head -c0 "/proc/$PID/exe"

# cwd trailing slash is dir
expect_is_dir "L[cwd dir] self"        /proc/self/cwd/
expect_is_dir "L[cwd dir] thread-self" /proc/thread-self/cwd/
expect_is_dir "L[cwd dir] pid"         "/proc/$PID/cwd/"

# fd/<n> trailing slash not dir
expect_fail   "L[fd slash] self"        stat "$FD_PATH_SELF/"
expect_fail   "L[fd slash] thread-self" stat "$FD_PATH_TSELF/"
expect_fail   "L[fd slash] pid"         stat "$FD_PATH_PID/"

# Summary
echo
printf "Total: %d  Pass: %d  Fail: %d  Skip: %d\n" "$TOTAL" "$PASS" "$FAIL" "$SKIP"
exit $(( FAIL > 0 ))
