#!/bin/sh
# test.sh — Kahf Router family-safe feature verification
#
# Standalone — no installation required on macOS or Ubuntu.
# Uses: ssh (always), dig or nslookup (DNS tests), nc (port tests)
# macOS: all tools pre-installed.
# Ubuntu: apt install dnsutils  (adds dig/nslookup; nc usually pre-installed)
#
# Usage:
#   lab/test.sh [options]
#   curl -fsSL https://router.kahf.co/router/main/test.sh | sh
#   curl -fsSL https://router.kahf.co/router/main/test.sh | sh -s -- --ip 192.168.1.1
#
# Options:
#   -i, --ip <host>       Router hostname/IP (default: auto-detected: kahf-router.lan, then openwrt.lan, then prompts)
#   -u, --user <user>     SSH user           (default: root)
#   -p, --password <pass> SSH password       (default: key auth)
#   -h, --help            Show this help

set -eu

# Package/firmware CDN host. Matches KAHF_CDN_DOMAIN in config/lab.env.
KAHF_HOST="${KAHF_HOST:-router.kahf.co}"

_R='\033[1;31m' _G='\033[1;32m' _Y='\033[1;33m' _B='\033[1;34m' _N='\033[0m'
PASS=0; FAIL=0; SKIP=0

ROUTER_IP="kahf-router.lan"
SSH_USER="root"
SSH_PASS=""
_IP_GIVEN=0   # set to 1 when --ip is passed explicitly, to skip auto-detection

usage() {
    cat <<'EOF'
Usage: test.sh [options]

  -i, --ip <host>       Router hostname/IP  (default: auto-detected; kahf-router.lan, then openwrt.lan, then prompts)
  -u, --user <user>     SSH user            (default: root)
  -p, --password <pass> SSH password
  -h, --help            Show this help

Run from a device connected to the router's LAN. Examples:
  ./test.sh
  ./test.sh --ip 192.168.1.1 --password admin
  curl -fsSL https://router.kahf.co/router/main/test.sh | sh
EOF
}

while [ $# -gt 0 ]; do
    case "$1" in
        -i|--ip)        ROUTER_IP="$2"; _IP_GIVEN=1; shift 2 ;;
        -u|--user)      SSH_USER="$2";  shift 2 ;;
        -p|--password)  SSH_PASS="$2";  shift 2 ;;
        -h|--help)      usage; exit 0 ;;
        *) printf "Unknown option: %s\n" "$1" >&2; exit 1 ;;
    esac
done

# ── SSH ───────────────────────────────────────────────────────────────────────
SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -o LogLevel=ERROR"
_ssh() {
    if [ -n "${SSH_PASS}" ] && command -v sshpass >/dev/null 2>&1; then
        sshpass -p "${SSH_PASS}" ssh -n ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "$@"
    else
        ssh -n ${SSH_OPTS} "${SSH_USER}@${ROUTER_IP}" "$@"
    fi
}

# ── Helpers ───────────────────────────────────────────────────────────────────
pass()    { PASS=$((PASS+1)); printf "  ${_G}[PASS]${_N} %s\n" "$*"; }
fail()    { FAIL=$((FAIL+1)); printf "  ${_R}[FAIL]${_N} %s\n" "$*"; }
skip()    { SKIP=$((SKIP+1)); printf "  ${_Y}[SKIP]${_N} %s\n" "$*"; }
info()    { printf "  ${_B}[INFO]${_N} %s\n" "$*"; }
section() { printf "\n${_B}── %s${_N}\n" "$*"; }

# ── Tool detection ─────────────────────────────────────────────────────────────
HAS_DIG=0; HAS_NSLOOKUP=0; HAS_NC=0
command -v dig      >/dev/null 2>&1 && HAS_DIG=1
command -v nslookup >/dev/null 2>&1 && HAS_NSLOOKUP=1
command -v nc       >/dev/null 2>&1 && HAS_NC=1

# _dns <server> <domain> — full resolver output suitable for grepping
_dns() {
    _ds="$1" _dd="$2"
    if [ "${HAS_DIG}" = "1" ]; then
        dig "@${_ds}" "${_dd}" +time=5 +tries=1 2>/dev/null || true
    elif [ "${HAS_NSLOOKUP}" = "1" ]; then
        nslookup "${_dd}" "${_ds}" 2>/dev/null || true
    fi
}

# ── Auto-detect router hostname ────────────────────────────────────────────────
# If no --ip was given, probe kahf-router.lan then openwrt.lan via SSH and use
# the first host that answers. If neither responds, prompt the user for an IP.
if [ "${_IP_GIVEN:-0}" = "0" ]; then
    _detected=""
    for _host in kahf-router.lan openwrt.lan; do
        ROUTER_IP="${_host}"
        if _ssh "echo __ok__" 2>/dev/null | grep -q "__ok__"; then
            _detected="${_host}"
            break
        fi
    done
    if [ -z "${_detected}" ]; then
        if [ -t 0 ]; then
            printf "Could not reach router at kahf-router.lan or openwrt.lan.\n"
            printf "Enter router IP or hostname: "
            read -r ROUTER_IP
            [ -z "${ROUTER_IP}" ] && { printf "No IP provided. Exiting.\n" >&2; exit 1; }
        else
            printf "Could not reach router at kahf-router.lan or openwrt.lan. Use --ip <host>.\n" >&2
            exit 1
        fi
    fi
fi

# ═════════════════════════════════════════════════════════════════════════════
printf "\n${_B}╔══════════════════════════════════════════════════════════╗${_N}\n"
printf "${_B}║   Kahf Router — Family Safe Feature Tests               ║${_N}\n"
printf "${_B}╚══════════════════════════════════════════════════════════╝${_N}\n"
printf "  Target: %s   User: %s\n" "${ROUTER_IP}" "${SSH_USER}"
printf "  Tools:  dig=%s  nslookup=%s  nc=%s\n" "${HAS_DIG}" "${HAS_NSLOOKUP}" "${HAS_NC}"

# ─────────────────────────────────────────────────────────────────────────────
section "1/9  Connectivity"
# ─────────────────────────────────────────────────────────────────────────────

SSH_OK=0
_probe=$(_ssh "echo __ok__" 2>/dev/null || true)
if echo "${_probe}" | grep -q "__ok__"; then
    SSH_OK=1
    _model=$(_ssh "cat /tmp/sysinfo/model 2>/dev/null || echo unknown" 2>/dev/null || echo "unknown")
    _fw=$(_ssh "grep DISTRIB_DESCRIPTION /etc/openwrt_release 2>/dev/null | cut -d= -f2 | tr -d \"'\"" 2>/dev/null || echo "unknown")
    pass "SSH to ${SSH_USER}@${ROUTER_IP}"
    printf "        Model:    %s\n" "${_model}"
    printf "        Firmware: %s\n" "${_fw}"
