#!/usr/bin/env bash
# test/integration.sh — integration test sections
# Tests run against the live wgctl binary.
# Sourced by test.command.sh — do not execute directly.

WGCTL_BINARY="$(command -v wgctl)"

# ============================================
# Helpers
# ============================================

function cmd::test::_strip_ansi() {
  sed 's/\x1b\[[0-9;]*m//g'
}

function cmd::test::run_cmd() {
  local desc="$1" expected="${2:-}"
  shift 2

  local tmp exit_code
  tmp=$(mktemp)

  set +e
  timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
  exit_code=$?
  set -e

  # Reset terminal color in case command output left ANSI state dirty
  printf "\033[0m" >&2

  if [[ $exit_code -eq 124 ]]; then
    test::warn "${desc} (timed out after 30s)"
    rm -f "$tmp"
    return 1
  fi

  local clean
  clean=$(cmd::test::_strip_ansi < "$tmp")

  if [[ $exit_code -ne 0 ]]; then
    local msg="${desc}"
    [[ -n "$expected" ]] && msg="${desc} (expected '${expected}', command failed)"
    test::fail "$msg"
    if [[ "${WGCTL_TEST_VERBOSE:-false}" == "true" ]]; then
      printf "    Output: %s\n" "$(echo "$clean" | head -3 | tr '\n' ' ')"
    fi
    rm -f "$tmp"
    return 1
  fi

  if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then
    local actual
    actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/  */ /g' | cut -c1-100)
    test::fail "${desc} (expected '${expected}', got: '${actual}')"
    rm -f "$tmp"
    return 1
  fi

  test::pass "$desc"
  rm -f "$tmp"
}

function cmd::test::run_cmd_any() {
  local desc="$1" expected="${2:-}"
  shift 2

  local tmp
  tmp=$(mktemp)

  set +e
  timeout 30 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
  set -e

  printf "\033[0m" >&2

  local clean
  clean=$(cmd::test::_strip_ansi < "$tmp")

  if [[ -n "$expected" ]] && ! echo "$clean" | grep -qF "$expected"; then
    local actual
    actual=$(echo "$clean" | head -3 | tr '\n' ' ' | sed 's/  */ /g' | cut -c1-100)
    test::fail "${desc} (expected '${expected}', got: '${actual}')"
    rm -f "$tmp"
    return 1
  fi

  test::pass "$desc"
  rm -f "$tmp"
}

function cmd::test::run_cmd_fails() {
  local desc="$1"
  shift

  local tmp exit_code
  tmp=$(mktemp)

  set +e
  timeout 10 "$WGCTL_BINARY" "$@" > "$tmp" 2>&1
  exit_code=$?
  set -e

  printf "\033[0m" >&2
  rm -f "$tmp"

  if [[ $exit_code -eq 124 ]]; then
    test::warn "${desc} (timed out)"
    return 1
  fi

  if [[ $exit_code -eq 0 ]]; then
    test::fail "${desc} (expected failure but succeeded)"
    return 1
  fi

  test::pass "$desc"
}

# ============================================
# Sections
# ============================================

function cmd::test::run_all_integration_sections() {
  cmd::test::section_list
  cmd::test::section_inspect
  cmd::test::section_config
  cmd::test::section_rules
  cmd::test::section_groups
  cmd::test::section_block_unblock
  cmd::test::section_audit
  cmd::test::section_logs
  cmd::test::section_fw
  cmd::test::section_net
  cmd::test::section_subnet
  cmd::test::section_identity
  cmd::test::section_activity
  cmd::test::section_policy
  cmd::test::section_hosts
  cmd::test::section_peer_cmd
  cmd::test::section_group_purge
  cmd::test::section_logs_clean
  cmd::test::section_display
  cmd::test::section_export
}

