#!/bin/bash

## Copyright (C) 2012 - 2026 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## AI-Assisted

## CI smoke test for libvirt-dist: assert that libvirt parses and accepts each
## domain definition, and that a pre-existing $HOME image imports into a
## dedicated qemu:///session storage pool. define/undefine never opens the disk
## image (that only happens on domain start), so this runs with no VM images
## and inside environments where KVM is unavailable (CI, Qubes, nested virt).

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

## Trace every command so the separator_line markers below land in the CI log.
set -x

# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/log_run_die.sh
source "${HELPER_SCRIPTS_PATH:-}"/usr/libexec/helper-scripts/log_run_die.sh

separator_line="------------------------------------------------------------"

## Names double as both the XML file stem and the libvirt domain <name>.
domain_list=(Whonix-Gateway Whonix-Workstation Whonix-Custom-Workstation Kicksecure)

mydir="$(dirname -- "$(readlink -f -- "$0")")"
xml_source_dir="${mydir}/xml"

## Throwaway names/paths so the test never touches a real session 'default'
## pool, the real images directory, or a real downloaded image (data-loss
## guard - this is an opt-in CI=true run, possibly on a real Whonix host).
test_pool="libvirt-dist-ci-test"
home_image="${HOME}/libvirt-dist-ci-test.qcow2"

## Set by main() before the trap can reference it; "" keeps it nounset-safe.
work_dir=""

ci_test_cleanup() {
   virsh -c qemu:///session pool-destroy "${test_pool}" &>/dev/null || true
   virsh -c qemu:///session pool-undefine "${test_pool}" &>/dev/null || true
   safe-rm --force -- "${home_image}"
   if [ -n "${work_dir}" ]; then
      ## work_dir also holds the test pool's target directory.
      safe-rm --recursive --force -- "${work_dir}"
   fi
}

main() {
   local cmd vm pool_target image_name

   ## virsh undefines domains; refuse to touch an operator's real libvirt
   ## state unless this is explicitly a CI (or opted-in) run.
   if [ "${CI:-}" != "true" ]; then
      die 1 "refusing to run outside CI: this undefines libvirt domains; set 'CI=true' to override"
   fi

   for cmd in virsh virt-xml-validate str_replace qemu-img; do
      has "${cmd}" \
         || die 1 "required command '${cmd}' not found on PATH"
   done

   ## Refuse to clobber a real file that happens to share the name. Checked
   ## BEFORE arming the cleanup trap, so a refusal can never delete that file.
   if [ -e "${home_image}" ]; then
      die 1 "refusing to overwrite existing '${home_image}'"
   fi

   trap ci_test_cleanup EXIT

   ## GIVEN: assume the user already downloaded an image into $HOME, before any
   ## libvirt command runs. It is imported into the storage pool further below.
   qemu-img create -f qcow2 "${home_image}" 4M

   ## Operate on a throwaway copy. Never edit the shipped XML in place: the
   ## kvm -> qemu rewrite below is a CI-only workaround, not a source change.
   work_dir="$(mktemp --directory)"
   cp -r -- "${xml_source_dir}" "${work_dir}/xml"

   true "${separator_line}"

   if virsh -c qemu:///session capabilities \
      | grep --quiet --fixed-strings -- "<domain type='kvm'>"; then
      log notice "KVM available: testing the XML unmodified."
   else
      log notice "KVM unavailable (CI / Qubes / nested virt): rewriting domain type 'kvm' -> 'qemu'."
      for vm in "${domain_list[@]}"; do
         ## error: could not find capabilities for domaintype=kvm
         str_replace "<domain type='kvm'>" "<domain type='qemu'>" "${work_dir}/xml/${vm}.xml"
         ## error: CPU mode 'host-passthrough' ... not supported by hypervisor
         str_replace "<cpu mode='host-passthrough'/>" "" "${work_dir}/xml/${vm}.xml"
      done
   fi

   true "${separator_line}"

   for vm in "${domain_list[@]}"; do
      test -f "${work_dir}/xml/${vm}.xml"
   done

   true "${separator_line}"

   ## Pass the explicit 'domain' type. Autodetection (xmllint --stream)
   ## is unreliable across libxml2 versions and otherwise fails with
   ## "cannot determine schema type". A schema violation must fail the test.
   for vm in "${domain_list[@]}"; do
      virt-xml-validate "${work_dir}/xml/${vm}.xml" domain
   done

   true "${separator_line}"

   ## domxml-to-native needs KVM capabilities, absent in nested CI; non-fatal.
   for vm in "${domain_list[@]}"; do
      virsh -c qemu:///session domxml-to-native qemu-argv "${work_dir}/xml/${vm}.xml" || true
   done

   true "${separator_line}"

   ## The actual assertion: libvirt accepts and registers each domain. Refuse
   ## to clobber a domain that already exists (real Whonix / Kicksecure host),
   ## since the undefine below would otherwise remove the operator's definition.
   for vm in "${domain_list[@]}"; do
      if virsh -c qemu:///session dominfo "${vm}" &>/dev/null; then
         die 1 "refusing to overwrite existing qemu:///session domain '${vm}'"
      fi
      virsh -c qemu:///session define "${work_dir}/xml/${vm}.xml"
      virsh -c qemu:///session undefine "${vm}"
   done

   true "${separator_line}"

   ## Import the pre-existing $HOME image into a dedicated throwaway pool whose
   ## target lives under work_dir (never the real session pool or images dir).
   ## For a dir pool this is the shortest correct path: pool-create-as --build
   ## creates the directory, then move the file in and refresh - no manual
   ## mkdir, no cp, no vol-create-as / vol-upload.
   pool_target="${work_dir}/pool"
   image_name="$(basename -- "${home_image}")"
   virsh -c qemu:///session pool-create-as "${test_pool}" dir --target "${pool_target}" --build
   mv -- "${home_image}" "${pool_target}/"
   virsh -c qemu:///session pool-refresh "${test_pool}"
   virsh -c qemu:///session vol-list "${test_pool}" \
      | grep --quiet --fixed-strings -- "${image_name}" \
      || die 1 "imported image was not registered as a pool volume"

   true "${separator_line}"

   log notice "${0}: all tests passed."
}

main "$@"