else
    fail "SSH to ${SSH_USER}@${ROUTER_IP} — check IP, user, password"
    printf "\n  ${_R}No router connection — SSH-based tests will be skipped.${_N}\n"
fi

# ─────────────────────────────────────────────────────────────────────────────
section "2/9  Installation State"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    _r=$(_ssh "test -f /etc/family-safe.installed && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "family-safe package installed (/etc/family-safe.installed)" \
        || fail "family-safe NOT installed (missing /etc/family-safe.installed)"

    _ver=$(_ssh "apk info family-safe 2>/dev/null | head -1 | sed 's/^family-safe-//' | cut -d' ' -f1 || opkg list-installed 2>/dev/null | awk '/^family-safe /{print \$3}'" 2>/dev/null || true)
    [ -n "${_ver}" ] || _ver="unknown"
    info "family-safe version: ${_ver}"

    _r=$(_ssh "apk info appfilter 2>/dev/null | head -1 || opkg list-installed 2>/dev/null | grep appfilter || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "OAF (appfilter) installed" \
        || fail "OAF not installed — check: logread | grep oaf-bootstrap"

    _r=$(_ssh "test -x /usr/sbin/oaf-update.sh && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "oaf-update.sh present and executable" \
        || fail "oaf-update.sh missing from /usr/sbin/"

    _cron=$(_ssh "cat /etc/crontabs/root 2>/dev/null || true" 2>/dev/null || true)
    echo "${_cron}" | grep -q "oaf-update" \
        && pass "Nightly auto-update cron configured" \
        || fail "Nightly cron for oaf-update.sh not found in /etc/crontabs/root"

    # Verify the cron has actually run by reading the log (tmpfs; clears on reboot)
    # Falls back to syslog if log file was cleared.
    _upd_log="/var/log/oaf-update.log"
    _cron_finish="" _cron_start="" _log_src="log"
    if _ssh "test -f ${_upd_log}" >/dev/null 2>&1; then
        _cron_finish=$(_ssh "grep 'event=finish' ${_upd_log} 2>/dev/null | tail -1 || true" 2>/dev/null || true)
        _cron_start=$(_ssh "grep 'event=start' ${_upd_log} 2>/dev/null | tail -1 || true" 2>/dev/null || true)
    else
        _log_src="syslog"
        _cron_finish=$(_ssh "logread 2>/dev/null | grep 'oaf-update' | grep 'event=finish' | tail -1 || true" 2>/dev/null || true)
        _cron_start=$(_ssh "logread 2>/dev/null | grep 'oaf-update' | grep 'event=start' | tail -1 || true" 2>/dev/null || true)
    fi
    if [ -n "${_cron_finish}" ]; then
        _cron_ts=$(echo "${_cron_finish}" | grep -oE 'ts=[^ ]+' | cut -d= -f2 || true)
        _cron_updates=""
        [ "${_log_src}" = "log" ] && \
            _cron_updates=$(_ssh "grep 'event=pkg_update' ${_upd_log} 2>/dev/null || true" 2>/dev/null || true)
        if [ -n "${_cron_updates}" ]; then
            pass "Nightly cron last run: ${_cron_ts} (${_log_src}) — updates applied"
            echo "${_cron_updates}" | while IFS= read -r _upd_line; do
                _upd_pkg=$(echo "${_upd_line}" | grep -oE 'pkg=[^ ]+' | cut -d= -f2 || true)
                _upd_from=$(echo "${_upd_line}" | grep -oE 'from=[^ ]+' | cut -d= -f2 || true)
                _upd_to=$(echo "${_upd_line}" | grep -oE 'to=[^ ]+' | cut -d= -f2 || true)
                [ -n "${_upd_pkg}" ] && info "  Updated: ${_upd_pkg}  ${_upd_from} -> ${_upd_to}"
            done
        else
            pass "Nightly cron last run: ${_cron_ts} (${_log_src}) — all packages up to date"
        fi
    elif [ -n "${_cron_start}" ]; then
        fail "Nightly cron started but did not finish — check ${_upd_log}"
    else
        skip "Nightly cron has not run since last reboot (run manually: /usr/sbin/oaf-update.sh)"
    fi

    _chan=$(_ssh "cat /etc/oaf-update.conf 2>/dev/null || true" 2>/dev/null || true)
    [ -n "${_chan}" ] \
        && pass "Update channel configured: $(echo "${_chan}" | head -1)" \
        || fail "oaf-update.conf missing"

    # Flash-wear: nightly update scratch must live on tmpfs (/tmp), not the
    # overlay, so repeated downloads/merges don't wear NAND/NOR flash.
    _tmpfs=$(_ssh "mount 2>/dev/null | awk '\$3==\"/tmp\"{print \$5}' | head -1" 2>/dev/null || true)
    case "${_tmpfs}" in
        tmpfs) pass "/tmp is tmpfs (nightly update scratch avoids flash wear)" ;;
        *)     fail "/tmp is not tmpfs (${_tmpfs:-unknown}) — nightly downloads would wear flash" ;;
    esac
    _scratch=$(_ssh "grep -cE '(^|[^A-Za-z0-9_])/tmp/' /usr/sbin/oaf-update.sh 2>/dev/null || echo 0" 2>/dev/null | tr -d ' \n' || echo 0)
    if [ "${_scratch:-0}" -gt 0 ] 2>/dev/null; then
        pass "oaf-update.sh uses /tmp (tmpfs) for scratch downloads"
    else
        fail "oaf-update.sh does not use /tmp scratch — flash-wear risk on nightly runs"
    fi

    _r=$(_ssh "if test -x /usr/libexec/rpcd/family-safe || ubus list 2>/dev/null | grep -q family-safe; then echo yes; else echo no; fi" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "family-safe rpcd endpoint registered" \
        || fail "family-safe rpcd endpoint missing (/usr/libexec/rpcd/family-safe)"

    _r=$(_ssh "test -f /etc/config/family-safe && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "Feature UCI config present (/etc/config/family-safe)" \
        || fail "Feature UCI config missing — run install-family-safe.sh"
else
    for _t in "family-safe installed" "OAF installed" "oaf-update.sh" "Nightly cron" \
              "Nightly cron last run" "Update channel" "/tmp is tmpfs" "Updater tmpfs scratch" \
              "rpcd endpoint" "Feature UCI config"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "3/9  Feature Flags (UCI)"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # Fetch all flags in one SSH call for efficiency
    _uci_features=$(_ssh "uci show family-safe.features 2>/dev/null || true" 2>/dev/null || true)

    # Core feature flags — each must be 0 or 1
    _missing=""
    for _flag in redirect_dns_53 block_dot_853 block_udp_443 block_doh_domains \
                 block_vpn_ports block_tor_ports block_doh_ips safesearch bedtime block_ads; do
        _val=$(echo "${_uci_features}" | grep "\.${_flag}=" | cut -d= -f2 | tr -d "'" | tr -d ' ')
        case "${_val}" in
            0|1) ;;
            *) _missing="${_missing} ${_flag}(${_val:-missing})" ;;
        esac
    done
    if [ -z "${_missing}" ]; then
        pass "All 10 core feature flags present in UCI"
    else
        fail "Core feature flags missing or invalid:${_missing}"
    fi

    # Category blocklist flags — each must be 0 or 1
    _missing=""
    for _cat in fake_news anti_islamic lgbt gambling pornography dating occult violence; do
        _val=$(echo "${_uci_features}" | grep "\.block_${_cat}=" | cut -d= -f2 | tr -d "'" | tr -d ' ')
        case "${_val}" in
            0|1) ;;
            *) _missing="${_missing} block_${_cat}(${_val:-missing})" ;;
        esac
    done
    if [ -z "${_missing}" ]; then
        pass "All 8 category blocklist flags present in UCI"
    else
        fail "Category flags missing or invalid:${_missing}"
    fi

    # Report which features are enabled
    _enabled=""
    for _flag in redirect_dns_53 block_dot_853 block_udp_443 block_doh_domains \
                 block_vpn_ports block_tor_ports block_doh_ips safesearch bedtime block_ads; do
        _val=$(echo "${_uci_features}" | grep "\.${_flag}=" | cut -d= -f2 | tr -d "'" | tr -d ' ')
        [ "${_val}" = "1" ] && _enabled="${_enabled} ${_flag}"
    done
    [ -n "${_enabled}" ] && info "Enabled core features:${_enabled}" || info "No core features currently enabled"

    # Bedtime rule — if enabled, firewall rule must exist
    _bt=$(echo "${_uci_features}" | grep '\.bedtime=' | cut -d= -f2 | tr -d "'" | tr -d ' ')
    if [ "${_bt}" = "1" ]; then
        _r=$(_ssh "uci show firewall 2>/dev/null | grep -i 'FAMILYSAFE-Bedtime' | head -1 || true" 2>/dev/null || true)
        [ -n "${_r}" ] \
            && pass "Bedtime: enabled + firewall rule present" \
            || fail "Bedtime: flag=1 but firewall rule missing"
    else
        skip "Bedtime rule (disabled; enable with: toggle_feature bedtime 1)"
    fi

    # YouTube Restrict — if enabled, dnsmasq CNAME must exist
    _yt=$(echo "${_uci_features}" | grep '\.youtube_restrict=' | cut -d= -f2 | tr -d "'" | tr -d ' ')
    if [ "${_yt}" = "1" ]; then
        _r=$(_ssh "uci show dhcp 2>/dev/null | grep 'restrict.youtube.com' | head -1 || true" 2>/dev/null || true)
        [ -n "${_r}" ] \
            && pass "YouTube Restrict: enabled + CNAME rule present" \
            || fail "YouTube Restrict: flag=1 but CNAME rule missing in dnsmasq"
    else
        skip "YouTube Restrict (disabled; enable with: toggle_feature youtube_restrict 1)"
    fi

    # Ad blocking — if enabled, adblock.conf must exist
    _ads=$(echo "${_uci_features}" | grep '\.block_ads=' | cut -d= -f2 | tr -d "'" | tr -d ' ')
    if [ "${_ads}" = "1" ]; then
        _r=$(_ssh "test -f /etc/dnsmasq.d/adblock.conf && echo yes || echo no" 2>/dev/null || echo "no")
        [ "${_r}" = "yes" ] \
            && pass "Ad blocking: enabled + /etc/dnsmasq.d/adblock.conf present" \
            || fail "Ad blocking: flag=1 but /etc/dnsmasq.d/adblock.conf missing"
    else
        skip "Ad blocking (disabled; enable with: toggle_feature block_ads 1)"
    fi
