#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail

# shellcheck source=test/units/util.sh
. "$(dirname "$0")"/util.sh

# Test IRQAffinityPolicy configuration

# Find a network interface with MSI IRQs (typically virtio-net in the VM)
# We need a real device to test actual IRQ affinity application
find_interface_with_msi_irqs() {
    for iface in /sys/class/net/*; do
        [[ -e "$iface" ]] || continue
        iface_name=$(basename "$iface")
        [[ "$iface_name" == "lo" ]] && continue
        msi_irqs_path="$iface/device/msi_irqs"
        if [[ -d "$msi_irqs_path" ]] && [[ -n "$(ls -A "$msi_irqs_path" 2>/dev/null)" ]]; then
            echo "$iface_name"
            return 0
        fi
    done
    return 1
}

get_interface_irqs() {
    local iface="$1"
    local msi_irqs_path="/sys/class/net/$iface/device/msi_irqs"
    if [[ -d "$msi_irqs_path" ]]; then
        ls "$msi_irqs_path"
    fi
}

check_irq_affinity() {
    local irq="$1"
    local expected_mask="$2"
    local affinity
    affinity=$(cat "/proc/irq/$irq/smp_affinity")
    # Remove leading zeros and commas for comparison
    affinity=$(echo "$affinity" | sed 's/^[0,]*//;s/,//g')
    expected_mask=$(echo "$expected_mask" | sed 's/^[0,]*//;s/,//g')
    [[ "$affinity" == "$expected_mask" ]]
}

# Test 1: Verify IRQ affinity is actually applied on a real device
if iface=$(find_interface_with_msi_irqs); then
    echo "Found interface with MSI IRQs: $iface"

    # Get the MAC address of the interface
    mac=$(cat "/sys/class/net/$iface/address")

    # Get IRQs before applying policy
    irqs=$(get_interface_irqs "$iface")
    echo "Interface $iface has IRQs: $irqs"

    # Test 1a: test single policy
    mkdir -p /run/systemd/network/
    cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=single
EOF

    udevadm control --reload

    # Trigger udev to re-apply the link configuration
    udevadm trigger --action=add "/sys/class/net/$iface"
    udevadm settle --timeout=30

    # Verify the link file was applied
    output=$(udevadm info --query property "/sys/class/net/$iface")
    assert_in "ID_NET_LINK_FILE=/run/systemd/network/00-test-irq-affinity.link" "$output"

    # Verify IRQ affinity was actually set to CPU 0 (mask "1")
    for irq in $irqs; do
        if check_irq_affinity "$irq" "1"; then
            echo "IRQ $irq correctly pinned to CPU 0"
        else
            actual=$(cat "/proc/irq/$irq/smp_affinity")
            echo "IRQ $irq affinity is '$actual', expected '1' (CPU 0)"
            exit 1
        fi
    done

    # Test 1b: test spread policy on the same interface
    cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=spread
EOF

    udevadm control --reload
    udevadm trigger --action=add "/sys/class/net/$iface"
    udevadm settle --timeout=30

    # Get the number of online CPUs
    n_cpus=$(nproc)
    irq_count=$(echo "$irqs" | wc -w)

    echo "System has $n_cpus CPUs, interface has $irq_count IRQs"

    # Verify IRQs are spread (not all on CPU 0)
    # With spread policy, if we have more than 1 CPU and more than 1 IRQ,
    # at least some IRQs should be on different CPUs
    if [[ "$n_cpus" -gt 1 ]] && [[ "$irq_count" -gt 1 ]]; then
        cpu_set=()
        for irq in $irqs; do
            affinity=$(cat "/proc/irq/$irq/smp_affinity_list")
            echo "IRQ $irq is on CPU(s): $affinity"
            cpu_set+=("$affinity")
        done

        # Check that we have at least 2 different CPU assignments
        unique_cpus=$(printf '%s\n' "${cpu_set[@]}" | sort -u | wc -l)
        if [[ "$unique_cpus" -lt 2 ]]; then
            echo "ERROR: spread policy should distribute IRQs across CPUs, but all are on same CPU"
            exit 1
        fi
        echo "IRQ affinity policy 'spread' successfully distributed IRQs across $unique_cpus CPUs"
    else
        echo "Skipping spread verification (need >1 CPU and >1 IRQ)"
    fi

    # Test 1c: Test IRQAffinity= CPU filtering with single policy
    # Pin to CPU 1 instead of default CPU 0
    cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=single
