#!/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) 2021 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 json

import machineslib
import testlib
from machinesxmls import TEST_NETWORK4_XML, TEST_NETWORK_XML


@testlib.nondestructive
class TestMachinesNICs(machineslib.VirtualMachinesCase):
    def setUp(self):
        super().setUp()
        # querying object manager often runs into that on network changes; irrelevant
        self.allow_journal_messages("org.freedesktop.NetworkManager: couldn't get managed objects at /org/freedesktop: Timeout was reached")  # noqa: E501

    @testlib.no_retry_when_changed
    def testVmNICs(self):
        b = self.browser
        m = self.machine

        self.createVm("subVmTest1")

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

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

        # Wait for the dynamic IP address to be assigned before logging in
        # If the IP will change or get assigned after fetching the domain data the user will not see any
        # changes until they refresh the page, since there is not any signal associated with this change
        cmd = "virsh domifaddr subVmTest1  | grep 192.168.122. | wc -l"
        testlib.wait(lambda: "1" in self.machine.execute(cmd), delay=3)
        if "rhel-8" not in m.image:
            cmd = "virsh domifaddr --source agent subVmTest1  | grep fe80:: | wc -l"
            testlib.wait(lambda: "1" in self.machine.execute(cmd), delay=3)

        self.goToVmPage("subVmTest1")

        b.wait_in_text("#vm-subVmTest1-network-1-type", "network")
        b.wait_in_text('#vm-subVmTest1-networks td[data-label="Source"]', "Network")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "default")
        b.wait_in_text('#vm-subVmTest1-networks td[data-label="Source"]', "TAP device")
        b.wait_in_text("#vm-subVmTest1-network-1-tapdevice", "vnet0")
        b.wait_in_text("#vm-subVmTest1-network-1-ipv4-address", "192.168.122.")
        if "rhel-8" not in m.image:
            b.wait_in_text("#vm-subVmTest1-network-1-ipv6-address", "fe80::")
        b.wait_visible('td[data-label="IP address"].pf-m-width-20')
        b.wait_in_text("#vm-subVmTest1-network-1-state", "up")
        b.assert_pixels("#vm-subVmTest1-networks",
                        "vm-details-nics-card",
                        ignore=["#vm-subVmTest1-network-1-mac",
                                "#vm-subVmTest1-network-1-ipv4-address",
                                "#vm-subVmTest1-network-1-ipv6-address"],
                        # MAC and IP address values are not static, and their values affect the width of
                        # their columns and neighbouring columns
                        # With medium layout this variable width of the columns makes pixel tests references
                        # of the table different each test run
                        skip_layouts=["medium", "rtl"])

        # Remove default interface
        self.deleteIface(1)

        # Test add network
        m.execute("virsh attach-interface --domain subVmTest1 --type network --source default --target vnet1 "
                  "--model virtio --mac 52:54:00:4b:73:5f --config --live", timeout=300)

        b.wait_in_text("#vm-subVmTest1-network-1-type", "network")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "default")
        b.wait_in_text("#vm-subVmTest1-network-1-tapdevice", "vnet1")
        b.wait_in_text("#vm-subVmTest1-network-1-model", "virtio")
        b.wait_in_text("#vm-subVmTest1-network-1-mac", "52:54:00:4b:73:5f")
        b.wait_in_text("#vm-subVmTest1-network-1-state", "up")

        # Test bridge network
        m.execute("virsh attach-interface --domain subVmTest1 --type bridge --source virbr0 --model virtio "
                  "--mac 52:54:00:4b:73:5e --config --live", timeout=300)

        # vNICs are alphabetically sorted by MAC address, so now the new vNIC is first in the list
        b.wait_in_text("#vm-subVmTest1-network-1-type", "bridge")
        b.wait_in_text('#vm-subVmTest1-networks tbody:nth-child(2) td[data-label="Source"]', "Bridge")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "virbr0")

        # Non-running VM doesn't have IP address present, so the "IP address" column should not be present
        b.wait_visible("th:contains('IP address')")
        self.performAction("subVmTest1", "forceOff")
        b.wait_not_present("th:contains('IP address')")
        b.wait_not_in_text('#vm-subVmTest1-networks tbody:nth-child(2) td[data-label="Source"]', "TAP device")

        # Sources in  Add vNIC dialog should be alphabetically sorted
        m.execute("ip link add name abridge type bridge")
        m.execute("ip link add name 0bridge type bridge")
        m.execute("ip link add name xbridge type bridge")
        self.addCleanup(m.execute, "ip link delete abridge")
        self.addCleanup(m.execute, "ip link delete 0bridge")
        self.addCleanup(m.execute, "ip link delete xbridge")
        b.reload()
        b.enter_page('/machines')
        self.waitPageInit()
        b.click("#vm-subVmTest1-add-iface-button")  # open the Network Interfaces subtab
        b.wait_visible("#vm-subVmTest1-add-iface-dialog")
        b.select_from_dropdown("#vm-subVmTest1-add-iface-type", "bridge")
        # dropdown options are not visible, but still part of DOM tree, so let's use _wait_present
        b._wait_present("#vm-subVmTest1-add-iface-source option:nth-of-type(1):contains(0bridge)")
        b._wait_present("#vm-subVmTest1-add-iface-source option:nth-of-type(2):contains(abridge)")
        b._wait_present("#vm-subVmTest1-add-iface-source option:nth-of-type(3):contains(virbr0)")
        b._wait_present("#vm-subVmTest1-add-iface-source option:nth-of-type(4):contains(xbridge)")
        b.click(".pf-v5-c-modal-box__footer button:contains(Cancel)")
        b.wait_not_present("#vm-subVmTest1-add-iface-dialog")

        m.execute("virsh start subVmTest1")
        # Attach vNIC only to live config
        m.execute("virsh attach-interface --domain subVmTest1 --type network --source default --model virtio "
                  "--mac 52:54:00:4b:73:6f --live", timeout=300)
        b.wait_in_text("#vm-subVmTest1-network-3-mac", "52:54:00:4b:73:6f")
        # vNIC which is only attached to live config cannot be edited
        b.wait_visible("#vm-subVmTest1-network-3-edit-dialog[aria-disabled=true]")
        # validate css is correct even if edit button is disabled
        # https://bugzilla.redhat.com/show_bug.cgi?id=2215597
        b.assert_pixels("#vm-subVmTest1-networks",
                        "vm-details-nics-card-edit-disabled",
                        ignore=["#vm-subVmTest1-network-1-mac",
                                "#vm-subVmTest1-network-1-ipv4-address",
                                "#vm-subVmTest1-network-1-source",
                                "#vm-subVmTest1-network-2-mac",
                                "#vm-subVmTest1-network-2-ipv4-address",
                                "#vm-subVmTest1-network-2-source",
                                "#vm-subVmTest1-network-3-mac",
                                "#vm-subVmTest1-network-3-ipv4-address",
                                "#vm-subVmTest1-network-3-source"],
                        # MAC and IP address values are not static, and their values affect the width of
                        # their columns and neighbouring columns
                        # With medium layout this variable width of the columns makes pixel tests references of
                        # the table different each test run
                        skip_layouts=["medium", "rtl"])

        # Start vm and wait until kernel is booted
        testlib.wait(lambda: "1" in self.machine.execute("virsh domifaddr subVmTest1 | grep 192.168.122. | wc -l"), delay=3)
        b.reload()
        b.enter_page('/machines')
        self.waitPageInit()

        # Check interfaces have different IP addresses
        # This tests a fix for a bug[1] where we used to show same IP address for every interface
        # https://github.com/cockpit-project/cockpit-machines/issues/1142
        b.wait_visible("#vm-subVmTest1-network-1-ip-unknown")
        b.wait_in_text("#vm-subVmTest1-network-2-ipv4-address", "192.168.122.")
        b.wait_visible("#vm-subVmTest1-network-3-ip-unknown")

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

        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")

        # Plug and unplug NICs
        def plugAndUnplugNICs():
            mac_address = b.text("#vm-subVmTest1-network-1-mac")

            b.wait_in_text("#vm-subVmTest1-network-1-state", "up")
            b.wait_in_text("#vm-subVmTest1-iface-1-unplug", "Unplug")

            # Click "Unplug"
            b.click("#vm-subVmTest1-iface-1-unplug")

            b.wait_in_text("#vm-subVmTest1-network-1-state", "down")
            b.wait_in_text("#vm-subVmTest1-iface-1-plug", "Plug")
            testlib.wait(lambda: "down" in m.execute(f"virsh domif-getlink subVmTest1 {mac_address}"))

            # Click "plug"
            b.click("#vm-subVmTest1-iface-1-plug")

            b.wait_in_text("#vm-subVmTest1-network-1-state", "up")
            b.wait_in_text("#vm-subVmTest1-iface-1-unplug", "Unplug")
            testlib.wait(lambda: "up" in m.execute(f"virsh domif-getlink subVmTest1 {mac_address}"))

        plugAndUnplugNICs()

        self.performAction("subVmTest1", "forceOff")

        # Check again after VM is off
        plugAndUnplugNICs()

    def testNICDelete(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.waitGuestBooted(args['logfile'])
        self.goToVmPage("subVmTest1")

        self.deleteIface(1)
        # Hot-unplug depends on cooperation with the guest OS and takes painfully long
        with b.wait_timeout(90):
            b.wait_not_present("#vm-subVmTest1-network-1-mac")

        # Detach NIC when the VM is shutoff, Also check this issue: https://bugzilla.redhat.com/show_bug.cgi?id=1791543
        self.performAction("subVmTest1", "off")
        # Attach since no NIC now
        m.execute("""
        virsh attach-interface --domain subVmTest1 --type network --source default --model virtio --mac 52:54:00:4b:73:5f --config
        virsh attach-interface --domain subVmTest1 --type bridge --source virbr0 --model virtio --mac 52:54:00:4b:73:6f --config
        virsh attach-interface --domain subVmTest1 --type network --source default --model virtio --mac 52:54:00:4b:73:4f --config
        # Add network with the same MAC address
        virsh attach-interface --domain subVmTest1 --type network --source default --model e1000e --mac 52:54:00:4b:73:5f --config
        """)

        # Refresh for getting new added NIC
        b.reload()
        b.enter_page('/machines')
        self.waitPageInit()
        # Check NIC MAC addresses
        b.wait_text("#vm-subVmTest1-network-1-mac", "52:54:00:4b:73:6f")
        b.wait_text("#vm-subVmTest1-network-2-mac", "52:54:00:4b:73:4f")
        b.wait_text("#vm-subVmTest1-network-3-mac", "52:54:00:4b:73:5f")
        b.wait_text("#vm-subVmTest1-network-3-model", "virtio")
        b.wait_text("#vm-subVmTest1-network-4-mac", "52:54:00:4b:73:5f")
        b.wait_text("#vm-subVmTest1-network-4-model", "e1000e")
        # Detach
        self.deleteIface(2, "52:54:00:4b:73:4f", "subVmTest1")
        with b.wait_timeout(90):
            b.wait_not_present("#vm-subVmTest1-network-4-mac")
        b.wait_text_not("#vm-subVmTest1-network-2-mac", "52:54:00:4b:73:4f")
        b.wait_text("#vm-subVmTest1-network-2-mac", "52:54:00:4b:73:5f")
        b.wait_text("#vm-subVmTest1-network-2-model", "virtio")
        # Test deleting the interface with same MAC address as the other vNIC will delete the correct one
        self.deleteIface(2, "52:54:00:4b:73:5f", "subVmTest1")
        with b.wait_timeout(90):
            b.wait_not_present("#vm-subVmTest1-network-3-mac")
        b.wait_text("#vm-subVmTest1-network-2-mac", "52:54:00:4b:73:5f")
        b.wait_text("#vm-subVmTest1-network-2-model", "e1000e")

        # Test for bugzilla #2030948
        b.click("#vm-subVmTest1-system-run")
        m.execute("virsh undefine subVmTest1")
        b.click(".machines-listing-breadcrumb li a:contains(Virtual machines)")
        b.wait_visible("tr[data-row-id=vm-subVmTest1-system][data-vm-transient=true]")
        self.goToVmPage("subVmTest1")
        self.deleteIface(1)
        self.performAction("subVmTest1", "forceOff", checkExpectedState=False)
        self.waitVmRow("subVmTest1", "system", False)

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

        m.write("/tmp/xml", TEST_NETWORK_XML)
        m.write("/tmp/xml2", TEST_NETWORK4_XML)
        m.execute("virsh net-define /tmp/xml; virsh net-start test_network")
        m.execute("virsh net-define /tmp/xml2")

        args = self.createVm("subVmTest1")

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

        self.goToVmPage("subVmTest1")
        self.deleteIface(1)  # Remove default vNIC

        mac_address = "52:54:00:a5:f8:00"
        # Test a error message handling
        self.NICAddDialog(
            self,
            source_type="network",
            source="test_network4",
            mac=mac_address,
            xfail=True,
            xfail_error="Network interface settings could not be saved",  # fail because network 'test_network4' is not active
        ).execute()

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

        # No NICs present
        b.wait_visible("#vm-subVmTest1-add-iface-button")  # open the Network Interfaces subtab

        mac_address = self.get_next_mac(mac_address)
        self.NICAddDialog(
            self,
            source_type="network",
            source="test_network",
            mac=mac_address,
            pixel_test_tag="vm-add-nic-modal"
        ).execute()

        # Test direct
        mac_address = self.get_next_mac(mac_address)
        self.NICAddDialog(
            self,
            mac=mac_address,
            source_type="direct",
        ).execute()

        # Test Bridge
        mac_address = self.get_next_mac(mac_address)
        self.NICAddDialog(
            self,
            mac=mac_address,
            source_type="bridge",
            source="virbr0",
        ).execute()

        # Test model
        mac_address = self.get_next_mac(mac_address)
        self.NICAddDialog(
            self,
            mac=mac_address,
            model="e1000e",
            remove=False,
        ).execute()

        # avoid duplicate MAC
        self.NICAddDialog(
            self,
            source_type="network",
            source="test_network",
            mac=mac_address,
            xfail=True,
            xfail_error="MAC address already in use",
        ).execute()

        # remove the first one from "Test model" again, it fails to start
        self.deleteIface(1)

        # Start vm and wait until kernel is booted
        m.execute(f"> {args['logfile']}")  # clear logfile
        b.click("#vm-subVmTest1-system-run")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")
        self.waitGuestBooted(args['logfile'])

        # Because of bug in debian-testing, attachment of virtio vNICs after restarting VM will fail
        # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1005284
        if m.image not in ["debian-stable", "debian-testing"]:
            # Test permanent attachment to running VM
            mac_address = self.get_next_mac(mac_address)
            self.NICAddDialog(
                self,
                mac=mac_address,
                source_type="network",
                source="test_network",
                permanent=True,
            ).execute()

            # Test NIC attaching to non-persistent VM
            m.execute("virsh dumpxml --inactive subVmTest1 > /tmp/subVmTest1.xml; virsh undefine subVmTest1")
            b.wait_visible("div[data-vm-transient=\"true\"]")
            mac_address = self.get_next_mac(mac_address)
            self.NICAddDialog(
                self,
                source_type="network",
                source="test_network",
                mac=mac_address,
                nic_num=2,
                persistent_vm=False,
            ).execute()
            m.execute("virsh define /tmp/subVmTest1.xml")
            b.wait_visible("div[data-vm-transient=\"false\"]")
            self.performAction("subVmTest1", "forceOff")

            mac_address = self.get_next_mac(mac_address)
            self.NICAddDialog(
                self,
                mac=mac_address,
                remove=False,
            ).execute()

            mac_address = self.get_next_mac(mac_address)
            self.NICAddDialog(
                self,
                mac=mac_address,
                nic_num=2,
                remove=False,
            ).execute()

    class NICEditDialog:

        def __init__(
            self,
            test_obj,
            mac="52:54:00:f0:eb:63",
            model=None,
            nic_num=1,
            source=None,
            source_type=None,
            xfail_error=None,
        ):
            self.assertEqual = test_obj.assertEqual
            self.browser = test_obj.browser
            self.mac = mac
            self.machine = test_obj.machine
            self.model = model
            self.nic_num = nic_num
            self.source = source
            self.source_type = source_type
            self.xfail_error = xfail_error
            self.vm_state = "running" if "running" in test_obj.machine.execute("virsh domstate subVmTest1") else "shut off"

        def execute(self):
            self.open()
            self.fill()
            self.save()
            if not self.xfail_error:
                self.verify()
                self.verify_overview()
            else:
                self.browser.wait_in_text(".pf-v5-c-modal-box__body .pf-v5-c-alert__title", self.xfail_error)
                self.browser.click(".pf-v5-c-modal-box__footer button:contains(Cancel)")

        def open(self):
            b = self.browser

            b.click(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog")
            b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-modal-window")

            # select widget options are never visible for the headless chrome - call therefore directly the js function
            self.source_type_current = b.attr(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-type", "data-value")
            self.source_current = b.attr(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-source", "data-value")
            self.mac_current = b.val(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-mac")
            self.model_current = b.attr(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-model", "data-value")

        def fill(self):
            b = self.browser

            if self.source_type:
                b.select_from_dropdown(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-type", self.source_type)
            if self.source:
                b.select_from_dropdown(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-source", self.source)
            if self.model:
                b.select_from_dropdown(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-model", self.model)

            if self.vm_state == "running":
                b.wait_attr(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-mac", "readonly", "")
            else:
                b.set_input_text(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-mac", self.mac)

            if (self.vm_state == "running" and
                    ((self.source_type is not None and self.source_type != self.source_type_current) or
                        (self.source is not None and self.source != self.source_current) or
                        (self.model is not None and self.model != self.model_current) or
                        (self.mac is not None and self.mac != self.mac_current))):
                b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-idle-message")
            else:
                b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-idle-message")

        def save(self):
            b = self.browser

            b.click(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-save")
            if not self.xfail_error:
                b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-modal-window")

        def cancel(self):
            b = self.browser

            b.click(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-cancel")
            b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-edit-dialog-modal-window")

        def verify(self):
            dom_xml = "virsh -c qemu:///system dumpxml --domain subVmTest1"
            mac_string = f'"{self.mac}"'
            xmllint_element = f"{dom_xml} | xmllint --xpath 'string(//domain/devices/interface[starts-with(mac/@address,{mac_string})]/{{prop}})' -"  # noqa: E501

            if self.source_type == "network":
                self.assertEqual(
                    "network" if self.vm_state == "shut off" else self.source_type_current,
                    self.machine.execute(xmllint_element.format(prop='@type')).strip()
                )
                if self.source:
                    self.assertEqual(
                        self.source if self.vm_state == "shut off" else self.source_current,
                        self.machine.execute(xmllint_element.format(prop='source/@network')).strip()
                    )
            elif self.source_type == "direct":
                self.assertEqual(
                    "direct" if self.vm_state == "shut off" else self.source_type_current,
                    self.machine.execute(xmllint_element.format(prop='@type')).strip()
                )
                if self.source:
                    self.assertEqual(
                        self.source,
                        self.machine.execute(xmllint_element.format(prop='source/@dev')).strip()
                    )
            if self.model:
                self.assertEqual(
                    self.model if self.vm_state == "shut off" else self.model_current,
                    self.machine.execute(xmllint_element.format(prop='model/@type')).strip()
                )

        def verify_overview(self):
            b = self.browser

            b.wait_in_text(
                f"#vm-subVmTest1-network-{self.nic_num}-type",
                self.source_type if self.source_type and self.vm_state == "shut off" else self.source_type_current
            )
            b.wait_in_text(
                f"#vm-subVmTest1-network-{self.nic_num}-source",
                self.source if self.source and self.vm_state == "shut off" else self.source_current
            )
            b.wait_in_text(
                f"#vm-subVmTest1-network-{self.nic_num}-model",
                self.model if self.model and self.vm_state == "shut off" else self.model_current
            )
            b.wait_in_text(
                f"#vm-subVmTest1-network-{self.nic_num}-mac",
                self.mac if self.mac and self.vm_state == "shut off" else self.mac_current
            )
            if self.vm_state == "running":
                needs_reboot_visible = False
                if self.source_type is not None and self.source_type != self.source_type_current:
                    b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-type-tooltip")
                    needs_reboot_visible = True
                else:
                    b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-type-tooltip")

                if self.source is not None and self.source != self.source_current:
                    b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-source-tooltip")
                    needs_reboot_visible = True
                # Changing source_type may in effect also change source, so tooltip being present is correct
                # Only changing source type between bridge and direct should not affect source
                elif self.source_type in ["direct", "bridge"] and self.source_type_current in ["direct", "bridge"]:
                    b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-source-tooltip")

                if self.mac is not None and self.mac != self.mac_current:
                    b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-mac-tooltip")
                    needs_reboot_visible = True
                else:
                    b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-mac-tooltip")

                if self.model is not None and self.model != self.model_current:
                    b.wait_visible(f"#vm-subVmTest1-network-{self.nic_num}-model-tooltip")
                    needs_reboot_visible = True
                else:
                    b.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-model-tooltip")

                if needs_reboot_visible:
                    b.wait_visible("#vm-subVmTest1-needs-shutdown")
                else:
                    b.wait_not_present("#vm-subVmTest1-needs-shutdown")

    def testNICEdit(self):
        b = self.browser

        self.add_veth("eth42")
        self.add_veth("eth43")

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

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

        self.goToVmPage("subVmTest1")

        # Remove default vNIC
        self.deleteIface(1)

        MAC1 = "52:54:00:f0:eb:63"

        self.NICAddDialog(
            self,
            source_type="bridge",
            source="virbr0",
            mac=MAC1,
            remove=False
        ).execute()

        # create a second NIC, and check that trying to edit to existing MAC is prohibited
        self.NICAddDialog(
            self,
            source_type="bridge",
            source="virbr0",
            nic_num=2,
            mac="52:54:00:f0:eb:64",
            remove=False
        ).execute()
        self.NICEditDialog(
            self,
            nic_num=2,
            mac=MAC1,
            source_type="network",
            xfail_error="MAC address already in use",
        ).execute()
        self.deleteIface(2)

        # The dialog fields should reflect the permanent configuration
        dialog = self.NICEditDialog(self)
        dialog.open()
        b.wait_in_text("#vm-subVmTest1-network-1-edit-dialog-source", "virbr0")
        b.wait_val("#vm-subVmTest1-network-1-edit-dialog-mac", MAC1)
        b.assert_pixels("#vm-subVmTest1-network-1-edit-dialog-modal-window", "vm-edit-nic-modal", skip_layouts=["rtl"])
        dialog.cancel()

        # Check "X" of the dialog
        dialog.open()
        b.click("#vm-subVmTest1-network-1-edit-dialog-modal-window button[aria-label=Close]")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")

        # Changing the NIC configuration for a shut off VM should not display any warning
        self.NICEditDialog(
            self,
            source_type="direct",
            source="eth43",
        ).execute()
        # Check "Source" of the 'direct' NIC
        b.wait_in_text('#vm-subVmTest1-networks tbody tr:nth-child(1) td[data-label="Source"]', "Direct")
        b.wait_not_in_text('#vm-subVmTest1-networks tbody tr:nth-child(1) td[data-label="Source"]', "TAP device")

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

        # Test a warning shows up when editing a vNIC for a running VM
        self.NICEditDialog(
            self,
            source_type="direct",
            source="eth43",
        ).execute()

        # Change source type from direct to virtual network - https://bugzilla.redhat.com/show_bug.cgi?id=1977669
        self.NICEditDialog(
            self,
            source_type="network",
        ).execute()

        # Changing settings of a transient NIC is not implemented - https://bugzilla.redhat.com/show_bug.cgi?id=1977669
        self.NICAddDialog(
            self,
            source_type="direct",
            source="eth43",
            mac="52:54:00:f0:eb:64",
            nic_num=2,
            remove=False
        ).execute()
        b.wait_visible("#vm-subVmTest1-network-2-edit-dialog[aria-disabled=true]")
        b.wait_in_text('#vm-subVmTest1-network-2-tapdevice', "macvtap")

    class NICAddDialog:

        def __init__(
            # We have always have to specify mac and source_type to identify the device in xml and $virsh detach-interface
            self, test_obj, source_type="direct", source=None, model=None, nic_num=1,
            permanent=False, mac=None, remove=True, persistent_vm=True,
            xfail=False, xfail_error=None, pixel_test_tag=None
        ):
            self.bridge_devices = (entry['ifname'] for entry in json.loads(test_obj.machine.execute("ip -j link show type bridge")))
            self.source_type = source_type
            self.source = source
            self.model = model
            self.permanent = permanent
            self.mac = mac
            self.remove = remove
            self.persistent_vm = persistent_vm
            self.nic_num = nic_num

            self.browser = test_obj.browser
            self.machine = test_obj.machine
            self.assertEqual = test_obj.assertEqual
            self.deleteIface = test_obj.deleteIface

            self.xfail = xfail
            self.xfail_error = xfail_error
            self.pixel_test_tag = pixel_test_tag

        def execute(self):
            self.open()
            self.fill()
            self.assert_pixels()
            self.create()
            if not self.xfail:
                self.verify()
                self.verify_overview()
                if self.remove:
                    self.cleanup()

        def open(self):
            self.browser.click("#vm-subVmTest1-add-iface-button")  # open the Network Interfaces subtab
            self.browser.wait_in_text(".pf-v5-c-modal-box .pf-v5-c-modal-box__header .pf-v5-c-modal-box__title",
                                      "Add virtual network interface")

        def fill(self):
            self.browser.select_from_dropdown("#vm-subVmTest1-add-iface-type", self.source_type)

            # Ensure that Bridge to LAN option offers only bridge devices for selection and vice versa
            for bridge in self.bridge_devices:
                if self.source_type != "bridge":
                    self.browser.wait_not_present(f"#vm-subVmTest1-add-iface-source option[value={bridge}]")
                else:
                    self.browser._wait_present(f"#vm-subVmTest1-add-iface-source option[value={bridge}]")

            if self.source:
                self.browser.select_from_dropdown("#vm-subVmTest1-add-iface-source", self.source)
            if self.model:
                self.browser.select_from_dropdown("#vm-subVmTest1-add-iface-model", self.model)

            if self.mac:
                self.browser.click("#vm-subVmTest1-add-iface-set-mac")
                self.browser.set_input_text("#vm-subVmTest1-add-iface-mac", self.mac)

            if self.permanent:
                self.browser.click("#vm-subVmTest1-add-iface-permanent")

            if not self.persistent_vm:
                self.browser.wait_not_present("#vm-subVmTest1-add-iface-permanent")

        def assert_pixels(self):
            if self.pixel_test_tag:
                self.browser.assert_pixels("#vm-subVmTest1-add-iface-dialog", self.pixel_test_tag, skip_layouts=["rtl"])

        def cancel(self):
            self.browser.click(".pf-v5-c-modal-box__footer button:contains(Cancel)")
            self.browser.wait_not_present("#vm-subVmTest1-add-iface-dialog")

        def create(self):
            self.browser.click(".pf-v5-c-modal-box__footer button:contains(Add)")

            if not self.xfail:
                with self.browser.wait_timeout(60):
                    self.browser.wait_not_present("#vm-subVmTest1-add-iface-dialog")
            else:
                self.browser.wait_in_text(".pf-v5-c-modal-box__body .pf-v5-c-alert__title", self.xfail_error)
                self.browser.click(".pf-v5-c-modal-box__footer button:contains(Cancel)")

        def verify(self):
            # Verify libvirt XML
            dom_xml = "virsh -c qemu:///system dumpxml --domain subVmTest1"
            mac_string = f'"{self.mac}"'
            xmllint_element = f"{dom_xml} | xmllint --xpath 'string(//domain/devices/interface[starts-with(mac/@address,{mac_string})]/{{prop}})' -"  # noqa: E501

            if (self.source_type == "network"):
                self.assertEqual("network", self.machine.execute(xmllint_element.format(prop='@type')).strip())
                if self.source:
                    self.assertEqual(self.source, self.machine.execute(xmllint_element.format(prop='source/@network')).strip())
            elif (self.source_type == "direct"):
                self.assertEqual("direct", self.machine.execute(xmllint_element.format(prop='@type')).strip())
                if self.source:
                    self.assertEqual(self.source, self.machine.execute(xmllint_element.format(prop='source/@dev')).strip())

            if (self.model):
                self.assertEqual(self.model, self.machine.execute(xmllint_element.format(prop='model/@type')).strip())

        def verify_overview(self):
            # The first NIC is default, our new NIC is second in row
            self.browser.wait_in_text(f"#vm-subVmTest1-network-{self.nic_num}-type", self.source_type)
            if self.model:
                self.browser.wait_in_text(f"#vm-subVmTest1-network-{self.nic_num}-model", self.model)
            if self.source:
                self.browser.wait_in_text(f"#vm-subVmTest1-network-{self.nic_num}-source", self.source)
            if self.mac:
                self.browser.wait_in_text(f"#vm-subVmTest1-network-{self.nic_num}-mac", self.mac)

        def cleanup(self):
            if self.permanent:
                self.machine.execute(f"virsh detach-interface --mac {self.mac} --domain subVmTest1 --type {self.source_type} --config")

                # we don't get any signal for interface detaching right now
                self.browser.reload()
                self.browser.enter_page('/machines')
                self.browser.wait_in_text("body", "Virtual machines")
            else:
                self.deleteIface(self.nic_num)

                # Check NIC is no longer in list; the device-removed signal can take *very* long to arrive
                with self.browser.wait_timeout(90):
                    self.browser.wait_not_present(f"#vm-subVmTest1-network-{self.nic_num}-mac")
                self.browser.wait_not_present(".pf-v5-c-modal-box")


if __name__ == '__main__':
    testlib.test_main()
