#!/usr/bin/env bash
# Verify the filesystem lifecycle of the Makefile package without touching the host.
set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
MAKE_COMMAND="${MAKE:-make}"
WORK="$(mktemp -d -t rustd-resolved-package-lifecycle.XXXXXX)"
PAYLOAD="$WORK/current-payload"
PREVIOUS_PAYLOAD="$WORK/previous-payload"
INSTALL_ROOT="$WORK/installed-root"
CURRENT_MANIFEST="$WORK/current.manifest"
PREVIOUS_MANIFEST="$WORK/previous.manifest"
SENTINEL_RELATIVE="usr/share/operator-owned.txt"
SENTINEL_CONTENT="operator data must survive package lifecycle"

cleanup() {
    rm -rf -- "$WORK"
}
trap cleanup EXIT HUP INT TERM

fail() {
    printf 'package lifecycle: ERROR: %s\n' "$*" >&2
    exit 1
}

for command in "$MAKE_COMMAND" chmod cmp cp find grep install ln mktemp readlink rm rmdir sort stat; do
    command -v "$command" >/dev/null 2>&1 \
        || fail "required command not found: $command"
done

case "$WORK" in
    /tmp/rustd-resolved-package-lifecycle.*) ;;
    *) fail "refusing to use unexpected temporary directory: $WORK" ;;
esac

write_manifest() {
    local root=$1
    local destination=$2

    (
        cd "$root"
        find . -mindepth 1 -printf '%P\n' | LC_ALL=C sort
    ) >"$destination"
}

remove_payload() {
    local root=$1
    local manifest=$2
    local relative

    # Files and links must disappear before their parent directories. Directory
    # removal is deliberately best-effort so files owned by another package or
    # the operator survive an uninstall.
    while IFS= read -r relative; do
        [[ -n $relative ]] || continue
        if [[ -L "$root/$relative" || -f "$root/$relative" ]]; then
            rm -f -- "$root/$relative"
        fi
    done <"$manifest"

    while IFS= read -r relative; do
        [[ -n $relative ]] || continue
        if [[ -d "$root/$relative" && ! -L "$root/$relative" ]]; then
            rmdir --ignore-fail-on-non-empty -- "$root/$relative"
        fi
    done < <(LC_ALL=C sort -r "$manifest")
}

copy_payload() {
    local source=$1
    local destination=$2

    install -d -m 0755 "$destination"
    cp -a -- "$source/." "$destination/"
}

assert_link() {
    local path=$1
    local expected=$2

    [[ -L $path ]] || fail "expected symbolic link: $path"
    [[ $(readlink "$path") == "$expected" ]] \
        || fail "unexpected target for $path: $(readlink "$path")"
}

assert_mode() {
    local path=$1
    local expected=$2
    local actual

    actual=$(stat -c '%a' "$path")
    [[ $actual == "$expected" ]] \
        || fail "unexpected mode for $path: $actual (expected $expected)"
}

