#!/usr/bin/env bash
# =============================================================================
#  THADD OS · desktop self-test
# =============================================================================
#  One script, two jobs.
#
#  1. Inside the image (`--image-build`, run by 0500-thadd-cleanup): prove that
#     the *shipped* files are all there and that the assistant and the DFIR
#     engine actually import - so a broken desktop is caught at build time
#     instead of at boot, on the user's screen.  Failures print `FAIL …` lines;
#     with THADD_ISO_STRICT=1 they fail the build.
#
#  2. On a running system (`thadd-desktop-selftest`): the same checks plus the
#     live ones - session, window manager, compositor, bridge server, toolkit -
#     and a verdict a human can act on.
#
#  Exit status: 0 when everything passed, 1 when something is broken.
# =============================================================================
set -u

IMAGE_BUILD=0
QUIET=0
ROOT=""
while [ $# -gt 0 ]; do
    case "$1" in
        --image-build) IMAGE_BUILD=1 ;;
        --quiet|-q)    QUIET=1 ;;
        # --root DIR checks a staged tree instead of a live system (what the ISO
        # build does with iso/config/includes.chroot).  Binaries are not in a
        # staged tree, so "tool missing" is reported as a warning there.
        --root)        ROOT="${2:-}"; shift ;;
        --help|-h)     sed -n '3,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
        *)             printf 'unknown argument: %s\n' "$1" >&2; exit 2 ;;
    esac
    shift
done
if [ -n "$ROOT" ]; then
    ROOT="$(cd "$ROOT" 2>/dev/null && pwd || true)"
    [ -n "$ROOT" ] || { printf 'no such --root directory\n' >&2; exit 2; }
fi
# every path check goes through this, so the same script tests a staged overlay
# and a running machine
p() { printf '%s%s' "$ROOT" "$1"; }

PASS=0; FAILED=0; WARNED=0
declare -a FAILURES=()

ok()   { PASS=$((PASS+1));   printf '  PASS  %s\n' "$1"; }
bad()  { FAILED=$((FAILED+1)); FAILURES+=("$1"); printf '  FAIL  %s\n' "$1"; }
soft() { WARNED=$((WARNED+1)); printf '  WARN  %s\n' "$1"; }

# ---------------------------------------------------------------- file checks
have() { # have <path> <hard|soft> <what>
    local path; path="$(p "$1")"
    if [ -e "$path" ]; then
        ok "$3 — $1"
    elif [ "$2" = hard ] && [ -z "$ROOT" ]; then
        bad "$3 missing — $1"
    else
        soft "$3 not in this tree ($1)"
    fi
}

have_exec() { # have_exec <path> <hard|soft> <what>
    local path; path="$(p "$1")"
    if [ -x "$path" ]; then
        ok "$3 — $1"
    elif [ -e "$path" ]; then
        bad "$3 present but NOT executable — $1"
    elif [ "$2" = hard ] && [ -z "$ROOT" ]; then
        bad "$3 missing — $1"
    else
        soft "$3 not in this tree ($1)"
    fi
}

have_cmd() { # have_cmd <command> <hard|soft> <what>
    if command -v "$1" >/dev/null 2>&1; then
        ok "$3 — $(command -v "$1")"
    elif [ "$2" = hard ] && [ -z "$ROOT" ]; then
        bad "$3 is not installed ($1)"
    else
        soft "$3 is not installed ($1)"
    fi
}

have_py() { # have_py <module> <hard|soft> <what> [pythonpath]
    local py=python3 pp; pp="$(p "${4:-/opt/thadd/app}")"
    if ! command -v "$py" >/dev/null 2>&1; then
        [ "$2" = hard ] && [ -z "$ROOT" ] && bad "$3 — python3 is not installed" \
                                        || soft "$3 — python3 is not installed"
        return
    fi
    if PYTHONPATH="$pp" "$py" -c "import $1" >/dev/null 2>&1; then
        ok "$3 — imports cleanly ($1)"
    elif [ "$2" = hard ] && [ -z "$ROOT" ]; then
        bad "$3 does not import ($1)"
    else
        soft "$3 does not import ($1)"
    fi
}

section() { [ "$QUIET" -eq 1 ] || printf '\n── %s ──\n' "$1"; }

if [ "$IMAGE_BUILD" -eq 0 ]; then
    [ -z "$ROOT" ] && printf 'THADD OS desktop self-test · %s\n' "$( . /etc/os-release 2>/dev/null && echo "${PRETTY_NAME:-unknown}" || echo unknown)"
    [ -n "$ROOT" ] && printf 'THADD OS desktop self-test · staged tree %s\n' "$ROOT"