else
    for _t in "Core feature flags" "Category blocklist flags" \
              "Bedtime rule" "YouTube Restrict" "Ad blocking"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "4/9  DNS Configuration"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # Upstream DNS servers. Two valid shapes depending on whether the family-safe
    # package is active:
    #   - base lab (configure-openwrt.sh): plaintext Cloudflare Family + OpenDNS
    #   - family-safe active: local encrypted DoT/DoH proxy only (127.0.0.1#54xx),
    #     no plaintext upstream (anti-ISP-hijack).
    _servers=$(_ssh "uci get dhcp.@dnsmasq[0].server 2>/dev/null || true" 2>/dev/null || true)
    _ok=1
    _has_opendns=0
    _all_loopback=1
    for _s in ${_servers}; do
        case "${_s}" in
            127.0.0.1#*) _has_opendns=1 ;;  # encrypted proxy (resilience satisfied by proxy tier)
            1.1.1.3|1.0.0.3|2606:4700:4700::1113|2606:4700:4700::1003) _all_loopback=0 ;;
            208.67.222.123|208.67.220.123) _has_opendns=1; _all_loopback=0 ;;
            *) _ok=0; _all_loopback=0 ;;
        esac
    done
    if [ "${_ok}" = "1" ] && [ -n "${_servers}" ]; then
        if [ "${_all_loopback}" = "1" ]; then
            pass "DNS upstream: local encrypted DoT/DoH proxy only (no plaintext)"
        else
            pass "DNS upstream: Cloudflare Family + OpenDNS FamilyShield only"
        fi
        [ "${_has_opendns}" = "1" ] \
            && pass "Multi-provider / encrypted-proxy fallback present (regional resilience)" \
            || fail "Provider fallback missing (208.67.222.123/220.123 or encrypted proxy)"
    else
        fail "DNS upstream has unapproved server(s): ${_servers}"
    fi

    # WAN must not accept ISP-provided DNS
    _p=$(_ssh "uci get network.wan.peerdns 2>/dev/null || true" 2>/dev/null || true)
    [ "${_p}" = "0" ] \
        && pass "WAN peerdns=0 (ignores ISP-provided DNS)" \
        || fail "WAN peerdns='${_p}' — router may use ISP DNS"

    # dnsmasq noresolv=1 (don't fall back to /etc/resolv.conf)
    _r=$(_ssh "uci get dhcp.@dnsmasq[0].noresolv 2>/dev/null || echo 0" 2>/dev/null || echo "0")
    [ "${_r}" = "1" ] \
        && pass "dnsmasq noresolv=1 (no /etc/resolv.conf fallback)" \
        || fail "dnsmasq noresolv='${_r}' — may leak to system resolver"

    # dnsmasq local=/lan/ (LAN hostnames stay local)
    _r=$(_ssh "uci get dhcp.@dnsmasq[0].local 2>/dev/null || true" 2>/dev/null || true)
    [ "${_r}" = "/lan/" ] \
        && pass "dnsmasq local=/lan/ (LAN hostnames resolve locally)" \
        || fail "dnsmasq local='${_r}' — expected '/lan/'"

    # dnsmasq confdir registered (needed for block confs to load)
    _r=$(_ssh "uci get dhcp.@dnsmasq[0].confdir 2>/dev/null | grep -qF '/etc/dnsmasq.d' && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "dnsmasq confdir /etc/dnsmasq.d/ registered" \
        || fail "dnsmasq confdir not registered — block confs won't load automatically"

    # Firefox DoH canary must return NXDOMAIN
    _r=$(_ssh "uci show dhcp 2>/dev/null | grep 'use-application-dns.net' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "Firefox DoH canary blocked (use-application-dns.net → NXDOMAIN)" \
        || fail "Firefox DoH canary NOT blocked — browsers may enable DoH and bypass filtering"

    # Block page conf (rewrites upstream 0.0.0.0 to kahfguard block page IP)
    _r=$(_ssh "test -f /etc/dnsmasq.d/blockpage.conf && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "Block page conf present (/etc/dnsmasq.d/blockpage.conf)" \
        || fail "Block page conf missing (/etc/dnsmasq.d/blockpage.conf) — blocked sites may not redirect"

    # 5 DoH provider domains — must resolve to NXDOMAIN, 0.0.0.0, or the
    # KahfGuard block-page IP (same redirect mechanism as category blocklists,
    # e.g. pornhub.com below — not just a bare 0.0.0.0/NXDOMAIN).
    _bp_ip_router=""
    _bp_ip_router=$(_ssh "grep '^alias=0\\.0\\.0\\.0,' /etc/dnsmasq.d/blockpage.conf 2>/dev/null | head -1 | cut -d, -f2" 2>/dev/null || true)
    for _dom in cloudflare-dns.com dns.google dns.quad9.net dns.adguard.com doh.mullvad.net; do
        _r=$(_ssh "nslookup '${_dom}' 127.0.0.1 2>/dev/null || true" 2>/dev/null || true)
        _dans=$(echo "${_r}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -v "^127\.0\.0\.1$" | head -1)
        if echo "${_r}" | grep -qiE 'NXDOMAIN|can.t find|not found|no answer'; then
            pass "DoH provider blocked: ${_dom} → NXDOMAIN"
        elif echo "${_r}" | grep -qE '0\.0\.0\.0'; then
            pass "DoH provider blocked: ${_dom} → 0.0.0.0"
        elif [ -n "${_bp_ip_router}" ] && [ "${_dans}" = "${_bp_ip_router}" ]; then
            pass "DoH provider blocked: ${_dom} → block page (${_bp_ip_router})"
        else
            fail "DoH provider NOT blocked on router: ${_dom}"
        fi
    done

    # SafeSearch CNAME entries
    _r=$(_ssh "uci show dhcp 2>/dev/null | grep 'forcesafesearch' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "SafeSearch: Google → forcesafesearch.google.com" \
        || fail "SafeSearch: Google not configured in dnsmasq"

    _r=$(_ssh "uci show dhcp 2>/dev/null | grep 'strict.bing.com' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "SafeSearch: Bing → strict.bing.com" \
        || fail "SafeSearch: Bing not configured in dnsmasq"

    _r=$(_ssh "uci show dhcp 2>/dev/null | grep 'safe.duckduckgo.com' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "SafeSearch: DuckDuckGo → safe.duckduckgo.com" \
        || fail "SafeSearch: DuckDuckGo not configured in dnsmasq"

    # SafeSearch hosts file (IPs for CNAME targets)
    _r=$(_ssh "test -f /etc/safesearch-hosts && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "SafeSearch hosts file present (/etc/safesearch-hosts)" \
        || fail "SafeSearch hosts file missing (/etc/safesearch-hosts)"

    # ── DNS upstream protocol (DoT / DoH / plaintext) ─────────────────────────
    # apply_encrypted_upstream tries DoT, then DoH, then filtered plaintext, each
    # gated by a <500ms average-latency probe, and records the winner.
    _mode=$(_ssh "uci -q get family-safe.resolver.upstream_mode 2>/dev/null || true" 2>/dev/null || true)
    _lat=$(_ssh "uci -q get family-safe.resolver.upstream_latency_ms 2>/dev/null || true" 2>/dev/null || true)
    case "${_mode}" in
        dot|doh)
            pass "DNS upstream protocol: ${_mode} (encrypted)${_lat:+ — avg ${_lat}ms}"
            # Latency must be under the 500ms budget the gate enforces.
            if [ -n "${_lat}" ] && [ "${_lat}" -lt 500 ] 2>/dev/null; then
                pass "Encrypted DNS latency within budget (${_lat}ms < 500ms)"
            else
                fail "Encrypted DNS latency missing/over budget (upstream_latency_ms='${_lat:-unset}')"
            fi
            # The daemon for the selected mode must be alive AND listening locally.
            if [ "${_mode}" = "dot" ]; then _svc="stubby"; _port=5453; else _svc="https-dns-proxy"; _port=5054; fi
            _alive=$(_ssh "pgrep ${_svc} >/dev/null 2>&1 && netstat -tuln 2>/dev/null | grep -qE '127[.]0[.]0[.]1:${_port}([^0-9]|\$)' && echo yes || echo no" 2>/dev/null || echo "no")
            [ "${_alive}" = "yes" ] \
                && pass "${_svc} healthy (running + listening on 127.0.0.1:${_port})" \
                || fail "${_svc} NOT healthy for mode=${_mode} — encrypted DNS daemon down"
            ;;
        plaintext)
            pass "DNS upstream protocol: plaintext (filtered family DNS — encrypted unavailable, guard retries)"
            ;;
        *)
            fail "DNS upstream protocol not set (family-safe.resolver.upstream_mode='${_mode:-unset}')"
            ;;
    esac

    # Health monitor must be installed and cronned so a dead daemon self-recovers.
    _r=$(_ssh "test -x /usr/lib/family-safe/dns-health-monitor.sh && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "DNS health monitor installed (/usr/lib/family-safe/dns-health-monitor.sh)" \
        || fail "DNS health monitor missing (dns-health-monitor.sh)"
    _r=$(_ssh "grep -q 'dns-health-monitor.sh' /etc/crontabs/root 2>/dev/null && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "DNS health monitor cron registered" \
        || fail "DNS health monitor cron not found in /etc/crontabs/root"
else
    for _t in "DNS upstream" "WAN peerdns" "dnsmasq noresolv" "dnsmasq local=/lan/" \
              "dnsmasq confdir" "Firefox DoH canary" "Block page conf" \
              "DoH: cloudflare-dns.com" "DoH: dns.google" "DoH: dns.quad9.net" \
              "DoH: dns.adguard.com" "DoH: doh.mullvad.net" \
              "SafeSearch Google" "SafeSearch Bing" "SafeSearch DuckDuckGo" \
              "SafeSearch hosts file" "DNS upstream protocol" "Encrypted DNS latency" \
              "Encrypted DNS daemon health" "DNS health monitor installed" \
              "DNS health monitor cron"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "5/9  Category Blocklists"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # Blocklists infrastructure directory
    _r=$(_ssh "test -d /usr/lib/family-safe/blocklists && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "Blocklists directory present (/usr/lib/family-safe/blocklists/)" \
        || fail "Blocklists directory missing (/usr/lib/family-safe/blocklists/)"

    # For each category: if flag=1, the dnsmasq conf must exist
    _any_enabled=0
    _uci_cats=$(_ssh "uci show family-safe.features 2>/dev/null | grep 'block_' || true" 2>/dev/null || true)
    for _cat in fake_news anti_islamic lgbt gambling pornography dating occult violence; do
        _val=$(echo "${_uci_cats}" | grep "\.block_${_cat}=" | cut -d= -f2 | tr -d "'" | tr -d ' ')
        if [ "${_val}" = "1" ]; then
            _any_enabled=1
            _r=$(_ssh "test -f /etc/dnsmasq.d/block-${_cat}.conf && echo yes || echo no" 2>/dev/null || echo "no")
            if [ "${_r}" = "yes" ]; then
                _count=$(_ssh "grep -c '^address=' /etc/dnsmasq.d/block-${_cat}.conf 2>/dev/null || echo 0" 2>/dev/null || echo "0")
                pass "Category blocklist active: ${_cat} (${_count} domains blocked)"
            else
                fail "Category ${_cat}: flag=1 but /etc/dnsmasq.d/block-${_cat}.conf missing"
            fi
        fi
    done
    [ "${_any_enabled}" = "0" ] && skip "All 8 category blocklists disabled (fake_news, anti_islamic, lgbt, gambling, pornography, dating, occult, violence)"
else
    for _t in "Blocklists directory" "Category blocklists"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "6/9  Firewall Rules"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # DNS port-53 DNAT redirect (in nftables dstnat_lan chain)
    _r=$(_ssh "nft list chain inet fw4 dstnat_lan 2>/dev/null | grep '53' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "DNS port 53 redirected to router (DNAT in dstnat_lan)" \
        || fail "DNS DNAT redirect rule not found in dstnat_lan"

    # Block-Other-DNS-53 (defense-in-depth: rejects non-approved DNS)
    _r=$(_ssh "uci show firewall 2>/dev/null | grep 'Block-Other-DNS-53' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "Block-Other-DNS-53 rule present (non-approved DNS servers blocked)" \
        || fail "Block-Other-DNS-53 missing — clients may bypass via alternative DNS"

    # DoT TCP/UDP 853 block rule
    _r=$(_ssh "uci show firewall 2>/dev/null | grep -iE 'Block-DoT|853' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "DoT/853 block rule configured" \
        || fail "DoT/853 block rule not found in UCI firewall"

    # QUIC / UDP-443 block rule
    _r=$(_ssh "uci show firewall 2>/dev/null | grep -iE 'Block-UDP-443|QUIC' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "QUIC/UDP-443 block rule configured" \
        || fail "QUIC/UDP-443 block rule not found"

    # VPN port block rules
    _r=$(_ssh "uci show firewall 2>/dev/null | grep 'Block-VPN' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "VPN port block rules configured" \
        || fail "VPN block rules not found in UCI firewall"

    for _vpnport in "OpenVPN-TCP-1194" "WireGuard-UDP-51820" "PPTP-TCP-1723"; do
        _r=$(_ssh "uci show firewall 2>/dev/null | grep \"Block-VPN-${_vpnport}\" | head -1 || true" 2>/dev/null || true)
        [ -n "${_r}" ] \
            && pass "VPN block: ${_vpnport}" \
            || fail "VPN block rule missing: FAMILYSAFE-Block-VPN-${_vpnport}"
    done

    # Proxy port block rules
    _r=$(_ssh "uci show firewall 2>/dev/null | grep 'Block-Proxy' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "Proxy port block rules configured (SOCKS/HTTP proxy)" \
        || fail "Proxy block rules not found in UCI firewall"

    # Tor port block rules
    _r=$(_ssh "uci show firewall 2>/dev/null | grep 'Block-TOR' | head -1 || true" 2>/dev/null || true)
    [ -n "${_r}" ] \
        && pass "Tor port block rules configured" \
        || fail "Tor block rules not found in UCI firewall"

    for _torport in "ORPort-TCP-9001" "SOCKS-TCP-9050"; do
        _r=$(_ssh "uci show firewall 2>/dev/null | grep \"Block-TOR-${_torport}\" | head -1 || true" 2>/dev/null || true)
        [ -n "${_r}" ] \
            && pass "Tor block: ${_torport}" \
            || fail "Tor block rule missing: FAMILYSAFE-Block-TOR-${_torport}"
    done

    # KahfGuard allowlist script
    _r=$(_ssh "test -x /etc/firewall.kahfguard-dns.sh && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "KahfGuard DNS allowlist script installed (/etc/firewall.kahfguard-dns.sh)" \
        || fail "KahfGuard allowlist script missing (/etc/firewall.kahfguard-dns.sh)"

    # KahfGuard nftables set — must have resolved IPs
    _ips=$(_ssh "nft list set inet fw4 kahfguard_dns_ips 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -3 || true" 2>/dev/null || true)
    [ -n "${_ips}" ] \
        && pass "KahfGuard IPs in nftables allowlist: $(echo "${_ips}" | tr '\n' ' ')" \
        || fail "KahfGuard nftables set empty or missing (kahfguard_dns_ips)"

    # DoH IP block script
    _r=$(_ssh "test -x /etc/firewall.doh-ip-block.sh && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "DoH IP block script installed (/etc/firewall.doh-ip-block.sh)" \
        || fail "DoH IP block script missing — known DoH provider IPs not blocked"

    # DoH IP nftables set (familysafe_doh_ips) — must have IPs
    _ips=$(_ssh "nft list set inet fw4 familysafe_doh_ips 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -3 || true" 2>/dev/null || true)
    [ -n "${_ips}" ] \
        && pass "DoH provider IPs in nftables block set (familysafe_doh_ips): $(echo "${_ips}" | tr '\n' ' ')" \
        || fail "DoH IP nftables set empty or missing (familysafe_doh_ips)"

    # Total FamilySafe UCI rule count sanity check
    _count=$(_ssh "uci show firewall 2>/dev/null | grep 'FAMILYSAFE' | awk 'END{print NR}'" 2>/dev/null || echo "0")
    _count=$(echo "${_count}" | tr -d ' \n')
    if [ "${_count:-0}" -gt 5 ] 2>/dev/null; then
        pass "FamilySafe firewall rules loaded (${_count} entries)"
    else
        fail "Too few FamilySafe rules (${_count:-0} found, expected >5) — install may be incomplete"
    fi
else
    for _t in "DNS DNAT redirect" "Block-Other-DNS-53" "DoT block rule" "QUIC block rule" \
              "VPN block rules" "VPN: OpenVPN-TCP-1194" "VPN: WireGuard-UDP-51820" \
              "VPN: PPTP-TCP-1723" "Proxy block rules" "Tor block rules" \
              "Tor: ORPort-TCP-9001" "Tor: SOCKS-TCP-9050" \
              "KahfGuard allowlist script" "KahfGuard nftables set" \
              "DoH IP block script" "DoH IP nftables set" "FamilySafe rules count"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "7/9  Device Profiles & MAC Guard"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # Profiles UCI config
    _r=$(_ssh "test -f /etc/config/profiles && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "Device profiles UCI config present (/etc/config/profiles)" \
        || fail "Device profiles config missing (/etc/config/profiles)"

    # Unrestricted range MAC guard script
    _r=$(_ssh "test -x /etc/firewall.unrestricted-guard.sh && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "Unrestricted MAC guard script present (/etc/firewall.unrestricted-guard.sh)" \
        || fail "Unrestricted guard script missing — devices can spoof unrestricted IP range"

    # unrestricted_macs nftables set
    _r=$(_ssh "nft list sets 2>/dev/null | grep -q 'unrestricted_macs' && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "nftables set unrestricted_macs present (MAC guard active)" \
        || fail "nftables set unrestricted_macs missing — MAC guard not loaded"

    # unrestricted_guard chain
    _r=$(_ssh "nft list chains 2>/dev/null | grep -q 'unrestricted_guard' && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "nftables chain unrestricted_guard present" \
        || fail "nftables chain unrestricted_guard missing — MAC enforcement not active"

    # DHCP pool configured for FamilySafe range (start=2, limit=199)
    _start=$(_ssh "uci get dhcp.lan.start 2>/dev/null || echo unknown" 2>/dev/null || echo "unknown")
    _limit=$(_ssh "uci get dhcp.lan.limit 2>/dev/null || echo unknown" 2>/dev/null || echo "unknown")
    if [ "${_start}" = "2" ] && [ "${_limit}" = "199" ]; then
        pass "DHCP pool: FamilySafe range (.2–.200, limit 199)"
    else
        info "DHCP pool: start=${_start} limit=${_limit} (expected start=2 limit=199 for FamilySafe default)"
    fi
else
    for _t in "Device profiles config" "Unrestricted MAC guard script" \
              "nftables unrestricted_macs set" "nftables unrestricted_guard chain" \
              "DHCP pool range"; do
        skip "${_t}"
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "8/9  DNS Behavior (from this machine)"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${HAS_DIG}" = "0" ] && [ "${HAS_NSLOOKUP}" = "0" ]; then
    printf "  ${_Y}[INFO]${_N} dig/nslookup not found. Install with:\n"
    printf "         Ubuntu: sudo apt install dnsutils\n"
    printf "         macOS:  pre-installed (should not see this message)\n"
    for _t in "Router DNS working" "DoH canary NXDOMAIN" "DoH: cloudflare-dns.com" \
              "DoH: dns.google" "Google SafeSearch" "Bing SafeSearch" \
              "DuckDuckGo SafeSearch" \
              "Porn blocked: pornhub.com" "Porn blocked: xvideos.com" "Porn blocked: xnxx.com"; do
        skip "${_t}"
    done
else
    # Router DNS resolves external domains
    _r=$(_dns "${ROUTER_IP}" "google.com")
    if echo "${_r}" | grep -qE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+'; then
        pass "Router DNS resolves google.com"
    else
        fail "Router DNS cannot resolve google.com — DNS service may be down"
    fi

    # Firefox DoH canary → NXDOMAIN
    _r=$(_dns "${ROUTER_IP}" "use-application-dns.net")
    if echo "${_r}" | grep -qi "NXDOMAIN\|can't find\|not found"; then
        pass "Firefox DoH canary → NXDOMAIN (use-application-dns.net)"
    else
        fail "Firefox DoH canary NOT blocked — resolved to: $(echo "${_r}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1)"
    fi

    # DoH providers → NXDOMAIN, 0.0.0.0, or the KahfGuard block-page IP (same
    # redirect mechanism as category blocklists, e.g. pornhub.com below).
    _bp_ip_early=""
    if [ "${SSH_OK}" = "1" ]; then
        _bp_ip_early=$(_ssh "grep '^alias=0\\.0\\.0\\.0,' /etc/dnsmasq.d/blockpage.conf 2>/dev/null | head -1 | cut -d, -f2" 2>/dev/null || true)
    fi
    for _dom in cloudflare-dns.com dns.google; do
        _r=$(_dns "${ROUTER_IP}" "${_dom}")
        _dans2=$(echo "${_r}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -v "^${ROUTER_IP}$" | head -1)
        if echo "${_r}" | grep -qi "NXDOMAIN\|can't find\|not found"; then
            pass "DoH provider blocked: ${_dom} → NXDOMAIN"
        elif echo "${_r}" | grep -qE '0\.0\.0\.0'; then
            pass "DoH provider blocked: ${_dom} → 0.0.0.0"
        elif [ -n "${_bp_ip_early}" ] && [ "${_dans2}" = "${_bp_ip_early}" ]; then
            pass "DoH provider blocked: ${_dom} → block page (${_bp_ip_early})"
        else
            fail "DoH provider NOT blocked: ${_dom} returned a public IP"
        fi
    done

    # Google SafeSearch redirect
    _r=$(_dns "${ROUTER_IP}" "www.google.com")
    if echo "${_r}" | grep -qi "forcesafesearch"; then
        pass "Google SafeSearch active (www.google.com → forcesafesearch.google.com)"
    else
        fail "Google SafeSearch NOT active for www.google.com"
    fi

    # Bing SafeSearch redirect
    _r=$(_dns "${ROUTER_IP}" "www.bing.com")
    if echo "${_r}" | grep -qi "strict.bing.com"; then
        pass "Bing SafeSearch active (www.bing.com → strict.bing.com)"
    else
        fail "Bing SafeSearch NOT active for www.bing.com"
    fi

    # DuckDuckGo SafeSearch redirect
    _r=$(_dns "${ROUTER_IP}" "duckduckgo.com")
    if echo "${_r}" | grep -qi "safe.duckduckgo.com"; then
        pass "DuckDuckGo SafeSearch active (duckduckgo.com → safe.duckduckgo.com)"
    else
        fail "DuckDuckGo SafeSearch NOT active for duckduckgo.com"
    fi

    # Pornography MUST always be blocked — regardless of any UCI flag.
    # Accepted responses: NXDOMAIN, 0.0.0.0 (Cloudflare Family), or the KahfGuard
    # block page IP (returned when OpenDNS FamilyShield wins the dnsmasq race and
    # its block-page IP is aliased to KahfGuard by blockpage.conf).
    _bp_ip=""
    if [ "${SSH_OK}" = "1" ]; then
        _bp_ip=$(_ssh "grep '^alias=0\\.0\\.0\\.0,' /etc/dnsmasq.d/blockpage.conf 2>/dev/null | head -1 | cut -d, -f2" 2>/dev/null || true)
    fi
    for _pdom in pornhub.com xvideos.com xnxx.com; do
        _r=$(_dns "${ROUTER_IP}" "${_pdom}")
        _ans=$(echo "${_r}" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | grep -v "^${ROUTER_IP}$" | head -1)
        if echo "${_r}" | grep -qi "NXDOMAIN\|can't find\|not found"; then
            pass "Porn blocked: ${_pdom} → NXDOMAIN"
        elif echo "${_r}" | grep -qE '0\.0\.0\.0'; then
            pass "Porn blocked: ${_pdom} → 0.0.0.0"
        elif [ -n "${_bp_ip}" ] && [ "${_ans}" = "${_bp_ip}" ]; then
            pass "Porn blocked: ${_pdom} → block page (${_bp_ip})"
        else
            fail "Porn NOT blocked: ${_pdom} resolved to ${_ans:-unknown} — porn must always be blocked"
        fi
    done
fi

# ─────────────────────────────────────────────────────────────────────────────
section "9/9  Port Blocking (from this machine)"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${HAS_NC}" = "0" ]; then
    printf "  ${_Y}[INFO]${_N} nc not found. Install with:\n"
    printf "         Ubuntu: sudo apt install netcat-openbsd\n"
    printf "         macOS:  pre-installed (should not see this message)\n"
    for _t in "Internet reachable" "DoT TCP/853 blocked" "KahfGuard DoT allowed" \
              "HTTPS TCP/443 allowed" "HTTP TCP/80 allowed"; do
        skip "${_t}"
    done
else
    # Sanity: confirm internet TCP/443 is reachable at all
    _inet=0
    if nc -z -w5 1.1.1.1 443 >/dev/null 2>&1; then
        _inet=1
        pass "Internet reachable (TCP/443 to 1.1.1.1)"
    else
        skip "Internet TCP/443 unreachable from this machine — skipping port tests"
    fi

    # The DoT-blocked / KahfGuard tests are only meaningful when THIS machine's
    # internet traffic actually routes through the Kahf router (i.e. the router is
    # our default gateway). In labs where the test host reaches the router's LAN
    # but uses a different default gateway, nc to 1.1.1.1:853 bypasses the router
    # entirely and the LAN-scoped reject rules never apply — skip, don't fail.
    _router_is_gw=0
    if [ "${SSH_OK}" = "1" ]; then
        _router_lan=$(_ssh "uci -q get network.lan.ipaddr 2>/dev/null | cut -d/ -f1" 2>/dev/null || true)
        _def_gw=$(route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}')
        [ -z "${_def_gw}" ] && _def_gw=$(ip route show default 2>/dev/null | awk '/default/{print $3; exit}')
        [ -n "${_router_lan}" ] && [ "${_def_gw}" = "${_router_lan}" ] && _router_is_gw=1
    fi

    if [ "${_inet}" = "1" ] && [ "${_router_is_gw}" = "0" ]; then
        skip "DoT TCP/853 blocked (test host's default gateway is '${_def_gw:-?}', not the router — internet bypasses it)"
        skip "KahfGuard DoT allowed (needs the router as default gateway)"
    fi

    if [ "${_inet}" = "1" ] && [ "${_router_is_gw}" = "1" ]; then
        # DoT TCP/853 to Cloudflare — must be BLOCKED (Cloudflare listens, so connection = bypassed)
        if nc -z -w3 1.1.1.1 853 >/dev/null 2>&1; then
            fail "DoT TCP/853 NOT blocked (reached 1.1.1.1:853 — DoT traffic can bypass DNS filtering)"
        else
            pass "DoT TCP/853 blocked (1.1.1.1:853 unreachable from LAN)"
        fi

        # KahfGuard DoT TCP/853 — must be ALLOWED (allowlist exception)
        _kg_ip=""
        if [ "${SSH_OK}" = "1" ]; then
            _kg_ip=$(_ssh "nft list set inet fw4 kahfguard_dns_ips 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true" 2>/dev/null || true)
        fi
        if [ -n "${_kg_ip}" ]; then
            if nc -z -w5 "${_kg_ip}" 853 >/dev/null 2>&1; then
                pass "KahfGuard DoT TCP/853 allowed through (${_kg_ip}:853)"
            else
                fail "KahfGuard DoT TCP/853 BLOCKED (${_kg_ip}:853) — KahfGuard allowlist may be broken"
            fi
        else
            skip "KahfGuard DoT allowlist test (no IPs found in nftables kahfguard_dns_ips set)"
        fi
    fi

    if [ "${_inet}" = "1" ]; then

        # Regression: normal HTTPS TCP/443 must still work
        if nc -z -w5 1.1.1.1 443 >/dev/null 2>&1; then
            pass "Normal HTTPS TCP/443 allowed (not over-blocked)"
        else
            fail "Normal HTTPS TCP/443 is blocked — over-blocking detected"
        fi

        # Regression: normal HTTP TCP/80 must still work
        if nc -z -w5 1.1.1.1 80 >/dev/null 2>&1; then
            pass "Normal HTTP TCP/80 allowed (not over-blocked)"
        else
            fail "Normal HTTP TCP/80 is blocked — over-blocking detected"
        fi
    fi
fi

# ─────────────────────────────────────────────────────────────────────────────
section "Per-device credentials & hardening"
# ─────────────────────────────────────────────────────────────────────────────

if [ "${SSH_OK}" = "1" ]; then
    # WiFi PSK is an intentional fleet-wide shared key (product decision — the
    # router ships with the same PSK on every unit, printed on the box). It MUST
    # be the standard Kahf fleet PSK; anything else means provisioning drifted.
    FLEET_WIFI_PSK="9876543210"
    _wkey=$(_ssh "uci get wireless.default_radio0.key 2>/dev/null || true" 2>/dev/null || true)
    if [ "${_wkey}" = "${FLEET_WIFI_PSK}" ]; then
        pass "WiFi PSK is the standard fleet key (${FLEET_WIFI_PSK}) — shared by design"
    elif [ -n "${_wkey}" ]; then
        fail "WiFi PSK is '${_wkey}', expected fleet key '${FLEET_WIFI_PSK}'"
    else
        skip "WiFi PSK unreadable"
    fi

    # Root must ship with a blank password — the Kahf app sets a unique one on first connect.
    # A non-empty hash means a password was baked into the firmware, which is forbidden.
    _shadow=$(_ssh "grep '^root:' /etc/shadow 2>/dev/null | cut -d: -f2 || true" 2>/dev/null || true)
    case "${_shadow}" in
        ""|"!"|"*"|"!!") pass "Root password is blank (app sets unique password on first connect)" ;;
        *) fail "Root password baked into firmware (shadow non-empty) — must ship blank" ;;
    esac

    # Credentials file present for the app/QA to surface.
    _r=$(_ssh "test -f /etc/kahf-credentials.txt && echo yes || echo no" 2>/dev/null || echo "no")
    [ "${_r}" = "yes" ] \
        && pass "/etc/kahf-credentials.txt present (per-device creds for app)" \
        || fail "/etc/kahf-credentials.txt missing"

    # Blocklist update channel: signing infrastructure must be present so a
    # malicious/corrupt CDN push can be rejected (usign + baked public key).
    _usign=$(_ssh "command -v usign >/dev/null 2>&1 && echo yes || echo no" 2>/dev/null || echo no)
    _blpub=$(_ssh "test -f /usr/lib/family-safe/blocklist-signing.pub && echo yes || echo no" 2>/dev/null || echo no)
    if [ "${_usign}" = "yes" ] && [ "${_blpub}" = "yes" ]; then
        pass "Blocklist signing infra present (usign + baked public key)"
    else
        fail "Blocklist signing infra missing (usign=${_usign}, pubkey=${_blpub})"
    fi

    # Signature verification must be ENFORCED, not optional — a tampered/corrupt
    # CDN push must be rejected fail-closed (BLOCKLIST_REQUIRE_SIG=1). The default
    # is 0, so this must be set explicitly in the baked /etc/family-safe.env.
    _reqsig=$(_ssh ". /etc/family-safe.env 2>/dev/null; echo \"\${BLOCKLIST_REQUIRE_SIG:-0}\"" 2>/dev/null | tr -d ' \n' || echo 0)
    [ "${_reqsig}" = "1" ] \
        && pass "Blocklist signature verification enforced (BLOCKLIST_REQUIRE_SIG=1)" \
        || fail "Blocklist signature verification NOT enforced (BLOCKLIST_REQUIRE_SIG=${_reqsig}) — tampered lists would be applied"

    # youtube_restrict must be a known feature flag now (regression for the
    # rpcd FS_KNOWN omission that made the app's toggle silently no-op).
    _yt=$(_ssh "ubus call family-safe get_features 2>/dev/null | grep -c youtube_restrict || echo 0" 2>/dev/null || echo 0)
    [ "${_yt}" != "0" ] \
        && pass "youtube_restrict exposed by get_features (app toggle works)" \
        || fail "youtube_restrict missing from get_features — app toggle will no-op"

    # IPv6 parity: our FAMILYSAFE reject/drop rules must be family=any so they
    # cover IPv6 (stock OpenWrt Allow-DHCP/Ping/IGMP are legitimately ipv4-only;
    # DNS DNAT redirects are @redirect and stay ipv4 by design). Count only
    # FAMILYSAFE-named @rule entries still pinned to ipv4.
    _fs_ipv4=$(_ssh 'i=0; c=0; while n=$(uci -q get firewall.@rule[$i].name 2>/dev/null); do [ -z "$n" ] && break; case "$n" in FAMILYSAFE-*) [ "$(uci -q get firewall.@rule[$i].family 2>/dev/null)" = "ipv4" ] && c=$((c+1)) ;; esac; i=$((i+1)); done; echo $c' 2>/dev/null | tr -d ' \n')
    [ -z "${_fs_ipv4}" ] && _fs_ipv4=0
    [ "${_fs_ipv4}" = "0" ] \
        && pass "FamilySafe reject/drop rules cover IPv6 (no IPv4-only FAMILYSAFE @rule)" \
        || fail "${_fs_ipv4} FAMILYSAFE @rule(s) still family=ipv4 — bypassable over IPv6"
else
    for _t in "WiFi fleet PSK" "Root password set" "Credentials file" \
              "Blocklist signing infra" "Blocklist sig enforced" \
              "youtube_restrict exposed" "DoT IPv6 parity"; do
        skip "${_t}"
    done
fi

# ═════════════════════════════════════════════════════════════════════════════
printf "\n${_B}══════════════════════════════════════════════════════════${_N}\n"
printf "  Results:  ${_G}%d passed${_N}  ${_R}%d failed${_N}  ${_Y}%d skipped${_N}\n" \
    "${PASS}" "${FAIL}" "${SKIP}"
printf "${_B}══════════════════════════════════════════════════════════${_N}\n\n"

if [ "${FAIL}" -gt 0 ]; then
    printf "  ${_R}✗ %d test(s) failed.${_N}\n\n" "${FAIL}"
    exit 1
fi
printf "  ${_G}✓ All tests passed.${_N}\n\n"
exit 0