function cmd::test::section_list() {
  test::section "List"
  cmd::test::run_cmd "list"                   "rule:"      list
  cmd::test::run_cmd "list --online"          ""           list --online
  cmd::test::run_cmd "list --offline"         ""           list --offline
  cmd::test::run_cmd "list --blocked"         ""           list --blocked
  cmd::test::run_cmd "list --type phone"      "phone"      list --type phone
  cmd::test::run_cmd "list --detailed"        "rule:"      list --detailed
  cmd::test::run_cmd "list --name phone-nuno" "phone-nuno" list --name phone-nuno
  cmd::test::run_cmd "list --json"              '"ok":true'          list --json
  cmd::test::run_cmd "list --json has peers"    '"peers":'           list --json
  cmd::test::run_cmd "list --json has meta"     '"meta":'            list --json
  cmd::test::run_cmd "list --json peer name"    '"name":'            list --json
  cmd::test::run_cmd "list --json peer ip"      '"ip":'              list --json
  cmd::test::run_cmd "list --json peer status"  '"status":'          list --json
}

function cmd::test::section_inspect() {
  test::section "Inspect"
  cmd::test::run_cmd "inspect --name phone-nuno"          "IP:"        inspect --name phone-nuno
  cmd::test::run_cmd "inspect --name nuno --type phone"   "IP:"        inspect --name nuno --type phone
  cmd::test::run_cmd "inspect --name phone-nuno --config" "PrivateKey" inspect --name phone-nuno --config
  cmd::test::run_cmd_fails "inspect nonexistent"                       inspect --name nonexistent-peer
  cmd::test::run_cmd "inspect --json"           '"ok":true'            inspect --name phone-nuno --json
  cmd::test::run_cmd "inspect --json rule"      '"rule":'              inspect --name phone-nuno --json
  cmd::test::run_cmd "inspect --json identity"  '"identity":'          inspect --name phone-nuno --json
  cmd::test::run_cmd "inspect --json groups"    '"groups":'            inspect --name phone-nuno --json
}

function cmd::test::section_config() {
  test::section "Config & QR"
  cmd::test::run_cmd "config --name phone-nuno"        "PrivateKey" config --name phone-nuno
  cmd::test::run_cmd "config --name nuno --type phone" "PrivateKey" config --name nuno --type phone
  cmd::test::run_cmd "qr --name phone-nuno"            ""           qr --name phone-nuno
  cmd::test::run_cmd "config migrate --dry-run" ""                  config migrate --dry-run
  cmd::test::run_cmd_fails "config missing --name"                  config
}

function cmd::test::section_rules() {
  test::section "Rules"
  cmd::test::run_cmd "rule list"                   "user"        rule list
  cmd::test::run_cmd "rule show --name guest"      "Description" rule show --name guest
  cmd::test::run_cmd "rule show --name user"       "Description" rule show --name user
  cmd::test::run_cmd "rule show --name admin"      "Description" rule show --name admin
  cmd::test::run_cmd "rule list --json" '"rules":'               rule list --json
  cmd::test::run_cmd "rule list --json"        '"rules":'        rule list --json
  cmd::test::run_cmd "rule --json is_base"     '"is_base":'      rule list --json
  cmd::test::run_cmd "rule --json extends"     '"extends":'      rule list --json
  cmd::test::run_cmd "rule --json allows"      '"allows":'       rule list --json
  cmd::test::run_cmd_fails "rule show nonexistent"               rule show --name nonexistent
}

function cmd::test::section_groups() {
  test::section "Groups"
  cmd::test::run_cmd "group list"               "Groups"            group list
  cmd::test::run_cmd "group show --name family" "Peers:"            group show --name family
  cmd::test::run_cmd "group list --json" '"name":'                  group list --json
  cmd::test::run_cmd "group list --json" '"command":"groups"'       group list --json
  cmd::test::run_cmd_fails "group show nonexistent"                 group show --name nonexistent
}

# function cmd::test::section_blocks() {
#   test::section "Blocks"
#   cmd::test::run_cmd "block --reason records history" "block-history" block --name guest-test --reason "test block" --force
#   cmd::test::run_cmd_succeeds "unblock clears history" unblock --name guest-test --force
# }