IRQAffinity=1
EOF

    udevadm control --reload
    udevadm trigger --action=add "/sys/class/net/$iface"
    udevadm settle --timeout=30

    if [[ "$n_cpus" -gt 1 ]]; then
        for irq in $irqs; do
            echo "Checking IRQ $irq affinity with IRQAffinity=1..."
            if check_irq_affinity "$irq" "2"; then
                echo "IRQ $irq correctly pinned to CPU 1"
            else
                actual=$(cat "/proc/irq/$irq/smp_affinity")
                echo "IRQ $irq affinity is '$actual', expected '2' (CPU 1)"
                exit 1
            fi
        done
        echo "IRQAffinity= filtering with single policy works correctly"
    else
        echo "Skipping IRQAffinity= test (need >1 CPU)"
    fi

    # Test 1d: Test IRQAffinity= with spread policy (restrict to subset of CPUs)
    if [[ "$n_cpus" -ge 4 ]] && [[ "$irq_count" -gt 1 ]]; then
        cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=spread
IRQAffinity=0-1
EOF

        udevadm control --reload
        udevadm trigger --action=add "/sys/class/net/$iface"
        udevadm settle --timeout=30

        # Verify all IRQs are on CPU 0 or 1 only
        for irq in $irqs; do
            affinity_list=$(cat "/proc/irq/$irq/smp_affinity_list")
            echo "IRQ $irq is on CPU(s): $affinity_list"
            # Check that affinity is 0 or 1 (not higher CPUs)
            if [[ "$affinity_list" =~ ^[01]$ ]]; then
                echo "IRQ $irq correctly restricted to CPUs 0-1"
            else
                echo "ERROR: IRQ $irq is on CPU $affinity_list, expected 0 or 1"
                exit 1
            fi
        done
        echo "IRQAffinity= filtering with spread policy works correctly"
    else
        echo "Skipping IRQAffinity= spread test (need >=4 CPUs and >1 IRQ)"
    fi

    # Test 1e: Test IRQAffinityNUMA= if NUMA is available
    if [[ -d /sys/devices/system/node/node0 ]]; then
        # Get CPUs on NUMA node 0
        numa0_cpus=$(cat /sys/devices/system/node/node0/cpulist)
        echo "NUMA node 0 has CPUs: $numa0_cpus"

        cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=spread
IRQAffinityNUMA=0
EOF

        udevadm control --reload
        udevadm trigger --action=add "/sys/class/net/$iface"
        udevadm settle --timeout=30

        # Verify IRQs are on NUMA node 0 CPUs
        # Parse the cpulist to get valid CPUs
        for irq in $irqs; do
            affinity_list=$(cat "/proc/irq/$irq/smp_affinity_list")
            echo "IRQ $irq is on CPU(s): $affinity_list (NUMA 0 CPUs: $numa0_cpus)"
        done
        echo "IRQAffinityNUMA= configuration applied"
    else
        echo "Skipping IRQAffinityNUMA= test (no NUMA available)"
    fi

    # Test 1f: Test empty intersection error case
    # This should log an error and skip affinity configuration
    if [[ -d /sys/devices/system/node/node0 ]] && [[ -d /sys/devices/system/node/node1 ]]; then
        # Get first CPU from node 0 that is NOT in node 1
        first_numa0_cpu=$(cut -d',' -f1 /sys/devices/system/node/node0/cpulist | cut -d'-' -f1)

        cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
[Match]
MACAddress=$mac

[Link]
IRQAffinityPolicy=spread
IRQAffinity=$first_numa0_cpu
IRQAffinityNUMA=1
EOF

        udevadm control --reload
        udevadm trigger --action=add "/sys/class/net/$iface"
        udevadm settle --timeout=30

        # The configuration should be applied but IRQ affinity skipped due to empty intersection
        # Check journal for the error message
        if journalctl -u systemd-udevd --since="1 minute ago" | grep "intersection is empty" >/dev/null; then
            echo "Empty intersection correctly detected and logged"
        else
            echo "Note: Empty intersection test - check journal for error message"
        fi
    else
        echo "Skipping empty intersection test (need 2 NUMA nodes)"
    fi

    # Cleanup
    rm -f /run/systemd/network/00-test-irq-affinity.link
    udevadm control --reload
else
    echo "No interface with MSI IRQs found, skipping actual IRQ affinity test"
fi