assert_install_layout() {
    local root=$1
    local relative

    for relative in \
        usr/lib/rustd/rustd-resolved \
        usr/lib/libnss_resolve.so.2 \
        usr/bin/rustd-resolvectl \
        usr/lib/systemd/rustd-resolved \
        usr/lib/systemd/systemd-resolved \
        usr/bin/resolvectl \
        usr/bin/systemd-resolve \
        usr/sbin/resolvconf \
        usr/lib/systemd/system/rustd-resolved.service \
        usr/lib/systemd/system/rustd-resolved-varlink.socket \
        usr/lib/systemd/system/rustd-resolved-monitor.socket \
        usr/lib/systemd/system/systemd-resolved.service \
        usr/lib/systemd/system/systemd-resolved-varlink.socket \
        usr/lib/systemd/system/systemd-resolved-monitor.socket \
        usr/lib/tmpfiles.d/systemd-resolved.conf \
        usr/lib/sysusers.d/systemd-resolve.conf \
        usr/share/dbus-1/system-services/org.rustd.resolve1.service \
        usr/share/dbus-1/system.d/org.rustd.resolve1.conf \
        usr/share/polkit-1/actions/org.rustd.resolve1.policy
    do
        [[ -e "$root/$relative" || -L "$root/$relative" ]] \
            || fail "installed payload is missing $relative"
    done

    assert_link "$root/usr/lib/systemd/rustd-resolved" ../rustd/rustd-resolved
    assert_link "$root/usr/lib/systemd/systemd-resolved" rustd-resolved
    assert_link "$root/usr/bin/resolvectl" rustd-resolvectl
    assert_link "$root/usr/bin/systemd-resolve" rustd-resolvectl
    assert_link "$root/usr/sbin/resolvconf" ../bin/rustd-resolvectl
    assert_mode "$root/usr/lib/rustd/rustd-resolved" 755
    assert_mode "$root/usr/lib/libnss_resolve.so.2" 755
    assert_mode "$root/usr/bin/rustd-resolvectl" 755
    assert_mode "$root/usr/lib/systemd/system/rustd-resolved.service" 644
    [[ ! -e "$root/usr/lib/systemd/system/rustd-resolved.socket" ]] \
        || fail "obsolete direct DNS socket unit is still installed"
    grep -Fx 'Type=notify-reload' "$root/usr/lib/systemd/system/rustd-resolved.service" >/dev/null \
        || fail "rustd-resolved service does not use upstream notify-reload semantics"
    grep -Fx 'Restart=always' "$root/usr/lib/systemd/system/rustd-resolved.service" >/dev/null \
        || fail "rustd-resolved service does not use upstream restart semantics"
    grep -Fx 'Sockets=rustd-resolved-varlink.socket rustd-resolved-monitor.socket' \
        "$root/usr/lib/systemd/system/rustd-resolved.service" >/dev/null \
        || fail "rustd-resolved service has an unexpected socket activation contract"
    assert_mode "$root/usr/lib/systemd/system/systemd-resolved.service" 644
    assert_mode "$root/usr/lib/tmpfiles.d/systemd-resolved.conf" 644
    grep -Fx 'L! /etc/resolv.conf - - - - ../run/systemd/resolve/stub-resolv.conf' \
        "$root/usr/lib/tmpfiles.d/systemd-resolved.conf" >/dev/null \
        || fail "systemd-resolved tmpfiles payload does not install the upstream resolv.conf link"
    assert_mode "$root/usr/lib/sysusers.d/systemd-resolve.conf" 644
}

assert_payload_matches() {
    local expected_root=$1
    local actual_root=$2
    local manifest=$3
    local relative source destination source_mode destination_mode

    while IFS= read -r relative; do
        [[ -n $relative ]] || continue
        source="$expected_root/$relative"
        destination="$actual_root/$relative"

        if [[ -L $source ]]; then
            [[ -L $destination ]] || fail "rollback type mismatch for $relative"
            [[ $(readlink "$source") == "$(readlink "$destination")" ]] \
                || fail "rollback link mismatch for $relative"
        elif [[ -d $source ]]; then
            [[ -d $destination && ! -L $destination ]] \
                || fail "rollback directory mismatch for $relative"
        elif [[ -f $source ]]; then
            [[ -f $destination && ! -L $destination ]] \
                || fail "rollback file mismatch for $relative"
            cmp -s -- "$source" "$destination" \
                || fail "rollback content mismatch for $relative"
        else
            fail "unsupported package payload type: $relative"
        fi

        if [[ ! -L $source ]]; then
            source_mode=$(stat -c '%a' "$source")
            destination_mode=$(stat -c '%a' "$destination")
            [[ $source_mode == "$destination_mode" ]] \
                || fail "rollback mode mismatch for $relative"
        fi
    done <"$manifest"
}

assert_sentinel() {
    local content

    [[ -f "$INSTALL_ROOT/$SENTINEL_RELATIVE" ]] \
        || fail "unowned operator file was removed"
    IFS= read -r content <"$INSTALL_ROOT/$SENTINEL_RELATIVE"
    [[ $content == "$SENTINEL_CONTENT" ]] \
        || fail "unowned operator file was modified"
}

printf 'package lifecycle: staging current package with Makefile install\n'
"$MAKE_COMMAND" -C "$ROOT" install DESTDIR="$PAYLOAD" PREFIX=/usr
assert_install_layout "$PAYLOAD"
write_manifest "$PAYLOAD" "$CURRENT_MANIFEST"

printf 'package lifecycle: verifying isolated clean install\n'
install -D -m 0644 /dev/null "$INSTALL_ROOT/$SENTINEL_RELATIVE"
printf '%s\n' "$SENTINEL_CONTENT" >"$INSTALL_ROOT/$SENTINEL_RELATIVE"
"$MAKE_COMMAND" -C "$ROOT" install DESTDIR="$INSTALL_ROOT" PREFIX=/usr
assert_install_layout "$INSTALL_ROOT"
assert_payload_matches "$PAYLOAD" "$INSTALL_ROOT" "$CURRENT_MANIFEST"
assert_sentinel
"$INSTALL_ROOT/usr/lib/rustd/rustd-resolved" --version >/dev/null
"$INSTALL_ROOT/usr/bin/rustd-resolvectl" --version >/dev/null

