#!/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 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 no_retry_when_changed, nondestructive, test_main, wait  # noqa
from machinesxmls import TEST_NETWORK_XML  # noqa


@nondestructive
class TestMachinesNICs(VirtualMachinesCase):
    def deleteIface(self, iface):
        b = self.browser

        b.click("#delete-vm-subVmTest1-iface-{0}".format(iface))
        b.wait_in_text(".pf-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Delete Network Interface")
        b.click(".pf-c-modal-box__footer button:contains(Delete)")

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

        self.createVm("subVmTest1")

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

        b.wait_in_text("#vm-subVmTest1-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
        wait(lambda: "1" in self.machine.execute("virsh domifaddr subVmTest1  | grep 192.168.122. | wc -l"), delay=3)
        self.goToVmPage("subVmTest1")

        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-ipv4-address", "192.168.122.")

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

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

        b.wait_in_text("#vm-subVmTest1-network-2-type", "network")
        b.wait_in_text("#vm-subVmTest1-network-2-source", "default")
        b.wait_in_text("#vm-subVmTest1-network-2-model", "virtio")
        b.wait_in_text("#vm-subVmTest1-network-2-mac", "52:54:00:4b:73:5f")
        b.wait_in_text("#vm-subVmTest1-network-2-ipv4-address", "192.168.122.")

        b.wait_in_text("#vm-subVmTest1-network-2-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")

        b.wait_in_text("#vm-subVmTest1-network-3-type", "bridge")
        b.wait_in_text("#vm-subVmTest1-network-3-source", "virbr0")
        b.wait_in_text("#vm-subVmTest1-network-3-ipv4-address", "192.168.122.")

    def testNICDelete(self):
        b = self.browser

        args = self.createVm("subVmTest1")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        self.waitVmRow("subVmTest1")
        b.wait_in_text("#vm-subVmTest1-state", "Running")
        wait(lambda: "login as 'cirros' user." in self.machine.execute("cat {0}".format(args["logfile"])), delay=3)
        self.goToVmPage("subVmTest1")

        self.deleteIface(1)
        b.wait_not_present(".pf-c-modal-box")
        b.wait_not_present("#vm-subVmTest1-network-1-mac")

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

        m.execute("echo \"{0}\" > /tmp/xml && virsh net-define /tmp/xml && virsh net-start test_network".format(TEST_NETWORK_XML))

        args = self.createVm("subVmTest1")

        self.login_and_go("/machines")
        b.wait_in_text("body", "Virtual machines")
        # Shut off domain
        b.click("#vm-subVmTest1-action-kebab button")
        b.click("#vm-subVmTest1-forceOff")
        b.wait_in_text("#vm-subVmTest1-state", "Shut off")

        self.goToVmPage("subVmTest1")

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

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

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

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

        # Test model
        self.NICAddDialog(
            self,
            model="e1000e",
        ).execute()

        # Start vm and wait until kernel is booted
        m.execute("> {0}".format(args["logfile"]))  # clear logfile
        b.click("#vm-subVmTest1-run")
        b.wait_in_text("#vm-subVmTest1-state", "Running")
        wait(lambda: "login as 'cirros' user." in self.machine.execute("cat {0}".format(args["logfile"])), delay=3)

        # Test permanent attachment to running VM
        self.NICAddDialog(
            self,
            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\"]")
        self.NICAddDialog(
            self,
            source_type="network",
            source="test_network",
            mac="52:54:00:a5:f8:c1",
            nic_num=3,
            persistent_vm=False,
        ).execute()
        m.execute("virsh define /tmp/subVmTest1.xml")
        b.wait_visible("div[data-vm-transient=\"false\"]")
        b.click("#vm-subVmTest1-action-kebab button")
        b.click("#vm-subVmTest1-forceOff")
        b.wait_in_text("#vm-subVmTest1-state", "Shut off")

        self.NICAddDialog(
            self,
            remove=False,
        ).execute()

        self.NICAddDialog(
            self,
            remove=False,
        ).execute()

        # Now there are three NICs present - try to delete the second one and make sure that it's deleted and the dialog is not present anymore
        b.wait_visible("#vm-subVmTest1-network-3-mac")
        self.deleteIface(2)
        # Check NIC is no longer in list
        b.wait_not_present("#vm-subVmTest1-network-3-mac")
        b.wait_not_present(".pf-c-modal-box")

    class NICEditDialog:

        def __init__(
            self,
            test_obj,
            mac="52:54:00:f0:eb:63",
            model=None,
            nic_num=2,
            source=None,
            source_type=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.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()
            self.verify()
            self.verify_overview()

        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.text(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")
            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 = '"{0}"'.format(self.mac)
            xmllint_element = "{0} | xmllint --xpath 'string(//domain/devices/interface[starts-with(mac/@address,{1})]/{{prop}})' - 2>&1 || true".format(dom_xml, mac_string)

            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 if self.vm_state == "shut off" else 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
            )

    def testNICEdit(self):
        b = self.browser

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

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

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

        self.goToVmPage("subVmTest1")

        # Test Warning message when changes are done in a running VM
        self.NICAddDialog(
            self,
            source_type="bridge",
            source="eth42",
            mac="52:54:00:f0:eb:63",
            remove=False
        ).execute()

        # The dialog fields should reflect the permanent configuration
        dialog = self.NICEditDialog(self)
        dialog.open()
        b.wait_in_text("#vm-subVmTest1-network-2-edit-dialog-source", "eth42")
        b.wait_val("#vm-subVmTest1-network-2-edit-dialog-mac", "52:54:00:f0:eb:63")
        b.assert_pixels("#vm-subVmTest1-network-2-edit-dialog-modal-window", "vm-edit-nic-modal")
        dialog.cancel()

        # Changing the NIC configuration for a shut off VM should not display any warning
        self.NICEditDialog(
            self,
            source_type="direct",
            source="eth43",
        ).execute()

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

        # Test a warning shows up when editing a vNIC for a running VM
        self.NICEditDialog(
            self,
            source="eth42",
        ).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=3,
            remove=False
        ).execute()
        b.wait_visible("#vm-subVmTest1-network-3-edit-dialog[aria-disabled=true]")

    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=2,
            permanent=False, mac="52:54:00:a5:f8:c0", remove=True, persistent_vm=True,
            pixel_test_tag=None
        ):
            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.pixel_test_tag = pixel_test_tag

        def execute(self):
            self.open()
            self.fill()
            self.assert_pixels()
            self.create()
            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-c-modal-box .pf-c-modal-box__header .pf-c-modal-box__title", "Add virtual network interface")

        def fill(self):
            self.browser.select_from_dropdown("#vm-subVmTest1-add-iface-type", self.source_type)
            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)

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

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

            self.browser.wait_not_present("#vm-subVmTest1-add-iface-dialog")

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

            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("#vm-subVmTest1-network-{0}-type".format(self.nic_num), self.source_type)
            if self.model:
                self.browser.wait_in_text("#vm-subVmTest1-network-{0}-model".format(self.nic_num), self.model)
            if self.source:
                self.browser.wait_in_text("#vm-subVmTest1-network-{0}-source".format(self.nic_num), self.source)
            if self.mac:
                self.browser.wait_in_text("#vm-subVmTest1-network-{0}-mac".format(self.nic_num), self.mac)

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

                # 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)
                vm_state = self.browser.text("#vm-subVmTest1-state")
                # On a running VM detaching NIC takes longer and we can see the spinner
                if vm_state == "Running":
                    self.browser.wait_visible(".pf-c-modal-box__footer button.pf-m-in-progress")

                # Check NIC is no longer in list
                self.browser.wait_not_present("#vm-subVmTest1-network-{0}-mac".format(self.nic_num))
                self.browser.wait_not_present(".pf-c-modal-box")


if __name__ == '__main__':
    test_main()