# Test 2: Config parsing with dummy interfaces (no MSI IRQs)
mkdir -p /run/systemd/network/
cat >/run/systemd/network/10-test-irq.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:20

[Link]
Name=testirq0
IRQAffinityPolicy=single
EOF

udevadm control --reload

# Create a dummy interface
ip link add address 00:50:56:c0:00:20 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq0

# Check that the link file was applied
output=$(udevadm info --query property /sys/class/net/testirq0)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq.link" "$output"
assert_in "ID_NET_NAME=testirq0" "$output"

# Test that udevadm test-builtin parses the config correctly
output=$(udevadm test-builtin --action add net_setup_link /sys/class/net/testirq0 2>&1)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq.link" "$output"

# Test 3: Invalid policy values are rejected/warned
cat >/run/systemd/network/10-test-irq-invalid.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:21

[Link]
Name=testirq1
IRQAffinityPolicy=invalid_policy
EOF

udevadm control --reload

# Create another dummy interface - invalid policy should be ignored
ip link add address 00:50:56:c0:00:21 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq1

# Check that the link file was still applied (invalid policy is just ignored/warned)
output=$(udevadm info --query property /sys/class/net/testirq1)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-invalid.link" "$output"

# Test 4: Empty policy (reset/disable)
cat >/run/systemd/network/10-test-irq-empty.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:22

[Link]
Name=testirq2
IRQAffinityPolicy=
EOF

udevadm control --reload

ip link add address 00:50:56:c0:00:22 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq2

output=$(udevadm info --query property /sys/class/net/testirq2)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-empty.link" "$output"

# Test 5: IRQAffinity= config parsing
cat >/run/systemd/network/10-test-irq-affinity-cpus.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:23

[Link]
Name=testirq3
IRQAffinityPolicy=spread
IRQAffinity=0-3,8-11
EOF

udevadm control --reload

ip link add address 00:50:56:c0:00:23 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq3

output=$(udevadm info --query property /sys/class/net/testirq3)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-cpus.link" "$output"

# Test that udevadm test-builtin parses the IRQAffinity= config correctly
output=$(udevadm test-builtin --action add net_setup_link /sys/class/net/testirq3 2>&1)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-cpus.link" "$output"

# Test 6: IRQAffinityNUMA= config parsing
cat >/run/systemd/network/10-test-irq-affinity-numa.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:24

[Link]
Name=testirq4
IRQAffinityPolicy=spread
IRQAffinityNUMA=local
EOF

udevadm control --reload

ip link add address 00:50:56:c0:00:24 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq4

output=$(udevadm info --query property /sys/class/net/testirq4)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-numa.link" "$output"

# Test 7: IRQAffinityNUMA= with explicit node number
cat >/run/systemd/network/10-test-irq-affinity-numa-explicit.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:25

[Link]
Name=testirq5
IRQAffinityPolicy=single
IRQAffinityNUMA=0
EOF

udevadm control --reload

ip link add address 00:50:56:c0:00:25 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq5

output=$(udevadm info --query property /sys/class/net/testirq5)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-numa-explicit.link" "$output"

# Test 8: Combined IRQAffinity= and IRQAffinityNUMA=
cat >/run/systemd/network/10-test-irq-affinity-combined.link <<EOF
[Match]
Kind=dummy
MACAddress=00:50:56:c0:00:26

[Link]
Name=testirq6
IRQAffinityPolicy=spread
IRQAffinity=0-7
IRQAffinityNUMA=0
EOF

udevadm control --reload

ip link add address 00:50:56:c0:00:26 type dummy
udevadm wait --settle --timeout=30 /sys/class/net/testirq6

output=$(udevadm info --query property /sys/class/net/testirq6)
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-combined.link" "$output"

# Cleanup
ip link del dev testirq0
ip link del dev testirq1
ip link del dev testirq2
ip link del dev testirq3
ip link del dev testirq4
ip link del dev testirq5
ip link del dev testirq6

rm -f /run/systemd/network/10-test-irq.link
rm -f /run/systemd/network/10-test-irq-invalid.link
rm -f /run/systemd/network/10-test-irq-empty.link
rm -f /run/systemd/network/10-test-irq-affinity-cpus.link
rm -f /run/systemd/network/10-test-irq-affinity-numa.link
rm -f /run/systemd/network/10-test-irq-affinity-numa-explicit.link
rm -f /run/systemd/network/10-test-irq-affinity-combined.link

exit 0
