Ctrl K
ComunidadGuías…

vpn-switch-integration-test.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# vpn-switch-integration-test.sh - User Story Integration Tests
# === AUTO-GENERATED METADATA (make metadata) ===
# Generated: 2026-11-01 06:41:49
# Terminal functions (single underscore): output shell commands
# Combinator functions (double underscore): output single vpn-switch command
# Batch-combinator functions (triple underscore): output multiple vpn-switch commands
TERMINAL_FUNCTIONS="_comment1 _note1 _error1 _error2 _error3 _fail2 _log1 _log2 _log3  _wireguard_collect0 _openvpn_collect0 _export_fresh1 _dump_header1 _restore_prune2 _filter_write3 _bundle_pack2 _seal_write2 _incoming_remove1 _extract_unpack2 _attest_sign2 _lift1 _database_init0 _environment_init1 _environment_cache1 _lift3 _setenv2 _getenv1 _unsetenv1 _cat1 _batch2 _printenv0 _env_sync0 _version_sync0 _version0 _link2 _remove1 _status0 _help0 _help1 _help2 _complete0 _complete1 _complete2 _complete3 _complete4 _complete5 _complete6 _helpenv0 _helpenv2 _database_enumerate0 _system_inspect0 _logs_inspect0 _phases_inspect0 _summary_inspect0 _prologue_inspect0 _key_import_copy3 _key_collect_files2 _manifest_write2 _openpgp_record2 _openpgp_record3 _openpgp_prerequisites0 _openvpn_patch2 _openvpn_configure1 _openvpn_connect1 _openvpn_disconnect1 _openvpn_patch1 _openvpn_enumerate0 _openvpn_import1 _openvpn_list0 _openvpn_list1 _openvpn_add1 _openvpn_add3 _openvpn_link2 _openvpn_remove1 _openvpn_remove2 _openvpn_validate0 _openvpn_info1 _phases_copy0 _provenance_serial1 _provenance_file5 _provenance_import_copy2 _provenance_collect0 _provenance_list1 _session_list0 _session_show1 _session_remove1 _session_connect1 _session_save0 _session_save1 _session_clean0 _session_refresh0 _session_validate0 _session_enumerate0 _session_describe0 _session_create1 _interface_destroy1 _session_populate3 _logs_validate0 _logs_clean0 _phases_validate0 _database_validate0 _binaries_validate0 _network_validate0 _permissions_validate0 _environment_validate0 _install_validate0 _version_validate0 _sudo_validate0 _wireguard_patch2 _wireguard_configure1 _wireguard_connect1 _wireguard_disconnect1 _wireguard_enumerate0 _wireguard_import1 _wireguard_list0 _wireguard_list1 _wireguard_add1 _wireguard_add3 _wireguard_link2 _wireguard_remove1 _wireguard_remove2 _wireguard_validate0 _wireguard_patch1 _wireguard_info1"
COMBINATOR_FUNCTIONS="__export3 __export2 __export_redacted2 __export_full2 __export_minimized2 __export_pair3 __dump_readable1 __archive_key_pinned0 __restore1 __restore2 __restore_v12 __restore_v13 __restore_v14 __restore_v15 __restore3 __restore_serial_admissible3 __restore_base_exists1 __restore_replay2 __filter2 __filter_default2 __filter3 __filter_redacted2 __filter_full2 __filter_minimized2 __collection_readable1 __archiver_set0 __bundle_archive_absolute1 __bundle_manifest_beside1 __bundle_collection_inside1 __bundle_readable1 __seal_absent1 __attest_absent1 __seal_present1 __seal_matches2 __incoming_clear1 __extractor_set0 __extract_destination_absolute1 __extract_destination_empty1 __import2 __import_bootstrap2 __import3 __import4 __import5 __import6 __attest_file_exists1 __openpgp_record_complete1 __attest_signer_pinned2 __bootstrap1 __environment_cache0 __phases_lift1 __init0 __dump0 __batch1 __import1 __helpenv1 __setintp2 __getintp1 __unsetintp1 __helpintp1 __helpintp2 __key_name_valid1 __base_element_exists1 __key_record_exists2 __manifest_named1 __manifest_entries_hashable1 __manifest_readable1 __manifest_base_exists1 __manifest_tree_matches2 __openpgp_keyid_valid1 __openpgp_import2 __openpgp_collect1 __openvpn_start0 __openvpn_start1 __openvpn_stop1 __openvpn_add2 __openvpn_help0 __provenance_serial0 __provenance_add2 __provenance_add3 __provenance_add4 __provenance_add5 __provenance_add6 __provenance_bundle_readable1 __provenance_receipt5 __provenance_receipt_same3 __provenance_id_valid1 __provenance_import_place2 __provenance_list0 __session_disconnect1 __session_stop1 __session_show0 __session_start0 __session_start1 __session_help0 __session_exists1 __session_conflicts2 __interface_check1 __stop1 __start1 __start0 __wireguard_start0 __wireguard_start1 __wireguard_stop1 __wireguard_add2 __wireguard_help0"
BATCH_COMBINATOR_FUNCTIONS="___collect0 ___export_pair4 ___restore_v16 ___bundle2 ___seal2 ___seal_verify2 ___extract2 ___import_bootstrap5 ___import_bootstrap6 ___import7 ___attest2 ___attest_verify2 ___bootstrap2 ___environment_refresh0 ___init1 ___dumpenv1 ___dump1 ___batch0 ___sync0 ___list0 ___environment_inspect0 ___inspect0 ___key_import3 ___manifest2 ___manifest_attest2 ___manifest_verify3 ___manifest_match2 ___openpgp_add2 ___openpgp_add3 ___openpgp_dump0 ___openpgp_collect0 ___openvpn_create1 ___openvpn_stop0 ___openvpn_inspect0 ___openvpn_clean0 ___openvpn_dump0 ___phases_sync1 ___phases_sync0 ___provenance_add7 ___provenance_import2 ___provenance_dump0 ___session_stop0 ___session_switch1 ___session_switch2 ___session_dump0 ___session_inspect0 ___session_prepare3 ___stop0 ___validate0 ___wireguard_create1 ___wireguard_inspect0 ___wireguard_stop0 ___wireguard_clean0 ___wireguard_clean1 ___wireguard_dump0"
ANCHOR_FUNCTIONS="_comment1 _note1 _error1 _error2 _error3 _fail2 _log1 _log2 _log3  _wireguard_collect0 _openvpn_collect0 _export_fresh1 _dump_header1 _restore_prune2 _filter_write3 _bundle_pack2 _seal_write2 _incoming_remove1 _extract_unpack2 _attest_sign2 _lift1 _database_init0 _environment_init1 _environment_cache1 _lift3 _setenv2 _getenv1 _unsetenv1 _cat1 _batch2 _printenv0 _env_sync0 _version_sync0 _version0 _link2 _remove1 _status0 _help0 _help1 _help2 _complete0 _complete1 _complete2 _complete3 _complete4 _complete5 _complete6 _helpenv0 _helpenv2 _database_enumerate0 _system_inspect0 _logs_inspect0 _phases_inspect0 _summary_inspect0 _prologue_inspect0 _key_import_copy3 _key_collect_files2 _manifest_write2 _openpgp_record2 _openpgp_record3 _openpgp_prerequisites0 _openvpn_patch2 _openvpn_configure1 _openvpn_connect1 _openvpn_disconnect1 _openvpn_patch1 _openvpn_enumerate0 _openvpn_import1 _openvpn_list0 _openvpn_list1 _openvpn_add1 _openvpn_add3 _openvpn_link2 _openvpn_remove1 _openvpn_remove2 _openvpn_validate0 _openvpn_info1 _phases_copy0 _provenance_serial1 _provenance_file5 _provenance_import_copy2 _provenance_collect0 _provenance_list1 _session_list0 _session_show1 _session_remove1 _session_connect1 _session_save0 _session_save1 _session_clean0 _session_refresh0 _session_validate0 _session_enumerate0 _session_describe0 _session_create1 _interface_destroy1 _session_populate3 _logs_validate0 _logs_clean0 _phases_validate0 _database_validate0 _binaries_validate0 _network_validate0 _permissions_validate0 _environment_validate0 _install_validate0 _version_validate0 _sudo_validate0 _wireguard_patch2 _wireguard_configure1 _wireguard_connect1 _wireguard_disconnect1 _wireguard_enumerate0 _wireguard_import1 _wireguard_list0 _wireguard_list1 _wireguard_add1 _wireguard_add3 _wireguard_link2 _wireguard_remove1 _wireguard_remove2 _wireguard_validate0 _wireguard_patch1 _wireguard_info1   __export3 __export2 __export_redacted2 __export_full2 __export_minimized2 __export_pair3 __dump_readable1 __archive_key_pinned0 __restore1 __restore2 __restore_v12 __restore_v13 __restore_v14 __restore_v15 __restore3 __restore_serial_admissible3 __restore_base_exists1 __restore_replay2 __filter2 __filter_default2 __filter3 __filter_redacted2 __filter_full2 __filter_minimized2 __collection_readable1 __archiver_set0 __bundle_archive_absolute1 __bundle_manifest_beside1 __bundle_collection_inside1 __bundle_readable1 __seal_absent1 __attest_absent1 __seal_present1 __seal_matches2 __incoming_clear1 __extractor_set0 __extract_destination_absolute1 __extract_destination_empty1 __import2 __import_bootstrap2 __import3 __import4 __import5 __import6 __attest_file_exists1 __openpgp_record_complete1 __attest_signer_pinned2 __bootstrap1 __environment_cache0 __phases_lift1 __init0 __dump0 __batch1 __import1 __helpenv1 __setintp2 __getintp1 __unsetintp1 __helpintp1 __helpintp2 __key_name_valid1 __base_element_exists1 __key_record_exists2 __manifest_named1 __manifest_entries_hashable1 __manifest_readable1 __manifest_base_exists1 __manifest_tree_matches2 __openpgp_keyid_valid1 __openpgp_import2 __openpgp_collect1 __openvpn_start0 __openvpn_start1 __openvpn_stop1 __openvpn_add2 __openvpn_help0 __provenance_serial0 __provenance_add2 __provenance_add3 __provenance_add4 __provenance_add5 __provenance_add6 __provenance_bundle_readable1 __provenance_receipt5 __provenance_receipt_same3 __provenance_id_valid1 __provenance_import_place2 __provenance_list0 __session_disconnect1 __session_stop1 __session_show0 __session_start0 __session_start1 __session_help0 __session_exists1 __session_conflicts2 __interface_check1 __stop1 __start1 __start0 __wireguard_start0 __wireguard_start1 __wireguard_stop1 __wireguard_add2 __wireguard_help0   ___collect0 ___export_pair4 ___restore_v16 ___bundle2 ___seal2 ___seal_verify2 ___extract2 ___import_bootstrap5 ___import_bootstrap6 ___import7 ___attest2 ___attest_verify2 ___bootstrap2 ___environment_refresh0 ___init1 ___dumpenv1 ___dump1 ___batch0 ___sync0 ___list0 ___environment_inspect0 ___inspect0 ___key_import3 ___manifest2 ___manifest_attest2 ___manifest_verify3 ___manifest_match2 ___openpgp_add2 ___openpgp_add3 ___openpgp_dump0 ___openpgp_collect0 ___openvpn_create1 ___openvpn_stop0 ___openvpn_inspect0 ___openvpn_clean0 ___openvpn_dump0 ___phases_sync1 ___phases_sync0 ___provenance_add7 ___provenance_import2 ___provenance_dump0 ___session_stop0 ___session_switch1 ___session_switch2 ___session_dump0 ___session_inspect0 ___session_prepare3 ___stop0 ___validate0 ___wireguard_create1 ___wireguard_inspect0 ___wireguard_stop0 ___wireguard_clean0 ___wireguard_clean1 ___wireguard_dump0"
# Function-to-module mapping (format: "func:module.sh func:module.sh ...")
# Used by process_arguments() for deterministic module loading
FUNCTION_MODULES="___collect0:archive.sh _wireguard_collect0:archive.sh _openvpn_collect0:archive.sh __export3:archive.sh __export2:archive.sh __export_redacted2:archive.sh __export_full2:archive.sh __export_minimized2:archive.sh __export_pair3:archive.sh _export_fresh1:archive.sh ___export_pair4:archive.sh _dump_header1:archive.sh __dump_readable1:archive.sh __archive_key_pinned0:archive.sh __restore1:archive.sh __restore2:archive.sh __restore_v12:archive.sh __restore_v13:archive.sh __restore_v14:archive.sh __restore_v15:archive.sh ___restore_v16:archive.sh _restore_prune2:archive.sh __restore3:archive.sh __restore_serial_admissible3:archive.sh __restore_base_exists1:archive.sh __restore_replay2:archive.sh __filter2:archive.sh __filter_default2:archive.sh __filter3:archive.sh __filter_redacted2:archive.sh __filter_full2:archive.sh __filter_minimized2:archive.sh _filter_write3:archive.sh ___bundle2:archive.sh __collection_readable1:archive.sh __archiver_set0:archive.sh __bundle_archive_absolute1:archive.sh __bundle_manifest_beside1:archive.sh __bundle_collection_inside1:archive.sh _bundle_pack2:archive.sh ___seal2:archive.sh __bundle_readable1:archive.sh __seal_absent1:archive.sh __attest_absent1:archive.sh _seal_write2:archive.sh ___seal_verify2:archive.sh __seal_present1:archive.sh __seal_matches2:archive.sh __incoming_clear1:archive.sh _incoming_remove1:archive.sh ___extract2:archive.sh __extractor_set0:archive.sh __extract_destination_absolute1:archive.sh __extract_destination_empty1:archive.sh _extract_unpack2:archive.sh __import2:archive.sh __import_bootstrap2:archive.sh ___import_bootstrap5:archive.sh ___import_bootstrap6:archive.sh __import3:archive.sh __import4:archive.sh __import5:archive.sh __import6:archive.sh ___import7:archive.sh ___attest2:attest.sh __attest_file_exists1:attest.sh __openpgp_record_complete1:attest.sh _attest_sign2:attest.sh ___attest_verify2:attest.sh __attest_signer_pinned2:attest.sh __bootstrap1:database.sh ___bootstrap2:database.sh _lift1:database.sh _database_init0:database.sh _environment_init1:database.sh ___environment_refresh0:database.sh __environment_cache0:database.sh _environment_cache1:database.sh _lift3:database.sh __phases_lift1:database.sh ___init1:database.sh __init0:database.sh _setenv2:database.sh _getenv1:database.sh _unsetenv1:database.sh ___dumpenv1:database.sh __dump0:database.sh ___dump1:database.sh _cat1:database.sh ___batch0:database.sh __batch1:database.sh _batch2:database.sh _printenv0:database.sh ___sync0:database.sh _env_sync0:database.sh _version_sync0:database.sh _version0:database.sh __import1:database.sh ___list0:database.sh _link2:database.sh _remove1:database.sh _status0:help.sh _help0:help.sh _help1:help.sh _help2:help.sh _complete0:help.sh _complete1:help.sh _complete2:help.sh _complete3:help.sh _complete4:help.sh _complete5:help.sh _complete6:help.sh _helpenv0:helpenv.sh __helpenv1:helpenv.sh _helpenv2:helpenv.sh __setintp2:helpenv.sh __getintp1:helpenv.sh __unsetintp1:helpenv.sh __helpintp1:helpenv.sh __helpintp2:helpenv.sh _database_enumerate0:inspect.sh ___environment_inspect0:inspect.sh _system_inspect0:inspect.sh _logs_inspect0:inspect.sh _phases_inspect0:inspect.sh _summary_inspect0:inspect.sh _prologue_inspect0:inspect.sh ___inspect0:inspect.sh __key_name_valid1:keys.sh ___key_import3:keys.sh __base_element_exists1:keys.sh _key_import_copy3:keys.sh _key_collect_files2:keys.sh __key_record_exists2:keys.sh ___manifest2:manifest.sh __manifest_named1:manifest.sh __manifest_entries_hashable1:manifest.sh _manifest_write2:manifest.sh ___manifest_attest2:manifest.sh ___manifest_verify3:manifest.sh ___manifest_match2:manifest.sh __manifest_readable1:manifest.sh __manifest_base_exists1:manifest.sh __manifest_tree_matches2:manifest.sh ___openpgp_add2:openpgp.sh ___openpgp_add3:openpgp.sh __openpgp_keyid_valid1:openpgp.sh _openpgp_record2:openpgp.sh _openpgp_record3:openpgp.sh _openpgp_prerequisites0:openpgp.sh ___openpgp_dump0:openpgp.sh __openpgp_import2:openpgp.sh ___openpgp_collect0:openpgp.sh __openpgp_collect1:openpgp.sh __openvpn_start0:openvpn.sh __openvpn_start1:openvpn.sh ___openvpn_create1:openvpn.sh _openvpn_patch2:openvpn.sh _openvpn_configure1:openvpn.sh _openvpn_connect1:openvpn.sh ___openvpn_stop0:openvpn.sh __openvpn_stop1:openvpn.sh _openvpn_disconnect1:openvpn.sh _openvpn_patch1:openvpn.sh ___openvpn_inspect0:openvpn.sh _openvpn_enumerate0:openvpn.sh _openvpn_import1:openvpn.sh _openvpn_list0:openvpn.sh _openvpn_list1:openvpn.sh _openvpn_add1:openvpn.sh __openvpn_add2:openvpn.sh _openvpn_add3:openvpn.sh _openvpn_link2:openvpn.sh _openvpn_remove1:openvpn.sh _openvpn_remove2:openvpn.sh _openvpn_validate0:openvpn.sh ___openvpn_clean0:openvpn.sh _openvpn_info1:openvpn.sh ___openvpn_dump0:openvpn.sh __openvpn_help0:openvpn.sh _phases_copy0:phase.sh ___phases_sync1:phase.sh ___phases_sync0:phase.sh __provenance_serial0:provenance.sh _provenance_serial1:provenance.sh __provenance_add2:provenance.sh __provenance_add3:provenance.sh __provenance_add4:provenance.sh __provenance_add5:provenance.sh __provenance_add6:provenance.sh ___provenance_add7:provenance.sh __provenance_bundle_readable1:provenance.sh __provenance_receipt5:provenance.sh __provenance_receipt_same3:provenance.sh _provenance_file5:provenance.sh ___provenance_import2:provenance.sh __provenance_id_valid1:provenance.sh __provenance_import_place2:provenance.sh _provenance_import_copy2:provenance.sh ___provenance_dump0:provenance.sh _provenance_collect0:provenance.sh __provenance_list0:provenance.sh _provenance_list1:provenance.sh __session_disconnect1:session.sh ___session_stop0:session.sh __session_stop1:session.sh _session_list0:session.sh __session_show0:session.sh _session_show1:session.sh _session_remove1:session.sh __session_start0:session.sh __session_start1:session.sh ___session_switch1:session.sh ___session_switch2:session.sh _session_connect1:session.sh _session_save0:session.sh _session_save1:session.sh _session_clean0:session.sh _session_refresh0:session.sh _session_validate0:session.sh ___session_dump0:session.sh __session_help0:session.sh ___session_inspect0:session.sh _session_enumerate0:session.sh _session_describe0:session.sh _session_create1:session.sh __session_exists1:session.sh __session_conflicts2:session.sh _interface_destroy1:session.sh __interface_check1:session.sh ___session_prepare3:session.sh _session_populate3:session.sh ___stop0:session.sh __stop1:session.sh __start1:session.sh __start0:session.sh _logs_validate0:validate.sh _logs_clean0:validate.sh _phases_validate0:validate.sh _database_validate0:validate.sh _binaries_validate0:validate.sh _network_validate0:validate.sh _permissions_validate0:validate.sh _environment_validate0:validate.sh _install_validate0:validate.sh _version_validate0:validate.sh _sudo_validate0:validate.sh ___validate0:validate.sh __wireguard_start0:wireguard.sh __wireguard_start1:wireguard.sh ___wireguard_create1:wireguard.sh _wireguard_patch2:wireguard.sh _wireguard_configure1:wireguard.sh _wireguard_connect1:wireguard.sh _wireguard_disconnect1:wireguard.sh ___wireguard_inspect0:wireguard.sh _wireguard_enumerate0:wireguard.sh ___wireguard_stop0:wireguard.sh __wireguard_stop1:wireguard.sh _wireguard_import1:wireguard.sh _wireguard_list0:wireguard.sh _wireguard_list1:wireguard.sh _wireguard_add1:wireguard.sh __wireguard_add2:wireguard.sh _wireguard_add3:wireguard.sh _wireguard_link2:wireguard.sh _wireguard_remove1:wireguard.sh _wireguard_remove2:wireguard.sh _wireguard_validate0:wireguard.sh ___wireguard_clean0:wireguard.sh ___wireguard_clean1:wireguard.sh _wireguard_patch1:wireguard.sh _wireguard_info1:wireguard.sh ___wireguard_dump0:wireguard.sh __wireguard_help0:wireguard.sh"
# === END AUTO-GENERATED ===
#
# Philosophy: Test real user workflows end-to-end
# - Each test starts with fresh database (independent, isolated)
# - Only UI commands (no filesystem shortcuts)
# - Property-based validation (prove correctness, not just "it works")
# - Profile selection via command line
#
# Usage: ./vpn-switch-integration-test.sh [profile]
#   profile: minimal (default) or help

# Note: set -e disabled - tests should handle errors explicitly via assertions
# set -e

# Configuration
# Options: --maxprocs N runs the user stories in parallel (xargs -P N, each
# story in its own suite process via VPN_SWITCH_TEST_STORY); default 1 = the
# classic sequential run. (Ported from elebake.)
MAXPROCS=1
while [ $# -gt 0 ]; do
  case "$1" in
    --maxprocs)   MAXPROCS="${2:?--maxprocs needs a value}"; shift 2 ;;
    --maxprocs=*) MAXPROCS="${1#--maxprocs=}"; shift ;;
    *) break ;;
  esac
done

PROFILE="${1:-minimal}"
SCRIPT="./vpn-switch.sh"

# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Test counters
STORIES_RUN=0
STORIES_PASSED=0
STORIES_FAILED=0
ASSERTIONS_RUN=0
ASSERTIONS_PASSED=0
ASSERTIONS_FAILED=0
STORY_ASSERTIONS_FAILED=0  # Per-story failure counter

# Test result tracking
pass() {
  ASSERTIONS_RUN=$((ASSERTIONS_RUN + 1))
  ASSERTIONS_PASSED=$((ASSERTIONS_PASSED + 1))
  printf "  ${GREEN}✓${NC} %s\n" "$1"
}

fail() {
  ASSERTIONS_RUN=$((ASSERTIONS_RUN + 1))
  ASSERTIONS_FAILED=$((ASSERTIONS_FAILED + 1))
  STORY_ASSERTIONS_FAILED=$((STORY_ASSERTIONS_FAILED + 1))  # Track per-story failures
  printf "  ${RED}✗${NC} %s\n" "$1"
  if [ -n "${LAST_TRACE_LABEL}" ]; then
    printf "    ${YELLOW}Trace:${NC} %s\n" "${LAST_TRACE_LABEL}"
  fi
  if [ -n "$2" ]; then
    printf "    ${YELLOW}Expected:${NC} %s\n" "$2"
  fi
  if [ -n "$3" ]; then
    printf "    ${YELLOW}Got:${NC} %s\n" "$3"
  fi
}

# User story execution
story() {
  local story_num="$1"
  local story_desc="$2"
  CURRENT_STORY_NUM="$story_num"
  STORIES_RUN=$((STORIES_RUN + 1))
  STORY_ASSERTIONS_FAILED=0  # Reset per-story counter
  printf "\n${BLUE}▶ User Story ${story_num}:${NC} %s\n" "$story_desc"
}

story_db() {
  printf "  ${BLUE}Database:${NC} %s\n" "$1"
}

story_passed() {
  STORIES_PASSED=$((STORIES_PASSED + 1))
  printf "${GREEN}✓ Story ${STORIES_RUN} completed successfully${NC}\n"
}

story_failed() {
  STORIES_FAILED=$((STORIES_FAILED + 1))
  printf "${RED}✗ Story ${STORIES_RUN} failed${NC}\n"
}

# Cleanup databases conditionally (only on success)
cleanup_on_success() {
  if [ "$STORY_ASSERTIONS_FAILED" -eq 0 ]; then
    # All assertions passed - safe to cleanup
    for path in "$@"; do
      rm -rf "$path"
    done
  else
    # Failures occurred - preserve databases for debugging
    printf "\nDatabase preserved for debugging at: %s\n" "$1"
    if [ $# -gt 1 ]; then
      shift
      for path in "$@"; do
        printf "                                      %s\n" "$path"
      done
    fi
  fi
}

# Assertions
assert_file_exists() {
  local file="$1"
  local message="$2"

  if [ -f "$file" ]; then
    pass "$message"
  else
    fail "$message" "File should exist: $file"
  fi
}

assert_dir_exists() {
  local dir="$1"
  local message="$2"

  if [ -d "$dir" ]; then
    pass "$message"
  else
    fail "$message" "Directory should exist: $dir"
  fi
}

assert_symlink_target() {
  local link="$1"
  local expected_target="$2"
  local message="$3"

  if [ ! -L "$link" ]; then
    fail "$message" "Symlink" "Not a symlink: $link"
    return
  fi

  local actual_target=$(readlink "$link")
  if [ "$expected_target" = "$actual_target" ]; then
    pass "$message"
  else
    fail "$message" "$expected_target" "$actual_target"
  fi
}

assert_empty() {
  local value="$1"
  local message="$2"

  if [ -z "$value" ]; then
    pass "$message"
  else
    fail "$message" "Empty string" "Non-empty: $value"
  fi
}

assert_file_empty() {
  local filepath="$1"
  local message="$2"

  if [ ! -f "$filepath" ]; then
    fail "$message" "File exists and is empty" "File not found: $filepath"
  elif [ ! -s "$filepath" ]; then
    pass "$message"
  else
    fail "$message" "Empty file" "File has content ($(wc -l < "$filepath") lines)"
  fi
}

assert_symlink_exists() {
  local link="$1"
  local message="$2"

  if [ -L "$link" ]; then
    pass "$message"
  else
    fail "$message" "Symlink exists" "Not found or not a symlink"
  fi
}

assert_contains() {
  local haystack="$1"
  local needle="$2"
  local message="$3"

  if echo "$haystack" | grep -q "$needle"; then
    pass "$message"
  else
    fail "$message" "Contains: $needle" "Not found in output"
  fi
}

#-----------------------------------------------------------------------------
# Helper Functions: Phase Testing
#-----------------------------------------------------------------------------

# Extract session ID from start command output
extract_session_id() {
  local output="$1"

  # Debug: Log the input to a file
  echo "========================================" >> /tmp/extract_session_id_debug.log
  echo "Called at: $(date)" >> /tmp/extract_session_id_debug.log
  echo "Input length: ${#output} chars" >> /tmp/extract_session_id_debug.log
  echo "First 500 chars of output:" >> /tmp/extract_session_id_debug.log
  echo "$output" | dd bs=500 count=1 2>/dev/null >> /tmp/extract_session_id_debug.log
  echo "" >> /tmp/extract_session_id_debug.log

  # Parse session from output
  # First try: Look for lines like: Created session 12345: /path/.session/12345
  local session_id=$(echo "$output" | grep 'Created session' | head -1 | sed 's/.*Created session \([0-9]*\).*/\1/')
  echo "Method 1 (Created session): '$session_id'" >> /tmp/extract_session_id_debug.log

  # Fallback: Look for "# Session: 12345" in connect script output
  if [ -z "$session_id" ]; then
    session_id=$(echo "$output" | grep '^# Session:' | head -1 | sed 's/^# Session: \([0-9]*\).*/\1/')
    echo "Method 2 (# Session:): '$session_id'" >> /tmp/extract_session_id_debug.log
  fi

  echo "Final result: '$session_id'" >> /tmp/extract_session_id_debug.log
  echo "$session_id"
}

# Assert file is executable
assert_file_executable() {
  local file="$1"
  local message="$2"

  if [ -x "$file" ]; then
    pass "$message"
  else
    fail "$message" "Executable file" "Not executable or not found"
  fi
}

# Assert script starts with specific content
assert_file_starts_with() {
  local file="$1"
  local expected="$2"
  local message="$3"

  if head -1 "$file" | grep -q "^$expected"; then
    pass "$message"
  else
    fail "$message" "$expected" "$(head -1 "$file")"
  fi
}

# Assert script contains substring
assert_script_contains() {
  local script="$1"
  local needle="$2"
  local message="${3:-Script contains: $needle}"

  if grep -q -e "$needle" "$script"; then
    pass "$message"
  else
    fail "$message" "Present" "Not found"
  fi
}

# Assert script does NOT contain substring
assert_script_not_contains() {
  local script="$1"
  local needle="$2"
  local message="${3:-Script does not contain: $needle}"

  if ! grep -q -e "$needle" "$script"; then
    pass "$message"
  else
    fail "$message" "Absent" "Found"
  fi
}

# Assert phases appear in specific order
assert_phase_order() {
  local script="$1"
  local expected_order="$2"  # Space-separated: "firewall vpn dns"
  local message="${3:-Phases in correct order}"

  # Extract phase markers from script (e.g., "==> Phase: firewall (prepare)")
  local actual_order=$(grep -o 'Phase: [a-z_]* (' "$script" | \
                      sed 's/Phase: \([a-z_]*\) (/\1/' | \
                      uniq | tr '\n' ' ' | sed 's/ $//')

  if [ "$expected_order" = "$actual_order" ]; then
    pass "$message"
  else
    fail "$message" "$expected_order" "$actual_order"
  fi
}

# Create custom phase for testing
create_custom_phase() {
  local basedir="$1"
  local protocol="$2"
  local phase_name="$3"

  mkdir -p "$basedir/.include/phase/$protocol/connect"

  cat > "$basedir/.include/phase/$protocol/connect/$phase_name.sh" <<'EOF'
#!/bin/sh
case "$1" in
  verify)
    cat <<'VERIFY_EOF'
verify_PHASE_NAME() {
  local step="$1"
  local exit_code="$2"
  case "$step" in
    execute)
      [ "$exit_code" -eq 0 ] && echo "✓ PHASE_NAME phase completed" >&2
      ;;
  esac
}
VERIFY_EOF
    ;;
  prepare)
    echo "echo '==> Phase: PHASE_NAME (prepare)' >&2"
    echo "verify_PHASE_NAME prepare 0"
    ;;
  execute)
    echo "echo '==> Phase: PHASE_NAME (execute)' >&2"
    echo "echo 'Custom phase commands here' >&2"
    echo "verify_PHASE_NAME execute 0"
    ;;
esac
EOF

  # Replace PHASE_NAME placeholder (FreeBSD sed vs GNU sed compatibility)
  if sed -i '' "s/PHASE_NAME/$phase_name/g" \
    "$basedir/.include/phase/$protocol/connect/$phase_name.sh" 2>/dev/null; then
    :  # FreeBSD sed succeeded
  else
    # GNU sed (Linux)
    sed -i "s/PHASE_NAME/$phase_name/g" \
      "$basedir/.include/phase/$protocol/connect/$phase_name.sh"
  fi

  chmod 0755 "$basedir/.include/phase/$protocol/connect/$phase_name.sh"
}

# Set interpreter for a specific function (using setenv API)
set_function_interpreter() {
  local basedir="$1"
  local func_name="$2"
  local interpreter="$3"

  # Use setenv to configure interpreter (API approach)
  run_vpn_switch "001" "$basedir" setenv "VPN_SWITCH_INTERPRETER_${func_name}" "$interpreter" > /dev/null 2>&1
}

# Override interpreter for command inspection (convenience wrapper)
override_for_command_inspection() {
  local basedir="$1"
  local func="$2"

  if [ -z "$func" ]; then
    echo "${RED}ERROR:${NC} override_for_command_inspection requires function name"
    exit 1
  fi

  # Set function-specific interpreter to cat (highest priority)
  set_function_interpreter "$basedir" "$func" "cat"
}

#-----------------------------------------------------------------------------
# Helper Functions: Test Fixtures
#-----------------------------------------------------------------------------

# Create temporary WireGuard test config file
create_test_config() {
  local filename="$1"
  local endpoint="${2:-test.example.com:51820}"
  local filepath="/tmp/$filename"

  cat > "$filepath" <<EOF
[Interface]
PrivateKey = $(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64)
Address = 10.2.0.2/32
DNS = 10.2.0.1

[Peer]
PublicKey = $(dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64)
AllowedIPs = 0.0.0.0/0
Endpoint = $endpoint
EOF
  echo "$filepath"
}

# Create temporary OpenVPN test config file
create_test_ovpn_config() {
  local filename="$1"
  local remote="${2:-test.example.com 1194}"
  local filepath="/tmp/$filename"

  cat > "$filepath" <<EOF
client
dev tun
proto udp
remote $remote
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-CBC
auth SHA256
verb 3
EOF
  echo "$filepath"
}

# Bootstrap fresh database with "experienced user" settings
bootstrap_database() {
  local basedir="$1"

  # Clean slate - remove database
  rm -rf "$basedir"

  # SAFETY: Do NOT destroy interfaces blindly!
  # Tests use test-specific interface names (wg_test0, tun_test0)
  # Production interfaces (wg0, tun0) must NEVER be touched by tests
  # The destroy of a session's interface is judged by its act when it runs: a
  # test interface is never there, the act notes that and touches nothing

  # Set minimal PATH for bootstrap operations (needed for run_env before database exists)
  export VPN_SWITCH_PATH="/bin:/usr/bin:/usr/local/bin"

  # Bootstrap with profile
  if ! $SCRIPT bootstrap "$basedir" "$PROFILE" > /dev/null 2>&1; then
    echo "${RED}ERROR: Bootstrap failed for $basedir${NC}" >&2
    exit 1
  fi

  # Configure test-specific settings via API
  # Note: dump interpreters are already provided by minimal profile (no need to set)

  # Configure as "experienced user" (auto-execution)
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_TERMINAL_INTERPRETER sh > /dev/null 2>&1
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_INTERPRETER_interface_destroy cat > /dev/null 2>&1

  # Set minimal PATH for vpn-switch operations
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_PATH "/bin:/usr/bin:/usr/local/bin" > /dev/null 2>&1

  # SAFETY: Use test-specific interface names to avoid conflicts with production
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_INTERFACE_wireguard wg_test0 > /dev/null 2>&1
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_INTERFACE_openvpn tun_test0 > /dev/null 2>&1
}

# run_vpn_switch "002" - Execute vpn-switch.sh through the user interface
#
# Arguments:
#   $1: label for error reporting
#   $2: database location (VPN_SWITCH_BASE)
#   $3-N: command arguments to pass to vpn-switch.sh
#
# Environment:
#   Uses only variables a normal user would set:
#   - VPN_SWITCH_BASE: database location (passed as $2)
#
# Note: Trace files are automatically generated by vpn-switch.sh based on
#       retention settings. No need to set VPN_SWITCH_TRACE_FILE manually.
#
# Returns: captured stdout and stderr
#
run_vpn_switch() {
  local label="$1"
  local basedir="$2"
  shift 2

  # Store label globally for assertion error reporting
  LAST_TRACE_LABEL="$label"

  # Execute vpn-switch exactly as a user would
  # The root above the database is the story database itself (hidden, so
  # no protocol): import unpacks under <root>/incoming/<bundle>, and the
  # stories run in parallel under the one /tmp -- a shared root would let
  # them clear each other's unpacked bundle mid-import
  VPN_SWITCH_BASE="$basedir" VPN_SWITCH_ROOT="$basedir/.root" $SCRIPT "$@"
}

# run_vpn_dump "003" - Execute vpn-switch dump with batch2 in output mode
#
# Arguments:
#   $1: label for error reporting
#   $2: database location (VPN_SWITCH_BASE)
#
# Note: Temporarily sets INTERPRETER_batch2=cat for dump operation,
#       then restores default (execute mode) for subsequent restores.
#       Trace files are automatically generated by vpn-switch.sh based on
#       retention settings.
#
run_vpn_dump() {
  local label="$1"
  local basedir="$2"

  # Store label globally for assertion error reporting
  LAST_TRACE_LABEL="$label"

  # Temporarily set dump interpreters via API
  # batch2=cat: prevents batch from executing commands (just outputs them)
  # dumpenv=cat: prevents setenv commands from being executed (just outputs them)
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_INTERPRETER_batch2 cat > /dev/null 2>&1
  VPN_SWITCH_BASE="$basedir" $SCRIPT setenv VPN_SWITCH_INTERPRETER_dumpenv cat > /dev/null 2>&1

  # Execute dump command
  VPN_SWITCH_BASE="$basedir" $SCRIPT dump

  # Remove temporary settings via API to allow restore to use default (execute mode)
  VPN_SWITCH_BASE="$basedir" $SCRIPT unsetenv VPN_SWITCH_INTERPRETER_batch2 > /dev/null 2>&1
  VPN_SWITCH_BASE="$basedir" $SCRIPT unsetenv VPN_SWITCH_INTERPRETER_dumpenv > /dev/null 2>&1
}

# story_attest_key <db> <record> - the throwaway OpenPGP key a story signs
# with: generated once per run in a home short enough for gpg-agent's
# socket (/tmp), registered in <db> as an openpgp record and pinned as the
# archive attest key. A story that moves a database between two databases
# calls it for both (the receiver pins the sender's key). Sets STORY_FPR.
STORY_GNUPGHOME="/tmp/vsit-gh-$$"
STORY_FPR=""
story_attest_key() {
  local db="$1" rec="$2"
  if [ -z "$STORY_FPR" ]; then
    mkdir -p "$STORY_GNUPGHOME" && chmod 700 "$STORY_GNUPGHOME"
    GNUPGHOME="$STORY_GNUPGHOME" gpg --batch --passphrase '' --pinentry-mode loopback \
      --quick-generate-key "Story <story@example.invalid>" rsa2048 sign never > /dev/null 2>&1
    STORY_FPR=$(GNUPGHOME="$STORY_GNUPGHOME" gpg --list-secret-keys --with-colons 2>/dev/null | awk -F: '/^fpr:/{print $10; exit}')
  fi
  VPN_SWITCH_BASE="$db" $SCRIPT openpgp add "$rec" "$STORY_FPR" "$STORY_GNUPGHOME" > /dev/null 2>&1
  VPN_SWITCH_BASE="$db" $SCRIPT setenv VPN_SWITCH_ARCHIVE_ATTEST_KEY "$rec" > /dev/null 2>&1
}

#-----------------------------------------------------------------------------
# User Story 1: Import → Organize → Dump → Restore → Idempotency
#-----------------------------------------------------------------------------
# As a user, I want to import configs, organize them into categories,
# dump my database, restore it elsewhere, and verify nothing was lost.

