#!/usr/bin/python3

# 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 os
import sys
import xml.etree.ElementTree as ET

# import Cockpit's machinery for test VMs and its browser test API
TEST_DIR = os.path.dirname(__file__)
sys.path.append(os.path.join(TEST_DIR, "common"))
sys.path.append(os.path.join(os.path.dirname(TEST_DIR), "bots/machine"))

from machineslib import VirtualMachinesCase  # noqa
from testlib import nondestructive, test_main, wait  # noqa
from machinesxmls import TEST_NETWORK2_XML, TEST_NETWORK3_XML, TEST_NETWORK4_XML, TEST_NETWORK_XML  # noqa


def getNetworkDevice(m):
    net_devices_str = m.execute("virsh iface-list")
    net_devices_str = net_devices_str.split("\n", 2)[2]  # Remove first 2 lines of table header
    if net_devices_str not in ["\n", "\r\n"]:
        device = net_devices_str.split(' ', 2)[1]  # Get the name of device, ignoring spacing before device string
    else:  # If $virsh-iface list did not return any device, check virsh nodedev-list net:noh
        net_devices_str = m.execute("virsh nodedev-list net")
        net_devices_str = net_devices_str.split("\n", 2)[0]
        device = net_devices_str.split("_", 2)[1]  # Ignore prefix (example: net_enp0s31f6_8c_16_45_5f_77_34)

    return device


