#!/usr/bin/python3

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

import 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 nondestructive, wait, test_main  # noqa

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


@nondestructive
class TestMachinesSettings(VirtualMachinesCase):

    def testVCPU(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")
        self.goToVmPage("subVmTest1")

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

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

        # Test basic vCPU properties
        b.wait_val("#machines-vcpu-count-field", "1")
        b.wait_val("#machines-vcpu-max-field", "1")

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

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

        # Save
        b.click("#machines-vcpu-modal-dialog-apply")
        b.wait_not_present(".pf-c-modal-box__body")

        # Make sure warning next to vcpus appears
        b.wait_visible("#vcpus-tooltip")

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

        # Make sure warning is gone after shut off
        b.wait_not_present("#vcpus-tooltip")

        # Check changes
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "3")

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        b.wait_in_text("#vm-subVmTest1-vcpus-count", "2")

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

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

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

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

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

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

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

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

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

            self.goToMainPage()

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

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

        self.createVm("subVmTest1")

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

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

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

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

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

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

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

    def testBootOrder(self):
        b = self.browser

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        # Check memory hotunplugging
        # The balloon driver needs to be loaded to descrease memory
        wait(lambda: "login as 'cirros' user" in self.machine.execute(f"cat {args['logfile']}"), delay=3)

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

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

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

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

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

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

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

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

        args = self.createVm("subVmTest1")

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

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

        wait(lambda: "login as 'cirros' user" in m.execute(f"cat {args['logfile']}"), delay=3)

        # Change CPU model setting
        b.click("#vm-subVmTest1-cpu-model button")
        b.select_from_dropdown("#cpu-model-select-group select", "host-passthrough")
        b.click("#cpu-config-dialog-apply")
        b.wait_visible("#cpu-tooltip")

        # Change vCPUs setting
        b.click("#vm-subVmTest1-vcpus-count button")  # Open dialog
        b.set_input_text("#machines-vcpu-max-field", "3")  # Change values
        b.set_input_text("#machines-vcpu-count-field", "3")
        b.click("#machines-vcpu-modal-dialog-apply")  # Save
        b.wait_not_present(".pf-c-modal-box__body")

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

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

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

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

        # Check both changes have been applied
        b.wait_in_text("#vm-subVmTest1-cpu-model", "host passthrough")
        b.wait_in_text("#vm-subVmTest1-vcpus-count", "3")


if __name__ == '__main__':
    test_main()