fi

# ------------------------------------------------------------------ the OS itself
section "identity & build fingerprint"
have /etc/os-release            hard "/etc/os-release"
have /etc/thadd/build-info      hard "build fingerprint"
have /etc/thadd/security-posture soft "security baseline notes"
if grep -q "THADD OS" "$(p /etc/os-release)" 2>/dev/null; then
    ok "os-release brands this system THADD OS"
else
    bad "os-release does not say THADD OS"
fi

# ------------------------------------------------------------------- the desktop
section "desktop shell"
have /opt/thadd/desktop/index.html     hard "desktop shell entry"
have /opt/thadd/desktop/shell.html     soft "window shell page"
have /opt/thadd/desktop/css            hard "shell styles"
have /opt/thadd/desktop/js             hard "shell scripts"
have /etc/skel/.config/xfce4/xfconf/xfce-perchannel-xml/xfce4-panel.xml hard "panel profile for new users"
have /etc/xdg/xfce4/xfconf/xfce-perchannel-xml/xfce4-keyboard-shortcuts.xml soft "shortcut map (hidden features)"
have /usr/share/xsessions/thadd-shell.desktop hard "X session entry"
have /etc/lightdm/lightdm.conf.d/60-thadd-shell.conf soft "lightdm autologin session"

section "session & window stack"
have_exec /usr/local/bin/thadd-shell-session hard "session launcher"
have_exec /usr/local/bin/thadd-desktop-server hard "bridge server"
have_cmd xfce4-session hard "XFCE session"
have_cmd xfwm4         hard "window manager"
have_cmd picom         hard "compositor"
have_cmd xfdesktop     hard "desktop shell binary"
have_cmd lightdm       soft "display manager"
have_cmd xfce4-terminal soft "terminal"
have_exec /usr/local/bin/thadd hard "THADD control centre"
have_exec /usr/local/bin/thadd-toolbox hard "THADD toolbox"
have /usr/share/thadd/packages/optional-security.txt hard "optional set lists"

# bridge server must at least be valid python
if command -v python3 >/dev/null 2>&1 && [ -f "$(p /usr/local/bin/thadd-desktop-server)" ]; then
    if python3 -m py_compile "$(p /usr/local/bin/thadd-desktop-server)" >/dev/null 2>&1; then
        ok "bridge server compiles"
    else
        bad "bridge server has a syntax error (/usr/local/bin/thadd-desktop-server)"
    fi
fi

# ------------------------------------------------------------------- the AI + DFIR
section "the commands the banner promises"
# Every command the login banner lists must exist and start without error; a
# banner that advertises something absent is worse than a shorter banner.
for cmd in thadd thadd-ai thadd-scan thadd-toolbox thadd-ssh thadd-passwd rea; do
    path="$(p "/usr/local/bin/$cmd")"
    if [ -x "$path" ]; then
        if "$path" version >/dev/null 2>&1 || "$path" help >/dev/null 2>&1; then
            ok "banner command works: $cmd"
        else
            soft "$cmd runs but its help/version exits non-zero"
        fi
    elif [ -e "$path" ]; then
        bad "banner command is present but NOT executable: $cmd"
    elif [ -z "$ROOT" ]; then
        bad "banner command is missing: $cmd"
    fi
done

section "AI assistant"
have_exec /usr/local/bin/thadd-ai          hard "thadd-ai launcher"
have_exec /opt/thadd/ai/thadd-ai.py        hard "assistant entry point"
# ...and it must do more than exist: this file was a placeholder in every release
# while every existence check passed.
if [ -x "$(p /opt/thadd/ai/thadd-ai.py)" ]; then
    AI_VER="$(PYTHONPATH="$(p /opt/thadd/app)" python3 "$(p /opt/thadd/ai/thadd-ai.py)" --version 2>&1 | head -1 || true)"
    # the leading anchor matters: the failure message also starts with "THADD AI",
    # so a loose match would report a broken entry point as a working one
    case "$AI_VER" in
        "THADD AI "*)  ok "assistant entry point answers: $AI_VER" ;;
        "")            soft "assistant entry point printed nothing" ;;
        *"not installed"*) bad "assistant entry point cannot find its package: $AI_VER" ;;
        *)             bad "assistant entry point answered '$AI_VER' instead of its version" ;;
    esac