user_story_1_import_organize_dump_restore() {
  story 1 "Import → Organize → Dump → Restore → Idempotency (Multi-Protocol)"

  # Setup
  local old_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-old.XXXXXX")
  local new_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-new.XXXXXX")
  story_db "$old_db"
  local dump1="$old_db/backup.sh"
  local dump2="$new_db/backup2.sh"
  local dump3="$new_db/backup3.sh"

  # Step 1: Bootstrap OLD database
  bootstrap_database "$old_db"

  # Step 2a: Import WireGuard configs via UI
  local wg_config1=$(create_test_config "wg-us-101.conf" "us.example.com:51820")
  local wg_config2=$(create_test_config "wg-uk-202.conf" "uk.example.com:51820")
  local wg_config3=$(create_test_config "wg-jp-303.conf" "jp.example.com:51820")

  run_vpn_switch "004" "$old_db" wireguard import "$wg_config1" > /dev/null 2>&1
  run_vpn_switch "005" "$old_db" wireguard import "$wg_config2" > /dev/null 2>&1
  run_vpn_switch "006" "$old_db" wireguard import "$wg_config3" > /dev/null 2>&1

  assert_file_exists "$old_db/wireguard/wg-us-101.conf" "WG: Imported config 1"
  assert_file_exists "$old_db/wireguard/wg-uk-202.conf" "WG: Imported config 2"
  assert_file_exists "$old_db/wireguard/wg-jp-303.conf" "WG: Imported config 3"

  # Step 2b: Import OpenVPN configs via UI
  local ovpn_config1=$(create_test_ovpn_config "us-server.ovpn" "us.example.com 1194")
  local ovpn_config2=$(create_test_ovpn_config "eu-server.ovpn" "eu.example.com 1194")

  run_vpn_switch "007" "$old_db" openvpn import "$ovpn_config1" > /dev/null 2>&1
  run_vpn_switch "008" "$old_db" openvpn import "$ovpn_config2" > /dev/null 2>&1

  assert_file_exists "$old_db/openvpn/us-server.ovpn" "OVPN: Imported config 1"
  assert_file_exists "$old_db/openvpn/eu-server.ovpn" "OVPN: Imported config 2"

  # Step 3a: Create WireGuard categories via UI
  run_vpn_switch "009" "$old_db" wireguard add privacy > /dev/null 2>&1
  run_vpn_switch "010" "$old_db" wireguard add streaming > /dev/null 2>&1

  assert_dir_exists "$old_db/wireguard/privacy" "WG: Created category: privacy"
  assert_dir_exists "$old_db/wireguard/streaming" "WG: Created category: streaming"

  # Step 3b: Create OpenVPN groups via UI
  run_vpn_switch "011" "$old_db" openvpn add work > /dev/null 2>&1

  assert_dir_exists "$old_db/openvpn/work" "OVPN: Created group: work"

  # Step 4a: Create WireGuard links via UI
  run_vpn_switch "012" "$old_db" wireguard link quick wg-us-101.conf > /dev/null 2>&1
  run_vpn_switch "013" "$old_db" wireguard add privacy wg-us-101.conf fast > /dev/null 2>&1
  run_vpn_switch "014" "$old_db" wireguard add privacy wg-uk-202.conf secure > /dev/null 2>&1
  run_vpn_switch "015" "$old_db" wireguard add streaming wg-jp-303.conf hd > /dev/null 2>&1

  assert_symlink_target "$old_db/wireguard/quick" "./wg-us-101.conf" "WG: Protocol-level link: quick"
  assert_symlink_target "$old_db/wireguard/privacy/fast" "../wg-us-101.conf" "WG: Category link: privacy/fast"
  assert_symlink_target "$old_db/wireguard/privacy/secure" "../wg-uk-202.conf" "WG: Category link: privacy/secure"
  assert_symlink_target "$old_db/wireguard/streaming/hd" "../wg-jp-303.conf" "WG: Category link: streaming/hd"

  # Step 4b: Create OpenVPN links via UI
  run_vpn_switch "016" "$old_db" openvpn link office us-server.ovpn > /dev/null 2>&1
  run_vpn_switch "017" "$old_db" openvpn add work us-server.ovpn primary > /dev/null 2>&1

  assert_symlink_target "$old_db/openvpn/office" "./us-server.ovpn" "OVPN: Protocol-level link: office"
  assert_symlink_target "$old_db/openvpn/work/primary" "../us-server.ovpn" "OVPN: Group link: work/primary"

  # Step 5: Dump OLD database (both protocols) -- the attest key registered
  # first, so the dump of OLD and the dump of NEW carry the same key record
  story_attest_key "$old_db" story
  local dump1_stderr=$(mktemp)
  run_vpn_dump "018" "$old_db" > "$dump1" 2>"$dump1_stderr"
  assert_file_empty "$dump1_stderr" "Dump stderr is clean"
#  rm -f "$dump1_stderr"
  echo "# ================================"
  echo "# db: $old_db"
  cat "$dump1_stderr"
  echo "# ================================"
  assert_file_exists "$dump1" "Dump file created"
  assert_contains "$(cat "$dump1")" "wireguard import.*wg-us-101.conf" "Dump contains WG import"
  assert_contains "$(cat "$dump1")" "openvpn import.*us-server.ovpn" "Dump contains OVPN import"
  assert_contains "$(cat "$dump1")" "wireguard add privacy" "Dump contains WG category"
  assert_contains "$(cat "$dump1")" "openvpn add work" "Dump contains OVPN group"
  assert_contains "$(cat "$dump1")" "wireguard link quick" "Dump contains WG link"
  assert_contains "$(cat "$dump1")" "openvpn link office" "Dump contains OVPN link"

  # Step 6: Bootstrap NEW database
  story_db "$new_db"
  bootstrap_database "$new_db"

  # Step 7: The database travels as the signed pair -- export from OLD
  # (dump + bundle, sealed and signed with the pinned key), import into NEW
  # (the same key pinned there): a plain dump names its files, the bundle
  # carries them; a dump without the pinned signature does not replay.
  story_attest_key "$new_db" story
  local pair_dump="$old_db/pair.sh" pair_bundle="$old_db/pair.tar.gz"
  run_vpn_switch "019" "$old_db" export full "$pair_dump" "$pair_bundle" > /dev/null 2>&1
  assert_file_exists "$pair_dump.asc" "Export: the dump is signed"
  assert_file_exists "$pair_bundle" "Export: the bundle is written"
  assert_contains "$(cat "$pair_dump")" "^# Bundle: sha256=" "Export: the dump is sealed to the bundle"
  run_vpn_switch "019b" "$new_db" import "$pair_dump" "$pair_bundle" > /dev/null 2>&1
  assert_dir_exists "$new_db/provenance" "Import: the receipt directory exists"
  assert_contains "$(cat "$new_db"/provenance/*/signer 2>/dev/null)" "$STORY_FPR" "Import: the receipt names the pinned signer"

  # Step 8a: Verify NEW database matches OLD structure (WireGuard)
  assert_file_exists "$new_db/wireguard/wg-us-101.conf" "NEW WG: Config 1 imported"
  assert_file_exists "$new_db/wireguard/wg-uk-202.conf" "NEW WG: Config 2 imported"
  assert_file_exists "$new_db/wireguard/wg-jp-303.conf" "NEW WG: Config 3 imported"
  assert_dir_exists "$new_db/wireguard/privacy" "NEW WG: Category privacy created"
  assert_dir_exists "$new_db/wireguard/streaming" "NEW WG: Category streaming created"
  assert_symlink_target "$new_db/wireguard/quick" "./wg-us-101.conf" "NEW WG: Protocol-level link"
  assert_symlink_target "$new_db/wireguard/privacy/fast" "../wg-us-101.conf" "NEW WG: Category link privacy/fast"
  assert_symlink_target "$new_db/wireguard/privacy/secure" "../wg-uk-202.conf" "NEW WG: Category link privacy/secure"
  assert_symlink_target "$new_db/wireguard/streaming/hd" "../wg-jp-303.conf" "NEW WG: Category link streaming/hd"

  # Step 8b: Verify NEW database matches OLD structure (OpenVPN)
  assert_file_exists "$new_db/openvpn/us-server.ovpn" "NEW OVPN: Config 1 imported"
  assert_file_exists "$new_db/openvpn/eu-server.ovpn" "NEW OVPN: Config 2 imported"
  assert_dir_exists "$new_db/openvpn/work" "NEW OVPN: Group work created"
  assert_symlink_target "$new_db/openvpn/office" "./us-server.ovpn" "NEW OVPN: Protocol-level link"
  assert_symlink_target "$new_db/openvpn/work/primary" "../us-server.ovpn" "NEW OVPN: Group link work/primary"

  # Step 9: IDEMPOTENCY TEST - Dump NEW, normalize paths, compare
  local dump2_stderr=$(mktemp)
  run_vpn_dump "020" "$new_db" > "$dump2" 2>"$dump2_stderr"
  assert_file_empty "$dump2_stderr" "Idempotency dump stderr is clean"
  rm -f "$dump2_stderr"

  # Normalize paths and filter non-deterministic lines
  # - Replace new_db paths with old_db paths
  # - Remove timestamp line (# Generated: ...) as it will differ
  # - Remove batch_id line (# Batch execution ...) as counter increments
  # - Remove serial and base (the lineage continued in NEW, the base differs)
  # - Remove the provenance block (NEW holds the receipt of this import, OLD none)
  grep -Ev "^# Generated:|^# Batch execution|^# Serial:|^# Base:|provenance import|no receipts to dump" "$dump1" > "$dump1.normalized"
  sed -e "s,${new_db},${old_db},g" "$dump2" | grep -Ev "^# Generated:|^# Batch execution|^# Serial:|^# Base:|provenance import|no receipts to dump" > "$dump3"

  # Compare normalized dumps - should be identical
  if diff -u "$dump1.normalized" "$dump3" > /dev/null 2>&1; then
    pass "Idempotency: dump(import(export(OLD))) == dump(OLD) [multi-protocol]"
  else
    fail "Idempotency: Dumps differ after restore" "Identical dumps" "See diff output"
    echo "${YELLOW}Diff output:${NC}"
    diff -u "$dump1.normalized" "$dump3" || true
  fi

  # Cleanup
  cleanup_on_success "$old_db" "$new_db"
  rm -f "$wg_config1" "$wg_config2" "$wg_config3" "$ovpn_config1" "$ovpn_config2"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 2: Session Management Workflow
#-----------------------------------------------------------------------------
# As a user, I want to create sessions, save them with names,
# dump the database, restore it, and verify named sessions work.

user_story_2_session_management() {
  story 2 "Session Management: Save → Dump → Restore → Verify (Multi-Protocol)"

  # Setup
  local old_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-old.XXXXXX")
  local new_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-new.XXXXXX")
  story_db "$old_db"
  local dump_file="$old_db/backup.sh"

  # Bootstrap OLD database
  bootstrap_database "$old_db"
  story_attest_key "$old_db" story

  # Import configs for both protocols
  local wg_config=$(create_test_config "wg-test.conf")
  local ovpn_config=$(create_test_ovpn_config "vpn-test.ovpn")
  run_vpn_switch "021" "$old_db" wireguard import "$wg_config" > /dev/null 2>&1
  run_vpn_switch "022" "$old_db" openvpn import "$ovpn_config" > /dev/null 2>&1

  # Create WireGuard session via UI (without actually starting VPN)
  # Prevent actual VPN connection (but allow configure to create session/latest symlink)
  set_function_interpreter "$old_db" "wireguard_connect1" "cat"

  # Start creates session metadata (configure creates session/latest, connect doesn't execute)
  run_vpn_switch "023" "$old_db" wireguard start wg-test.conf > /dev/null 2>&1

  # Save WireGuard session with name via UI
  run_vpn_switch "024" "$old_db" session save work > /dev/null 2>&1

  # Verify WireGuard named session was created
  local wg_session_link="$old_db/session/work"
  if [ -L "$wg_session_link" ]; then
    local target=$(readlink "$wg_session_link")
    pass "WG: Named session created: work -> $target"
  else
    fail "WG: Named session created: work" "Symlink exists" "Not found"
  fi

  # Create OpenVPN session via UI (without actually starting VPN)
  # Prevent actual VPN connection (but allow configure to create session/latest symlink)
  set_function_interpreter "$old_db" "openvpn_connect1" "cat"
  set_function_interpreter "$old_db" "openvpn_disconnect1" "cat"

  run_vpn_switch "025" "$old_db" openvpn start vpn-test.ovpn > /dev/null 2>&1
  run_vpn_switch "026" "$old_db" session save home > /dev/null 2>&1

  # Verify OpenVPN named session was created
  local ovpn_session_link="$old_db/session/home"
  if [ -L "$ovpn_session_link" ]; then
    local target=$(readlink "$ovpn_session_link")
    pass "OVPN: Named session created: home -> $target"
  else
    fail "OVPN: Named session created: home" "Symlink exists" "Not found"
  fi

  # Dump database
  local dump_stderr=$(mktemp)
  run_vpn_dump "027" "$old_db" > "$dump_file" 2>"$dump_stderr"
  assert_file_empty "$dump_stderr" "Dump stderr is clean"
  rm -f "$dump_stderr"

  # A saved session travels as the user creates one: '<protocol> start <config>'
  # then 'session save <name>', the connect terminals bound to cat around them
  assert_contains "$(cat "$dump_file")" "wireguard start wg-test.conf" "Dump contains WG session start"
  assert_contains "$(cat "$dump_file")" "openvpn start vpn-test.ovpn" "Dump contains OVPN session start"
  assert_contains "$(cat "$dump_file")" "setenv VPN_SWITCH_INTERPRETER_wireguard_connect1 cat" "Dump guards the session block (connect bound to cat)"
  assert_contains "$(cat "$dump_file")" "setenv VPN_SWITCH_INTERPRETER_interface_destroy cat" "Dump guards the session block (the interface destroy bound to cat)"
  assert_contains "$(cat "$dump_file")" "environment refresh" "Dump refreshes the environment after the pins of the session block"
  assert_contains "$(cat "$dump_file")" "session save work" "Dump contains WG save command"
  assert_contains "$(cat "$dump_file")" "session save home" "Dump contains OVPN save command"

  # Bootstrap NEW database
  story_db "$new_db"
  bootstrap_database "$new_db"

  # The database travels as the signed pair (as in story 1): export from
  # OLD, import into NEW with the same key pinned on both sides -- a plain
  # dump names its files against the archive base, the bundle carries them
  story_attest_key "$new_db" story
  local pair_dump="$old_db/pair.sh" pair_bundle="$old_db/pair.tar.gz"
  run_vpn_switch "027b" "$old_db" export full "$pair_dump" "$pair_bundle" > /dev/null 2>&1
  local import_out
  import_out=$(run_vpn_switch "028" "$new_db" import "$pair_dump" "$pair_bundle" 2>&1)
  if ! printf '%s\n' "$import_out" | grep -q "^\(# \)\{0,1\}==> Phase: .* (execute)" \
     && printf '%s\n' "$import_out" | grep -q '^echo "==> Phase: .* (execute)" >&2'; then
    pass "NEW: the replay built the sessions and connected nothing -- the connect scripts were displayed, no phase ran (the pins of the session block held)"
  else
    fail "NEW: a phase ran during the replay (the pins of the session block did not hold)" "connect displayed" "$(printf '%s\n' "$import_out" | grep "==> Phase" | head -3 | tr '\n' ' ')"
  fi

  # Verify configs were restored
  assert_file_exists "$new_db/wireguard/wg-test.conf" "NEW: WG config imported"
  assert_file_exists "$new_db/openvpn/vpn-test.ovpn" "NEW: OVPN config imported"

  # The sessions exist in NEW as saved sessions (built, not connected):
  # session list names them with their protocol and config
  local new_sessions
  new_sessions=$(run_vpn_switch "028b" "$new_db" session list 2>&1)
  if printf '%s\n' "$new_sessions" | grep -q "work.*wireguard.*wg-test.conf"; then
    pass "NEW: WG session work exists after import (start + save replayed)"
  else
    fail "NEW: WG session work missing after import" "work  wireguard  wg-test.conf" "$new_sessions"
  fi

  if printf '%s\n' "$new_sessions" | grep -q "home.*openvpn.*vpn-test.ovpn"; then
    pass "NEW: OVPN session home exists after import (start + save replayed)"
  else
    fail "NEW: OVPN session home missing after import" "home  openvpn  vpn-test.ovpn" "$new_sessions"
  fi

  if [ "$(ls -1 "$new_db/.session" 2>/dev/null | wc -l | tr -d ' ')" = "2" ]; then
    pass "NEW: exactly two sessions were built, none connected"
  else
    fail "NEW: session count after import" "2" "$(ls -1 "$new_db/.session" 2>/dev/null | wc -l | tr -d ' ')"
  fi

  # Cleanup
  cleanup_on_success "$old_db" "$new_db"
  rm -f "$wg_config" "$ovpn_config"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 3: Config Expiry Workflow
#-----------------------------------------------------------------------------
# As a user, I want to check config expiry status,
# import configs, organize them, and verify info shows expiry warnings.

user_story_3_config_expiry() {
  story 3 "Config Expiry: Import → Organize → Check Status"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-test.XXXXXX")
  story_db "$test_db"

  # Bootstrap
  bootstrap_database "$test_db"

  # Import configs
  local config1=$(create_test_config "wg-fresh.conf")
  local config2=$(create_test_config "wg-old.conf")

  run_vpn_switch "029" "$test_db" wireguard import "$config1" > /dev/null 2>&1
  run_vpn_switch "030" "$test_db" wireguard import "$config2" > /dev/null 2>&1

  # Make one config "old" by setting file timestamp to 370 days ago
  # (configs expire after 365 days)
  touch -t $(date -v-370d +%Y%m%d%H%M.%S 2>/dev/null || date -d '370 days ago' +%Y%m%d%H%M.%S 2>/dev/null) "$test_db/wireguard/wg-old.conf" 2>/dev/null || true

  # Create category and add both configs
  run_vpn_switch "031" "$test_db" wireguard add testing > /dev/null 2>&1
  run_vpn_switch "032" "$test_db" wireguard add testing wg-fresh.conf fresh > /dev/null 2>&1
  run_vpn_switch "033" "$test_db" wireguard add testing wg-old.conf old > /dev/null 2>&1

  # Check category info (should show expiry status)
  local info_output=$(run_vpn_switch "034" "$test_db" wireguard info testing 2>&1)

  # Verify output contains expiry information
  if echo "$info_output" | grep -q "wg-fresh.conf"; then
    pass "Info output contains fresh config"
  else
    fail "Info output missing fresh config" "Contains: wg-fresh.conf" "$info_output"
  fi

  if echo "$info_output" | grep -q "wg-old.conf"; then
    pass "Info output contains old config"
  else
    fail "Info output missing old config" "Contains: wg-old.conf" "$info_output"
  fi

  # Verify validate command detects configs
  local validate_output=$(run_vpn_switch "035" "$test_db" wireguard validate 2>&1)
  if echo "$validate_output" | grep -q -E "(wireguard/wg-fresh.conf|wireguard/wg-old.conf)"; then
    pass "Validate command checks configs"
  else
    pass "Validate command found no issues (acceptable)"
  fi

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$config1" "$config2"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 4: Multi-Category Organization
#-----------------------------------------------------------------------------
# As a user, I want to organize configs into multiple categories,
# create aliases at different levels, and verify structure.

user_story_4_multi_category_organization() {
  story 4 "Multi-Category Organization: Complex Structure (Multi-Protocol)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-test.XXXXXX")
  story_db "$test_db"

  # Bootstrap
  bootstrap_database "$test_db"

  # Import multiple WireGuard configs
  local us1=$(create_test_config "wg-us-ny.conf" "us-ny.vpn.com:51820")
  local us2=$(create_test_config "wg-us-sf.conf" "us-sf.vpn.com:51820")
  local eu1=$(create_test_config "wg-eu-uk.conf" "eu-uk.vpn.com:51820")
  local eu2=$(create_test_config "wg-eu-de.conf" "eu-de.vpn.com:51820")
  local asia1=$(create_test_config "wg-asia-jp.conf" "asia-jp.vpn.com:51820")

  run_vpn_switch "036" "$test_db" wireguard import "$us1" > /dev/null 2>&1
  run_vpn_switch "037" "$test_db" wireguard import "$us2" > /dev/null 2>&1
  run_vpn_switch "038" "$test_db" wireguard import "$eu1" > /dev/null 2>&1
  run_vpn_switch "039" "$test_db" wireguard import "$eu2" > /dev/null 2>&1
  run_vpn_switch "040" "$test_db" wireguard import "$asia1" > /dev/null 2>&1

  # Import multiple OpenVPN configs
  local ovpn_us=$(create_test_ovpn_config "us-office.ovpn" "us-office.vpn.com 1194")
  local ovpn_eu=$(create_test_ovpn_config "eu-office.ovpn" "eu-office.vpn.com 1194")
  local ovpn_home=$(create_test_ovpn_config "home-server.ovpn" "home.vpn.com 1194")

  run_vpn_switch "041" "$test_db" openvpn import "$ovpn_us" > /dev/null 2>&1
  run_vpn_switch "042" "$test_db" openvpn import "$ovpn_eu" > /dev/null 2>&1
  run_vpn_switch "043" "$test_db" openvpn import "$ovpn_home" > /dev/null 2>&1

  # Create WireGuard geographic categories
  run_vpn_switch "044" "$test_db" wireguard add usa > /dev/null 2>&1
  run_vpn_switch "045" "$test_db" wireguard add europe > /dev/null 2>&1
  run_vpn_switch "046" "$test_db" wireguard add asia > /dev/null 2>&1

  # Create WireGuard use-case categories
  run_vpn_switch "047" "$test_db" wireguard add work > /dev/null 2>&1
  run_vpn_switch "048" "$test_db" wireguard add personal > /dev/null 2>&1

  # Create OpenVPN groups
  run_vpn_switch "049" "$test_db" openvpn add corporate > /dev/null 2>&1
  run_vpn_switch "050" "$test_db" openvpn add remote > /dev/null 2>&1

  # Add WireGuard configs to geographic categories
  run_vpn_switch "051" "$test_db" wireguard add usa wg-us-ny.conf ny > /dev/null 2>&1
  run_vpn_switch "052" "$test_db" wireguard add usa wg-us-sf.conf sf > /dev/null 2>&1
  run_vpn_switch "053" "$test_db" wireguard add europe wg-eu-uk.conf uk > /dev/null 2>&1
  run_vpn_switch "054" "$test_db" wireguard add europe wg-eu-de.conf de > /dev/null 2>&1
  run_vpn_switch "055" "$test_db" wireguard add asia wg-asia-jp.conf jp > /dev/null 2>&1

  # Add WireGuard configs to use-case categories (same configs, different aliases)
  run_vpn_switch "056" "$test_db" wireguard add work wg-us-ny.conf office > /dev/null 2>&1
  run_vpn_switch "057" "$test_db" wireguard add work wg-eu-uk.conf london-office > /dev/null 2>&1
  run_vpn_switch "058" "$test_db" wireguard add personal wg-us-sf.conf home > /dev/null 2>&1

  # Add OpenVPN configs to groups
  run_vpn_switch "059" "$test_db" openvpn add corporate us-office.ovpn us > /dev/null 2>&1
  run_vpn_switch "060" "$test_db" openvpn add corporate eu-office.ovpn eu > /dev/null 2>&1
  run_vpn_switch "061" "$test_db" openvpn add remote home-server.ovpn home > /dev/null 2>&1

  # Create protocol-level aliases for quick access (both protocols)
  run_vpn_switch "062" "$test_db" wireguard link default wg-us-ny.conf > /dev/null 2>&1
  run_vpn_switch "063" "$test_db" wireguard link fast wg-us-sf.conf > /dev/null 2>&1
  run_vpn_switch "064" "$test_db" openvpn link quick us-office.ovpn > /dev/null 2>&1

  # Verify WireGuard structure
  assert_dir_exists "$test_db/wireguard/usa" "WG: Category: usa"
  assert_dir_exists "$test_db/wireguard/europe" "WG: Category: europe"
  assert_dir_exists "$test_db/wireguard/asia" "WG: Category: asia"
  assert_dir_exists "$test_db/wireguard/work" "WG: Category: work"
  assert_dir_exists "$test_db/wireguard/personal" "WG: Category: personal"

  # Verify OpenVPN structure
  assert_dir_exists "$test_db/openvpn/corporate" "OVPN: Group: corporate"
  assert_dir_exists "$test_db/openvpn/remote" "OVPN: Group: remote"

  # Verify WireGuard geographic links
  assert_symlink_target "$test_db/wireguard/usa/ny" "../wg-us-ny.conf" "WG: usa/ny → wg-us-ny.conf"
  assert_symlink_target "$test_db/wireguard/europe/uk" "../wg-eu-uk.conf" "WG: europe/uk → wg-eu-uk.conf"
  assert_symlink_target "$test_db/wireguard/asia/jp" "../wg-asia-jp.conf" "WG: asia/jp → wg-asia-jp.conf"

  # Verify WireGuard use-case links
  assert_symlink_target "$test_db/wireguard/work/office" "../wg-us-ny.conf" "WG: work/office → wg-us-ny.conf"
  assert_symlink_target "$test_db/wireguard/personal/home" "../wg-us-sf.conf" "WG: personal/home → wg-us-sf.conf"

  # Verify OpenVPN group links
  assert_symlink_target "$test_db/openvpn/corporate/us" "../us-office.ovpn" "OVPN: corporate/us → us-office.ovpn"
  assert_symlink_target "$test_db/openvpn/corporate/eu" "../eu-office.ovpn" "OVPN: corporate/eu → eu-office.ovpn"
  assert_symlink_target "$test_db/openvpn/remote/home" "../home-server.ovpn" "OVPN: remote/home → home-server.ovpn"

  # Verify protocol-level aliases
  assert_symlink_target "$test_db/wireguard/default" "./wg-us-ny.conf" "WG: default → wg-us-ny.conf"
  assert_symlink_target "$test_db/wireguard/fast" "./wg-us-sf.conf" "WG: fast → wg-us-sf.conf"
  assert_symlink_target "$test_db/openvpn/quick" "./us-office.ovpn" "OVPN: quick → us-office.ovpn"

  # List all categories/groups (both protocols)
  local wg_list=$(run_vpn_switch "065" "$test_db" wireguard list 2>&1)
  assert_contains "$wg_list" "wireguard/usa/" "WG: List shows usa category"
  assert_contains "$wg_list" "wireguard/europe/" "WG: List shows europe category"
  assert_contains "$wg_list" "wireguard/work/" "WG: List shows work category"

  local ovpn_list=$(run_vpn_switch "066" "$test_db" openvpn list 2>&1)
  assert_contains "$ovpn_list" "openvpn/corporate/" "OVPN: List shows corporate group"
  assert_contains "$ovpn_list" "openvpn/remote/" "OVPN: List shows remote group"

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$us1" "$us2" "$eu1" "$eu2" "$asia1" "$ovpn_us" "$ovpn_eu" "$ovpn_home"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 5: Completeness and Deletion Proof
#-----------------------------------------------------------------------------
# As a developer, I want to prove that dump captures ALL database state
# and correctly reflects deletions at each hierarchy level.
#
# Phase 1: Completeness - every operation appears in dump
# Phase 2: Delete protocol links - verify absence + remaining items
# Phase 3: Delete category links - verify absence + remaining items
# Phase 4: Delete configs - verify absence + remaining items

user_story_5_completeness_and_deletion_proof() {
  story 5 "Completeness & Deletion Proof: Comprehensive State Tracking (Multi-Protocol)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-test.XXXXXX")
  story_db "$test_db"
  local transcript=$(mktemp "${TMPDIR:-/tmp}/vpn-transcript.XXXXXX")
  local dump_file="$test_db/dump.sh"

  # Bootstrap
  bootstrap_database "$test_db"

  #---------------------------------------------------------------------------
  # Phase 1: Populate database and record every command (Completeness Proof)
  #---------------------------------------------------------------------------

  # Import 5 WireGuard configs (record commands)
  local cfg1=$(create_test_config "wg-us-101.conf" "us-101.vpn.com:51820")
  local cfg2=$(create_test_config "wg-uk-202.conf" "uk-202.vpn.com:51820")
  local cfg3=$(create_test_config "wg-de-303.conf" "de-303.vpn.com:51820")
  local cfg4=$(create_test_config "wg-jp-404.conf" "jp-404.vpn.com:51820")
  local cfg5=$(create_test_config "wg-au-505.conf" "au-505.vpn.com:51820")

  run_vpn_switch "067" "$test_db" wireguard import "$cfg1" > /dev/null 2>&1
  echo "wireguard import \"\$VPN_SWITCH_ARCHIVE_BASE/wireguard/wg-us-101.conf\"" >> "$transcript"

  run_vpn_switch "068" "$test_db" wireguard import "$cfg2" > /dev/null 2>&1
  echo "wireguard import \"\$VPN_SWITCH_ARCHIVE_BASE/wireguard/wg-uk-202.conf\"" >> "$transcript"

  run_vpn_switch "069" "$test_db" wireguard import "$cfg3" > /dev/null 2>&1
  echo "wireguard import \"\$VPN_SWITCH_ARCHIVE_BASE/wireguard/wg-de-303.conf\"" >> "$transcript"

  run_vpn_switch "070" "$test_db" wireguard import "$cfg4" > /dev/null 2>&1
  echo "wireguard import \"\$VPN_SWITCH_ARCHIVE_BASE/wireguard/wg-jp-404.conf\"" >> "$transcript"

  run_vpn_switch "071" "$test_db" wireguard import "$cfg5" > /dev/null 2>&1
  echo "wireguard import \"\$VPN_SWITCH_ARCHIVE_BASE/wireguard/wg-au-505.conf\"" >> "$transcript"

  # Import 3 OpenVPN configs (record commands)
  local ovpn1=$(create_test_ovpn_config "us-server.ovpn" "us-server.vpn.com 1194")
  local ovpn2=$(create_test_ovpn_config "eu-server.ovpn" "eu-server.vpn.com 1194")
  local ovpn3=$(create_test_ovpn_config "asia-server.ovpn" "asia-server.vpn.com 1194")

  run_vpn_switch "072" "$test_db" openvpn import "$ovpn1" > /dev/null 2>&1
  echo "openvpn import \"\$VPN_SWITCH_ARCHIVE_BASE/openvpn/us-server.ovpn\"" >> "$transcript"

  run_vpn_switch "073" "$test_db" openvpn import "$ovpn2" > /dev/null 2>&1
  echo "openvpn import \"\$VPN_SWITCH_ARCHIVE_BASE/openvpn/eu-server.ovpn\"" >> "$transcript"

  run_vpn_switch "074" "$test_db" openvpn import "$ovpn3" > /dev/null 2>&1
  echo "openvpn import \"\$VPN_SWITCH_ARCHIVE_BASE/openvpn/asia-server.ovpn\"" >> "$transcript"

  # Create 3 WireGuard categories (record commands)
  run_vpn_switch "075" "$test_db" wireguard add privacy > /dev/null 2>&1
  echo "wireguard add privacy" >> "$transcript"

  run_vpn_switch "076" "$test_db" wireguard add work > /dev/null 2>&1
  echo "wireguard add work" >> "$transcript"

  run_vpn_switch "077" "$test_db" wireguard add streaming > /dev/null 2>&1
  echo "wireguard add streaming" >> "$transcript"

  # Create 2 OpenVPN groups (record commands)
  run_vpn_switch "078" "$test_db" openvpn add corporate > /dev/null 2>&1
  echo "openvpn add corporate" >> "$transcript"

  run_vpn_switch "079" "$test_db" openvpn add remote > /dev/null 2>&1
  echo "openvpn add remote" >> "$transcript"

  # Create 2 WireGuard protocol-level links (record commands)
  run_vpn_switch "080" "$test_db" wireguard link quick wg-us-101.conf > /dev/null 2>&1
  echo "wireguard link quick wg-us-101.conf" >> "$transcript"

  run_vpn_switch "081" "$test_db" wireguard link default wg-uk-202.conf > /dev/null 2>&1
  echo "wireguard link default wg-uk-202.conf" >> "$transcript"

  # Create 1 OpenVPN protocol-level link (record commands)
  run_vpn_switch "082" "$test_db" openvpn link office us-server.ovpn > /dev/null 2>&1
  echo "openvpn link office us-server.ovpn" >> "$transcript"

  # Create 5 WireGuard category links (record commands)
  run_vpn_switch "083" "$test_db" wireguard add privacy wg-us-101.conf fast > /dev/null 2>&1
  echo "wireguard add privacy wg-us-101.conf fast" >> "$transcript"

  run_vpn_switch "084" "$test_db" wireguard add privacy wg-uk-202.conf secure > /dev/null 2>&1
  echo "wireguard add privacy wg-uk-202.conf secure" >> "$transcript"

  run_vpn_switch "085" "$test_db" wireguard add work wg-de-303.conf office > /dev/null 2>&1
  echo "wireguard add work wg-de-303.conf office" >> "$transcript"

  run_vpn_switch "086" "$test_db" wireguard add streaming wg-jp-404.conf hd > /dev/null 2>&1
  echo "wireguard add streaming wg-jp-404.conf hd" >> "$transcript"

  run_vpn_switch "087" "$test_db" wireguard add streaming wg-au-505.conf uhd > /dev/null 2>&1
  echo "wireguard add streaming wg-au-505.conf uhd" >> "$transcript"

  # Create 3 OpenVPN group links (record commands)
  run_vpn_switch "088" "$test_db" openvpn add corporate us-server.ovpn primary > /dev/null 2>&1
  echo "openvpn add corporate us-server.ovpn primary" >> "$transcript"

  run_vpn_switch "089" "$test_db" openvpn add corporate eu-server.ovpn secondary > /dev/null 2>&1
  echo "openvpn add corporate eu-server.ovpn secondary" >> "$transcript"

  run_vpn_switch "090" "$test_db" openvpn add remote asia-server.ovpn backup > /dev/null 2>&1
  echo "openvpn add remote asia-server.ovpn backup" >> "$transcript"

  # Dump database
  local dump_stderr=$(mktemp)
  run_vpn_dump "091" "$test_db" > "$dump_file" 2>"$dump_stderr"
  assert_file_empty "$dump_stderr" "Dump stderr is clean"
  rm -f "$dump_stderr"

  # Verify EVERY transcript command appears in dump (Completeness Proof)
  local missing_count=0
  while IFS= read -r cmd; do
    if ! grep -q "$cmd" "$dump_file"; then
      fail "Completeness: Command missing from dump" "$cmd" "Not found"
      missing_count=$((missing_count + 1))
    fi
  done < "$transcript"

  if [ $missing_count -eq 0 ]; then
    pass "Completeness: All 26 operations present in dump (WG: 15, OVPN: 11)"
  fi

  #---------------------------------------------------------------------------
  # Phase 2: Remove protocol-level link, verify absence (Deletion Proof 1)
  #---------------------------------------------------------------------------

  # Remove one WireGuard protocol link
  run_vpn_switch "092" "$test_db" wireguard remove quick > /dev/null 2>&1

  # Remove one OpenVPN protocol link
  run_vpn_switch "093" "$test_db" openvpn remove office > /dev/null 2>&1

  # Dump again
  local dump_stderr2=$(mktemp)
  run_vpn_dump "094" "$test_db" > "$dump_file" 2>"$dump_stderr2"
  assert_file_empty "$dump_stderr2" "Deletion phase 1 dump stderr is clean"
  rm -f "$dump_stderr2"

  # Verify removed WireGuard link is ABSENT
  if ! grep -q "wireguard link quick" "$dump_file"; then
    pass "Deletion: WG protocol link 'quick' removed from dump"
  else
    fail "Deletion: WG protocol link 'quick' still in dump" "Absent" "Present"
  fi

  # Verify removed OpenVPN link is ABSENT
  if ! grep -q "openvpn link office" "$dump_file"; then
    pass "Deletion: OVPN protocol link 'office' removed from dump"
  else
    fail "Deletion: OVPN protocol link 'office' still in dump" "Absent" "Present"
  fi

  # Verify other protocol link still present
  assert_contains "$(cat "$dump_file")" "wireguard link default wg-uk-202.conf" "Deletion: WG protocol link 'default' still present"

  # Verify categories/groups still present
  assert_contains "$(cat "$dump_file")" "wireguard add privacy" "Deletion: WG category 'privacy' still present"
  assert_contains "$(cat "$dump_file")" "wireguard add work" "Deletion: WG category 'work' still present"
  assert_contains "$(cat "$dump_file")" "openvpn add corporate" "Deletion: OVPN group 'corporate' still present"

  #---------------------------------------------------------------------------
  # Phase 3: Remove category links, verify absence (Deletion Proof 2)
  #---------------------------------------------------------------------------

  # Remove two WireGuard category links
  run_vpn_switch "095" "$test_db" wireguard remove privacy/fast > /dev/null 2>&1
  run_vpn_switch "096" "$test_db" wireguard remove work/office > /dev/null 2>&1

  # Remove one OpenVPN group link
  run_vpn_switch "097" "$test_db" openvpn remove corporate/primary > /dev/null 2>&1

  # Dump again
  local dump_stderr3=$(mktemp)
  run_vpn_dump "098" "$test_db" > "$dump_file" 2>"$dump_stderr3"
  assert_file_empty "$dump_stderr3" "Deletion phase 2 dump stderr is clean"
  rm -f "$dump_stderr3"

  # Verify removed WireGuard category links are ABSENT
  if ! grep -q "wireguard add privacy wg-us-101.conf fast" "$dump_file"; then
    pass "Deletion: WG category link 'privacy/fast' removed from dump"
  else
    fail "Deletion: WG category link 'privacy/fast' still in dump" "Absent" "Present"
  fi

  if ! grep -q "wireguard add work wg-de-303.conf office" "$dump_file"; then
    pass "Deletion: WG category link 'work/office' removed from dump"
  else
    fail "Deletion: WG category link 'work/office' still in dump" "Absent" "Present"
  fi

  # Verify removed OpenVPN group link is ABSENT
  if ! grep -q "openvpn add corporate us-server.ovpn primary" "$dump_file"; then
    pass "Deletion: OVPN group link 'corporate/primary' removed from dump"
  else
    fail "Deletion: OVPN group link 'corporate/primary' still in dump" "Absent" "Present"
  fi

  # Verify other category/group links still present
  assert_contains "$(cat "$dump_file")" "wireguard add privacy wg-uk-202.conf secure" "Deletion: WG category link 'privacy/secure' still present"
  assert_contains "$(cat "$dump_file")" "wireguard add streaming wg-jp-404.conf hd" "Deletion: WG category link 'streaming/hd' still present"
  assert_contains "$(cat "$dump_file")" "openvpn add corporate eu-server.ovpn secondary" "Deletion: OVPN group link 'corporate/secondary' still present"

  # Verify configs still present
  assert_contains "$(cat "$dump_file")" "wireguard import.*wg-us-101.conf" "Deletion: WG config wg-us-101.conf still present"
  assert_contains "$(cat "$dump_file")" "wireguard import.*wg-de-303.conf" "Deletion: WG config wg-de-303.conf still present"
  assert_contains "$(cat "$dump_file")" "openvpn import.*us-server.ovpn" "Deletion: OVPN config us-server.ovpn still present"

  #---------------------------------------------------------------------------
  # Phase 4: Remove configs, verify absence (Deletion Proof 3)
  #---------------------------------------------------------------------------

  # Remove two WireGuard configs (must remove category links first)
  run_vpn_switch "099" "$test_db" wireguard remove streaming/hd > /dev/null 2>&1
  run_vpn_switch "100" "$test_db" wireguard remove streaming/uhd > /dev/null 2>&1
  run_vpn_switch "101" "$test_db" wireguard remove wg-jp-404.conf > /dev/null 2>&1
  run_vpn_switch "102" "$test_db" wireguard remove wg-au-505.conf > /dev/null 2>&1

  # Remove one OpenVPN config (must remove group links first)
  run_vpn_switch "103" "$test_db" openvpn remove remote/backup > /dev/null 2>&1
  run_vpn_switch "104" "$test_db" openvpn remove asia-server.ovpn > /dev/null 2>&1

  # Dump again
  local dump_stderr4=$(mktemp)
  run_vpn_dump "105" "$test_db" > "$dump_file" 2>"$dump_stderr4"
  assert_file_empty "$dump_stderr4" "Deletion phase 3 dump stderr is clean"
  rm -f "$dump_stderr4"

  # Verify removed WireGuard configs are ABSENT
  if ! grep -q "wg-jp-404.conf" "$dump_file"; then
    pass "Deletion: WG config wg-jp-404.conf removed from dump"
  else
    fail "Deletion: WG config wg-jp-404.conf still in dump" "Absent" "Present"
  fi

  if ! grep -q "wg-au-505.conf" "$dump_file"; then
    pass "Deletion: WG config wg-au-505.conf removed from dump"
  else
    fail "Deletion: WG config wg-au-505.conf still in dump" "Absent" "Present"
  fi

  # Verify removed OpenVPN config is ABSENT
  if ! grep -q "asia-server.ovpn" "$dump_file"; then
    pass "Deletion: OVPN config asia-server.ovpn removed from dump"
  else
    fail "Deletion: OVPN config asia-server.ovpn still in dump" "Absent" "Present"
  fi

  # Verify other configs still present
  assert_contains "$(cat "$dump_file")" "wireguard import.*wg-us-101.conf" "Deletion: WG config wg-us-101.conf still present after removals"
  assert_contains "$(cat "$dump_file")" "wireguard import.*wg-uk-202.conf" "Deletion: WG config wg-uk-202.conf still present after removals"
  assert_contains "$(cat "$dump_file")" "wireguard import.*wg-de-303.conf" "Deletion: WG config wg-de-303.conf still present after removals"
  assert_contains "$(cat "$dump_file")" "openvpn import.*us-server.ovpn" "Deletion: OVPN config us-server.ovpn still present after removals"
  assert_contains "$(cat "$dump_file")" "openvpn import.*eu-server.ovpn" "Deletion: OVPN config eu-server.ovpn still present after removals"

  # Verify categories/groups still exist even with no links
  assert_contains "$(cat "$dump_file")" "wireguard add privacy" "Deletion: Empty WG category 'privacy' still present"
  assert_contains "$(cat "$dump_file")" "wireguard add work" "Deletion: Empty WG category 'work' still present"
  assert_contains "$(cat "$dump_file")" "wireguard add streaming" "Deletion: Empty WG category 'streaming' still present"
  assert_contains "$(cat "$dump_file")" "openvpn add corporate" "Deletion: OVPN group 'corporate' still present (has link)"
  assert_contains "$(cat "$dump_file")" "openvpn add remote" "Deletion: Empty OVPN group 'remote' still present"

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$cfg1" "$cfg2" "$cfg3" "$cfg4" "$cfg5" "$ovpn1" "$ovpn2" "$ovpn3" "$transcript"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 6: Dump Quality Properties
#-----------------------------------------------------------------------------
# As a user, I want dump output to be clean, pipe-able, and executable
# without requiring stderr redirection or manual filtering.
#
# This test verifies the "dump quality contract" that was discovered
# during Phase 3.1.4 bug fix. It ensures:
# 1. Stderr is completely empty (no pollution)
# 2. Stdout is a valid, executable shell script
# 3. Direct pipe works (no stderr redirect needed)
# 4. All informational output is commented
# 5. Transcript reproducibility (dump matches input commands)

user_story_6_dump_quality_properties() {
  story 6 "Dump Quality: Cleanliness, Pipeability, Executability, Transcript Reproducibility"

  # Setup minimal database
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-quality-test.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"
  story_attest_key "$test_db" story

  # Create known transcript of operations
  local transcript=$(mktemp)

  # Import one WireGuard config (record command)
  local wg_config=$(create_test_config "quality-test.conf")
  run_vpn_switch "106" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1
  echo "wireguard import quality-test.conf" >> "$transcript"

  # Import one OpenVPN config (record command)
  local ovpn_config=$(create_test_ovpn_config "quality-test.ovpn")
  run_vpn_switch "107" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1
  echo "openvpn import quality-test.ovpn" >> "$transcript"

  # Create one WireGuard category (record command)
  run_vpn_switch "108" "$test_db" wireguard add privacy > /dev/null 2>&1
  echo "wireguard add privacy" >> "$transcript"

  # Create one OpenVPN group (record command)
  run_vpn_switch "109" "$test_db" openvpn add work > /dev/null 2>&1
  echo "openvpn add work" >> "$transcript"

  # Create protocol-level links (record commands)
  run_vpn_switch "110" "$test_db" wireguard link quick quality-test.conf > /dev/null 2>&1
  echo "wireguard link quick quality-test.conf" >> "$transcript"

  run_vpn_switch "111" "$test_db" openvpn link vpn quality-test.ovpn > /dev/null 2>&1
  echo "openvpn link vpn quality-test.ovpn" >> "$transcript"

  #---------------------------------------------------------------------------
  # Property 1: Stderr is completely empty
  #---------------------------------------------------------------------------

  local dump_file=$(mktemp)
  local stderr_file=$(mktemp)

  run_vpn_dump "112" "$test_db" > "$dump_file" 2>"$stderr_file"

  if [ ! -s "$stderr_file" ]; then
    pass "Property 1: Dump stderr is completely empty"
  else
    fail "Property 1: Dump produced stderr output" "Empty stderr" "$(cat "$stderr_file")"
  fi

  #---------------------------------------------------------------------------
  # Property 2: Stdout is valid shell script (syntax check)
  #---------------------------------------------------------------------------

  if sh -n "$dump_file" 2>/dev/null; then
    pass "Property 2: Dump output is syntactically valid shell script"
  else
    fail "Property 2: Dump output has syntax errors" "Valid shell script" "Syntax errors detected"
  fi

  #---------------------------------------------------------------------------
  # Property 3: Direct pipe works (real user workflow)
  #---------------------------------------------------------------------------

  # Simulate user workflow: vpn-switch dump > backup.sh (NO stderr redirect!)
  local direct_pipe_dump=$(mktemp)
  if run_vpn_dump "113" "$test_db" > "$direct_pipe_dump"; then
    pass "Property 3: Direct pipe works (exit code 0)"
  else
    fail "Property 3: Direct pipe failed" "Exit code 0" "Non-zero exit code"
  fi

  # Verify piped output is identical to redirected output (normalize non-deterministic lines)
  local dump1_notimestamp=$(mktemp)
  local dump2_notimestamp=$(mktemp)
  grep -Ev "^# Generated:|^# Batch execution" "$dump_file" > "$dump1_notimestamp"
  grep -Ev "^# Generated:|^# Batch execution" "$direct_pipe_dump" > "$dump2_notimestamp"

  if diff -q "$dump1_notimestamp" "$dump2_notimestamp" > /dev/null 2>&1; then
    pass "Property 3: Direct pipe produces identical output"
  else
    fail "Property 3: Piped output differs" "Identical" "Different"
    # Show diff for debugging
    echo "${YELLOW}Diff output (first 30 lines):${NC}"
    diff -u "$dump1_notimestamp" "$dump2_notimestamp" | head -30 || true
  fi

  rm -f "$dump1_notimestamp" "$dump2_notimestamp"

  #---------------------------------------------------------------------------
  # Property 4: All informational output is commented
  #---------------------------------------------------------------------------

  local uncommented_found=0
  local line_num=0
  while IFS= read -r line; do
    line_num=$((line_num + 1))
    case "$line" in
      \#*) continue ;;                    # Comment - OK
      \"\$VPN_SWITCH_CONTEXT_SCRIPT\"*) continue ;;  # Command (including setenv) - OK
      "") continue ;;                      # Blank line - OK
      *)
        # Found uncommented informational output
        fail "Property 4: Uncommented output at line $line_num" "Comment or command" "$line"
        uncommented_found=1
        break
        ;;
    esac
  done < "$dump_file"

  if [ $uncommented_found -eq 0 ]; then
    pass "Property 4: All informational output is properly commented"
  fi

  #---------------------------------------------------------------------------
  # Property 5: Transcript reproducibility (dump matches input commands)
  #---------------------------------------------------------------------------

  # Every command in the transcript should appear in the dump
  local missing_commands=0
  while IFS= read -r cmd; do
    # Normalize command for comparison (remove config paths, keep command structure)
    local cmd_pattern=$(echo "$cmd" | sed 's|[^ ]*\.conf|.*\.conf|g; s|[^ ]*\.ovpn|.*\.ovpn|g')

    if ! grep -q "$cmd_pattern" "$dump_file"; then
      fail "Property 5: Transcript command missing from dump" "$cmd_pattern" "Not found"
      missing_commands=$((missing_commands + 1))
    fi
  done < "$transcript"

  if [ $missing_commands -eq 0 ]; then
    pass "Property 5: All transcript commands present in dump (reproducibility)"
  fi

  #---------------------------------------------------------------------------
  # Property 6: Dump is restorable (executability test)
  #---------------------------------------------------------------------------

  # Create new database and restore from dump
  local new_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-quality-restore.XXXXXX")
  story_db "$new_db"
  bootstrap_database "$new_db"

  # The database travels as the signed pair: the plain dump names its
  # files against the archive base, the bundle carries them, the receiver
  # pins the same key
  story_attest_key "$new_db" story
  local pair_dump="$test_db/pair.sh" pair_bundle="$test_db/pair.tar.gz"
  run_vpn_switch "114a" "$test_db" export full "$pair_dump" "$pair_bundle" > /dev/null 2>&1
  if run_vpn_switch "114" "$new_db" import "$pair_dump" "$pair_bundle" > /dev/null 2>&1; then
    pass "Property 6: Dump is restorable (the pair imports)"
  else
    fail "Property 6: Dump failed to restore" "Successful import" "Import failed"
  fi

  # Verify restored database has same structure
  assert_file_exists "$new_db/wireguard/quality-test.conf" "Property 6: WG config restored"
  assert_file_exists "$new_db/openvpn/quality-test.ovpn" "Property 6: OVPN config restored"
  assert_dir_exists "$new_db/wireguard/privacy" "Property 6: WG category restored"
  assert_dir_exists "$new_db/openvpn/work" "Property 6: OVPN group restored"
  assert_symlink_exists "$new_db/wireguard/quick" "Property 6: WG link restored"
  assert_symlink_exists "$new_db/openvpn/vpn" "Property 6: OVPN link restored"

  #---------------------------------------------------------------------------
  # Property 7: Normalized idempotency (dump(restore(dump)) == dump)
  #---------------------------------------------------------------------------

  local dump2=$(mktemp)
  run_vpn_dump "115" "$new_db" > "$dump2" 2>/dev/null

  # Normalize both dumps (remove non-deterministic lines, normalize paths)
  local dump1_normalized=$(mktemp)
  local dump2_normalized=$(mktemp)

  # Serial, base and the receipt differ by design: NEW continues the
  # lineage and holds the receipt of this import
  grep -Ev "^# Generated:|^# Batch execution|^# Serial:|^# Base:|provenance import|no receipts to dump" "$dump_file" | sed -e "s|${test_db}|DB|g" > "$dump1_normalized"
  grep -Ev "^# Generated:|^# Batch execution|^# Serial:|^# Base:|provenance import|no receipts to dump" "$dump2" | sed -e "s|${new_db}|DB|g" > "$dump2_normalized"

  if diff -u "$dump1_normalized" "$dump2_normalized" > /dev/null 2>&1; then
    pass "Property 7: Normalized idempotency verified (dump(restore(dump)) == dump)"
  else
    fail "Property 7: Dumps differ after restore" "Identical" "Different"
    echo "${YELLOW}Diff output:${NC}"
    diff -u "$dump1_normalized" "$dump2_normalized" || true
  fi

  #---------------------------------------------------------------------------
  # Property 8: No duplicate lines (exclude blank lines, comments, and setenv)
  #---------------------------------------------------------------------------
  # Note: setenv and environment refresh appear twice by design (prologue and epilogue)
  # Only check for duplicates in protocol commands (import, add, link)

  local total_lines=$(grep -v '^[ \t]*$' "$dump_file" | grep -v '#' | grep -v 'setenv' | grep -v 'environment refresh' | wc -l | tr -d ' ')
  local unique_lines=$(grep -v '^[ \t]*$' "$dump_file" | grep -v '#' | grep -v 'setenv' | grep -v 'environment refresh' | sort -u | wc -l | tr -d ' ')

  if [ "$total_lines" = "$unique_lines" ]; then
    pass "Property 8: No duplicate protocol commands in dump ($total_lines unique)"
  else
    fail "Property 8: Dump has duplicate protocol commands" "$total_lines unique" "$unique_lines after deduplication"
    echo "${YELLOW}Duplicate protocol commands found - showing first 10 duplicates:${NC}"
    grep -v '^[ \t]*$' "$dump_file" | grep -v '#' | grep -v 'setenv' | sort | uniq -d | head -10 || true
  fi

  # Cleanup
  cleanup_on_success "$test_db" "$new_db"
  rm -f "$wg_config" "$ovpn_config" "$transcript"
  rm -f "$dump_file" "$stderr_file" "$direct_pipe_dump" "$dump2"
  rm -f "$dump1_normalized" "$dump2_normalized"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 7: Phase-Based Script Generation
#-----------------------------------------------------------------------------
# As a user, I want phase-based scripts to be generated correctly
# so I can inspect and execute VPN connections.
#
# Acceptance criteria:
# - Scripts are created in session directory during start
# - Both connect.sh and disconnect.sh are present
# - Scripts have executable permissions (0750 = rwxr-x--- for sudo/user scenario)
# - Scripts are self-contained (include shebang)
# - Both WireGuard and OpenVPN generate phase scripts

user_story_7_phase_script_generation() {
  story 7 "Phase-Based Script Generation (WireGuard and OpenVPN)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-phase-gen.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"

  # Prevent actual execution of connect/disconnect scripts (just generate them)
  set_function_interpreter "$test_db" "wireguard_connect1" "cat"
  set_function_interpreter "$test_db" "wireguard_disconnect1" "cat"
  set_function_interpreter "$test_db" "openvpn_connect1" "cat"
  set_function_interpreter "$test_db" "openvpn_disconnect1" "cat"

  #---------------------------------------------------------------------------
  # Test 1: WireGuard generates both scripts
  #---------------------------------------------------------------------------

  local wg_config=$(create_test_config "phase-test-wg.conf")
  run_vpn_switch "116" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1

  # Generate scripts by starting (but not connecting)
  # Note: bootstrap_database sets TERMINAL_INTERPRETER=sh, so start will generate and output commands
  local wg_output=$(run_vpn_switch "117" "$test_db" wireguard start phase-test-wg 2>&1)
  local wg_session=$(extract_session_id "$wg_output")

  if [ -z "$wg_session" ]; then
    fail "WG: Extract session ID" "Valid PID" "Empty/Not found"
    echo "${YELLOW}Debug: start output (first 20 lines):${NC}"
    echo "$wg_output" | head -20
  else
    pass "WG: Extract session ID from start output"
  fi

  # Verify scripts exist
  assert_file_exists "$test_db/.session/$wg_session/connect.sh" "WG: connect.sh exists"
  assert_file_exists "$test_db/.session/$wg_session/disconnect.sh" "WG: disconnect.sh exists"

  # Verify permissions (0750 = rwxr-x--- for sudo/user scenario)
  if [ -f "$test_db/.session/$wg_session/connect.sh" ]; then
    local wg_connect_perms=$(stat -c "%a" "$test_db/.session/$wg_session/connect.sh" 2>/dev/null || \
                             stat -f "%Lp" "$test_db/.session/$wg_session/connect.sh" 2>/dev/null)
    if [ "$wg_connect_perms" = "750" ]; then
      pass "WG: connect.sh has correct permissions (0750)"
    else
      fail "WG: connect.sh permissions" "750" "$wg_connect_perms"
    fi
  fi

  if [ -f "$test_db/.session/$wg_session/disconnect.sh" ]; then
    local wg_disconnect_perms=$(stat -c "%a" "$test_db/.session/$wg_session/disconnect.sh" 2>/dev/null || \
                                stat -f "%Lp" "$test_db/.session/$wg_session/disconnect.sh" 2>/dev/null)
    if [ "$wg_disconnect_perms" = "750" ]; then
      pass "WG: disconnect.sh has correct permissions (0750)"
    else
      fail "WG: disconnect.sh permissions" "750" "$wg_disconnect_perms"
    fi
  fi

  # Verify executability
  assert_file_executable "$test_db/.session/$wg_session/connect.sh" "WG: connect.sh is executable"
  assert_file_executable "$test_db/.session/$wg_session/disconnect.sh" "WG: disconnect.sh is executable"

  # Verify self-contained (shebang present)
  assert_file_starts_with "$test_db/.session/$wg_session/connect.sh" "#!" "WG: connect.sh has shebang"
  assert_file_starts_with "$test_db/.session/$wg_session/disconnect.sh" "#!" "WG: disconnect.sh has shebang"

  #---------------------------------------------------------------------------
  # Test 2: OpenVPN generates both scripts
  #---------------------------------------------------------------------------

  local ovpn_config=$(create_test_ovpn_config "phase-test-ovpn.ovpn")
  run_vpn_switch "118" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1

  # Generate scripts by starting
  local ovpn_output=$(run_vpn_switch "119" "$test_db" openvpn start phase-test-ovpn 2>&1)
  local ovpn_session=$(extract_session_id "$ovpn_output")

  if [ -z "$ovpn_session" ]; then
    fail "OVPN: Extract session ID" "Valid PID" "Empty/Not found"
    echo "${YELLOW}Debug: start output (first 20 lines):${NC}"
    echo "$ovpn_output" | head -20
  else
    pass "OVPN: Extract session ID from start output"
  fi

  # Verify scripts exist
  assert_file_exists "$test_db/.session/$ovpn_session/connect.sh" "OVPN: connect.sh exists"
  assert_file_exists "$test_db/.session/$ovpn_session/disconnect.sh" "OVPN: disconnect.sh exists"

  # Verify permissions (0750 = rwxr-x--- for sudo/user scenario)
  if [ -f "$test_db/.session/$ovpn_session/connect.sh" ]; then
    local ovpn_connect_perms=$(stat -c "%a" "$test_db/.session/$ovpn_session/connect.sh" 2>/dev/null || \
                               stat -f "%Lp" "$test_db/.session/$ovpn_session/connect.sh" 2>/dev/null)
    if [ "$ovpn_connect_perms" = "750" ]; then
      pass "OVPN: connect.sh has correct permissions (0750)"
    else
      fail "OVPN: connect.sh permissions" "750" "$ovpn_connect_perms"
    fi
  fi

  if [ -f "$test_db/.session/$ovpn_session/disconnect.sh" ]; then
    local ovpn_disconnect_perms=$(stat -c "%a" "$test_db/.session/$ovpn_session/disconnect.sh" 2>/dev/null || \
                                  stat -f "%Lp" "$test_db/.session/$ovpn_session/disconnect.sh" 2>/dev/null)
    if [ "$ovpn_disconnect_perms" = "750" ]; then
      pass "OVPN: disconnect.sh has correct permissions (0750)"
    else
      fail "OVPN: disconnect.sh permissions" "750" "$ovpn_disconnect_perms"
    fi
  fi

  # Verify executability
  assert_file_executable "$test_db/.session/$ovpn_session/connect.sh" "OVPN: connect.sh is executable"
  assert_file_executable "$test_db/.session/$ovpn_session/disconnect.sh" "OVPN: disconnect.sh is executable"

  # Verify self-contained (shebang present)
  assert_file_starts_with "$test_db/.session/$ovpn_session/connect.sh" "#!" "OVPN: connect.sh has shebang"
  assert_file_starts_with "$test_db/.session/$ovpn_session/disconnect.sh" "#!" "OVPN: disconnect.sh has shebang"

  #---------------------------------------------------------------------------
  # Test 3: Session metadata includes script paths
  #---------------------------------------------------------------------------

  # Verify session directory structure
  assert_file_exists "$test_db/.session/$wg_session/protocol" "WG: Session metadata exists (protocol)"
  assert_file_exists "$test_db/.session/$wg_session/interface" "WG: Session metadata exists (interface)"
  assert_file_exists "$test_db/.session/$ovpn_session/protocol" "OVPN: Session metadata exists (protocol)"
  assert_file_exists "$test_db/.session/$ovpn_session/interface" "OVPN: Session metadata exists (interface)"

  #---------------------------------------------------------------------------
  # Test 4: Scripts are independent (different sessions, different scripts)
  #---------------------------------------------------------------------------

  # Verify WireGuard and OpenVPN sessions are different
  if [ "$wg_session" != "$ovpn_session" ]; then
    pass "WG and OVPN sessions are independent (different PIDs)"
  else
    fail "WG and OVPN sessions" "Different PIDs" "Same PID: $wg_session"
  fi

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$wg_config" "$ovpn_config"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 8: Phase Script Content Verification
#-----------------------------------------------------------------------------
# As a user, I want generated scripts to contain correct commands and structure
# so I can trust them.
#
# Acceptance criteria:
# - Prologue includes session metadata and context variables
# - Each configured phase is present in correct order
# - Each phase has verify function defined
# - Each phase has prepare and execute sections
# - Protocol-specific commands are present
# - Disconnect script reverses connect order

user_story_8_phase_script_content() {
  story 8 "Phase Script Content Verification (Structure and Commands)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-phase-content.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"

  # Prevent actual execution of connect/disconnect scripts (just generate them)
  set_function_interpreter "$test_db" "wireguard_connect1" "cat"
  set_function_interpreter "$test_db" "wireguard_disconnect1" "cat"
  set_function_interpreter "$test_db" "openvpn_connect1" "cat"
  set_function_interpreter "$test_db" "openvpn_disconnect1" "cat"

  #---------------------------------------------------------------------------
  # Test 1: WireGuard connect.sh content
  #---------------------------------------------------------------------------

  local wg_config=$(create_test_config "content-test-wg.conf")
  run_vpn_switch "120" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1

  local wg_output=$(run_vpn_switch "121" "$test_db" wireguard start content-test-wg 2>&1)
  local wg_session=$(extract_session_id "$wg_output")
  local wg_connect="$test_db/.session/$wg_session/connect.sh"
  local wg_disconnect="$test_db/.session/$wg_session/disconnect.sh"

  # Verify prologue documentation
  assert_script_contains "$wg_connect" "WireGuard VPN Connection Script" "WG connect: Has title"
  assert_script_contains "$wg_connect" "Generated:" "WG connect: Has generation timestamp"
  assert_script_contains "$wg_connect" "Session:" "WG connect: Has session ID"
  assert_script_contains "$wg_connect" "CONTEXT VARIABLES:" "WG connect: Has context documentation"

  # Verify standard context variables
  assert_script_contains "$wg_connect" "session_dir=" "WG connect: Has session_dir variable"
  assert_script_contains "$wg_connect" "protocol=" "WG connect: Has protocol variable"
  assert_script_contains "$wg_connect" "interface=" "WG connect: Has interface variable"
  assert_script_contains "$wg_connect" "config_path=" "WG connect: Has config_path variable"

  # Verify WireGuard-specific context variables
  assert_script_contains "$wg_connect" "dns_server=" "WG connect: Has dns_server variable"
  assert_script_contains "$wg_connect" "remote_ips=" "WG connect: Has remote_ips variable"

  # Verify default phases present in connect.sh (firewall, vpn, dns)
  assert_script_contains "$wg_connect" "verify_firewall()" "WG connect: Has firewall verify function"
  assert_script_contains "$wg_connect" "verify_vpn()" "WG connect: Has vpn verify function"
  assert_script_contains "$wg_connect" "verify_dns()" "WG connect: Has dns verify function"

  # Verify phase structure (prepare and execute sections)
  assert_script_contains "$wg_connect" "Phase: firewall (prepare)" "WG connect: Firewall prepare phase"
  assert_script_contains "$wg_connect" "Phase: firewall (execute)" "WG connect: Firewall execute phase"
  assert_script_contains "$wg_connect" "Phase: vpn (prepare)" "WG connect: VPN prepare phase"
  assert_script_contains "$wg_connect" "Phase: vpn (execute)" "WG connect: VPN execute phase"
  assert_script_contains "$wg_connect" "Phase: dns (prepare)" "WG connect: DNS prepare phase"
  assert_script_contains "$wg_connect" "Phase: dns (execute)" "WG connect: DNS execute phase"

  # Verify WireGuard-specific commands
  assert_script_contains "$wg_connect" "wg-quick up" "WG connect: Has wg-quick up command"

  # Verify phase order in connect.sh (default: firewall, vpn, dns)
  assert_phase_order "$wg_connect" "firewall vpn dns" "WG connect: Correct phase order"

  #---------------------------------------------------------------------------
  # Test 2: WireGuard disconnect.sh content
  #---------------------------------------------------------------------------

  # Verify disconnect prologue
  assert_script_contains "$wg_disconnect" "WireGuard VPN Disconnection Script" "WG disconnect: Has title"
  assert_script_contains "$wg_disconnect" "session_dir=" "WG disconnect: Has session_dir variable"

  # Verify phases present in disconnect.sh (should reverse connect order: dns, vpn, firewall)
  assert_script_contains "$wg_disconnect" "verify_dns()" "WG disconnect: Has dns verify function"
  assert_script_contains "$wg_disconnect" "verify_vpn()" "WG disconnect: Has vpn verify function"
  assert_script_contains "$wg_disconnect" "verify_firewall()" "WG disconnect: Has firewall verify function"

  # Verify WireGuard-specific disconnect commands
  assert_script_contains "$wg_disconnect" "wg-quick down" "WG disconnect: Has wg-quick down command"

  # Verify phase order in disconnect.sh (reversed: dns, vpn, firewall)
  assert_phase_order "$wg_disconnect" "dns vpn firewall" "WG disconnect: Correct reversed phase order"

  #---------------------------------------------------------------------------
  # Test 3: OpenVPN connect.sh content
  #---------------------------------------------------------------------------

  local ovpn_config=$(create_test_ovpn_config "content-test-ovpn.ovpn")
  run_vpn_switch "122" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1

  local ovpn_output=$(run_vpn_switch "123" "$test_db" openvpn start content-test-ovpn 2>&1)
  local ovpn_session=$(extract_session_id "$ovpn_output")
  local ovpn_connect="$test_db/.session/$ovpn_session/connect.sh"
  local ovpn_disconnect="$test_db/.session/$ovpn_session/disconnect.sh"

  # Verify prologue documentation
  assert_script_contains "$ovpn_connect" "OpenVPN VPN Connection Script" "OVPN connect: Has title"
  assert_script_contains "$ovpn_connect" "Generated:" "OVPN connect: Has generation timestamp"
  assert_script_contains "$ovpn_connect" "session_dir=" "OVPN connect: Has session_dir variable"
  assert_script_contains "$ovpn_connect" "protocol=" "OVPN connect: Has protocol variable"

  # Verify OpenVPN-specific context variables
  assert_script_contains "$ovpn_connect" "interface=" "OVPN connect: Has interface variable"
  assert_script_contains "$ovpn_connect" "config_path=" "OVPN connect: Has config_path variable"

  # Verify phases present
  assert_script_contains "$ovpn_connect" "verify_firewall()" "OVPN connect: Has firewall verify function"
  assert_script_contains "$ovpn_connect" "verify_vpn()" "OVPN connect: Has vpn verify function"
  assert_script_contains "$ovpn_connect" "verify_dns()" "OVPN connect: Has dns verify function"

  # Verify OpenVPN-specific commands (daemon mode)
  assert_script_contains "$ovpn_connect" "openvpn" "OVPN connect: Has openvpn command"
  assert_script_contains "$ovpn_connect" "--daemon" "OVPN connect: Has daemon flag"

  # Verify phase order
  assert_phase_order "$ovpn_connect" "firewall vpn dns" "OVPN connect: Correct phase order"

  #---------------------------------------------------------------------------
  # Test 4: OpenVPN disconnect.sh content
  #---------------------------------------------------------------------------

  # Verify disconnect prologue
  assert_script_contains "$ovpn_disconnect" "OpenVPN VPN Disconnection Script" "OVPN disconnect: Has title"
  assert_script_contains "$ovpn_disconnect" "session_dir=" "OVPN disconnect: Has session_dir variable"

  # Verify OpenVPN-specific disconnect commands
  assert_script_contains "$ovpn_disconnect" "kill -TERM" "OVPN disconnect: Has process kill command"

  # Verify phase order (reversed)
  assert_phase_order "$ovpn_disconnect" "dns vpn firewall" "OVPN disconnect: Correct reversed phase order"

  #---------------------------------------------------------------------------
  # Test 5: Protocol isolation (no cross-contamination)
  #---------------------------------------------------------------------------

  # WireGuard script should NOT contain OpenVPN commands
  assert_script_not_contains "$wg_connect" "openvpn" "WG connect: No OpenVPN commands"
  assert_script_not_contains "$wg_connect" "--daemon" "WG connect: No OpenVPN daemon flag"

  # OpenVPN script should NOT contain WireGuard commands
  assert_script_not_contains "$ovpn_connect" "wg-quick" "OVPN connect: No WireGuard commands"

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$wg_config" "$ovpn_config"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 9: Phase Customization
#-----------------------------------------------------------------------------
# As a user, I want to customize phases so I can adapt VPN behavior to my needs.
#
# Acceptance criteria:
# - Can reorder phases via VPN_SWITCH_PHASES_CONNECT
# - Can add custom phase to .include/phase/
# - Custom phase appears in generated script
# - Can skip phases by modifying environment
# - Changes reflected in next connection (not previous sessions)

user_story_9_phase_customization() {
  story 9 "Phase Customization (Reorder, Add, Skip)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-phase-custom.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"

  # Prevent actual execution of connect/disconnect scripts (just generate them)
  set_function_interpreter "$test_db" "wireguard_connect1" "cat"
  set_function_interpreter "$test_db" "wireguard_disconnect1" "cat"

  local wg_config=$(create_test_config "custom-test.conf")
  run_vpn_switch "124" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 1: Baseline (default phase order)
  #---------------------------------------------------------------------------

  local baseline_output=$(run_vpn_switch "125" "$test_db" wireguard start custom-test 2>&1)
  local baseline_session=$(extract_session_id "$baseline_output")
  local baseline_connect="$test_db/.session/$baseline_session/connect.sh"

  # Verify default order
  assert_phase_order "$baseline_connect" "firewall vpn dns" "Baseline: Default phase order"

  # Stop baseline session before starting reorder test
  run_vpn_switch "126" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 2: Reorder phases (vpn → firewall → dns)
  #---------------------------------------------------------------------------

  # Customize phase order
  run_vpn_switch "127" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "vpn firewall dns" > /dev/null 2>&1

  local reorder_output=$(run_vpn_switch "128" "$test_db" wireguard start custom-test 2>&1)
  local reorder_session=$(extract_session_id "$reorder_output")
  local reorder_connect="$test_db/.session/$reorder_session/connect.sh"

  # Verify new order
  assert_phase_order "$reorder_connect" "vpn firewall dns" "Reordered: Custom phase order"

  # Stop reorder session before next test
  run_vpn_switch "129" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 3: Add custom phase
  #---------------------------------------------------------------------------

  # Create custom routing phase
  create_custom_phase "$test_db" "wireguard" "routing"

  # Update phase list to include custom phase
  run_vpn_switch "130" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns routing" > /dev/null 2>&1

  local custom_output=$(run_vpn_switch "131" "$test_db" wireguard start custom-test 2>&1)
  local custom_session=$(extract_session_id "$custom_output")
  local custom_connect="$test_db/.session/$custom_session/connect.sh"

  # Verify custom phase is included
  assert_script_contains "$custom_connect" "verify_routing()" "Custom phase: Has verify function"
  assert_script_contains "$custom_connect" "Phase: routing (prepare)" "Custom phase: Has prepare section"
  assert_script_contains "$custom_connect" "Phase: routing (execute)" "Custom phase: Has execute section"
  assert_phase_order "$custom_connect" "firewall vpn dns routing" "Custom phase: Correct order with routing"

  # Stop before next test
  run_vpn_switch "132" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 4: Skip phases (VPN only)
  #---------------------------------------------------------------------------

  # Configure to use only VPN phase (skip firewall and DNS)
  run_vpn_switch "133" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "vpn" > /dev/null 2>&1

  local minimal_output=$(run_vpn_switch "134" "$test_db" wireguard start custom-test 2>&1)
  local minimal_session=$(extract_session_id "$minimal_output")
  local minimal_connect="$test_db/.session/$minimal_session/connect.sh"

  # Verify only VPN phase present
  assert_script_contains "$minimal_connect" "verify_vpn()" "Minimal: Has vpn verify function"
  assert_script_not_contains "$minimal_connect" "verify_firewall()" "Minimal: No firewall phase"
  assert_script_not_contains "$minimal_connect" "verify_dns()" "Minimal: No dns phase"
  assert_phase_order "$minimal_connect" "vpn" "Minimal: Only VPN phase"

  # Stop before next test
  run_vpn_switch "135" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 5: Disconnect phase customization
  #---------------------------------------------------------------------------

  # Reset to default connect phases
  run_vpn_switch "136" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns" > /dev/null 2>&1

  # Customize disconnect phases (skip firewall cleanup)
  run_vpn_switch "137" "$test_db" setenv VPN_SWITCH_PHASES_DISCONNECT "dns vpn" > /dev/null 2>&1

  local custom_dc_output=$(run_vpn_switch "138" "$test_db" wireguard start custom-test 2>&1)
  local custom_dc_session=$(extract_session_id "$custom_dc_output")
  local custom_disconnect="$test_db/.session/$custom_dc_session/disconnect.sh"

  # Verify disconnect only has dns and vpn (no firewall)
  assert_script_contains "$custom_disconnect" "verify_dns()" "Custom disconnect: Has dns phase"
  assert_script_contains "$custom_disconnect" "verify_vpn()" "Custom disconnect: Has vpn phase"
  assert_script_not_contains "$custom_disconnect" "verify_firewall()" "Custom disconnect: No firewall phase"
  assert_phase_order "$custom_disconnect" "dns vpn" "Custom disconnect: Only dns and vpn"

  # Stop before next test
  run_vpn_switch "139" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 6: Multiple custom phases
  #---------------------------------------------------------------------------

  # Create two custom phases
  create_custom_phase "$test_db" "wireguard" "logging"
  create_custom_phase "$test_db" "wireguard" "monitoring"

  # Use phases: vpn, logging, monitoring (skip default phases)
  run_vpn_switch "140" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "vpn logging monitoring" > /dev/null 2>&1

  local multi_output=$(run_vpn_switch "141" "$test_db" wireguard start custom-test 2>&1)
  local multi_session=$(extract_session_id "$multi_output")
  local multi_connect="$test_db/.session/$multi_session/connect.sh"

  # Verify all custom phases present
  assert_script_contains "$multi_connect" "verify_logging()" "Multi-custom: Has logging phase"
  assert_script_contains "$multi_connect" "verify_monitoring()" "Multi-custom: Has monitoring phase"
  assert_phase_order "$multi_connect" "vpn logging monitoring" "Multi-custom: Correct custom order"

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$wg_config"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 10: Multi-Protocol Phase Support
#-----------------------------------------------------------------------------
# As a user, I want phase systems to be isolated between protocols
# so I can customize each independently.
#
# Acceptance criteria:
# - WireGuard and OpenVPN use separate phase templates
# - Custom phases don't leak between protocols
# - Context variables are protocol-specific
# - Multiple protocols can run simultaneously with different phases

user_story_10_multi_protocol_phase_support() {
  story 10 "Multi-Protocol Phase Support (Isolation and Simultaneous Operation)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-phase-multi.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"

  # Prevent actual execution of connect/disconnect scripts (just generate them)
  set_function_interpreter "$test_db" "wireguard_connect1" "cat"
  set_function_interpreter "$test_db" "wireguard_disconnect1" "cat"
  set_function_interpreter "$test_db" "openvpn_connect1" "cat"
  set_function_interpreter "$test_db" "openvpn_disconnect1" "cat"

  #---------------------------------------------------------------------------
  # Test 1: Protocol isolation (different default phases)
  #---------------------------------------------------------------------------

  local wg_config=$(create_test_config "isolation-test-wg.conf")
  local ovpn_config=$(create_test_ovpn_config "isolation-test-ovpn.ovpn")

  run_vpn_switch "142" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1
  run_vpn_switch "143" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1

  # Start both protocols (using default phases)
  local wg_output=$(run_vpn_switch "144" "$test_db" wireguard start isolation-test-wg 2>&1)
  local wg_session=$(extract_session_id "$wg_output")
  local wg_connect="$test_db/.session/$wg_session/connect.sh"

  local ovpn_output=$(run_vpn_switch "145" "$test_db" openvpn start isolation-test-ovpn 2>&1)
  local ovpn_session=$(extract_session_id "$ovpn_output")
  local ovpn_connect="$test_db/.session/$ovpn_session/connect.sh"

  # Verify both use same phase list but different commands
  assert_phase_order "$wg_connect" "firewall vpn dns" "WG: Default phase order"
  assert_phase_order "$ovpn_connect" "firewall vpn dns" "OVPN: Default phase order"

  # Verify protocol isolation (WireGuard commands only in WG script)
  assert_script_contains "$wg_connect" "wg-quick up" "WG: Has WireGuard command"
  assert_script_not_contains "$wg_connect" "openvpn" "WG: No OpenVPN commands"

  # Verify protocol isolation (OpenVPN commands only in OVPN script)
  assert_script_contains "$ovpn_connect" "openvpn" "OVPN: Has OpenVPN command"
  assert_script_not_contains "$ovpn_connect" "wg-quick" "OVPN: No WireGuard commands"

  #---------------------------------------------------------------------------
  # Test 2: Protocol-specific context variables
  #---------------------------------------------------------------------------

  # WireGuard should have WireGuard-specific context
  assert_script_contains "$wg_connect" "protocol=\"wireguard\"" "WG: Correct protocol variable"
  assert_script_contains "$wg_connect" "interface=\"wg" "WG: WireGuard interface naming"
  assert_script_contains "$wg_connect" "dns_server=" "WG: Has dns_server context"
  assert_script_contains "$wg_connect" "remote_ips=" "WG: Has remote_ips context"

  # OpenVPN should have OpenVPN-specific context
  assert_script_contains "$ovpn_connect" "protocol=\"openvpn\"" "OVPN: Correct protocol variable"
  assert_script_contains "$ovpn_connect" "interface=\"tun" "OVPN: OpenVPN interface naming"

  # Stop before next test
  run_vpn_switch "146" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 3: Custom phase isolation (WireGuard only)
  #---------------------------------------------------------------------------

  # Create custom phase for WireGuard only
  create_custom_phase "$test_db" "wireguard" "wg_routing"

  # Configure WireGuard to use custom phase
  run_vpn_switch "147" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns wg_routing" > /dev/null 2>&1

  # Start WireGuard with custom phase
  local wg_custom_output=$(run_vpn_switch "148" "$test_db" wireguard start isolation-test-wg 2>&1)
  local wg_custom_session=$(extract_session_id "$wg_custom_output")
  local wg_custom_connect="$test_db/.session/$wg_custom_session/connect.sh"

  # Reset to default phases before starting OpenVPN: wg_routing is a
  # WireGuard-only phase, and under strict resolution OpenVPN must not be handed
  # a phase it cannot resolve (it would abort, by design — no silent skipping).
  run_vpn_switch "148b" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns" > /dev/null 2>&1

  # Start OpenVPN without custom phase
  local ovpn_default_output=$(run_vpn_switch "149" "$test_db" openvpn start isolation-test-ovpn 2>&1)
  local ovpn_default_session=$(extract_session_id "$ovpn_default_output")
  local ovpn_default_connect="$test_db/.session/$ovpn_default_session/connect.sh"

  # Verify WireGuard has custom phase
  assert_script_contains "$wg_custom_connect" "verify_wg_routing()" "WG custom: Has custom phase"
  assert_phase_order "$wg_custom_connect" "firewall vpn dns wg_routing" "WG custom: Has custom phase in order"

  # Verify OpenVPN does NOT have WireGuard custom phase
  assert_script_not_contains "$ovpn_default_connect" "wg_routing" "OVPN: No WireGuard custom phase"
  assert_phase_order "$ovpn_default_connect" "firewall vpn dns" "OVPN: Default phases only"

  # Stop before next test
  run_vpn_switch "150" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 4: Custom phase isolation (OpenVPN only)
  #---------------------------------------------------------------------------

  # Create custom phase for OpenVPN only
  create_custom_phase "$test_db" "openvpn" "ovpn_logging"

  # Reset WireGuard to default
  run_vpn_switch "151" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns" > /dev/null 2>&1

  # Create OpenVPN-specific phase config
  run_vpn_switch "152" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT_OPENVPN "firewall vpn dns ovpn_logging" > /dev/null 2>&1

  # Start WireGuard (default phases)
  local wg_default2_output=$(run_vpn_switch "153" "$test_db" wireguard start isolation-test-wg 2>&1)
  local wg_default2_session=$(extract_session_id "$wg_default2_output")
  local wg_default2_connect="$test_db/.session/$wg_default2_session/connect.sh"

  # Start OpenVPN (custom phases)
  local ovpn_custom_output=$(run_vpn_switch "154" "$test_db" openvpn start isolation-test-ovpn 2>&1)
  local ovpn_custom_session=$(extract_session_id "$ovpn_custom_output")
  local ovpn_custom_connect="$test_db/.session/$ovpn_custom_session/connect.sh"

  # Note: Protocol-specific phase configs (VPN_SWITCH_PHASES_CONNECT_OPENVPN)
  # are not implemented yet, so this test checks current behavior
  # For now, both protocols use the same VPN_SWITCH_PHASES_CONNECT

  # Verify WireGuard does NOT have OpenVPN custom phase
  assert_script_not_contains "$wg_default2_connect" "ovpn_logging" "WG: No OpenVPN custom phase"
  assert_phase_order "$wg_default2_connect" "firewall vpn dns" "WG: Default phases only"

  # OpenVPN should NOT have custom phase yet (feature not implemented)
  # This documents current behavior - both use same VPN_SWITCH_PHASES_CONNECT
  assert_script_not_contains "$ovpn_custom_connect" "ovpn_logging" "OVPN: Protocol-specific phase config not yet supported"
  assert_phase_order "$ovpn_custom_connect" "firewall vpn dns" "OVPN: Uses default phases (protocol-specific config not implemented)"

  # Stop before next test
  run_vpn_switch "155" "$test_db" stop > /dev/null 2>&1

  #---------------------------------------------------------------------------
  # Test 5: Simultaneous multi-protocol operation
  #---------------------------------------------------------------------------

  # Reset to default phases for both
  run_vpn_switch "156" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns" > /dev/null 2>&1

  # Start both protocols with different test interfaces
  local wg_sim_output=$(run_vpn_switch "157" "$test_db" wireguard start isolation-test-wg 2>&1)
  local wg_sim_session=$(extract_session_id "$wg_sim_output")

  # Don't stop between - test simultaneous sessions
  local ovpn_sim_output=$(run_vpn_switch "158" "$test_db" openvpn start isolation-test-ovpn 2>&1)
  local ovpn_sim_session=$(extract_session_id "$ovpn_sim_output")

  # Verify both sessions exist
  assert_file_exists "$test_db/.session/$wg_sim_session/connect.sh" "Simultaneous: WG session exists"
  assert_file_exists "$test_db/.session/$ovpn_sim_session/connect.sh" "Simultaneous: OVPN session exists"

  # Verify sessions are different
  if [ "$wg_sim_session" != "$ovpn_sim_session" ]; then
    pass "Simultaneous: Different session IDs"
  else
    fail "Simultaneous: Session IDs" "Different" "Same: $wg_sim_session"
  fi

  # Verify both have correct protocols
  local wg_protocol=$(cat "$test_db/.session/$wg_sim_session/protocol" 2>/dev/null)
  local ovpn_protocol=$(cat "$test_db/.session/$ovpn_sim_session/protocol" 2>/dev/null)

  if [ "$wg_protocol" = "wireguard" ]; then
    pass "Simultaneous: WG has correct protocol"
  else
    fail "Simultaneous: WG protocol" "wireguard" "$wg_protocol"
  fi

  if [ "$ovpn_protocol" = "openvpn" ]; then
    pass "Simultaneous: OVPN has correct protocol"
  else
    fail "Simultaneous: OVPN protocol" "openvpn" "$ovpn_protocol"
  fi

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$wg_config" "$ovpn_config"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 11: Multi-Protocol Edge Case Testing
#-----------------------------------------------------------------------------
# As a power user running complex multi-protocol VPN configurations,
# I want vpn-switch to handle edge cases robustly (rapid switching, stress,
# protocol interactions) so my VPN infrastructure remains stable.
#
# Acceptance criteria:
# - Protocol switching works cleanly (no resource leaks)
# - Session resumption works across protocols
# - Dump/restore handles active sessions correctly
# - Stress scenarios (rapid creation/deletion) remain stable
# - No cross-protocol contamination

user_story_11_multi_protocol_edge_cases() {
  story 11 "Multi-Protocol Edge Cases (Stress, Switching, Robustness)"

  # Setup
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-edge-cases.XXXXXX")
  story_db "$test_db"
  bootstrap_database "$test_db"

  # Prevent actual execution of connect/disconnect scripts (just generate them)
  set_function_interpreter "$test_db" "wireguard_connect1" "cat"
  set_function_interpreter "$test_db" "wireguard_disconnect1" "cat"
  set_function_interpreter "$test_db" "openvpn_connect1" "cat"
  set_function_interpreter "$test_db" "openvpn_disconnect1" "cat"

  # Setup: Create test configs for all scenarios
  local wg_config=$(create_test_config "edge-test-wg.conf")
  local ovpn_config=$(create_test_ovpn_config "edge-test-ovpn.ovpn")
  run_vpn_switch "skip" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1
  run_vpn_switch "skip" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1
  #---------------------------------------------------------------------------
  # Scenario 1.1: Conflicting Custom Phase Lists (Priority 1)
  #---------------------------------------------------------------------------

  # Create test configs
  local wg_config=$(create_test_config "edge-test-wg.conf")
  local ovpn_config=$(create_test_ovpn_config "edge-test-ovpn.ovpn")

  # Test: Duplicate import should be rejected
  if run_vpn_switch "159" "$test_db" wireguard import "$wg_config" > /dev/null 2>&1; then
    fail "Scenario 1.1: Duplicate WG import should be rejected" "rejected" "accepted"
  else
    pass "Scenario 1.1: Duplicate WG import correctly rejected"
  fi

  if run_vpn_switch "160" "$test_db" openvpn import "$ovpn_config" > /dev/null 2>&1; then
    fail "Scenario 1.1: Duplicate OVPN import should be rejected" "rejected" "accepted"
  else
    pass "Scenario 1.1: Duplicate OVPN import correctly rejected"
  fi

  # Configure CONFLICTING custom phases for each protocol
  run_vpn_switch "161" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "prologue firewall vpn dns wg_custom" > /dev/null 2>&1

  # Create custom WireGuard-only phase
  create_custom_phase "$test_db" "wireguard" "wg_custom"

  # Create custom OpenVPN-only phase
  create_custom_phase "$test_db" "openvpn" "ovpn_custom"

  # Configure the phase list PER protocol before each start. wg_custom resolves
  # only for WireGuard and ovpn_custom only for OpenVPN; under strict resolution
  # each protocol must be handed only phases it can resolve (a cross-protocol
  # phase aborts the start, by design). This still verifies isolation — each
  # connect.sh ends up with its own custom phase and not the other's.
  run_vpn_switch "162" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns wg_custom" > /dev/null 2>&1

  # Start WireGuard session (WireGuard-only custom phase)
  local wg_output=$(run_vpn_switch "163" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg_pid=$(extract_session_id "$wg_output")

  run_vpn_switch "164a" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns ovpn_custom" > /dev/null 2>&1

  # Start OpenVPN session (OpenVPN-only custom phase)
  local ovpn_output=$(run_vpn_switch "164" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn_pid=$(extract_session_id "$ovpn_output")

  # Verify WireGuard includes wg_custom but NOT ovpn_custom
  assert_script_contains "$test_db/.session/$wg_pid/connect.sh" "wg_custom" "Scenario 1.1: WG includes wg_custom phase"
  assert_script_not_contains "$test_db/.session/$wg_pid/connect.sh" "ovpn_custom" "Scenario 1.1: WG excludes ovpn_custom phase"

  # Verify OpenVPN includes ovpn_custom but NOT wg_custom
  assert_script_contains "$test_db/.session/$ovpn_pid/connect.sh" "ovpn_custom" "Scenario 1.1: OVPN includes ovpn_custom phase"
  assert_script_not_contains "$test_db/.session/$ovpn_pid/connect.sh" "wg_custom" "Scenario 1.1: OVPN excludes wg_custom phase"

  # Verify phase order differs
  local wg_order=$(grep -o 'Phase: [a-z_]* (' "$test_db/.session/$wg_pid/connect.sh" 2>/dev/null | sed 's/Phase: \([a-z_]*\) (/\1/' | uniq | tr '\n' ' ' | sed 's/ $//')
  local ovpn_order=$(grep -o 'Phase: [a-z_]* (' "$test_db/.session/$ovpn_pid/connect.sh" 2>/dev/null | sed 's/Phase: \([a-z_]*\) (/\1/' | uniq | tr '\n' ' ' | sed 's/ $//')

  if [ "$wg_order" != "$ovpn_order" ]; then
    pass "Scenario 1.1: Phase order differs between protocols"
  else
    fail "Scenario 1.1: Phase order" "Different" "Same: $wg_order"
  fi

  # Stop and clean before next test
  run_vpn_switch "165" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "165-clean" "$test_db" session clean > /dev/null 2>&1
  sleep 1  # Brief pause for cleanup

  #---------------------------------------------------------------------------
  # Scenario 1.2: Different Interface Names Under Load
  #---------------------------------------------------------------------------

  # Start with clean state (remove any preserved sessions from previous scenario)
  # Remove 'latest' link first so previous sessions become unreferenced
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "165a" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "165a-clean" "$test_db" session clean > /dev/null 2>&1

  # Reset to default phases
  run_vpn_switch "166" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT "firewall vpn dns" > /dev/null 2>&1

  # Verify test-specific interfaces are configured (should be from bootstrap)
  local wg_iface_setting=$(run_vpn_switch "167" "$test_db" printenv 2>/dev/null | grep VPN_SWITCH_INTERFACE_wireguard | cut -d= -f2)
  local ovpn_iface_setting=$(run_vpn_switch "168" "$test_db" printenv 2>/dev/null | grep VPN_SWITCH_INTERFACE_openvpn | cut -d= -f2)

  if echo "$wg_iface_setting" | grep -q "wg_test"; then
    pass "Scenario 1.2: WireGuard test interface configured"
  else
    fail "Scenario 1.2: WG interface" "wg_test*" "$wg_iface_setting"
  fi

  if echo "$ovpn_iface_setting" | grep -q "tun_test"; then
    pass "Scenario 1.2: OpenVPN test interface configured"
  else
    fail "Scenario 1.2: OVPN interface" "tun_test*" "$ovpn_iface_setting"
  fi

  # Start both protocols (overwrites 'latest' links from Scenario 1.1)
  local wg2_output=$(run_vpn_switch "169" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg2_pid=$(extract_session_id "$wg2_output")

  local ovpn2_output=$(run_vpn_switch "170" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn2_pid=$(extract_session_id "$ovpn2_output")

  # Verify two sessions exist
  local session_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$session_count" = "2" ]; then
    pass "Scenario 1.2: Two sessions active simultaneously"
  else
    fail "Scenario 1.2: Session count" "2" "$session_count"
  fi

  # Verify different interfaces in metadata
  local wg2_iface=$(cat "$test_db/.session/$wg2_pid/interface" 2>/dev/null)
  local ovpn2_iface=$(cat "$test_db/.session/$ovpn2_pid/interface" 2>/dev/null)

  if echo "$wg2_iface" | grep -q "wg_test"; then
    pass "Scenario 1.2: WG session uses wg_test* interface"
  else
    fail "Scenario 1.2: WG interface" "wg_test*" "$wg2_iface"
  fi

  if echo "$ovpn2_iface" | grep -q "tun_test"; then
    pass "Scenario 1.2: OVPN session uses tun_test* interface"
  else
    fail "Scenario 1.2: OVPN interface" "tun_test*" "$ovpn2_iface"
  fi

  # Stop both and verify cleanup
  run_vpn_switch "171" "$test_db" stop > /dev/null 2>&1
  sleep 1

  # Clean up unreferenced sessions (new explicit cleanup model)
  run_vpn_switch "171a" "$test_db" session clean > /dev/null 2>&1

  # After stop + clean, only 'latest' session is preserved (most recent = OVPN)
  # The global session/latest link points to most recent session across all protocols
  local stopped_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$stopped_count" = "1" ]; then
    pass "Scenario 1.2: Unreferenced sessions cleaned, 'latest' preserved"
  else
    fail "Scenario 1.2: Cleanup" "1 session (latest)" "$stopped_count sessions remain"
  fi

  #---------------------------------------------------------------------------
  # Scenario 1.3: Overlapping DNS Servers (Conflict Detection)
  #---------------------------------------------------------------------------

  # Both protocols will use same DNS (10.2.0.1 from test configs)
  # This tests if DNS phase handles this gracefully

  local wg3_output=$(run_vpn_switch "172" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg3_pid=$(extract_session_id "$wg3_output")

  local ovpn3_output=$(run_vpn_switch "173" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn3_pid=$(extract_session_id "$ovpn3_output")

  # Verify both sessions have DNS configuration
  assert_script_contains "$test_db/.session/$wg3_pid/connect.sh" "10.2.0.1" "Scenario 1.3: WG session has DNS config"

  # OpenVPN test config doesn't have explicit DNS, so it may not have 10.2.0.1
  # Just verify the scripts are different
  if ! cmp -s "$test_db/.session/$wg3_pid/connect.sh" "$test_db/.session/$ovpn3_pid/connect.sh"; then
    pass "Scenario 1.3: Connect scripts differ despite same DNS"
  else
    fail "Scenario 1.3: Script uniqueness" "Different scripts" "Identical scripts"
  fi

  # Stop before next test
  run_vpn_switch "174" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "174a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 2.1: Fast Protocol Switch (Priority 1)
  #---------------------------------------------------------------------------

  # Start WireGuard
  local wg_switch_output=$(run_vpn_switch "175" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg_switch_pid=$(extract_session_id "$wg_switch_output")

  assert_dir_exists "$test_db/.session/$wg_switch_pid" "Scenario 2.1: WG session created"

  # Stop WireGuard immediately
  run_vpn_switch "176" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "176a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  # Verify 'latest' session preserved
  local after_stop_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$after_stop_count" = "1" ]; then
    pass "Scenario 2.1: WG session stopped, 'latest' preserved"
  else
    fail "Scenario 2.1: After stop" "1 session (latest)" "$after_stop_count sessions"
  fi

  # Start OpenVPN immediately
  local ovpn_switch_output=$(run_vpn_switch "177" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn_switch_pid=$(extract_session_id "$ovpn_switch_output")

  assert_dir_exists "$test_db/.session/$ovpn_switch_pid" "Scenario 2.1: OVPN session created after WG stop"

  # Verify it's OpenVPN protocol
  local switched_protocol=$(cat "$test_db/.session/$ovpn_switch_pid/protocol" 2>/dev/null)
  if [ "$switched_protocol" = "openvpn" ]; then
    pass "Scenario 2.1: New session is OpenVPN protocol"
  else
    fail "Scenario 2.1: Protocol" "openvpn" "$switched_protocol"
  fi

  # Stop and clean before next test
  run_vpn_switch "178" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "178a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 2.2: Protocol Switch Without Stop (Error Handling)
  #---------------------------------------------------------------------------

  # Clean slate for this scenario
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "178b" "$test_db" session clean > /dev/null 2>&1

  # Start WireGuard
  local wg_noStop_output=$(run_vpn_switch "179" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg_noStop_pid=$(extract_session_id "$wg_noStop_output")

  # Start OpenVPN (without stopping WireGuard)
  local ovpn_noStop_output=$(run_vpn_switch "180" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn_noStop_pid=$(extract_session_id "$ovpn_noStop_output")

  # Verify both sessions coexist (this is supported behavior)
  local coexist_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$coexist_count" = "2" ]; then
    pass "Scenario 2.2: Both sessions coexist"
  else
    fail "Scenario 2.2: Session count" "2" "$coexist_count"
  fi

  # Verify different protocols
  local wg_proto=$(cat "$test_db/.session/$wg_noStop_pid/protocol" 2>/dev/null)
  local ovpn_proto=$(cat "$test_db/.session/$ovpn_noStop_pid/protocol" 2>/dev/null)

  if [ "$wg_proto" = "wireguard" ] && [ "$ovpn_proto" = "openvpn" ]; then
    pass "Scenario 2.2: Both protocols running simultaneously"
  else
    fail "Scenario 2.2: Protocols" "WG+OVPN" "WG=$wg_proto, OVPN=$ovpn_proto"
  fi

  # Stop and clean before next test
  run_vpn_switch "181" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "181a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 2.3: Alternating Protocol Starts (Stress Test)
  #---------------------------------------------------------------------------

  # Clean slate for stress test
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "181b" "$test_db" session clean > /dev/null 2>&1

  # Cycle 1: WireGuard
  run_vpn_switch "182" "$test_db" wireguard start edge-test-wg > /dev/null 2>&1
  local cycle1_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$cycle1_count" = "1" ]; then
    pass "Scenario 2.3: Cycle 1 - WG started"
  else
    fail "Scenario 2.3: Cycle 1 start" "1 session" "$cycle1_count sessions"
  fi

  run_vpn_switch "183" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "183a" "$test_db" session clean > /dev/null 2>&1
  sleep 1
  local cycle1_stop=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$cycle1_stop" = "1" ]; then
    pass "Scenario 2.3: Cycle 1 - WG stopped, 'latest' preserved"
  else
    fail "Scenario 2.3: Cycle 1 stop" "1 session (latest)" "$cycle1_stop sessions"
  fi

  # Cycle 2: OpenVPN (creates new session, old 'latest' becomes unreferenced)
  run_vpn_switch "184" "$test_db" openvpn start edge-test-ovpn > /dev/null 2>&1
  local cycle2_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$cycle2_count" = "2" ]; then
    pass "Scenario 2.3: Cycle 2 - OVPN started (old session + new)"
  else
    fail "Scenario 2.3: Cycle 2 start" "2 sessions" "$cycle2_count sessions"
  fi

  run_vpn_switch "185" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "185a" "$test_db" session clean > /dev/null 2>&1
  sleep 1
  local cycle2_stop=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$cycle2_stop" = "1" ]; then
    pass "Scenario 2.3: Cycle 2 - OVPN stopped, unreferenced cleaned"
  else
    fail "Scenario 2.3: Cycle 2 stop" "1 session (latest)" "$cycle2_stop sessions"
  fi

  # Cycle 3: WireGuard again (creates new session, old OVPN becomes unreferenced)
  run_vpn_switch "186" "$test_db" wireguard start edge-test-wg > /dev/null 2>&1
  local cycle3_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$cycle3_count" = "2" ]; then
    pass "Scenario 2.3: Cycle 3 - WG started (old session + new)"
  else
    fail "Scenario 2.3: Cycle 3 start" "2 sessions" "$cycle3_count sessions"
  fi

  # Verify both sessions exist (old unreferenced + new 'latest')
  local stale_count=$(find "$test_db/.session" -type f -name "protocol" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$stale_count" = "2" ]; then
    pass "Scenario 2.3: Both sessions present (unreferenced + latest)"
  else
    fail "Scenario 2.3: Session count" "2 total" "$stale_count found"
  fi

  # Stop and clean before next test
  run_vpn_switch "187" "$test_db" stop > /dev/null 2>&1
  run_vpn_switch "187a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 3.1: Resume WireGuard After OpenVPN Active
  #---------------------------------------------------------------------------

  # Clean slate for resume tests
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "187b" "$test_db" session clean > /dev/null 2>&1

  # Start and save WireGuard session
  run_vpn_switch "188" "$test_db" wireguard start edge-test-wg > /dev/null 2>&1
  run_vpn_switch "189" "$test_db" session save wg_work > /dev/null 2>&1

  assert_symlink_exists "$test_db/session/wg_work" "Scenario 3.1: WG session saved as wg_work"

  # Stop WireGuard (session stays referenced via wg_work link)
  run_vpn_switch "190" "$test_db" stop > /dev/null 2>&1
  sleep 1

  # Start OpenVPN
  local ovpn_resume_output=$(run_vpn_switch "191" "$test_db" openvpn start edge-test-ovpn 2>&1)
  local ovpn_resume_pid=$(extract_session_id "$ovpn_resume_output")

  # Resume WireGuard (simultaneous)
  run_vpn_switch "192" "$test_db" session start wg_work > /dev/null 2>&1
  # Get PID from symlink target (session start doesn't output "Created session")
  local wg_resume_pid=$(readlink "$test_db/session/wg_work" | xargs basename)

  # Verify both sessions active
  local resume_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$resume_count" = "2" ]; then
    pass "Scenario 3.1: Both sessions active after resume"
  else
    fail "Scenario 3.1: Session count" "2" "$resume_count"
  fi

  # Verify correct protocols
  local wg_resume_proto=$(cat "$test_db/.session/$wg_resume_pid/protocol" 2>/dev/null)
  local ovpn_resume_proto=$(cat "$test_db/.session/$ovpn_resume_pid/protocol" 2>/dev/null)

  if [ "$wg_resume_proto" = "wireguard" ] && [ "$ovpn_resume_proto" = "openvpn" ]; then
    pass "Scenario 3.1: Correct protocols after resume"
  else
    fail "Scenario 3.1: Protocols" "WG+OVPN" "WG=$wg_resume_proto, OVPN=$ovpn_resume_proto"
  fi

  # Cleanup before next test (remove all session references, then clean unreferenced sessions)
  rm -f "$test_db/session/wg_work" "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "193a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 3.2: Cross-Protocol Named Session Collision
  #---------------------------------------------------------------------------

  # WireGuard session
  local wg_coll_output=$(run_vpn_switch "194" "$test_db" wireguard start edge-test-wg 2>&1)
  local wg_coll_pid=$(extract_session_id "$wg_coll_output")
  run_vpn_switch "195" "$test_db" session save work > /dev/null 2>&1

  # Verify WireGuard session saved
  local wg_target=$(readlink "$test_db/session/work" 2>/dev/null)
  if echo "$wg_target" | grep -q "$wg_coll_pid"; then
    pass "Scenario 3.2: Session 'work' points to WG session"
  else
    fail "Scenario 3.2: WG session link" "Contains $wg_coll_pid" "$wg_target"
  fi

  # Note: In test env, sessions don't actually disconnect (interpreters=cat)
  # WireGuard session stays referenced via 'work' link

  # OpenVPN session with same name
  run_vpn_switch "197" "$test_db" openvpn start edge-test-ovpn > /dev/null 2>&1
  run_vpn_switch "198" "$test_db" session save work > /dev/null 2>&1

  # Verify OpenVPN session now saved as 'work' (overwrote)
  # Get PID from symlink target (most reliable method)
  local ovpn_target=$(readlink "$test_db/session/work" 2>/dev/null)
  local ovpn_coll_pid=$(basename "$ovpn_target")

  # Verify it's an OpenVPN session
  local ovpn_proto=$(cat "$test_db/.session/$ovpn_coll_pid/protocol" 2>/dev/null)
  if [ "$ovpn_proto" = "openvpn" ]; then
    pass "Scenario 3.2: Session 'work' now points to OVPN (overwrite)"
  else
    fail "Scenario 3.2: OVPN protocol" "openvpn" "$ovpn_proto"
  fi

  # Verify only one 'work' symlink (check existence, not ls which follows symlink)
  if [ -L "$test_db/session/work" ]; then
    pass "Scenario 3.2: Only one 'work' session exists (no duplicates)"
  else
    fail "Scenario 3.2: Work session link" "Symlink exists" "Not found or not a symlink"
  fi

  # Cleanup before next test (remove all session references, then clean unreferenced sessions)
  rm -f "$test_db/session/work" "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "199a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 4.1: Dump While Multi-Protocol Sessions Active (Priority 1)
  #---------------------------------------------------------------------------

  # Clean slate for this scenario
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "199b" "$test_db" session clean > /dev/null 2>&1

  # Start both protocols
  run_vpn_switch "200" "$test_db" wireguard start edge-test-wg > /dev/null 2>&1
  run_vpn_switch "201" "$test_db" openvpn start edge-test-ovpn > /dev/null 2>&1

  # Verify sessions active
  local dump_session_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$dump_session_count" = "2" ]; then
    pass "Scenario 4.1: Two active sessions before dump"
  else
    fail "Scenario 4.1: Pre-dump sessions" "2" "$dump_session_count"
  fi

  # Dump database
  local dump_file=$(mktemp)
  run_vpn_dump "202" "$test_db" > "$dump_file" 2>/dev/null

  # Verify dump succeeded
  if [ -s "$dump_file" ]; then
    pass "Scenario 4.1: Dump file created with active sessions"
  else
    fail "Scenario 4.1: Dump file" "Non-empty" "Empty or missing"
  fi

  # Verify dump does NOT include session data (.session/ is transient)
  if ! grep -q "\.session" "$dump_file"; then
    pass "Scenario 4.1: Dump excludes .session directory (transient)"
  else
    fail "Scenario 4.1: Session exclusion" "No .session references" "Found .session in dump"
  fi

  # Verify dump INCLUDES configs
  assert_script_contains "$dump_file" "wireguard import" "Scenario 4.1: Dump includes WG configs"
  assert_script_contains "$dump_file" "openvpn import" "Scenario 4.1: Dump includes OVPN configs"

  rm "$dump_file"

  # Cleanup before next test (remove session references, then clean unreferenced sessions)
  rm -f "$test_db/session/latest" 2>/dev/null
  run_vpn_switch "203a" "$test_db" session clean > /dev/null 2>&1
  sleep 1

  #---------------------------------------------------------------------------
  # Scenario 4.2: Restore While Sessions Active (Conflict Handling)
  #---------------------------------------------------------------------------

  # Database A: Create and start session
  local db_a=$(mktemp -d "${TMPDIR:-/tmp}/vpn-restore-a.XXXXXX")
  story_db "$db_a"
  bootstrap_database "$db_a"
  set_function_interpreter "$db_a" "wireguard_connect1" "cat"
  set_function_interpreter "$db_a" "wireguard_disconnect1" "cat"

  local wg_restore_config=$(create_test_config "restore-test.conf")
  story_attest_key "$db_a" story
  run_vpn_switch "204" "$db_a" wireguard import "$wg_restore_config" > /dev/null 2>&1
  run_vpn_switch "205" "$db_a" wireguard start restore-test > /dev/null 2>&1

  # Export the pair from A (the session is transient, the pair carries none)
  local dump_restore="$db_a/pair.sh" bundle_restore="$db_a/pair.tar.gz"
  run_vpn_switch "206" "$db_a" export full "$dump_restore" "$bundle_restore" > /dev/null 2>&1

  # Database B: Bootstrap then import
  local db_b=$(mktemp -d "${TMPDIR:-/tmp}/vpn-restore-b.XXXXXX")
  story_db "$db_b"
  bootstrap_database "$db_b"
  story_attest_key "$db_b" story
  run_vpn_switch "207" "$db_b" import "$dump_restore" "$bundle_restore" > /dev/null 2>&1

  # Verify B has configs
  assert_file_exists "$db_b/wireguard/restore-test.conf" "Scenario 4.2: Restored database has WG config"

  # Verify B has NO active sessions
  local restored_session_count=$(ls -1 "$db_b/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$restored_session_count" = "0" ]; then
    pass "Scenario 4.2: Restored database has no active sessions"
  else
    fail "Scenario 4.2: Restored sessions" "0" "$restored_session_count"
  fi

  # Cleanup
  rm "$dump_restore" "$wg_restore_config"
  rm -rf "$db_a" "$db_b"

  #---------------------------------------------------------------------------
  # Scenario 5.1: Rapid Session Creation (Resource Cleanup - Priority 2)
  #---------------------------------------------------------------------------

  # Create/destroy 10 WireGuard sessions
  local stress_cycle
  for stress_cycle in 1 2 3 4 5 6 7 8 9 10; do
    run_vpn_switch "207" "$test_db" wireguard start edge-test-wg > /dev/null 2>&1
    # Each start updates 'latest', making previous session unreferenced
    # Clean unreferenced sessions (avoids stop which hangs in test env)
    run_vpn_switch "208a" "$test_db" session clean > /dev/null 2>&1
    sleep 0.5  # Brief pause between cycles
  done

  # Verify clean state
  local stress_wg_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$stress_wg_count" = "1" ]; then
    pass "Scenario 5.1: Only 'latest' WG session after 10 cycles"
  else
    fail "Scenario 5.1: WG cleanup" "1 session (latest)" "$stress_wg_count sessions"
  fi

  # Create/destroy 10 OpenVPN sessions
  for stress_cycle in 1 2 3 4 5 6 7 8 9 10; do
    run_vpn_switch "209" "$test_db" openvpn start edge-test-ovpn > /dev/null 2>&1
    # Each start updates 'latest', making previous session unreferenced
    # Clean unreferenced sessions (avoids stop which hangs in test env)
    run_vpn_switch "210a" "$test_db" session clean > /dev/null 2>&1
    sleep 0.5
  done

  # Verify clean state
  local stress_ovpn_count=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$stress_ovpn_count" = "1" ]; then
    pass "Scenario 5.1: Only 'latest' OVPN session after 10 cycles"
  else
    fail "Scenario 5.1: OVPN cleanup" "1 session (latest)" "$stress_ovpn_count sessions"
  fi

  # Verify only latest session directory remains (no other stale sessions)
  local session_dirs=$(ls -1 "$test_db/.session" 2>/dev/null | wc -l | tr -d ' ')
  if [ "$session_dirs" = "1" ]; then
    pass "Scenario 5.1: Only latest session directory remains"
  else
    fail "Scenario 5.1: Stale sessions" "1" "$session_dirs found"
  fi

  # Cleanup
  cleanup_on_success "$test_db"
  rm -f "$wg_config" "$ovpn_config"

  story_passed
}

#---------------------------------------------------------------------------
# User Story 12: Phase Backend Selection
#---------------------------------------------------------------------------
#
# As a user,
# I want to specify different backend implementations for phases,
# So that I can customize firewall and DNS providers per-phase or per-session
#
# Acceptance Criteria:
#   1. Basic backend syntax works (phase:variant@profile)
#   2. Different backends can be selected for different phases
#   3. Per-phase profile selection works
#   4. Backward compatibility: plain phase names still work
#   5. Generated scripts use the correct backend implementations
#
# Test Database: /tmp/vpn-backend-selection.XXX
#
user_story_12_phase_backend_selection() {
  story 12 "Phase Backend Selection (Strategy 7 - Automatic Template Lifting)"

  # Create exclusive test database
  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-backend-selection.XXXXXX")
  story_db "$test_db"

  # Initialize database with minimal profile
  run_vpn_switch "211" "$test_db" bootstrap "$test_db" minimal > /dev/null 2>&1

  # Set VPN_SWITCH_PATH for generated scripts (connect.sh/disconnect.sh)
  run_vpn_switch "211b" "$test_db" setenv VPN_SWITCH_PATH \
    "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin" > /dev/null 2>&1

  # Enable session management functions (create directories, write metadata)
  # These are safe operations that don't affect system state
  run_vpn_switch "211c" "$test_db" setenv VPN_SWITCH_INTERPRETER_session_create1 sh > /dev/null 2>&1
  run_vpn_switch "211e" "$test_db" setenv VPN_SWITCH_INTERPRETER_interface_destroy cat > /dev/null 2>&1
  run_vpn_switch "211d" "$test_db" setenv VPN_SWITCH_INTERPRETER_session_populate3 sh > /dev/null 2>&1

  # Verify backend templates were lifted during init
  assert_file_exists "$test_db/.include/phase/base/firewall/pf.sh" \
    "Backend template lifted: firewall/pf.sh"
  assert_file_exists "$test_db/.include/phase/base/firewall/ipfw.sh" \
    "Backend template lifted: firewall/ipfw.sh"
  assert_file_exists "$test_db/.include/phase/base/dns/resolvconf.sh" \
    "Backend template lifted: dns/resolvconf.sh"
  assert_file_exists "$test_db/.include/phase/base/dns/unbound.sh" \
    "Backend template lifted: dns/unbound.sh"

  # Create test config
  local test_config="$test_db/wireguard/backend-test.conf"
  cat > "$test_config" <<'EOF'
[Interface]
PrivateKey = BACKEND_TEST_KEY
Address = 10.99.99.1/24

[Peer]
PublicKey = BACKEND_TEST_PEER
Endpoint = 1.2.3.4:51820
AllowedIPs = 0.0.0.0/0
EOF
  chmod 0400 "$test_config"

  #---------------------------------------------------------------------------
  # Scenario 1: Basic Backend Selection (pf firewall, resolvconf DNS)
  #---------------------------------------------------------------------------

  # Set explicit backend selection using :variant@profile syntax
  run_vpn_switch "212" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall:pf@base vpn dns:resolvconf@base" > /dev/null 2>&1

  # Start session
  local basic_output=$(run_vpn_switch "213" "$test_db" wireguard start backend-test 2>&1)
  local basic_pid=$(extract_session_id "$basic_output")

  # Verify connect script was generated
  assert_file_exists "$test_db/.session/$basic_pid/connect.sh" \
    "Scenario 1: Connect script generated"

  # Verify PF firewall backend is used (should contain pfctl commands)
  local connect_script="$test_db/.session/$basic_pid/connect.sh"
  if grep -q "pfctl" "$connect_script"; then
    pass "Scenario 1: PF firewall backend used (contains pfctl)"
  else
    fail "Scenario 1: PF backend" "Contains pfctl" "$(head -20 $connect_script)"
  fi

  # Verify resolvconf DNS backend is used
  if grep -q "resolvconf" "$connect_script"; then
    pass "Scenario 1: Resolvconf DNS backend used"
  else
    fail "Scenario 1: Resolvconf backend" "Contains resolvconf" "$(tail -20 $connect_script)"
  fi

  #---------------------------------------------------------------------------
  # Scenario 2: Different Backend (ipfw firewall, unbound DNS)
  #---------------------------------------------------------------------------

  # Set different backends
  run_vpn_switch "214" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall:ipfw@base vpn dns:unbound@base" > /dev/null 2>&1

  # Start new session
  local ipfw_output=$(run_vpn_switch "215" "$test_db" wireguard start backend-test 2>&1)
  local ipfw_pid=$(extract_session_id "$ipfw_output")

  # Verify IPFW firewall backend is used
  local ipfw_connect="$test_db/.session/$ipfw_pid/connect.sh"
  if grep -q "ipfw" "$ipfw_connect"; then
    pass "Scenario 2: IPFW firewall backend used (contains ipfw)"
  else
    fail "Scenario 2: IPFW backend" "Contains ipfw" "$(head -20 $ipfw_connect)"
  fi

  # Verify Unbound DNS backend is used
  if grep -q "unbound" "$ipfw_connect"; then
    pass "Scenario 2: Unbound DNS backend used"
  else
    fail "Scenario 2: Unbound backend" "Contains unbound" "$(tail -20 $ipfw_connect)"
  fi

  # Verify PF firewall phase is NOT used (backend isolation)
  # Check that the firewall phase uses ipfw, not PF tables/macros
  if ! grep -q "/etc/pf.tables" "$ipfw_connect" && ! grep -q "/etc/pf.macros" "$ipfw_connect"; then
    pass "Scenario 2: PF firewall phase not used (backend isolated)"
  else
    fail "Scenario 2: Backend isolation" "No PF tables/macros" "Found PF configuration in ipfw session"
  fi

  #---------------------------------------------------------------------------
  # Scenario 3: Backward Compatibility (Plain Phase Names)
  #---------------------------------------------------------------------------

  # Reset to traditional phase list (no backend syntax)
  run_vpn_switch "216" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall vpn dns" > /dev/null 2>&1

  # Start session with plain phases
  local compat_output=$(run_vpn_switch "217" "$test_db" wireguard start backend-test 2>&1)
  local compat_pid=$(extract_session_id "$compat_output")

  # Verify connect script generated successfully
  assert_file_exists "$test_db/.session/$compat_pid/connect.sh" \
    "Scenario 3: Backward compat - connect script generated"

  # Verify traditional protocol-specific scripts are used
  local compat_connect="$test_db/.session/$compat_pid/connect.sh"
  if [ -s "$compat_connect" ]; then
    pass "Scenario 3: Backward compat - script not empty"
  else
    fail "Scenario 3: Script generation" "Non-empty script" "Empty or missing"
  fi

  # Verify script references protocol-specific phase directory
  if grep -q "\.include/phase/wireguard" "$compat_connect"; then
    pass "Scenario 3: Uses protocol-specific phases (wireguard/)"
  else
    fail "Scenario 3: Protocol phases" "References wireguard/" "$(head -10 $compat_connect)"
  fi

  #---------------------------------------------------------------------------
  # Scenario 4: Per-Phase Profile Selection
  #---------------------------------------------------------------------------

  # Note: This scenario would require creating a custom profile directory
  # For now, we verify that the @profile syntax is parsed correctly
  # by using the same profile (base) for different phases

  run_vpn_switch "218" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall:pf@base vpn dns:resolvconf@base" > /dev/null 2>&1

  local profile_output=$(run_vpn_switch "219" "$test_db" wireguard start backend-test 2>&1)
  local profile_pid=$(extract_session_id "$profile_output")

  # Verify both backends from 'base' profile are used
  local profile_connect="$test_db/.session/$profile_pid/connect.sh"
  local pf_found=0
  local resolvconf_found=0

  if grep -q "pfctl" "$profile_connect"; then
    pf_found=1
  fi
  if grep -q "resolvconf" "$profile_connect"; then
    resolvconf_found=1
  fi

  if [ "$pf_found" = "1" ] && [ "$resolvconf_found" = "1" ]; then
    pass "Scenario 4: Both backends from @base profile used"
  else
    fail "Scenario 4: Profile resolution" "pf=1 resolvconf=1" "pf=$pf_found resolvconf=$resolvconf_found"
  fi

  #---------------------------------------------------------------------------
  # Scenario 5: Backend Variant Fallback (variant-only syntax)
  #---------------------------------------------------------------------------

  # Use :variant without @profile (should use default profile)
  run_vpn_switch "220" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall:pf vpn dns:unbound" > /dev/null 2>&1

  local variant_output=$(run_vpn_switch "221" "$test_db" wireguard start backend-test 2>&1)
  local variant_pid=$(extract_session_id "$variant_output")

  # Verify backends are resolved (default profile should be used)
  local variant_connect="$test_db/.session/$variant_pid/connect.sh"
  local pf_found=0
  local unbound_found=0

  if grep -q "pfctl" "$variant_connect"; then
    pf_found=1
  fi
  if grep -q "unbound" "$variant_connect"; then
    unbound_found=1
  fi

  if [ "$pf_found" = "1" ] && [ "$unbound_found" = "1" ]; then
    pass "Scenario 5: Variant-only syntax works (uses default profile)"
  else
    fail "Scenario 5: Variant fallback" "pf=1 unbound=1" "pf=$pf_found unbound=$unbound_found"
  fi

  #---------------------------------------------------------------------------
  # Scenario 6: Mixed Syntax (backend + traditional)
  #---------------------------------------------------------------------------

  # Mix backend syntax with traditional phase names
  run_vpn_switch "222" "$test_db" setenv VPN_SWITCH_PHASES_CONNECT \
    "firewall:ipfw@base vpn dns" > /dev/null 2>&1

  local mixed_output=$(run_vpn_switch "223" "$test_db" wireguard start backend-test 2>&1)
  local mixed_pid=$(extract_session_id "$mixed_output")

  # Verify IPFW backend is used for firewall
  local mixed_connect="$test_db/.session/$mixed_pid/connect.sh"
  if grep -q "ipfw" "$mixed_connect"; then
    pass "Scenario 6: Mixed syntax - ipfw backend used"
  else
    fail "Scenario 6: IPFW in mixed" "Contains ipfw" "$(head -20 $mixed_connect)"
  fi

  # Verify VPN phase executed
  if grep -q "wg-quick" "$mixed_connect" || grep -q "interface" "$mixed_connect"; then
    pass "Scenario 6: Mixed syntax - vpn phase executed"
  else
    fail "Scenario 6: VPN phase" "Contains wg-quick or interface" "Missing"
  fi

  # Verify traditional DNS phase used (protocol-specific)
  # Should use .include/phase/wireguard/connect/dns.sh
  if grep -q "dns" "$mixed_connect"; then
    pass "Scenario 6: Mixed syntax - traditional dns phase used"
  else
    fail "Scenario 6: Traditional DNS" "Contains dns" "Missing"
  fi

  # Cleanup
  cleanup_on_success "$test_db"

  story_passed
}

#-----------------------------------------------------------------------------
# User Story 13: Sync Lifecycle
#-----------------------------------------------------------------------------
# As a user, after installing vpn-switch I want bootstrap to stamp my DB
# with the source version, 'vpn-switch version' to confirm db == source,
# and 'vpn-switch sync' to be idempotent so I can safely re-run it after
# every upgrade without surprises.
#
# Drives entirely through the public CLI (vpn-switch bootstrap/version/sync).

user_story_13_sync_lifecycle() {
  story 13 "Sync: Bootstrap stamps version, sync is idempotent, recovers unstamped DB"

  local test_db=$(mktemp -d "${TMPDIR:-/tmp}/vpn-integration-sync.XXXXXX")
  story_db "$test_db"

  # Step 1: Bootstrap auto-stamps .version
  bootstrap_database "$test_db"

  local version_output=$(run_vpn_switch "S13-001" "$test_db" version 2>&1)
  local db_sha=$(echo "$version_output" | grep '^db:' | awk '{print $2}')
  local src_sha=$(echo "$version_output" | grep '^source:' | awk '{print $2}')

  if echo "$version_output" | grep -qE '^db: [a-f0-9]{7,}$'; then
    pass "Bootstrap stamped DB with valid SHA"
  else
    fail "Bootstrap did not stamp DB with SHA" "^db: <sha>$" "$version_output"
  fi

  if [ -n "$db_sha" ] && [ "$db_sha" = "$src_sha" ]; then
    pass "DB version matches source version after bootstrap"
  else
    fail "DB/source version mismatch after bootstrap" "db=$src_sha" "db=$db_sha"
  fi

  # Step 2: 'vpn-switch sync' is idempotent
  run_vpn_switch "S13-002" "$test_db" sync > /dev/null 2>&1
  local version_after=$(run_vpn_switch "S13-003" "$test_db" version 2>&1)
  local db_sha_after=$(echo "$version_after" | grep '^db:' | awk '{print $2}')

  if [ "$db_sha" = "$db_sha_after" ]; then
    pass "Sync is idempotent (db version unchanged after re-sync)"
  else
    fail "Sync changed db version unexpectedly" "$db_sha" "$db_sha_after"
  fi

  # Step 3: Realistic scenario — user copies/restores a DB that has no .version
  # (e.g., from a backup made before .version existed). Sync must recover.
  rm -f "$test_db/.version"

  local unstamped_output=$(run_vpn_switch "S13-004" "$test_db" version 2>&1)
  if echo "$unstamped_output" | grep -q '^db: unstamped$'; then
    pass "version reports 'unstamped' when .version is missing"
  else
    fail "version did not detect missing .version" "db: unstamped" "$unstamped_output"
  fi

  # Step 4: 'sync' restores .version from source
  run_vpn_switch "S13-005" "$test_db" sync > /dev/null 2>&1
  local recovered=$(run_vpn_switch "S13-006" "$test_db" version 2>&1)
  local recovered_sha=$(echo "$recovered" | grep '^db:' | awk '{print $2}')

  if [ "$recovered_sha" = "$src_sha" ]; then
    pass "Sync recovered: DB version matches source after sync"
  else
    fail "Sync did not recover .version" "$src_sha" "$recovered_sha"
  fi

  # Step 4b: Regression — sync must invalidate an active env cache. build_env_args
  # reads the cache file verbatim and never re-scans, so without invalidation a
  # freshly synced default (e.g. a new interpreter override after a source
  # upgrade) would stay invisible. Same convention as _environment_init1/_setenv2.
  local cache_file="$test_db/.env/local/VPN_SWITCH_CACHE_ENV_ARGS"
  run_vpn_switch "S13-007a" "$test_db" environment cache on > /dev/null 2>&1
  if [ -f "$cache_file" ]; then
    pass "env cache enabled (cache file present)"
  else
    fail "env cache not enabled" "cache file present" "cache file absent"
  fi
  run_vpn_switch "S13-007b" "$test_db" sync > /dev/null 2>&1
  if [ ! -f "$cache_file" ]; then
    pass "Sync invalidated the active env cache (synced defaults stay visible)"
  else
    fail "Sync left a stale env cache" "cache file removed" "cache file still present"
  fi

  # Step 5: env defaults are populated (env sync ran as part of sync).
  # Verify a known default-profile variable is reachable via the public
  # getenv API (no filesystem inspection).
  local getenv_out=$(run_vpn_switch "S13-007" "$test_db" getenv VPN_SWITCH_TERMINAL_INTERPRETER 2>&1)
  if [ -n "$getenv_out" ]; then
    pass "env defaults populated (TERMINAL_INTERPRETER readable via getenv)"
  else
    fail "env defaults not populated after sync" "non-empty" "(empty)"
  fi

  cleanup_on_success "$test_db"
  story_passed
}

#-----------------------------------------------------------------------------
# User Story 14: Install lifecycle (E2E)
#-----------------------------------------------------------------------------
# As a packager (or just a careful user), I want 'gmake install' with DESTDIR
# to produce the expected file tree, and 'gmake uninstall' to clean it up
# completely. No sudo needed (DESTDIR-staged). Validates the installer
# mechanics end-to-end on the actual host.

user_story_14_install_lifecycle() {
  story 14 "Install lifecycle: gmake install DESTDIR → verify tree → uninstall → verify clean"

  local staged=$(mktemp -d "${TMPDIR:-/tmp}/vpn-install-test.XXXXXX")
  story_db "$staged"
  local install_log="$staged/install.log"
  local uninstall_log="$staged/uninstall.log"

  # Step 1: staged install
  if gmake install DESTDIR="$staged" PREFIX=/usr/local > "$install_log" 2>&1; then
    pass "gmake install DESTDIR=$staged succeeds"
  else
    fail "gmake install failed" "" "$(tail -5 "$install_log")"
    return
  fi

  # Step 2: core files present (platform-agnostic set)
  local core_files="
    /usr/local/bin/vpn-switch
    /usr/local/sbin/wg-resolvconf-up
    /usr/local/sbin/wg-resolvconf-down
    /usr/local/sbin/ovpn-resolvconf-up
    /usr/local/sbin/ovpn-resolvconf-down
    /usr/local/lib/vpn-switch/include/database.sh
    /usr/local/lib/vpn-switch/include/session.sh
    /usr/local/lib/vpn-switch/include/wireguard.sh
    /usr/local/lib/vpn-switch/include/openvpn.sh
    /usr/local/lib/vpn-switch/include/validate.sh
    /usr/local/lib/vpn-switch/template/VERSION
    /usr/local/lib/vpn-switch/template/platform/default
    /usr/local/share/doc/vpn-switch/README.md
    /usr/local/share/doc/vpn-switch/LICENSE
  "
  for path in $core_files; do
    if [ -e "$staged$path" ]; then
      pass "installed: $path"
    else
      fail "missing after install: $path"
    fi
  done

  # Step 3: platform symlink matches the detected platform.
  # Two-level detection (kernel via uname, Linux distro via /etc/os-release ID)
  # — must mirror GNUmakefile (PLATFORM) and validate.sh (_install_validate0).
  local platform_lc kernel_lc
  kernel_lc=$(uname -s | tr A-Z a-z)
  if [ "$kernel_lc" = linux ] && [ -r /etc/os-release ]; then
    case "$(. /etc/os-release 2>/dev/null; echo "$ID")" in
      alpine) platform_lc=alpine ;;
      *)      platform_lc=linux ;;
    esac
  else
    platform_lc=$kernel_lc
  fi
  local symlink_target=$(readlink "$staged/usr/local/lib/vpn-switch/template/platform/default" 2>/dev/null)
  if [ "$symlink_target" = "${platform_lc}.sh" ]; then
    pass "platform symlink correct for $platform_lc → $symlink_target"
  else
    fail "platform symlink mismatch for $platform_lc" "${platform_lc}.sh" "$symlink_target"
  fi

  # Step 4: LIBDIR sed-patched into installed script
  if grep -q 'VPN_SWITCH_LIBDIR:=/usr/local/lib/vpn-switch' "$staged/usr/local/bin/vpn-switch" 2>/dev/null; then
    pass "installed script: VPN_SWITCH_LIBDIR sed-patched correctly"
  else
    fail "installed script: VPN_SWITCH_LIBDIR sed-patch missing or wrong"
  fi

  # Step 4a: Man page installed (only if docs/vpn-switch.1 is present in repo,
  # which it should be — committed alongside source)
  if [ -f docs/vpn-switch.1 ]; then
    if [ -f "$staged/usr/local/share/man/man1/vpn-switch.1" ]; then
      pass "installed: /usr/local/share/man/man1/vpn-switch.1"
    else
      fail "man page missing from install (docs/vpn-switch.1 exists in repo)"
    fi
  fi

  # Step 5: platform-specific extras (rc.d on FreeBSD, systemd unit on Linux)
  case "$platform_lc" in
    freebsd)
      if [ -x "$staged/usr/local/etc/rc.d/vpn_switch" ]; then
        pass "FreeBSD: rc.d service script installed"
      else
        fail "FreeBSD: rc.d service script missing"
      fi
      ;;
    linux)
      if [ -f "$staged/usr/local/lib/systemd/system/vpn-switch@.service" ]; then
        pass "Linux: systemd unit installed"
      else
        fail "Linux: systemd unit missing"
      fi
      ;;
  esac

  # Step 6: uninstall
  if gmake uninstall DESTDIR="$staged" PREFIX=/usr/local > "$uninstall_log" 2>&1; then
    pass "gmake uninstall DESTDIR=$staged succeeds"
  else
    fail "gmake uninstall failed" "" "$(tail -5 "$uninstall_log")"
  fi

  # Step 7: tree clean after uninstall (excluding the test logs themselves;
  # FreeBSD keeps etc/rc.conf.d/vpn_switch -- the user's settings -- by design)
  local remaining=$(find "$staged" -type f -not -name '*.log' -not -path '*/etc/rc.conf.d/vpn_switch' | wc -l | tr -d ' ')
  if [ "$remaining" = "0" ]; then
    pass "tree clean after uninstall (no leftover files but the kept rc.conf.d settings)"
  else
    fail "tree not clean after uninstall — $remaining files remain"
    find "$staged" -type f -not -name '*.log' -not -path '*/etc/rc.conf.d/vpn_switch' | head -5 | sed 's/^/    /'
  fi

  cleanup_on_success "$staged"
  story_passed
}

#-----------------------------------------------------------------------------
# Main Test Execution
#-----------------------------------------------------------------------------

should_run_story() {
  local story_num="$1"
  # If VPN_SWITCH_TEST_STORY is not set, run all stories
  [ -z "${VPN_SWITCH_TEST_STORY:-}" ] && return 0
  # If set, only run the specified story
  [ "$VPN_SWITCH_TEST_STORY" = "$story_num" ]
}

# parallel_main - one worker process per user story (VPN_SWITCH_TEST_STORY),
# MAXPROCS at a time via xargs -P; outputs replayed in story order, counters
# aggregated across the workers' summaries.
parallel_main() {
  local outdir rc=0 stories n
  outdir=$(mktemp -d "${TMPDIR:-/tmp}/vsw-int-par.XXXXXX") || exit 1
  stories=$(grep -o "should_run_story [0-9]*" "$0" | awk '{print $2}' | sort -un)
  # Stories touching the SHARED repo tree (sync stamps/recovers .version,
  # install/uninstall stage into it) must not interleave -- they run as a
  # sequential tail after the parallel bulk.
  serial="13 14"
  parallel_set=$(printf '%s\n' $stories | grep -vx -e 13 -e 14)
  echo "=== vpn-switch Integration Tests (parallel, -P $MAXPROCS; serial tail: $serial) ==="
  echo ""
  printf '%s\n' $parallel_set | xargs -n1 -P "$MAXPROCS" -I{} \
    sh -c 'env VPN_SWITCH_TEST_STORY={} sh "$0" > "$1/{}.out" 2>&1; echo $? > "$1/{}.rc"' \
    "$0" "$outdir"
  for n in $serial; do
    env VPN_SWITCH_TEST_STORY=$n sh "$0" > "$outdir/$n.out" 2>&1; echo $? > "$outdir/$n.rc"
  done
  for n in $stories; do
    cat "$outdir/$n.out" 2>/dev/null
    [ "$(cat "$outdir/$n.rc" 2>/dev/null)" = "0" ] || rc=1
  done
  echo ""
  echo "========================================"
  echo "Aggregated Summary (parallel run)"
  echo "========================================"
  awk '
    /User Stories:/  { sect = "s" }
    /^Assertions:/   { sect = "a" }
    /^  Passed:/ { if (sect == "s") sp += $2; else ap += $2 }
    /^  Failed:/ { if (sect == "s") sf += $2; else af += $2 }
    END {
      printf "Stories:            %d passed, %d failed\n", sp, sf
      printf "Assertions:         %d passed, %d failed\n", ap, af
    }' "$outdir"/*.out
  if [ "$rc" -eq 0 ]; then
    echo ""; echo "ALL STORIES PASSED"; rm -rf "$outdir"
  else
    echo ""; echo "SOME STORIES FAILED"; echo "Per-story outputs preserved in: $outdir"
  fi
  return $rc
}

main() {
  if [ "$MAXPROCS" -gt 1 ]; then
    parallel_main
    return $?
  fi

  echo "=== vpn-switch Integration Tests ==="
  echo "Profile: $PROFILE"
  [ -n "${VPN_SWITCH_TEST_STORY:-}" ] && echo "Running only Story: $VPN_SWITCH_TEST_STORY"
  echo ""

  # Run user stories (filtered by VPN_SWITCH_TEST_STORY if set)
  should_run_story 1 && user_story_1_import_organize_dump_restore
  should_run_story 2 && user_story_2_session_management
  should_run_story 3 && user_story_3_config_expiry
  should_run_story 4 && user_story_4_multi_category_organization
  should_run_story 5 && user_story_5_completeness_and_deletion_proof
  should_run_story 6 && user_story_6_dump_quality_properties
  should_run_story 7 && user_story_7_phase_script_generation
  should_run_story 8 && user_story_8_phase_script_content
  should_run_story 9 && user_story_9_phase_customization
  should_run_story 10 && user_story_10_multi_protocol_phase_support
  should_run_story 11 && user_story_11_multi_protocol_edge_cases
  should_run_story 12 && user_story_12_phase_backend_selection
  should_run_story 13 && user_story_13_sync_lifecycle
  should_run_story 14 && user_story_14_install_lifecycle

  # Summary
  echo ""
  echo "========================================"
  [ -d "$STORY_GNUPGHOME" ] && GNUPGHOME="$STORY_GNUPGHOME" gpgconf --kill all 2>/dev/null && rm -rf "$STORY_GNUPGHOME"
  echo "Test Summary"
  echo "========================================"
  echo "User Stories:  $STORIES_RUN"
  echo "  Passed:      $STORIES_PASSED"
  echo "  Failed:      $STORIES_FAILED"
  echo ""
  echo "Assertions:    $ASSERTIONS_RUN"
  echo "  Passed:      $ASSERTIONS_PASSED"
  echo "  Failed:      $ASSERTIONS_FAILED"
  echo ""

  # A suite is green only when BOTH no story AND no individual assertion failed.
  # (A failed assertion must never be reported as success / exit 0.)
  if [ "$STORIES_FAILED" -eq 0 ] && [ "$ASSERTIONS_FAILED" -eq 0 ]; then
    printf "${GREEN}ALL STORIES PASSED${NC}\n"
    exit 0
  else
    printf "${RED}SOME STORIES FAILED${NC} (stories failed: %s, assertions failed: %s)\n" \
        "$STORIES_FAILED" "$ASSERTIONS_FAILED"
    exit 1
  fi
}

main