@nondestructive
class TestMachinesNetworks(VirtualMachinesCase):

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

        connectionName = m.execute("virsh uri | head -1 | cut -d/ -f4").strip()

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        b.wait_in_text("#card-pf-networks .card-pf-title-link", "1 Network")

        # Create dummy network
        m.execute(f"echo \"{TEST_NETWORK2_XML}\" > /tmp/xml; virsh net-define /tmp/xml")
        b.wait_in_text("#card-pf-networks .card-pf-title-link", "2 Networks")
        m.execute(f"echo \"{TEST_NETWORK3_XML}\" > /tmp/xml; virsh net-define /tmp/xml")
        m.execute(f"echo \"{TEST_NETWORK4_XML}\" > /tmp/xml; virsh net-create /tmp/xml")

        # Click on Networks card
        b.click(".pf-c-card .pf-c-card__header button:contains(Networks)")

        # Check that all networks are there
        b.wait_in_text("body", "Networks")
        self.waitNetworkRow("test_network2", connectionName)
        self.waitNetworkRow("test_network3", connectionName)

        # Check headers of networks
        b.wait_in_text(f"#network-test_network2-{connectionName}-name", "test_network2")
        b.wait_in_text(f"#network-test_network2-{connectionName}-device", "virbr1")
        b.wait_in_text(f"#network-test_network2-{connectionName}-forwarding", "None (isolated network)")
        b.wait_in_text(f"#network-test_network3-{connectionName}-name", "test_network3")
        b.wait_in_text(f"#network-test_network3-{connectionName}-device", "br0")
        b.wait_in_text(f"#network-test_network3-{connectionName}-forwarding", "Bridge")

        # Expand row for first network
        self.toggleNetworkRow("test_network2", connectionName)

        # Check overview network properties are present
        b.wait_in_text(f"#network-test_network2-{connectionName}-persistent", "yes")
        b.wait_visible(f"#network-test_network2-{connectionName}-autostart:not(:checked)")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv4-address", "192.168.100.1")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv4-netmask", "255.255.255.0")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv4-dhcp-range", "192.168.100.128 - 192.168.100.170")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv4-dhcp-host-0", "Name: paul, MAC: 00:16:3E:5D:C7:9E, IP: 192.168.122.254")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv6-address", "fd00:e81d:a6d7:55::1")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv6-prefix", "64")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv6-dhcp-range", "fd00:e81d:a6d7:55::100 - fd00:e81d:a6d7:55::1ff")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv6-dhcp-host-0", "Name: simon, IP: 2001:db8:ca2:2:3::1")
        b.wait_in_text(f"#network-test_network2-{connectionName}-ipv6-dhcp-host-1", "ID: 0:1:0:1:18:aa:62:fe:0:16:3e:44:55:66, IP: 2001:db8:ca2:2:3::2")

        # Close expanded row for this pool
        self.toggleNetworkRow("test_network2", connectionName)

        # Expand row for second network
        self.toggleNetworkRow("test_network3", connectionName)

        # Check overview network properties are present
        b.wait_in_text(f"#network-test_network3-{connectionName}-persistent", "yes")
        b.wait_visible(f"#network-test_network3-{connectionName}-autostart:not(:checked)")

        # Check overview network properties are not present
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv4-address")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv4-netmask")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv4-dhcp-range")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv4-dhcp-host-0")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv6-address")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv6-prefix")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv6-dhcp-range")
        b.wait_not_present(f"#network-test_network3-{connectionName}-ipv6-dhcp-host-0")

        # Transient network
        self.toggleNetworkRow("test_network4", connectionName)
        b.wait_in_text(f"#network-test_network4-{connectionName}-persistent", "no")
        b.wait_not_present(f"#network-test_network4-{connectionName}-autostart")  # Transient network shouldn't have autostart option
        b.click(f"#network-test_network4-{connectionName}-action-kebab button")
        b.wait_visible(f'#delete-network-test_network4-{connectionName} a.pf-m-aria-disabled')  # Transient network cannot be deleted
        b.click(f'#deactivate-network-test_network4-{connectionName}')  # Deactivate transient network
        self.waitNetworkRow("test_network4", connectionName, False)  # Check it's not present after deactivation

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

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        # Click on Networks card
        b.wait_in_text("#card-pf-networks .pf-c-card__header button", "Network")
        b.click(".pf-c-card .pf-c-card__header button:contains(Network)")

        class NetworkCreateDialog(object):
            def __init__(
                self, test_obj, name, forward_mode=None, ip_conf=None, ipv4_address=None, ipv4_netmask=None, ipv6_address=None, ipv6_prefix=None, device=None,
                ipv4_dhcp_start=None, ipv4_dhcp_end=None, ipv6_dhcp_start=None, ipv6_dhcp_end=None, xfail=False, xfail_error=None, xfail_objects=None,
                remove=True, activate=False
            ):
                self.test_obj = test_obj
                self.name = name
                self.forward_mode = forward_mode
                self.device = device
                self.ip_conf = ip_conf
                self.ipv4_address = ipv4_address
                self.ipv4_netmask = ipv4_netmask
                self.ipv6_address = ipv6_address
                self.ipv6_prefix = ipv6_prefix
                self.ipv4_dhcp_start = ipv4_dhcp_start
                self.ipv4_dhcp_end = ipv4_dhcp_end
                self.ipv6_dhcp_start = ipv6_dhcp_start
                self.ipv6_dhcp_end = ipv6_dhcp_end
                self.xfail = xfail
                self.xfail_objects = xfail_objects
                self.xfail_error = xfail_error
                self.remove = remove
                self.activate = activate

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

            def open(self):
                b.click("#create-network")
                b.wait_visible("#create-network-dialog")
                b.wait_in_text(".pf-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Create virtual network")

            def fill(self):
                b.set_input_text("#create-network-name", self.name)

                if self.forward_mode:
                    b.set_val("#create-network-forward-mode", self.forward_mode)

                if self.device:
                    b.select_from_dropdown("#create-network-device", self.device)

                if self.ip_conf:
                    b.select_from_dropdown("#create-network-ip-configuration", self.ip_conf)

                    if "4" in self.ip_conf:
                        b.set_input_text("#network-ipv4-address", self.ipv4_address)
                        b.set_input_text("#network-ipv4-netmask", self.ipv4_netmask)
                        if self.ipv4_dhcp_start is not None and self.ipv4_dhcp_end is not None:
                            b.set_checked("#network-ipv4-dhcp", True)
                            b.set_input_text("#network-ipv4-dhcp-range-start", self.ipv4_dhcp_start)
                            b.set_input_text("#network-ipv4-dhcp-range-end", self.ipv4_dhcp_end)

                    if "6" in self.ip_conf:
                        b.set_input_text("#network-ipv6-address", self.ipv6_address)
                        b.set_input_text("#network-ipv6-prefix", self.ipv6_prefix)
                        if self.ipv6_dhcp_start is not None and self.ipv6_dhcp_end is not None:
                            b.set_checked("#network-ipv6-dhcp", True)
                            b.set_input_text("#network-ipv6-dhcp-range-start", self.ipv6_dhcp_start)
                            b.set_input_text("#network-ipv6-dhcp-range-end", self.ipv6_dhcp_end)

            def cancel(self):
                b.click(".pf-c-modal-box__footer button:contains(Cancel)")
                b.wait_not_present("#create-network-dialog")

            def create(self):
                b.click(".pf-c-modal-box__footer button:contains(Create)")

                if (self.xfail):
                    # Check incomplete dialog
                    if "name" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #create-network-name + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv4_address" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv4-address + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv4_netmask" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv4-netmask + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv4_dhcp_start" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv4-dhcp-range-start + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv4_dhcp_end" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv4-dhcp-range-end + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv6_address" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv6-address + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv6_prefix" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv6-prefix + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv6_dhcp_start" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv6-dhcp-range-start + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "ipv6_dhcp_end" in self.xfail_objects:
                        b.wait_in_text("#create-network-dialog .pf-c-modal-box__body #network-ipv6-dhcp-range-end + .pf-c-form__helper-text.pf-m-error", self.xfail_error)
                    if "footer" in self.xfail_objects:
                        error_location = "#create-network-dialog .pf-c-modal-box__body .pf-m-danger"
                        b.wait_visible(error_location)
                        error_message = b.text(error_location)
                        self.test_obj.assertIn(self.xfail_error, error_message)

                    self.cancel()
                else:
                    b.wait_not_present("#create-network-dialog")

            def verify_expected_error_on_head(self, error_message):
                b.click(f'#network-{self.name}-system-state button:contains("view more")')
                b.wait_in_text(".pf-c-popover", error_message)
                b.click(f'#network-{self.name}-system-state button[aria-label=Close]')

            def verify_dialog(self):
                # Check that the defined network is now visible
                b.wait_in_text("body", "Networks")

                # Verify libvirt XML
                net_xml = f"virsh -c qemu:///system net-dumpxml {self.name}"
                xmllint_element = f"{net_xml} | xmllint --xpath 'string(//network/{{prop}})' - 2>&1 || true"

                self.test_obj.assertEqual(self.name, m.execute(xmllint_element.format(prop='name')).strip())
                if (self.forward_mode == "none"):
                    self.test_obj.assertEqual("", m.execute(xmllint_element.format(prop='forward/@mode')).strip())
                else:
                    self.test_obj.assertEqual(self.forward_mode, m.execute(xmllint_element.format(prop='forward/@mode')).strip())

                if self.device:
                    self.test_obj.assertEqual(self.device, m.execute(xmllint_element.format(prop='forward/interface/@dev')).strip())

                if (self.ip_conf != "None"):
                    if "4" in self.ip_conf:
                        self.test_obj.assertEqual(self.ipv4_address, m.execute(xmllint_element.format(prop='ip/@address')).strip())
                        self.test_obj.assertEqual(self.ipv4_netmask, m.execute(xmllint_element.format(prop='ip/@netmask')).strip())
                        if self.ipv4_dhcp_start and self.ipv4_dhcp_start:
                            self.test_obj.assertEqual(self.ipv4_dhcp_start, m.execute(xmllint_element.format(prop='ip/dhcp/range/@start')).strip())
                            self.test_obj.assertEqual(self.ipv4_dhcp_end, m.execute(xmllint_element.format(prop='ip/dhcp/range/@end')).strip())
                    if "6" in self.ip_conf:
                        self.test_obj.assertEqual(self.ipv6_address, m.execute(xmllint_element.format(prop='ip[starts-with(@family,"ipv6")]/@address')).strip())
                        self.test_obj.assertEqual(self.ipv6_prefix, m.execute(xmllint_element.format(prop='ip[starts-with(@family,"ipv6")]/@prefix')).strip())
                        if self.ipv6_dhcp_start and self.ipv6_dhcp_start:
                            self.test_obj.assertEqual(self.ipv6_dhcp_start, m.execute(xmllint_element.format(prop='ip[starts-with(@family,"ipv6")]/dhcp/range/@start')).strip())
                            self.test_obj.assertEqual(self.ipv6_dhcp_end, m.execute(xmllint_element.format(prop='ip[starts-with(@family,"ipv6")]/dhcp/range/@end')).strip())
                else:
                    self.test_obj.assertEqual("", m.execute(xmllint_element.format(prop='ip')).strip())

            def verify_overview(self):
                # Check basic network properties
                modes = {"nat": "NAT", "none": "None (isolated network)", "open": "Open", "route": "Routed",
                         "bridge": "Bridge", "private": "Private", "vepa": "VEPA", "passthrough": "Passthrough", "hostdev": "Hostdev"}
                connectionName = m.execute("virsh uri | head -1 | cut -d/ -f4").strip()

                b.wait_in_text(f"#network-{self.name}-{connectionName}-forwarding", modes[self.forward_mode])
                self.test_obj.toggleNetworkRow(self.name, connectionName)

                if self.activate:
                    b.click(f"#activate-network-{self.name}-{connectionName}")
                    # For checking danger alter, only set xfail_objects, and don't set xfail
                    if self.xfail_objects and 'danger_alert' in self.xfail_objects:
                        self.verify_expected_error_on_head(self.xfail_error)

                if self.ip_conf != "None":
                    if "4" in self.ip_conf:
                        b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv4-address", self.ipv4_address)
                        b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv4-netmask", self.ipv4_netmask)
                        if self.ipv4_dhcp_start and self.ipv4_dhcp_start:
                            b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv4-dhcp-range", self.ipv4_dhcp_start + " - " + self.ipv4_dhcp_end)
                    if "6" in self.ip_conf:
                        b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv6-address", self.ipv6_address)
                        b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv6-prefix", self.ipv6_prefix)
                        if self.ipv6_dhcp_start and self.ipv6_dhcp_start:
                            b.wait_in_text(f"#network-{self.name}-{connectionName}-ipv6-dhcp-range", self.ipv6_dhcp_start + " - " + self.ipv6_dhcp_end)

                    if "4" not in self.ip_conf:
                        b.wait_not_present(f"#network-{self.name}-{connectionName}-ipv4-address")
                    if "6" not in self.ip_conf:
                        b.wait_not_present(f"#network-{self.name}-{connectionName}-ipv6-address")
                else:
                    b.wait_not_present(f"#network-{self.name}-{connectionName}-ipv4-address")
                    b.wait_not_present(f"#network-{self.name}-{connectionName}-ipv6-address")

            def cleanup(self):
                if self.activate and not self.xfail_objects:
                    m.execute(f"virsh net-destroy {self.name}")
                m.execute(f"virsh net-undefine {self.name}")

        # Test various forward Modes
        duplicated_net = NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="nat",
            ip_conf="IPv4 only",
            ipv4_address="192.168.110.1",
            ipv4_netmask="255.255.255.0",
            remove=False,
            activate=True,
        )
        duplicated_net.execute()

        # XFail: Activate a network which has a same ipv4 address with last one
        NetworkCreateDialog(
            self,
            name="test_network_duplication",
            forward_mode="nat",
            ip_conf="IPv4 only",
            ipv4_address="192.168.110.1",
            ipv4_netmask="255.255.255.0",
            activate=True,
            xfail_objects='danger_alert',
            xfail_error="Network is already in use",
        ).execute()
        duplicated_net.cleanup()

        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="open",
            ip_conf="IPv6 only",
            ipv6_address="fd00:e81d:a6d7:55::100",
            ipv6_prefix="64",
        ).execute()

        # Compressed IPv6 addresses should work as well
        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="open",
            ip_conf="IPv6 only",
            ipv6_address="fec0::1",
            ipv6_prefix="48",
            ipv6_dhcp_start="fec0::1",
            ipv6_dhcp_end="fec0::10",
        ).execute()

        tmp = NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="none",
            ip_conf="None",
            remove=False,
        )
        tmp.execute()

        # Try footer error
        NetworkCreateDialog(
            self,
            name="test_network",
            xfail=True,
            xfail_objects=["footer"],
            xfail_error="network 'test_network' already exists",
        ).execute()

        tmp.cleanup()

        # Test full configuration
        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="nat",
            ip_conf="IPv4 and IPv6",
            ipv4_address="192.168.110.1",
            ipv4_netmask="255.255.255.0",
            ipv4_dhcp_start="192.168.110.130",
            ipv4_dhcp_end="192.168.110.170",
            ipv6_address="fd00:e81d:a6d7:55::100",
            ipv6_prefix="64",
            ipv6_dhcp_start="fd00:e81d:a6d7:55::105",
            ipv6_dhcp_end="fd00:e81d:a6d7:55::108",
        ).execute()

        # Check "... should not be empty" warnings
        NetworkCreateDialog(
            self,
            name="",
            forward_mode="open",
            ip_conf="IPv4 and IPv6",
            ipv4_address="",
            ipv4_netmask="",
            ipv4_dhcp_start="",
            ipv4_dhcp_end="",
            ipv6_address="",
            ipv6_prefix="",
            ipv6_dhcp_start="",
            ipv6_dhcp_end="",
            xfail=True,
            xfail_objects=["name", "ipv4_address", "ipv4_netmask", "ipv4_dhcp_start", "ipv4_dhcp_end",
                            "ipv6_address", "ipv6_prefix", "ipv6_dhcp_start", "ipv6_dhcp_end"],
            xfail_error="should not be empty",
        ).execute()

        # Check "Invalid..." (invalid IP format or prefix length)
        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="open",
            ip_conf="IPv4 and IPv6",
            ipv4_address="ABC.168.22.10",
            ipv4_netmask="99",
            ipv4_dhcp_start="300.2.1",
            ipv4_dhcp_end="168..1.1.1",
            ipv6_address="xz00:e81d:a6d7:55::100",
            ipv6_prefix="-1",
            ipv6_dhcp_start="fd00:e81d.a6d7:55:::100",
            ipv6_dhcp_end="fd00:e81d:a6d7:55::1:1:1:1:1:1:1:1",
            xfail=True,
            xfail_objects=["ipv4_address", "ipv4_netmask", "ipv4_dhcp_start", "ipv4_dhcp_end",
                            "ipv6_address", "ipv6_prefix", "ipv6_dhcp_start", "ipv6_dhcp_end"],
            xfail_error="Invalid",
        ).execute()

        # Check "Address not within subnet"
        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="open",
            ip_conf="IPv4 and IPv6",
            ipv4_address="192.168.100.1",
            ipv4_netmask="24",
            ipv4_dhcp_start="192.168.101.1",
            ipv4_dhcp_end="191.168.100.1",
            ipv6_address="fd00:e81d:a6d7:55::100",
            ipv6_prefix="64",
            ipv6_dhcp_start="fd00:e81d:a6d7:54::100",
            ipv6_dhcp_end="ad00:e81d:a6d7:55::100",
            xfail=True,
            xfail_objects=["ipv4_dhcp_start", "ipv4_dhcp_end", "ipv6_dhcp_start", "ipv6_dhcp_end"],
            xfail_error="Address not within subnet",
        ).execute()

        # Check "IPv4 address cannot be same as the network identifier"
        NetworkCreateDialog(
            self,
            name="test_network",
            ip_conf="IPv4 only",
            ipv4_address="192.168.100.0",
            ipv4_netmask="24",
            xfail=True,
            xfail_objects=["ipv4_address"],
            xfail_error="IPv4 address cannot be same as the network identifier",
        ).execute()

        # Check "IPv4 address cannot be same as the network's broadcast address"
        NetworkCreateDialog(
            self,
            name="test_network",
            ip_conf="IPv4 only",
            ipv4_address="192.168.100.255",
            ipv4_netmask="24",
            xfail=True,
            xfail_objects=["ipv4_address"],
            xfail_error="IPv4 address cannot be same as the network's broadcast address",
        ).execute()

        # Test network devices
        device = getNetworkDevice(m)
        NetworkCreateDialog(
            self,
            name="test_network",
            forward_mode="nat",
            device=device,
            ip_conf="IPv4 only",
            ipv4_address="192.168.110.1",
            ipv4_netmask="255.255.255.0",
        ).execute()

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

        self.createVm("subVmTest1")

        # Create dummy network
        m.write("/tmp/xml", TEST_NETWORK_XML)
        m.execute("virsh net-define /tmp/xml; virsh net-start test_network")

        # Create a second bridge to LAN NIC, virbr0 does not make sense but let's use it for test purposes
        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")

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

        # Make sure that the Networks are loaded into the global state
        b.wait_in_text("#card-pf-networks .card-pf-title-link", "2 Networks")

        self.goToVmPage("subVmTest1")

        mac = b.text("#vm-subVmTest1-network-1-mac")
        next_mac = self.get_next_mac(mac)
        m.execute(f'virsh attach-interface --persistent subVmTest1 bridge virbr0 --mac {next_mac}')

        # Wait for the edit button
        b.click("#vm-subVmTest1-network-1-edit-dialog")

        # Make sure the footer warning does not appear until we change something
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-idle-message")

        # Cancel dialog
        b.click("#vm-subVmTest1-network-1-edit-dialog-cancel")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")

        # Fetch current NIC model type
        current_model_type = b.text("#vm-subVmTest1-network-2-model")

        # Reopen dialog modal
        b.click("#vm-subVmTest1-network-2-edit-dialog")

        # Change network model type of a running domain
        b.select_from_dropdown("#vm-subVmTest1-network-2-edit-dialog-model", "e1000e")
        # Wait for the footer warning to appear
        b.wait_visible("#vm-subVmTest1-network-2-edit-dialog-idle-message")
        # Change network type and source of a running domain
        b.wait_val("#vm-subVmTest1-network-2-edit-dialog-type", "network")
        b.wait_val("#vm-subVmTest1-network-2-edit-dialog-source", "default")
        b.select_from_dropdown("#vm-subVmTest1-network-2-edit-dialog-source", "test_network")
        # Save the network settings
        b.click("#vm-subVmTest1-network-2-edit-dialog-save")
        b.wait_not_present("#vm-subVmTest1-network-2-edit-dialog-modal-window")
        # Wait for the tooltips to appear next to the elements we changed
        b.wait_in_text("#vm-subVmTest1-network-2-model", current_model_type)
        b.wait_visible("#vm-subVmTest1-network-2-model-tooltip")
        b.wait_in_text("#vm-subVmTest1-network-2-type", 'network')
        b.wait_in_text("#vm-subVmTest1-network-2-source", 'default')
        b.wait_visible("#vm-subVmTest1-network-2-source-tooltip")

        # Shut off domain and check changes are applied
        self.performAction("subVmTest1", "forceOff")
        b.wait_in_text("#vm-subVmTest1-network-2-model", "e1000e")
        b.wait_not_present("#vm-subVmTest1-network-2-model-tooltip")
        b.wait_in_text("#vm-subVmTest1-network-2-type", "network")
        b.wait_not_present("#vm-subVmTest1-network-2-type-tooltip")
        b.wait_in_text("#vm-subVmTest1-network-2-source", "test_network")
        b.wait_not_present("#vm-subVmTest1-network-2-source-tooltip")

        # Remove the network interface
        m.execute("virsh detach-interface --persistent --type network --domain subVmTest1")

        # We don't get events for shut off VMs so reload the page
        b.reload()
        b.enter_page('/machines')
        b.wait_in_text("body", "Virtual machines")

        self.goToMainPage()

        b.wait_in_text("#card-pf-networks .card-pf-title-link", "2 Networks")
        self.waitVmRow("subVmTest1")
        self.goToVmPage("subVmTest1")

        # Change network type and source from the bridge NIC
        b.wait_in_text("#vm-subVmTest1-network-1-type", "bridge")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "virbr0")

        # Change interface type to direct
        # Open the modal dialog
        b.click("#vm-subVmTest1-network-1-edit-dialog")

        b.wait_val("#vm-subVmTest1-network-1-edit-dialog-type", "bridge")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-type", "direct")
        source = b.val("#vm-subVmTest1-network-1-edit-dialog-source")

        # Save the network settings
        b.click("#vm-subVmTest1-network-1-edit-dialog-save")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")

        b.wait_in_text("#vm-subVmTest1-network-1-type", "direct")
        b.wait_in_text("#vm-subVmTest1-network-1-source", source)

        # Change interface type to bridge
        # Open the modal dialog
        b.click("#vm-subVmTest1-network-1-edit-dialog")

        b.wait_val("#vm-subVmTest1-network-1-edit-dialog-type", "direct")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-type", "bridge")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-source", "virbr0")

        # Save the network settings
        b.click("#vm-subVmTest1-network-1-edit-dialog-save")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")

        b.wait_in_text("#vm-subVmTest1-network-1-type", "bridge")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "virbr0")

        b.click("#vm-subVmTest1-network-1-edit-dialog")
        b.wait_in_text("#vm-subVmTest1-network-1-edit-dialog-source", "virbr0")
        b.click("#vm-subVmTest1-network-1-edit-dialog-save")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "virbr0")

        # Change interface type to network
        # Open the modal dialog
        b.click("#vm-subVmTest1-network-1-edit-dialog")

        b.wait_val("#vm-subVmTest1-network-1-edit-dialog-type", "bridge")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-type", "network")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-source", "test_network")

        # Save the network settings
        b.click("#vm-subVmTest1-network-1-edit-dialog-save")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog-modal-window")

        b.wait_in_text("#vm-subVmTest1-network-1-type", "network")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "test_network")

        # Remove test_network from the VM again
        self.deleteIface(1)

        # Remove all Virtual Networks and confirm that trying to choose
        # Virtual Networks type for a NIC disables the save button
        m.execute("virsh net-dumpxml default > /tmp/net-default.xml")
        m.execute("virsh net-dumpxml test_network > /tmp/net-test-network.xml")
        m.execute("virsh net-destroy test_network; virsh net-destroy default")
        self.addCleanup(m.execute, "virsh net-define /tmp/net-default.xml; virsh net-autostart default")

        self.goToMainPage()

        b.wait_in_text("#card-pf-networks .active-resources:nth-of-type(1)", "0")
        m.execute("virsh net-undefine test_network; virsh net-undefine default")
        b.wait_in_text("#card-pf-networks .active-resources:nth-of-type(2)", "0")
        b.wait_in_text("#card-pf-networks .card-pf-title-link", "0 Networks")

        self.goToVmPage("subVmTest1")

        # Create a second bridge to LAN NIC
        next_mac = self.get_next_mac(next_mac)
        m.execute(f"ip link add name br1 type bridge; virsh attach-interface --current subVmTest1 bridge br1 --mac {next_mac}")
        self.addCleanup(m.execute, "ip link delete br1")

        m.execute("virsh start subVmTest1")
        b.wait_in_text("#vm-subVmTest1-system-state", "Running")

        # Open the modal dialog
        b.click("#vm-subVmTest1-network-1-edit-dialog")

        # And ensure that the network sources dropdown is disabled
        b.wait_val("#vm-subVmTest1-network-1-edit-dialog-type", "bridge")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-type", "network")
        b.wait_visible("#vm-subVmTest1-network-1-edit-dialog-save:disabled")
        b.click(".pf-c-modal-box__footer button:contains(Cancel)")

        # Ensure that when the source of a NIC was removed we can still change it

        # Redefine deleted networks and attach an interface with source a deleted network
        next_mac = self.get_next_mac(next_mac)
        m.execute(f"""
            virsh net-define /tmp/net-default.xml
            virsh net-start default
            virsh attach-interface --persistent --type network --source default --mac {next_mac} --domain subVmTest1
            virsh net-destroy default; virsh net-undefine default
            virsh net-define /tmp/net-test-network.xml; virsh net-start test_network""")

        # First shut of the VM otherwise the interface will not be the same in the live and config XML https://www.redhat.com/archives/libvir-list/2019-August/msg01034.html
        self.performAction("subVmTest1", "forceOff")

        # Try to edit the interface changing the source to a non deleted network
        b.click("#vm-subVmTest1-network-1-edit-dialog")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-type", "network")
        b.select_from_dropdown("#vm-subVmTest1-network-1-edit-dialog-source", "test_network")
        b.click("#vm-subVmTest1-network-1-edit-dialog-save")
        b.wait_in_text("#vm-subVmTest1-network-1-source", "test_network")

        # Test detaching of disk on non-persistent VM
        m.execute("virsh undefine subVmTest1")
        b.wait_not_present("#vm-subVmTest1-network-1-edit-dialog")

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

        # Create dummy network
        m.write("/tmp/xml", TEST_NETWORK2_XML)
        m.execute("virsh net-define /tmp/xml")

        connectionName = m.execute("virsh uri | head -1 | cut -d/ -f4").strip()

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        # Click on Networks card
        b.wait_in_text("#card-pf-networks .pf-c-card__header button", "Network")
        b.click(".pf-c-card .pf-c-card__header button:contains(Network)")

        # Check that all networks are there
        b.wait_in_text("body", "Networks")
        self.waitNetworkRow("test_network2", connectionName)

        # Expand row for first network
        self.toggleNetworkRow("test_network2", connectionName)

        b.wait_visible(f"#network-test_network2-{connectionName}-autostart")

        # set checkbox state and check state of checkbox
        b.set_checked(f"#network-test_network2-{connectionName}-autostart", True)  # don't know the initial state of checkbox, so set it to checked
        b.wait_visible(f"#network-test_network2-{connectionName}-autostart:checked")
        # check virsh state
        autostartState = m.execute("virsh net-info test_network2 | grep 'Autostart:' | awk '{print $2}'").strip()
        self.assertEqual(autostartState, "yes")

        # change checkbox state and check state of checkbox
        b.click(f"#network-test_network2-{connectionName}-autostart")
        b.wait_visible(f"#network-test_network2-{connectionName}-autostart:not(:checked)")
        # check virsh state
        autostartState = m.execute("virsh net-info test_network2 | grep 'Autostart:' | awk '{print $2}'").strip()
        self.assertEqual(autostartState, "no")

        # change checkbox state and check state of checkbox
        b.click(f"#network-test_network2-{connectionName}-autostart")
        b.wait_visible(f"#network-test_network2-{connectionName}-autostart:checked")
        # check virsh state
        autostartState = m.execute("virsh net-info test_network2 | grep 'Autostart:' | awk '{print $2}'").strip()
        self.assertEqual(autostartState, "yes")

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

        # Create dummy network
        m.write("/tmp/xml", TEST_NETWORK2_XML)
        m.execute("virsh net-define /tmp/xml")

        connectionName = m.execute("virsh uri | head -1 | cut -d/ -f4").strip()

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        # Click on Networks card
        b.click(".pf-c-card .pf-c-card__header button:contains(Network)")

        # Check that all networks are there
        b.wait_in_text("body", "Networks")
        self.waitNetworkRow("test_network2", connectionName)

        # Expand row for first network
        self.toggleNetworkRow("test_network2", connectionName)
        # wait until the expanded content is visible
        b.wait_visible("#network-test_network2-system-autostart-off")
        b.wait_in_text("#network-test_network2-system-ipv6-dhcp-host-0", "simon")

        b.assert_pixels("#app", "networks-page")

        # activate network
        b.wait_visible(f"#activate-network-test_network2-{connectionName}")
        b.click(f"#activate-network-test_network2-{connectionName}")
        b.wait_in_text(f"#network-test_network2-{connectionName}-state", "active")
        # check virsh state
        wait(lambda: "yes" == m.execute("virsh net-info test_network2 | grep 'Active:' | awk '{print $2}'").strip(), tries=5)

        # deactivate network
        b.wait_visible(f"#deactivate-network-test_network2-{connectionName}")
        b.click(f"#deactivate-network-test_network2-{connectionName}")
        b.wait_in_text(f"#network-test_network2-{connectionName}-state", "inactive")
        b.wait_visible(f"#activate-network-test_network2-{connectionName}")
        # check virsh state
        wait(lambda: "no" == m.execute("virsh net-info test_network2 | grep 'Active:' | awk '{print $2}'").strip(), tries=5)

        # Delete an inactive network
        b.click(f"#network-test_network2-{connectionName}-action-kebab button")
        b.click(f'#delete-network-test_network2-{connectionName}')
        b.wait_in_text(".pf-c-modal-box__body .pf-c-description-list", "test_network2")
        b.click(".pf-c-modal-box__footer button:contains(Delete)")
        self.waitNetworkRow("test_network2", connectionName, False)

        # Delete an active network
        m.write("/tmp/xml", TEST_NETWORK2_XML)
        m.execute("virsh net-define /tmp/xml; virsh net-start test_network2")
        b.wait_in_text(f"#network-test_network2-{connectionName}-state", "active")
        self.toggleNetworkRow("test_network2", connectionName)
        b.click(f"#network-test_network2-{connectionName}-action-kebab button")
        b.click(f'#delete-network-test_network2-{connectionName}')
        b.wait_in_text(".pf-c-modal-box__body .pf-c-description-list", "test_network2")
        b.click(".pf-c-modal-box__footer button:contains(Delete)")
        self.waitNetworkRow("test_network2", connectionName, False)

    def testNetworkAddStaticDCHPHosts(self):
        class NetworkAddStaticDCHPHostsDialog(object):
            def __init__(
                self, test_obj=None, network_name="default", connection_name="system", mac=None, ip=None, entry_id=0,
                xfail=None, remove=True
            ):
                self.test_obj = test_obj
                self.network_name = network_name
                self.connection_name = connection_name
                self.mac = mac
                self.ip = ip
                self.entry_id = entry_id
                self.xfail = xfail
                self.remove = remove

            def execute(self):
                self.open()
                self.fill()
                self.add()
                if not self.xfail:
                    self.verify_dialog()
                    self.verify_overview()
                    if self.remove:
                        self.cleanup()

            def open(self):
                b.click(f"#network-{self.network_name}-{self.connection_name}-static-host-entries-add")
                b.wait_visible("#add-new-static-entry-mac-address")
                b.wait_in_text(".pf-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Add a DHCP static host entry")

            def fill(self):
                b.set_input_text("#add-new-static-entry-mac-address", self.mac)
                b.set_input_text("#add-new-static-entry-ip-address", self.ip)

            def cancel(self):
                b.click(".pf-c-modal-box__footer button:contains(Cancel)")
                b.wait_not_present("#add-new-static-entry")

            def add(self):
                b.click(".pf-c-modal-box__footer button:contains(Add)")

                if (self.xfail):
                    # Check incomplete dialog
                    b.wait_in_text(".pf-c-modal-box .pf-c-alert.pf-m-danger", self.xfail)
                    self.cancel()
                else:
                    b.wait_not_present("#add-new-static-entry")

            def verify_dialog(self):
                # Check that the defined network is now visible
                b.wait_in_text("body", "Networks")

                # Verify libvirt XML
                net_xml = m.execute(f"virsh -c qemu:///system net-dumpxml {self.network_name}")
                network_node = ET.fromstring(net_xml)
                host_by_mac = network_node.find(f'./ip/dhcp/host[@mac="{self.mac}"]')
                if host_by_mac is None:
                    raise Exception(f'No DHCP host with MAC address {self.mac} found in network XML:\n{net_xml}')
                host_by_ip = network_node.find(f'./ip/dhcp/host[@ip="{self.ip}"]')
                if host_by_ip is None:
                    raise Exception(f'No DHCP host with IP address {self.ip} found in network XML:\n{net_xml}')

            def verify_overview(self):
                b.wait_in_text(f"#network-{self.network_name}-{self.connection_name}-ipv4-dhcp-host-{self.entry_id}", f"MAC: {self.mac}, IP: {self.ip}")

            def cleanup(self):
                b.click(f"#delete-network-{self.network_name}-{self.connection_name}-ipv4-dhcp-host-{self.entry_id}-button")
                b.wait_in_text(".pf-c-modal-box__body .pf-c-description-list", self.ip)
                b.wait_in_text("#delete-resource-modal-ip", self.ip)
                b.wait_in_text("#delete-resource-modal-mac", self.mac)
                b.click(".pf-c-modal-box__footer button:contains(Remove)")
                b.wait_not_present(f"#network-{self.network_name}-{self.connection_name}-ipv4-dhcp-host-{self.entry_id}")

        b = self.browser
        m = self.machine

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

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")

        # Click on Networks card
        b.click(".pf-c-card .pf-c-card__header button:contains(Network)")

        # Check that all networks are there
        b.wait_in_text("body", "Networks")
        self.waitNetworkRow("default")

        # Expand row for first network
        self.toggleNetworkRow("default")

        # Attach basic static DHCP host and try to remove it
        NetworkAddStaticDCHPHostsDialog(
            self,
            mac=self.getDomainMacAddress("subVmTest1"),
            ip="192.168.122.222",
        ).execute()

        # Attach basic static and keep it persistent
        NetworkAddStaticDCHPHostsDialog(
            self,
            mac=self.getDomainMacAddress("subVmTest1"),
            ip="192.168.122.222",
            remove=False,
        ).execute()

        # Check "IPv4 address cannot be same as the network identifier"
        NetworkAddStaticDCHPHostsDialog(
            self,
            mac=self.getDomainMacAddress("subVmTest1"),
            ip="192.168.122.222",
            xfail="there is an existing dhcp host entry",
        ).execute()

        # Check that the static IP is correctly assigned to the VM
        m.execute("virsh start subVmTest1")
        wait(lambda: "1" in self.machine.execute("virsh domifaddr subVmTest1 | grep 192.168.122.222 | wc -l"), delay=3)


if __name__ == '__main__':
    test_main()