fi
have_exec /opt/thadd/security/thadd-scan   hard "thadd-scan engine"
have_exec /usr/local/bin/thadd-scan        hard "thadd-scan launcher"
have /opt/thadd/app/thadd/server/ai/agent.py hard "agent loop"
have /opt/thadd/app/thadd/server/ai/tools.py hard "agent tool registry"
have /opt/thadd/app/thadd/server/ai/groq.py  hard "Groq client"
have /opt/thadd/app/thadd/server/ai/local.py hard "offline assistant"
have /opt/thadd/app/thadd/forensics/triage.py hard "DFIR triage engine"
have_py thadd.server.ai.tools    hard "assistant tool registry loads"
have_py thadd.forensics.triage   hard "DFIR engine loads"

TOOLS=$(PYTHONPATH="$(p /opt/thadd/app)" python3 -c \
    "from thadd.server.ai import tools; print(len(tools.TOOL_FUNCS))" 2>/dev/null || echo 0)
if [ "${TOOLS:-0}" -ge 20 ]; then
    ok "assistant exposes $TOOLS tools"
else
    bad "assistant exposes only ${TOOLS:-0} tools (expected 20+)"
fi

# the scanner must run offline and write a report
if [ -f "$(p /opt/thadd/security/thadd-scan)" ] && command -v python3 >/dev/null 2>&1; then
    SCAN_OUT="$(mktemp -d /tmp/thadd-selftest-scan.XXXXXX 2>/dev/null || echo /tmp/thadd-selftest-scan)"
    if PYTHONPATH="$(p /opt/thadd/app)" timeout 120 python3 "$(p /opt/thadd/security/thadd-scan)" \
            --summary --quick --out "$SCAN_OUT" > "$SCAN_OUT/summary.txt" 2>&1; then
        ok "thadd-scan completes a live scan ($(wc -l < "$SCAN_OUT/summary.txt") lines of evidence)"
    else
        bad "thadd-scan failed (see $SCAN_OUT/summary.txt)"
    fi
    [ -f "$SCAN_OUT/report.json" ] && ok "thadd-scan wrote machine-readable evidence" \
                                   || soft "thadd-scan did not write report.json"
fi

# --------------------------------------------------------------------- the toolkit
section "toolchain & browsers"
for t in git python3 node npm; do have_cmd "$t" hard "$t"; done
for t in gcc make; do have_cmd "$t" soft "$t"; done
have_cmd pwsh  soft "PowerShell"
have_cmd rea   soft "REA reverse-engineering"
have_cmd nmap  soft "nmap"
have_cmd firefox-esr soft "Firefox ESR"
have_cmd chromium    soft "Chromium"

# ---------------------------------------------------------------- the running one
if [ "$IMAGE_BUILD" -eq 0 ] && [ -z "$ROOT" ]; then
    section "this running session"
    if [ -n "${DISPLAY:-}" ] || [ -n "${WAYLAND_DISPLAY:-}" ]; then
        ok "graphical session present (DISPLAY=${DISPLAY:-none})"
    else
        soft "no DISPLAY — running headless or over SSH"
    fi
    for proc in xfce4-session xfwm4 picom; do
        if pgrep -x "$proc" >/dev/null 2>&1; then ok "$proc is running"; else soft "$proc is not running"; fi
    done
    if command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':8787\b'; then
        ok "bridge server listening on 8787"
    else
        soft "bridge server not listening on 8787 (start the session: thadd-shell-session)"
    fi
    AI_KEY=$([ -n "${GROQ_API_KEY:-}" ] && echo set || echo unset)
    if [ "$AI_KEY" = set ]; then ok "Groq API key present in the environment"
    else soft "no Groq API key (assistant runs offline; set it in Settings → AI)"; fi
fi

# ------------------------------------------------------------------------- verdict
if [ -n "$ROOT" ]; then
    printf '\n  note: checked the staged tree %s — binaries are not installed there, so\n' "$ROOT"
    printf '        "not in this tree" means the file is absent, not that the tool is broken\n'
fi
printf '\n%s\n' "──────────────────────────────────────────────────────────────"
printf '  %d passed · %d failed · %d warned\n' "$PASS" "$FAILED" "$WARNED"
if [ "$FAILED" -gt 0 ]; then
    printf '  THADD desktop: INCOMPLETE\n'
    for f in "${FAILURES[@]}"; do printf '    · %s\n' "$f"; done
    exit 1
fi
printf '  THADD desktop: OK\n'
exit 0