function cmd::test::section_block_unblock() {
  test::section "Block / Unblock"
 
  # ── Setup fixture ──
  local fixture="phone-testblock"
  wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
  wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
  wgctl add --name testblock --type phone >/dev/null 2>&1 || true
 
  local history_file
  history_file="$(ctx::block_history)/${fixture}.json"

  # ── Block ──
  echo "DEBUG about to run: $WGCTL_BINARY block --name $fixture --force" >&2
  cmd::test::run_cmd "block peer"            "blocked"      block --name "$fixture" --force
  cmd::test::run_cmd "block already blocked" "already"      block --name "$fixture" --force

  wgctl unblock --name "$fixture" --force >/dev/null 2>&1 || true
  cmd::test::run_cmd "block with reason"     "blocked"      block --name "$fixture" --force \
    --reason "test reason"
 
  # ── Block history file created ──
  [[ -f "$history_file" ]] && test::pass "block history file created" \
                           || test::fail "block history file not created"
 
  # ── Block history fields ──
  if [[ -f "$history_file" ]]; then
    local has_id has_blocked_at has_endpoint has_reason
    has_id=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if d['history'] and 'id' in d['history'][-1] else 'no')
" 2>/dev/null)
    has_blocked_at=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if d['history'] and d['history'][-1].get('blocked_at') else 'no')
" 2>/dev/null)
    has_endpoint=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if 'endpoint_at_block' in d['history'][-1] else 'no')
" 2>/dev/null)
    has_reason=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if d['history'][-1].get('reason') == 'test reason' else 'no')
" 2>/dev/null)
    cmd::test::assert "history has id"          "$has_id"       "yes"
    cmd::test::assert "history has blocked_at"  "$has_blocked_at" "yes"
    cmd::test::assert "history has endpoint"    "$has_endpoint" "yes"
    cmd::test::assert "history has reason"      "$has_reason"   "yes"
  fi
 
  # ── Unblock ──
  cmd::test::run_cmd "unblock peer"          "unblocked"    unblock --name "$fixture" --force \
    --reason "test cleanup"
  cmd::test::run_cmd "unblock not blocked"   "not blocked"  unblock --name "$fixture" --force
 
  # ── Unblock history updated ──
  if [[ -f "$history_file" ]]; then
    local has_unblocked has_unblock_reason
    has_unblocked=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if d['history'] and d['history'][-1].get('unblocked_at') else 'no')
" 2>/dev/null)
    has_unblock_reason=$(python3 -c "
import json
d=json.load(open('$history_file'))
print('yes' if d['history'][-1].get('unblock_reason') == 'test cleanup' else 'no')
" 2>/dev/null)
    cmd::test::assert "history has unblocked_at"    "$has_unblocked"       "yes"
    cmd::test::assert "history has unblock_reason"  "$has_unblock_reason"  "yes"
  fi
 
  # ── Teardown fixture ──
  wgctl remove --name "$fixture" --force >/dev/null 2>&1 || true
  rm -f "$history_file"
}
function cmd::test::section_audit() {
  test::section "Audit"
  cmd::test::run_cmd_any "audit"                   "passed" audit
  cmd::test::run_cmd_any "audit --peer phone-nuno" "passed" audit --peer phone-nuno
  cmd::test::run_cmd_any "audit --type phone"      "passed" audit --type phone
}

function cmd::test::section_logs() {
  test::section "Logs"
  cmd::test::run_cmd "logs"                   "Activity" logs
  cmd::test::run_cmd "logs --name phone-nuno" "Activity" logs --name phone-nuno
  cmd::test::run_cmd "logs --fw"              "Firewall Drops" logs --fw
  cmd::test::run_cmd "logs --wg"              "WireGuard Events" logs --wg
  cmd::test::run_cmd "logs --since 2099-01-01"      "No logs" logs --since "2099-01-01"
  cmd::test::run_cmd "logs --wg --since 2099-01-01" "No logs" logs --wg --since "2099-01-01"
  cmd::test::run_cmd "logs --fw --since 2099-01-01" "No logs" logs --fw --since "2099-01-01"
  cmd::test::run_cmd "logs --wg --event attempt"    ""   logs --wg --event attempt
  cmd::test::run_cmd "logs --detailed"              ""   logs --detailed
  cmd::test::run_cmd "logs --resolved"              ""   logs --resolved
  cmd::test::run_cmd "logs --ascending"             ""   logs --ascending
  cmd::test::run_cmd "logs --descending"            ""   logs --descending
  cmd::test::run_cmd "logs --wg --ascending"        ""   logs --wg --ascending
}