printf 'package lifecycle: constructing a distinguishable previous package\n'
copy_payload "$PAYLOAD" "$PREVIOUS_PAYLOAD"
printf 'previous daemon payload\n' >"$PREVIOUS_PAYLOAD/usr/lib/rustd/rustd-resolved"
printf 'previous client payload\n' >"$PREVIOUS_PAYLOAD/usr/bin/rustd-resolvectl"
printf 'previous unit payload\n' \
    >"$PREVIOUS_PAYLOAD/usr/lib/systemd/system/rustd-resolved.service"
printf 'previous policy payload\n' \
    >"$PREVIOUS_PAYLOAD/usr/share/dbus-1/system.d/org.rustd.resolve1.conf"
chmod 0700 \
    "$PREVIOUS_PAYLOAD/usr/lib/rustd/rustd-resolved" \
    "$PREVIOUS_PAYLOAD/usr/bin/rustd-resolvectl"
ln -sfn wrong-daemon "$PREVIOUS_PAYLOAD/usr/lib/systemd/systemd-resolved"
ln -sfn wrong-client "$PREVIOUS_PAYLOAD/usr/bin/resolvectl"
install -D -m 0644 /dev/null \
    "$PREVIOUS_PAYLOAD/usr/lib/rustd/removed-after-upgrade"
printf 'owned only by the previous package\n' \
    >"$PREVIOUS_PAYLOAD/usr/lib/rustd/removed-after-upgrade"
write_manifest "$PREVIOUS_PAYLOAD" "$PREVIOUS_MANIFEST"

# Seed the isolated root with the previous version. This is fixture setup, not
# a host operation: every path remains beneath the mktemp directory.
remove_payload "$INSTALL_ROOT" "$CURRENT_MANIFEST"
copy_payload "$PREVIOUS_PAYLOAD" "$INSTALL_ROOT"
assert_payload_matches "$PREVIOUS_PAYLOAD" "$INSTALL_ROOT" "$PREVIOUS_MANIFEST"
assert_sentinel

printf 'package lifecycle: verifying upgrade to the current package\n'
# Package managers delete paths no longer owned by the new version. Model that
# manifest transition before invoking the project's real installer.
while IFS= read -r relative; do
    [[ -n $relative ]] || continue
    if ! LC_ALL=C grep -Fqx -- "$relative" "$CURRENT_MANIFEST"; then
        if [[ -L "$INSTALL_ROOT/$relative" || -f "$INSTALL_ROOT/$relative" ]]; then
            rm -f -- "$INSTALL_ROOT/$relative"
        fi
    fi
done <"$PREVIOUS_MANIFEST"
"$MAKE_COMMAND" -C "$ROOT" install DESTDIR="$INSTALL_ROOT" PREFIX=/usr
assert_install_layout "$INSTALL_ROOT"
assert_payload_matches "$PAYLOAD" "$INSTALL_ROOT" "$CURRENT_MANIFEST"
[[ ! -e "$INSTALL_ROOT/usr/lib/rustd/removed-after-upgrade" ]] \
    || fail "upgrade retained an obsolete package file"
assert_sentinel

printf 'package lifecycle: verifying exact rollback\n'
remove_payload "$INSTALL_ROOT" "$CURRENT_MANIFEST"
copy_payload "$PREVIOUS_PAYLOAD" "$INSTALL_ROOT"
assert_payload_matches "$PREVIOUS_PAYLOAD" "$INSTALL_ROOT" "$PREVIOUS_MANIFEST"
assert_sentinel

printf 'package lifecycle: verifying package removal\n'
remove_payload "$INSTALL_ROOT" "$PREVIOUS_MANIFEST"
while IFS= read -r relative; do
    [[ -n $relative ]] || continue
    if [[ -L "$PREVIOUS_PAYLOAD/$relative" || -f "$PREVIOUS_PAYLOAD/$relative" ]]; then
        if [[ -e "$INSTALL_ROOT/$relative" || -L "$INSTALL_ROOT/$relative" ]]; then
            fail "package-owned path survived removal: $relative"
        fi
    fi
done <"$PREVIOUS_MANIFEST"
assert_sentinel

printf 'Package lifecycle tests completed successfully.\n'
