#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)

# This file is part of Cockpit.
#
# Copyright (C) 2013 Red Hat, Inc.
#
# Cockpit is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Cockpit is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.

import subprocess

from machineslib import VirtualMachinesCase
from testlib import nondestructive, test_main, wait

# If this test fails to run, the host machine needs:
# echo "options kvm-intel nested=1" > /etc/modprobe.d/kvm-intel.conf
# rmmod kvm-intel && modprobe kvm-intel || true

# virt-install changed the default to "host-passthrough"
# https://github.com/virt-manager/virt-manager/commit/2c477f330244e04614e174f50fbf37260c535705
distrosWithDefaultHostModel = ["centos-8-stream"]


@nondestructive
class TestMachinesSettings(VirtualMachinesCase):

    def testVCPU(self):
        b = self.browser
        m = self.machine

        self.createVm("subVmTest1", running=False)

        self.login_and_go("/machines")
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

        m.execute("virt-xml subVmTest1 -c qemu:///system --vcpu 2 --edit")

        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        b.click("#vm-subVmTest1-cpu button")  # open VCPU modal detail window

        b.wait_visible("#machines-cpu-modal-dialog")

        # Test basic vCPU properties
        b.wait_val("#machines-vcpu-count-field input", "2")
        b.wait_val("#machines-vcpu-max-field input", "2")
        b.wait_val("#socketsSelect", "1")
        b.wait_val("#coresSelect", "2")
        b.wait_val("#threadsSelect", "1")

        # Set new values
        b.set_input_text("#machines-vcpu-max-field input", "4")
        b.set_input_text("#machines-vcpu-count-field input", "3")

        # Set new socket value
        b.wait_val("#socketsSelect", "4")
        b.set_val("#socketsSelect", "2")
        b.wait_val("#coresSelect", "1")
        b.wait_val("#threadsSelect", "2")

        # Save
        b.click("#machines-cpu-modal-dialog-apply")
        b.wait_not_present("#machines-cpu-modal-dialog")

        # Make sure warning next to vcpus appears
        b.wait_visible("#cpu-tooltip")
        b.wait_visible("#vm-subVmTest1-needs-shutdown")

        # Shut off VM for applying changes after save
        self.performAction("subVmTest1", "forceOff")

        # Make sure warning is gone after shut off
        b.wait_not_present("#cpu-tooltip")
        b.wait_not_present("#vm-subVmTest1-needs-shutdown")

        # Check changes
        b.wait_in_text("#vm-subVmTest1-cpu", "3 vCPUs")

        # Check after boot
        # Run VM
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Check VCPU count
        b.wait_in_text("#vm-subVmTest1-cpu", "3 vCPUs")

        # Open dialog window
        b.click("#vm-subVmTest1-cpu button")
        b.wait_visible(".pf-v5-c-modal-box__body")

        # Check basic values
        b.wait_val("#machines-vcpu-count-field input", "3")
        b.wait_val("#machines-vcpu-max-field input", "4")

        # Check sockets, cores and threads
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#coresSelect", "1")
        b.wait_val("#threadsSelect", "2")

        b.click("#machines-cpu-modal-dialog-cancel")
        b.wait_not_present("#machines-cpu-modal-dialog")

        # Shut off VM
        self.performAction("subVmTest1", "forceOff")

        # Open dialog
        b.click("#vm-subVmTest1-cpu button")

        b.wait_visible(".pf-v5-c-modal-box__body")

        b.set_input_text("#machines-vcpu-count-field input", "2")

        # Set new socket value
        b.set_val("#coresSelect", "2")
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#threadsSelect", "1")

        # Save
        b.click("#machines-cpu-modal-dialog-apply")
        b.wait_not_present("#machines-cpu-modal-dialog")

        wait(lambda: m.execute(
            "virsh dumpxml subVmTest1 | tee /tmp/subVmTest1.xml | xmllint --xpath '/domain/cpu/topology[@sockets=\"2\"][@threads=\"1\"][@cores=\"2\"]' -"))

        # Run VM - this ensures that the internal state is updated before we move on.
        # We need this here because we can't wait for UI updates after we open the modal dialog.
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Wait for the VCPUs link to get new values before opening the dialog
        b.wait_visible("#vm-subVmTest1-cpu")
        b.wait_in_text("#vm-subVmTest1-cpu", "2 vCPUs")

        # Open dialog
        b.click("#vm-subVmTest1-cpu button")

        b.wait_visible(".pf-v5-c-modal-box__body")

        # Set new socket value
        b.wait_val("#coresSelect", "2")
        b.wait_val("#socketsSelect", "2")
        b.wait_val("#threadsSelect", "1")

        b.wait_in_text("#vm-subVmTest1-cpu", "2 vCPUs")

        # Check value of sockets, threads and cores from VM dumpxml
        m.execute(
            "virsh dumpxml subVmTest1 | xmllint --xpath '/domain/cpu/topology[@sockets=\"2\"][@threads=\"1\"][@cores=\"2\"]' -")

        # non-persistent VM doesn't have configurable vcpu
        m.execute("virsh undefine subVmTest1")
        b.wait_visible("#vm-subVmTest1-cpu button:disabled")

    def testAutostart(self):
        b = self.browser
        m = self.machine

        self.login_and_go("/machines")
        self.waitPageInit()

        def checkAutostart(vm_name, running):
            self.createVm(vm_name, running=running)
            self.waitVmRow(vm_name)
            b.wait_in_text(f"#vm-{vm_name}-system-state", "Running" if running else "Shut off")
            self.goToVmPage(vm_name)

            # set checkbox state and check state of checkbox
            b.set_checked(f"#vm-{vm_name}-autostart-switch", True)  # don't know the initial state of checkbox, so set it to checked
            b.wait_visible(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "enable")

            # change checkbox state and check state of checkbox
            b.click(f"#vm-{vm_name}-autostart-switch")
            b.wait_not_present(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "disable")

            # change checkbox state and check state of checkbox
            b.click(f"#vm-{vm_name}-autostart-switch")
            b.wait_visible(f"#vm-{vm_name}-autostart-switch:checked")
            # check virsh state
            autostartState = m.execute("virsh dominfo %s | grep 'Autostart:' | awk '{print $2}'" % vm_name).strip()
            self.assertEqual(autostartState, "enable")

            # non-persistent VM doesn't have autostart
            if running:
                m.execute(f"virsh undefine {vm_name}")
                b.wait_not_present(f"#vm-{vm_name}-autostart-switch")

            self.goToMainPage()

        checkAutostart("subVmTest1", True)
        checkAutostart("subVmTest2", False)

    def testCPUModel(self):
        b = self.browser
        m = self.machine

        self.createVm("subVmTest1")

        self.login_and_go("/machines")
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

        self.goToVmPage("subVmTest1")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        cpu_model = "host-model"
        if m.image in distrosWithDefaultHostModel:
            cpu_model = "host-passthrough"

        # Copy host CPU configuration
        b.click("#vm-subVmTest1-cpu button")
        b.wait_visible("#machines-cpu-modal-dialog")
        b.select_from_dropdown("#cpu-model-select-group select", cpu_model)
        b.click("#machines-cpu-modal-dialog-apply")
        b.wait_not_present("#machines-cpu-modal-dialog")

        # Warning about about difference in persistent and non-persitent XML should appear
        b.wait_visible("#cpu-tooltip")
        b.wait_visible("#vm-subVmTest1-needs-shutdown")
        self.performAction("subVmTest1", "forceOff")
        # Warning should disappear and changes should be visible in the UI
        b.wait_not_present("#cpu-tooltip")
        b.wait_not_present("#vm-subVmTest1-needs-shutdown")
        b.wait_in_text("#vm-subVmTest1-cpu .pf-v5-c-description-list__text", "host")

        # Choose manually a CPU model
        b.click("#vm-subVmTest1-cpu button")
        b.wait_visible("#machines-cpu-modal-dialog")
        b.select_from_dropdown("#cpu-model-select-group select", "coreduo")
        b.click("#machines-cpu-modal-dialog-apply")
        b.wait_not_present("#machines-cpu-modal-dialog")
        b.wait_in_text("#vm-subVmTest1-cpu .pf-v5-c-description-list__text", "custom (coreduo)")

        # Verify libvirt XML
        dom_xml = "virsh dumpxml subVmTest1"
        xmllint_element = f"{dom_xml} | xmllint --xpath 'string(//domain/{{prop}})' - 2>&1 || true"
        self.assertEqual("coreduo", m.execute(xmllint_element.format(prop='cpu/model')).strip())

        # Host-model gets expanded  to custom mode when the VM is running
        b.click("#vm-subVmTest1-cpu button")
        b.wait_visible("#machines-cpu-modal-dialog")
        b.select_from_dropdown("#cpu-model-select-group select", "host-model")
        b.click("#machines-cpu-modal-dialog-apply")
        b.wait_not_present("#machines-cpu-modal-dialog")
        b.wait_in_text("#vm-subVmTest1-cpu .pf-v5-c-description-list__text", "host")
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-cpu .pf-v5-c-description-list__text", "custom")
        # In the test ENV libvirt does not properly set the CPU model so we see a tooltip https://bugzilla.redhat.com/show_bug.cgi?id=1913337
        # b.wait_not_present("#cpu-tooltip")

    def testBootOrder(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1")
        m.execute("touch /var/lib/libvirt/images/phonycdrom; virsh attach-disk subVmTest1 /var/lib/libvirt/images/phonycdrom sdb --type cdrom --config")

        self.login_and_go("/machines")
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        # Wait for the edit button
        bootOrder = b.text("#vm-subVmTest1-boot-order")

        # Ensure that it's disabled for running VMs
        b.wait_visible("#vm-subVmTest1-boot-order button[aria-disabled=true]")
        self.performAction("subVmTest1", "forceOff")

        # No host device is attached to VM by default, so attach a PCI device to have at least 1 host device in boot order
        # Take a device from the end of the list, since first device might be a root pci device
        pci_output = m.execute("virsh nodedev-list --cap pci").strip().splitlines()[-1]
        # Turn 'pci_0123_45_67_8' into '0123:45:67.8'
        split = pci_output[len("pci_"):].split('_')
        pci_slot = f"{split[0]}:{split[1]}:{split[2]}.{split[3]}"
        m.execute(f"virt-xml subVmTest1 --add-device --hostdev {pci_slot}")

        # Older libvirt versions don't fire events for host device attachement so we have to reload the page
        b.reload()
        b.enter_page('/machines')

        # Open dialog
        b.click("#vm-subVmTest1-boot-order button")
        b.wait_visible("#vm-subVmTest1-order-modal-window")
        # Check boot options details:
        # Checking the VM system disk path is the same with virsh command results
        # Checking the VM MAC address is the same with virsh command results
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-additional-info",
                       m.execute("virsh domblklist subVmTest1 | awk 'NR==3{print $2}'").strip())
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-1 .boot-order-additional-info",
                       m.execute("virsh domiflist subVmTest1 | awk 'NR==3{print $5}'").strip())
        b.wait_visible(f".boot-order-additional-info .pf-v5-c-description-list__description:contains('{pci_slot}')")
        # Check a cdrom attributes are shown correctly
        cdrom_row = b.text(".boot-order-list-view li:nth-child(3) .boot-order-additional-info")
        self.assertIn("cdrom", cdrom_row)
        self.assertIn("/var/lib/libvirt/images/phonycdrom", cdrom_row)
        # Move first device down and check whether succeeded
        row = b.text("#vm-subVmTest1-order-modal-device-row-1 .boot-order-additional-info")
        b.click("#vm-subVmTest1-order-modal-device-row-0 #vm-subVmTest1-order-modal-down")
        b.wait_in_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-additional-info", row)
        # Save
        b.click("#vm-subVmTest1-order-modal-save")
        b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Check boot order has changed and no warning is shown
        b.wait_not_in_text("#vm-subVmTest1-boot-order", bootOrder)

        bootOrder = b.text("#vm-subVmTest1-boot-order")

        # Open dialog
        b.click("#vm-subVmTest1-boot-order button")
        b.wait_visible("#vm-subVmTest1-order-modal-window")
        # Unselect second device
        b.set_checked("#vm-subVmTest1-order-modal-device-1-checkbox", False)

        # Save
        b.click("#vm-subVmTest1-order-modal-save")
        b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Check boot order has changed and no warning is shown
        b.wait_not_in_text("#vm-subVmTest1-boot-order", bootOrder)

        # After unchecking all the boot options, the VM should fall back to using the first hard disk as a boot image
        # Check that UI is able to detect this fallback
        def uncheckTheLastBootOption(firstOptionType="disk"):
            b.click("#vm-subVmTest1-boot-order button")
            b.wait_visible("#vm-subVmTest1-order-modal-window")

            # check boot option order
            # use the first and the third option since network will be the third option if unchecking it
            row0Type = "disk" if firstOptionType == "disk" else "network"
            row2Type = "network" if firstOptionType == "disk" else "disk"
            b.wait_text("#vm-subVmTest1-order-modal-device-row-0 .boot-order-description",
                        row0Type)
            b.wait_text("#vm-subVmTest1-order-modal-device-row-2 .boot-order-description",
                        row2Type)

            b.set_checked("#vm-subVmTest1-order-modal-device-row-0 input", False)
            b.click("#vm-subVmTest1-order-modal-save")
            b.wait_not_present("#vm-subVmTest1-order-modal-window")

            b.wait_text("#vm-subVmTest1-boot-order", "diskedit")

            # re-open and check the boot option is the disk
            b.click("#vm-subVmTest1-boot-order button")
            b.wait_visible("#vm-subVmTest1-order-modal-window")
            b.wait_text("#vm-subVmTest1-order-modal-device-row-0 .pf-v5-c-description-list__description .pf-v5-c-description-list__text", args["image"])

            b.click("#vm-subVmTest1-order-modal-cancel")
            b.wait_not_present("#vm-subVmTest1-order-modal-window")

        # Uncheck the last boot option which is network
        uncheckTheLastBootOption(firstOptionType="network")
        # Uncheck the last boot option which is disk
        uncheckTheLastBootOption()

    def testDomainMemorySettings(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1", memory=256)

        self.login_and_go("/machines")
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        # Wait for the edit link
        b.click("#vm-subVmTest1-memory-count button")

        # Change memory
        b.wait_visible("#vm-subVmTest1-memory-modal-memory")

        b.wait_attr("#vm-subVmTest1-memory-modal-memory-slider  div[role=slider]", "aria-valuemin", "128")
        b.wait_attr("#vm-subVmTest1-memory-modal-max-memory-slider  div[role=slider]", "aria-valuemin", "128")

        current_memory = int(b.attr("#vm-subVmTest1-memory-modal-memory", "value"))
        self.assertEqual(current_memory, 256)
        b.wait_attr("#vm-subVmTest1-memory-modal-max-memory", "disabled", "")

        # Check memory hotunplugging
        # The balloon driver needs to be loaded to descrease memory
        self.waitCirrOSBooted(args['logfile'])

        b.set_input_text("#vm-subVmTest1-memory-modal-memory", str(current_memory - 10))
        b.blur("#vm-subVmTest1-memory-modal-memory")
        # Save the memory settings
        b.click("#vm-subVmTest1-memory-modal-save")
        b.wait_not_present("#vm-memory-modal")

        b.wait_in_text("#vm-subVmTest1-memory-count", f"{f'{current_memory - 10:.1f}'} MiB")

        # Shut off domain and check changes are  still there
        self.performAction("subVmTest1", "forceOff")
        b.wait_in_text("#vm-subVmTest1-memory-count", f"{f'{current_memory - 10:.1f}'} MiB")

        # Click for the edit link
        b.click("#vm-subVmTest1-memory-count button")

        # Test slider
        current_max_memory = int(b.val("#vm-subVmTest1-memory-modal-max-memory"))
        slider = "#vm-subVmTest1-memory-modal-max-memory-slider .pf-v5-c-slider__rail"
        width = b.call_js_func('(function (sel) { return ph_find(sel).offsetWidth; })', slider)
        about_half_way = width / 2 + 1

        b.mouse(slider, "click", about_half_way, 0)
        b.wait_not_val("#vm-subVmTest1-memory-modal-max-memory", current_max_memory)

        # Verify that limiting max memory in offline VMs below memory will decrease memory as well
        b.set_input_text("#vm-subVmTest1-memory-modal-max-memory", str(current_memory - 20))
        b.blur("#vm-subVmTest1-memory-modal-max-memory")
        self.assertEqual(int(b.attr("#vm-subVmTest1-memory-modal-memory", "value")), current_memory - 20)

        # Verify that unit conversions work
        b.select_from_dropdown("#vm-subVmTest1-memory-modal-memory-unit-select", "GiB")
        b.wait_attr("#vm-subVmTest1-memory-modal-memory", "value", "0")

        # Run VM
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        # Non-persistent VM doesn't have configurable memory
        m.execute("virsh undefine subVmTest1")
        b.wait_visible("#vm-subVmTest1-memory-count button:disabled")

    def testMultipleSettings(self):
        b = self.browser
        m = self.machine

        args = self.createVm("subVmTest1")

        self.login_and_go("/machines")
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.goToVmPage("subVmTest1")

        self.waitCirrOSBooted(args['logfile'])

        # Change CPU model setting
        b.click("#vm-subVmTest1-cpu button")
        b.wait_visible("#machines-cpu-modal-dialog")
        cpu_model = "host-model"
        if m.image in distrosWithDefaultHostModel:
            cpu_model = "host-passthrough"
        b.select_from_dropdown("#cpu-model-select-group select", cpu_model)
        b.set_input_text("#machines-vcpu-max-field input", "3")  # Change values
        b.set_input_text("#machines-vcpu-count-field input", "3")
        b.click("#machines-cpu-modal-dialog-apply")  # Save
        b.wait_not_present("#machines-cpu-modal-dialog")

        # Change watchdog
        b.click("#vm-subVmTest1-watchdog-button")
        # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
        b.wait_visible("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box")
        b.click("#reset")
        b.click("#watchdog-dialog-apply")
        b.wait_not_present("#vm-subVmTest1-watchdog-modal")

        # Shut off domain
        self.performAction("subVmTest1", "forceOff")

        dom_xml = "virsh -c qemu:///system dumpxml subVmTest1"

        xmllint_elem = f"{dom_xml} | xmllint --xpath 'string(//domain/cpu/@mode)' - 2>&1 || true"
        wait(lambda: cpu_model in m.execute(xmllint_elem).strip())

        xmllint_elem = f"{dom_xml} | xmllint --xpath 'string(//domain/vcpu)' - 2>&1 || true"
        wait(lambda: "3" in m.execute(xmllint_elem).strip())

        virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
        self.assertEqual(virsh_output, 'action="reset"')

        # Check both changes have been applied
        b.wait_in_text("#vm-subVmTest1-cpu", "3 vCPUs, host passthrough" if m.image in distrosWithDefaultHostModel else "3 vCPUs, host")
        b.wait_in_text("#vm-subVmTest1-watchdog-state", "Reset")

    def testWatchdog(self):
        b = self.browser
        m = self.machine
        action_strings = {
            "reset": "Reset",
            "shutdown": "Gracefully shutdown",
            "poweroff": "Power off",
            "pause": "Pause",
            "none": "Do nothing",
            "dump": "Dump",
            "inject-nmi": "Inject a non-maskable interrupt",
        }

        def openWatchDogDialog():
            b.click("#vm-subVmTest1-watchdog-button")
            # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
            b.wait_visible("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box")

        def closeWatchDogDialog(selector):
            b.click(selector)
            b.wait_not_present("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box")

        def setWatchdogAction(action, machine_has_no_watchdog=False, pixel_test_tag=None, reboot_machine=False):
            # If no watchdog action is set, we are attaching a new watchdog device. If watchdog already is set, we are editing an exiting watchdog device
            if machine_has_no_watchdog:
                b.wait_in_text("#vm-subVmTest1-watchdog-button", "add")
            else:
                b.wait_in_text("#vm-subVmTest1-watchdog-button", "edit")

            openWatchDogDialog()

            if pixel_test_tag:
                b.assert_pixels("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box", pixel_test_tag, skip_layouts=["rtl"])

            b.click(f"#{action}")

            if machine_has_no_watchdog:
                b.wait_in_text("#watchdog-dialog-apply", "Add")
            else:
                b.wait_in_text("#watchdog-dialog-apply", "Save")

            closeWatchDogDialog("#watchdog-dialog-apply")

            b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[action])

            virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output, f'action="{action}"')

        def setWatchdogActionLive(action, previous_action=None, reboot_machine=False):
            openWatchDogDialog()

            b.click(f"#{action}")
            if previous_action:
                # When editing an exiting watchdog, message warning user that changes will take effect after reboot should be present
                b.wait_visible("#vm-subVmTest1-watchdog-modal #vm-subVmTest1-idle-message")
            else:
                # When attaching adding a new watchdog, no message should be present
                b.wait_not_present("#vm-subVmTest1-watchdog-modal #vm-subVmTest1-idle-message")

            closeWatchDogDialog("#watchdog-dialog-apply")

            if previous_action:
                # When editing an exiting watchdog, tooltip should be present on overview card warning user that changes will take effect after reboot
                b.wait_visible("#watchdog-tooltip")
                b.wait_visible("#vm-subVmTest1-needs-shutdown")
            else:
                # When attaching adding a new watchdog, no tooltip should be present
                b.wait_not_present("#watchdog-tooltip")
                b.wait_not_present("#vm-subVmTest1-needs-shutdown")

            # Libvirt doesn't support editing wathdog device on RUNNING vm, so a live VM config will persist the previously configured watchdog action until reboot
            # On the other hand, libvirt does support attaching a watchdog to a RUNNING VM
            # So in summary:
            # - if no previous watchdog action is configured on a VM (meaning VM has no watchdog device), we are attaching a new wathdog device
            #   and live VM config have new action set
            # - if watchdog action is configured on a VM (meaning VM has a watchdog device), we are editing an editing wathdog device
            #   and live VM config will persist the old setting until VM is rebooted
            expected_action = previous_action if previous_action else action
            b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[expected_action])
            virsh_output_offline = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output_offline, f'action="{expected_action}"')

            # Offline VM config will always have newly configured watchdog action, no matter if we are attaching a new watchdog device or editing an existing one
            virsh_output_offline = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
            self.assertEqual(virsh_output_offline, f'action="{action}"')

            if reboot_machine:
                # Check that after rebooting machine, live VM config will have new watchdog condiguration
                self.performAction("subVmTest1", "forceOff")
                b.click("#vm-subVmTest1-system-run")
                b.wait_in_text("#vm-subVmTest1-system-state", "Running")

                b.wait_in_text("#vm-subVmTest1-watchdog-state", action_strings[action])
                virsh_output_current = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog/@action' -").strip()
                self.assertEqual(virsh_output_current, f'action="{action}"')

        def removeWatchdogDevice(live=True):
            b.click("#vm-subVmTest1-watchdog-button")
            # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
            b.wait_visible("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box")

            b.click("#watchdog-dialog-detach")
            b.wait_not_present("#vm-subVmTest1-watchdog-modal.pf-v5-c-modal-box")

            b.wait_in_text("#vm-subVmTest1-watchdog-state", "none")
            # check no watchdog is present in VM's xml (also xmllint is expected to return non-zero code)
            virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/watchdog' - 2>&1 || true").strip()
            self.assertEqual(virsh_output, 'XPath set is empty')

            if live:
                # check no watchdog is present in VM's xml (also xmllint is expected to return non-zero code)
                virsh_output = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/watchdog' - 2>&1 || true").strip()
                self.assertEqual(virsh_output, 'XPath set is empty')

        args = self.createVm("subVmTest1", running=False)

        self.login_and_go("/machines")
        self.waitPageInit()
        self.goToVmPage("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-watchdog-state", "none")

        # General checks for watchdog
        # Cancel
        openWatchDogDialog()
        closeWatchDogDialog("#watchdog-dialog-apply + button")

        # "X" of the dialog
        openWatchDogDialog()
        closeWatchDogDialog("#vm-subVmTest1-watchdog-modal button[aria-label=Close]")

        openWatchDogDialog()
        # Check no default watchdog device is selected
        self.assertTrue(b.get_checked("#reset"))  # Reset is pre-selected as default when no watchdog is attached
        self.assertFalse(b.get_checked("#poweroff"))
        self.assertFalse(b.get_checked("#inject-nmi"))
        self.assertFalse(b.get_checked("#pause"))
        closeWatchDogDialog("#vm-subVmTest1-watchdog-modal button[aria-label=Close]")

        # Test configuring watchdog for shutoff VM
        setWatchdogAction(action="reset", machine_has_no_watchdog=True)
        setWatchdogAction(action="poweroff", pixel_test_tag="watchdog")
        setWatchdogAction(action="inject-nmi")
        removeWatchdogDevice()

        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Test configuring watchdog for running VM
        setWatchdogActionLive(action="reset")
        setWatchdogActionLive(action="pause", previous_action="reset")
        # Make sure that the VM booted normally before attempting to hotunplug
        self.waitCirrOSBooted(args['logfile'])
        removeWatchdogDevice(live=True)
        # Check rebooting machine will not unexpectedly affect watchdog configuration
        setWatchdogActionLive(action="poweroff", reboot_machine=True)

        m.execute("virsh destroy subVmTest1")

        # Some OSes don't support target.hotplug=off
        if m.image.startswith("rhel-8") or m.image in ["centos-8-stream"]:
            self.assertIn("Unknown --controller options", m.execute(
                "! virt-xml subVmTest1 --edit model=pci-root --controller target.hotplug=off 2>&1")
            )
            m.execute("virsh start subVmTest1")
        else:
            # Disable hotplugging for subVmTest1
            m.execute("virt-xml subVmTest1 --edit model=pci-root --controller target.hotplug=off")
            # Cleanup watchdog configuration
            m.execute("virt-xml subVmTest1 --remove-device --watchdog 1")
            m.execute("virsh start subVmTest1")

            # Test hotplug will fail and warning is shown
            openWatchDogDialog()
            b.wait_not_present("#watchdog-dialog-apply-next-boot")
            b.click("#reset")
            b.click("#watchdog-dialog-apply")
            b.wait_in_text("#vm-subVmTest1-watchdog-modal .pf-m-warning", "Could not dynamically add watchdog")
            b.wait_visible("#watchdog-dialog-apply[aria-disabled=true]")
            # Apply coldplug
            closeWatchDogDialog("#watchdog-dialog-apply-next-boot")
            # Watchdog requires fresh boot
            b.wait_visible("#watchdog-tooltip")

            # Destroy and start a VM so libvirt reloads updated XML
            m.execute("virsh destroy subVmTest1; virsh start subVmTest1")
            b.wait_not_present("#watchdog-tooltip")

        args = self.createVm("subVmTest2")
        self.goToMainPage()
        self.waitPageInit()
        self.goToVmPage("subVmTest2")
        m.execute("virsh undefine subVmTest2")
        wait(lambda: "no" in m.execute("virsh dominfo subVmTest2 | grep ^Persistent"), delay=3)
        # UI was notified that VM is transient
        b.wait_visible("#vm-subVmTest2-memory-count button[aria-disabled=true]")

        # Watchdog can be attached to a transient VM
        b.click("#vm-subVmTest2-watchdog-button")
        # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
        b.wait_visible("#vm-subVmTest2-watchdog-modal.pf-v5-c-modal-box")
        b.click("#pause")
        b.click("#watchdog-dialog-apply")
        b.wait_not_present("#vm-subVmTest2-watchdog-modal")
        b.wait_in_text("#vm-subVmTest2-watchdog-state", action_strings["pause"])

        # Watchdog of transient VM should not be editable
        b.click("#vm-subVmTest2-watchdog-button")
        # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
        b.wait_visible("#vm-subVmTest2-watchdog-modal.pf-v5-c-modal-box")
        b.wait_visible("#watchdog-dialog-apply[aria-disabled=true]")
        b.mouse("#watchdog-dialog-apply", "mouseenter")
        b.wait_visible("#watchdog-live-edit-tooltip")

    def testVsock(self):
        b = self.browser
        m = self.machine

        default_libvirt_vsock_address = "3"  # When we don't specify address, libvit will choose lowest available (3 in case of clean VM)

        def setVsock(auto, address=None, machine_has_no_vsock=False, pixel_test_tag=None, reboot_machine=False):
            # If no vsock configured, we are attaching a new vsock device. If vsock already is set, we are editing an exiting vsock device
            if machine_has_no_vsock:
                b.wait_in_text("#vm-subVmTest1-vsock-button", "add")
            else:
                b.wait_in_text("#vm-subVmTest1-vsock-button", "edit")

            b.click("#vm-subVmTest1-vsock-button")
            # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
            b.wait_visible("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box")

            if pixel_test_tag:
                b.assert_pixels("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box", pixel_test_tag)

            b.set_checked("#vsock-cid-generate", not auto)
            if not auto:
                b.set_input_text("#vsock-context-identifier input", address)

            if machine_has_no_vsock:
                b.wait_in_text("#vsock-dialog-apply", "Add")
            else:
                b.wait_in_text("#vsock-dialog-apply", "Save")

            b.click("#vsock-dialog-apply")
            b.wait_not_present("#vm-subVmTest1-vsock-modal")

            b.wait_in_text("#vm-subVmTest1-vsock-address", "assign automatically" if auto else address)

            virsh_output_auto = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@auto' -").strip()
            virsh_output_address = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@address' -").strip()
            self.assertEqual(virsh_output_auto, 'auto="yes"' if auto else 'auto="no"')
            if not auto:
                self.assertEqual(virsh_output_address, f'address="{address}"')

        def setVsockLive(new_auto, new_address=None, previous_address=None, previous_auto=None, reboot_machine=False):
            b.click("#vm-subVmTest1-vsock-button")
            # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
            b.wait_visible("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box")

            b.set_checked("#vsock-cid-generate", not new_auto)
            if not new_auto:
                b.set_input_text("#vsock-context-identifier input", new_address)

            if previous_address or previous_auto:
                # When editing an exiting vsock, message warning user that changes will take effect after reboot should be present
                b.wait_visible("#vm-subVmTest1-vsock-modal #vm-subVmTest1-idle-message")
            else:
                # When attaching adding a new vsock, no message should be present
                b.wait_not_present("#vm-subVmTest1-vsock-modal #vm-subVmTest1-idle-message")

            b.click("#vsock-dialog-apply")
            b.wait_not_present("#vm-subVmTest1-vsock-modal")

            if previous_address or previous_auto:
                # When editing an exiting vsock, tooltip should be present on overview card warning user that changes will take effect after reboot
                b.wait_visible("#vsock-tooltip")
            else:
                # When attaching adding a new vsock, no tooltip should be present
                b.wait_not_present("#vsock-tooltip")

            # Libvirt doesn't support editing vsock device on RUNNING vm, so a live VM config will persist the previously configured vsock action until reboot
            # On the other hand, libvirt does support attaching a vsock to a RUNNING VM
            # So in summary:
            # - if no previous vsock is configured on a VM, we are attaching a new vsock device and live VM config will get updated
            # - if vsock action is configured on a VM, and we are editing an editing vsock device, the live VM config will persist the old setting until VM is rebooted
            virsh_output_auto = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@auto' -").strip()
            if previous_auto is not None:  # Editing exiting vsock
                expected_auto = "yes" if previous_auto else "no"
            else:  # Attaching new vsock
                expected_auto = "yes" if new_auto else "no"
            self.assertEqual(virsh_output_auto, f'auto="{expected_auto}"')

            if previous_address is not None:
                expected_address = previous_address  # Editing exiting vsock, old vsock is still present in live XML
            else:
                if new_address is not None:
                    expected_address = new_address   # Attaching new vsock, new vsock is already present in live VM
                else:
                    expected_address = default_libvirt_vsock_address  # When we don't specify address, libvit will choose
            b.wait_in_text("#vm-subVmTest1-vsock-address", expected_address)
            virsh_output_address = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@address' -").strip()
            self.assertEqual(virsh_output_address, f'address="{expected_address}"')

            # Offline VM config will always have newly configured vsock, no matter if we are attaching a new vsock device or editing an existing one
            expected_auto_offline = "yes" if new_auto else "no"
            virsh_output_offline_auto = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/vsock/cid/@auto' -").strip()
            self.assertEqual(virsh_output_offline_auto, f'auto="{expected_auto_offline}"')

            expected_address_offline = new_address
            virsh_output_offline_address = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/vsock/cid/@address' -").strip()
            if expected_address_offline:
                self.assertEqual(virsh_output_offline_address, f'address="{expected_address_offline}"')

            if reboot_machine:
                # Check that after rebooting machine, live VM config will have new vsock condiguration
                self.performAction("subVmTest1", "forceOff")
                b.click("#vm-subVmTest1-system-run")
                b.wait_in_text("#vm-subVmTest1-system-state", "Running")

                expected_after_reboot_auto = "yes" if new_auto else "no"
                virsh_output_auto = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@auto' -").strip()
                self.assertEqual(virsh_output_auto, f'auto="{expected_after_reboot_auto}"')

                expected_after_reboot_address = new_address if new_address else default_libvirt_vsock_address
                b.wait_in_text("#vm-subVmTest1-vsock-address", expected_after_reboot_address)
                virsh_output_address = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock/cid/@address' -").strip()
                self.assertEqual(virsh_output_address, f'address="{expected_after_reboot_address}"')

        def removeVsockDevice(live=True):
            b.click("#vm-subVmTest1-vsock-button")
            # HACK: PF modal with duplicate id's https://github.com/patternfly/patternfly-react/issues/9399
            b.wait_visible("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box")

            b.click("#vsock-dialog-detach")
            b.wait_not_present("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box")

            b.wait_in_text("#vm-subVmTest1-vsock-address", "none")
            # check no vsock is present in VM's xml (also xmllint is expected to return non-zero code)
            virsh_output = m.execute("virsh dumpxml subVmTest1 | xmllint --xpath '/domain/devices/vsock' - 2>&1 || true").strip()
            self.assertEqual(virsh_output, 'XPath set is empty')

            if live:
                # check no vsock is present in VM's xml (also xmllint is expected to return non-zero code)
                virsh_output = m.execute("virsh dumpxml subVmTest1 --inactive | xmllint --xpath '/domain/devices/vsock' - 2>&1 || true").strip()
                self.assertEqual(virsh_output, 'XPath set is empty')

        self.createVm("subVmTest1", running=False)

        self.login_and_go("/machines")
        self.waitPageInit()
        self.goToVmPage("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-vsock-address", "none")

        # The whole UI is based around expectation that libvirt allows maximum 1 vsock per VM
        # Have a canary here which will let us know once this is no longer true
        m.execute("virt-xml subVmTest1 --add-device --vsock cid.auto=yes")
        # Attaching second vsock is expected to fail
        self.assertRaises(subprocess.CalledProcessError, m.execute, "virt-xml subVmTest1 --add-device --vsock cid.auto=no,cid.address=8")
        m.execute("virt-xml subVmTest1 --remove-device --vsock 1")

        # Test configuring vsock for shutoff VM
        setVsock(auto=True, machine_has_no_vsock=True)
        setVsock(auto=False, address="5", pixel_test_tag="vsock")
        removeVsockDevice()

        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Because of bug in debian-testing, hotplug of vsock device fails
        # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1033872
        if m.image not in ["debian-testing", "debian-stable"]:
            # Test configuring vsock for running VM
            setVsockLive(new_auto=False, new_address="5")
            setVsockLive(new_auto=True, previous_auto=False, previous_address="5", reboot_machine=True)
            setVsockLive(new_auto=False, new_address="4", previous_auto=True, previous_address=default_libvirt_vsock_address)

            m.execute("virsh undefine subVmTest1")
            wait(lambda: "no" in m.execute("virsh dominfo subVmTest1 | grep ^Persistent"), delay=3)
            # UI was notified that VM is transient
            b.wait_visible("#vm-subVmTest1-memory-count button[aria-disabled=true]")

            # Vsock of transient VM should not be editable
            b.click("#vm-subVmTest1-vsock-button")
            b.wait_visible("#vm-subVmTest1-vsock-modal.pf-v5-c-modal-box")
            b.wait_visible("#vsock-dialog-apply[aria-disabled=true]")
            b.mouse("#vsock-dialog-apply", "mouseenter")
            b.wait_visible("#vsock-live-edit-tooltip")


if __name__ == '__main__':
    test_main()