function cmd::test::section_fw() {
  test::section "Firewall"
  cmd::test::run_cmd "fw list"                   "FORWARD"    fw list
  cmd::test::run_cmd "fw list --peer phone-nuno" ""           fw list --peer phone-nuno
  cmd::test::run_cmd "fw list --no-nflog"        ""           fw list --no-nflog
  cmd::test::run_cmd "fw list --no-accept"       ""           fw list --no-accept
  cmd::test::run_cmd "fw list --no-drop"         ""           fw list --no-drop
  cmd::test::run_cmd "fw nat"                    "PREROUTING" fw nat
  cmd::test::run_cmd "fw count"                  "TOTAL"      fw count
}

function cmd::test::section_net() {
  test::section "Net"
  "$WGCTL_BINARY" net rm --name test-svc --force > /dev/null 2>&1 || true

  cmd::test::run_cmd "net add service"     "added"              net add --name test-svc --ip 10.0.0.99 --desc "Test service"
  cmd::test::run_cmd "net add port"        "Added"              net add --name test-svc:web --port 9999:tcp
  cmd::test::run_cmd "net list"            "test-svc"           net list
  cmd::test::run_cmd "net list --detailed" "web"                net list --detailed
  cmd::test::run_cmd "net show"            "9999"               net show --name test-svc
  cmd::test::run_cmd "net rm port"         "Removed"            net rm --name test-svc:web --force
  cmd::test::run_cmd "net add port again"  "Added"              net add --name test-svc:web --port 9999:tcp
  cmd::test::run_cmd "net rm all ports"    "Removed"            net rm --name test-svc:ports --force
  cmd::test::run_cmd "net rm service"      "Removed"            net rm --name test-svc --force
  cmd::test::run_cmd "net list --json"          '"services":'   net list --json
  cmd::test::run_cmd "net --json has tags"      '"tags":'       net list --json
  cmd::test::run_cmd "net --json port_count"    '"port_count":' net list --json
  cmd::test::run_cmd_fails "net show nonexistent"               net show --name nonexistent-svc
  cmd::test::run_cmd_fails "net add port no service"            net add --name nonexistent:web --port 80:tcp
}

function cmd::test::section_subnet() {
  test::section "Subnet"
  "$WGCTL_BINARY" subnet rm --name test-subnet-2 > /dev/null 2>&1 || true
  "$WGCTL_BINARY" subnet rm --name test-subnet   > /dev/null 2>&1 || true

  cmd::test::run_cmd "subnet list"              "desktop"         subnet list
  cmd::test::run_cmd "subnet show desktop"      "tunnel:"         subnet show --name desktop
  cmd::test::run_cmd "subnet show guests group" "guests"          subnet show --name guests
  cmd::test::run_cmd_fails "subnet show nonexistent"              subnet show --name nonexistent
  cmd::test::run_cmd "subnet add" "added"                         subnet add --name test-subnet --subnet 10.1.250.0/24 --type iot --desc "Test"
  cmd::test::run_cmd "subnet list shows new" "test-subnet"        subnet list
  cmd::test::run_cmd_fails "subnet rename in-use (desktop)"       subnet rename --name desktop --new-name workstation
  cmd::test::run_cmd "subnet rename unused" "renamed"             subnet rename --name test-subnet --new-name test-subnet-2
  cmd::test::run_cmd "subnet rm" "removed"                        subnet rm --name test-subnet-2
  cmd::test::run_cmd "subnet list --json"       '"subnets":'      subnet list --json
  cmd::test::run_cmd "subnet --json has cidr"   '"cidr":'         subnet list --json
  cmd::test::run_cmd "subnet --json is_group"   '"is_group":'     subnet list --json
  cmd::test::run_cmd_fails "subnet rm nonexistent"                subnet rm --name nonexistent-subnet
}

function cmd::test::section_identity() {
  test::section "Identity"
  cmd::test::run_cmd "identity list"              ""              identity list
  cmd::test::run_cmd "identity migrate --dry-run" "Dry run"       identity migrate --dry-run
  cmd::test::run_cmd "identity show nuno"         "nuno"          identity show --name nuno
  cmd::test::run_cmd "identity list --json"       '"identities":' identity list --json
  cmd::test::run_cmd "identity --json types array" '"types":['    identity list --json
  cmd::test::run_cmd "identity --json rules array" '"rules":['    identity list --json
  cmd::test::run_cmd_fails "identity show nonexistent"            identity show --name nonexistent
}

function cmd::test::section_activity() {
  test::section "Activity"
  cmd::test::run_cmd "activity"        "Activity"   activity
  cmd::test::run_cmd "activity --json"          '"command":"activity"'  activity --json
  cmd::test::run_cmd "activity --json has data" '"data":'               activity --json
  cmd::test::run_cmd "activity --json has rx"   '"rx":'                 activity --json
}

function cmd::test::section_policy() {
  test::section "Policy"
  cmd::test::run_cmd "policy list --json"            '"policies":'          policy list --json
  cmd::test::run_cmd "policy --json has tunnel_mode" '"tunnel_mode":'       policy list --json
  cmd::test::run_cmd "policy --json strict_rule bool" '"strict_rule":false' policy list --json
}

function cmd::test::section_hosts() {
  test::section "Hosts"
 
  # Cleanup
  "$WGCTL_BINARY" hosts rm --ip 192.0.2.1 --force > /dev/null 2>&1 || true
  "$WGCTL_BINARY" hosts rm --port 9999    --force > /dev/null 2>&1 || true
 
  cmd::test::run_cmd "hosts list"                   "Hosts"     hosts list
  cmd::test::run_cmd "hosts add --ip"               "Added"     hosts add --ip 192.0.2.1 --name test-host --desc "Test" --tags test,unit
  cmd::test::run_cmd "hosts list shows new"         "test-host" hosts list
  cmd::test::run_cmd "hosts show --ip"              "Name"      hosts show --ip 192.0.2.1
  cmd::test::run_cmd "hosts add --port"             "Added"     hosts add --port 9999 --name test-port
  cmd::test::run_cmd "hosts list shows port"        "test-port" hosts list
  cmd::test::run_cmd "hosts rm --ip"                "Removed"   hosts rm --ip 192.0.2.1 --force
  cmd::test::run_cmd "hosts rm --port"              "Removed"   hosts rm --port 9999 --force
  cmd::test::run_cmd "hosts list --json"     '"hosts":'         hosts list --json
  cmd::test::run_cmd "hosts --json has type" '"type":'          hosts list --json
  cmd::test::run_cmd "hosts --json has tags" '"tags":'          hosts list --json
  cmd::test::run_cmd_fails "hosts show nonexistent"             hosts show --ip 192.0.2.99
  cmd::test::run_cmd_fails "hosts add missing --name"           hosts add --ip 192.0.2.1
}
 
function cmd::test::section_peer_cmd() {
  test::section "Peer command"
 
  "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
  "$WGCTL_BINARY" add --name testunit --type phone > /dev/null 2>&1
 
  # update-dns
  cmd::test::run_cmd "peer update-dns --name"       "Updated DNS"  peer update-dns --name phone-testunit
  cmd::test::run_cmd "peer update-dns applies"      "10.0.0.103"   config --name phone-testunit
  cmd::test::run_cmd "peer update-dns --all"        "peer(s)"      peer update-dns --all
 
  # update-tunnel
  cmd::test::run_cmd "peer update-tunnel split"     "split"        peer update-tunnel --name phone-testunit --mode split
  cmd::test::run_cmd "peer update-tunnel full"      "Updated"      peer update-tunnel --name phone-testunit --mode full
  cmd::test::run_cmd_fails "peer update-tunnel bad mode"           peer update-tunnel --name phone-testunit --mode invalid
  cmd::test::run_cmd_fails "peer update-tunnel missing --mode"     peer update-tunnel --name phone-testunit
  cmd::test::run_cmd_fails "peer update-tunnel missing --name"     peer update-tunnel --mode split
 
  # Restore split tunnel
  "$WGCTL_BINARY" peer update-tunnel --name phone-testunit --mode split > /dev/null 2>&1 || true
  "$WGCTL_BINARY" remove --name phone-testunit --force > /dev/null 2>&1 || true
}
 
 function cmd::test::section_group_purge() {
  test::section "Group: purge-stale"
 
  # dry-run should not modify anything
  cmd::test::run_cmd "purge-stale --all --dry-run" \
    "[dry-run]" \
    group purge-stale --all --dry-run --force
 
  # single group dry-run
  cmd::test::run_cmd "purge-stale --name family --dry-run" \
    "[dry-run]" \
    group purge-stale --name family --dry-run --force
}

function cmd::test::section_logs_clean() {
  test::section "Logs: clean"
 
  cmd::test::run_cmd "logs clean --force" \
    "keepalive" \
    logs clean --force
}

function cmd::test::section_export() {
  test::section "Export"

  # Single peer export
  cmd::test::run_cmd "export --peer"              '"export_type":"peer"'  export --peer phone-nuno
  cmd::test::run_cmd "export --peer has name"     '"name":"phone-nuno"'   export --peer phone-nuno
  cmd::test::run_cmd "export --peer has conf"     '"conf":'               export --peer phone-nuno
  cmd::test::run_cmd "export --peer has identity" '"identity":"nuno"'     export --peer phone-nuno
  cmd::test::run_cmd "export --peer has groups"   '"groups":'             export --peer phone-nuno
  cmd::test::run_cmd "export --peer has blocks"   '"blocks":'             export --peer phone-nuno
  cmd::test::run_cmd "export --peer conf-only"    '"export_type":"peer_conf"' export --peer phone-nuno --conf-only
  cmd::test::run_cmd "export --peer meta-only"    '"export_type":"peer_meta"' export --peer phone-nuno --meta-only
  cmd::test::run_cmd_fails "export missing flag"  export
  # Identity export
  cmd::test::run_cmd "export --identity"          '"export_type":"identity"' export --identity nuno

  # Full backup
  cmd::test::run_cmd "export --all"                '"export_type"'    export --all
  cmd::test::run_cmd "export --all is full"        '"full"'           export --all
  cmd::test::run_cmd "export --all has peers"      '"peers"'          export --all
  cmd::test::run_cmd "export --all has rules"      '"rules"'          export --all
  cmd::test::run_cmd "export --all has identities" '"identities"'     export --all
  cmd::test::run_cmd "export --all has config"     '"config"'         export --all
  cmd::test::run_cmd "export --all no-config"      '"peers"'          export --all --no-config
  # --no-config should NOT have config section
  local no_config_out
  no_config_out=$(wgctl export --all --no-config 2>/dev/null)
  if echo "$no_config_out" | grep -qF '"config":'; then
    test::fail "export --all --no-config should not have config section"
  else
    test::pass "export --all --no-config has no config section"
  fi

  # Export to file
  local tmp_file="/tmp/wgctl_test_export_$$.json"
  cmd::test::run_cmd "export --peer --out file"   "Exported to" export --peer phone-nuno --out "$tmp_file"
  [[ -f "$tmp_file" ]] && test::pass "export --out file exists" \
                       || test::fail "export --out file not created"
  rm -f "$tmp_file"

  # Version in export
  cmd::test::run_cmd "export has version"         '"wgctl_version":'      export --peer phone-nuno
}

function cmd::test::section_display() {
  test::section "Display config"

  cmd::test::run_cmd "display config exists"  "" \
    config show --name phone-nuno  # just check wgctl works, not display directly

  # Test style via unit tests (see unit section)
}

# ============================================
# Helpers
# ============================================

function cmd::test::assert() {
  local desc="${1:-}" result="${2:-}" expected="${3:-}"
  if [[ "$result" == "$expected" ]]; then
    test::pass "$desc"
  else
    test::fail "${desc} (expected '${expected}', got '${result}')"
  fi
}

function cmd::test::assert_true() {
  local desc="${1:-}"
  shift
  if "$@" 2>/dev/null; then
    test::pass "$desc"
  else
    test::fail "$desc (expected true, got false)"
  fi
}

function cmd::test::assert_false() {
  local desc="${1:-}"
  shift
  if ! "$@" 2>/dev/null; then
    test::pass "$desc"
  else
    test::fail "$desc (expected false, got true)"
  fi
}