#!/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 time

# 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, skipImage  # noqa


@nondestructive
class TestMachinesConsoles(VirtualMachinesCase):

    @skipImage('spice-server does not exist on RHEL 9', "rhel-9-1", "rhel-9-2", "centos-9-stream")
    def testExternalConsole(self):
        b = self.browser

        self.createVm("subVmTest1", graphics="spice")

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

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

        # since VNC is not defined for this VM, the view for "Desktop Viewer" is rendered by default
        b.wait_in_text(".pf-c-console__manual-connection dl > div:first-child dd", "127.0.0.1")
        b.wait_in_text(".pf-c-console__manual-connection dl > div:nth-child(2) dd", "5900")

        b.click(".pf-c-console__remote-viewer-launch-vv")  # "Launch Remote Viewer" button
        b.wait_visible("#dynamically-generated-file")  # is .vv file generated for download?
        self.assertEqual(b.attr("#dynamically-generated-file", "href"),
                         u"data:application/x-virt-viewer,%5Bvirt-viewer%5D%0Atype%3Dspice%0Ahost%3D127.0.0.1%0Aport%3D5900%0Adelete-this-file%3D1%0Afullscreen%3D0%0A")

        # HACK: clicking 'Launch Remote Viewer' kills execution context and thus CDP fails
        b.reload()
        b.enter_page("/machines")

        # Go to the expanded console view
        b.click("button:contains(Expand)")

        # Check "More information"
        b.click('.pf-c-console__remote-viewer .pf-c-expandable-section__toggle')
        b.wait_in_text('.pf-c-expandable-section__content',
                       'Clicking "Launch remote viewer" will download')

        b.assert_pixels("#vm-subVmTest1-consoles-page", "vm-details-console-external", skip_layouts=["rtl"])

    def testInlineConsole(self, urlroot=""):
        b = self.browser

        args = self.createVm("subVmTest1", "vnc")

        if urlroot != "":
            self.machine.write("/etc/cockpit/cockpit.conf", f"[WebService]\nUrlRoot={urlroot}")

        self.login_and_go("/machines", urlroot=urlroot)
        self.waitPageInit()
        self.waitVmRow("subVmTest1")

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

        # since VNC is defined for this VM, the view for "In-Browser Viewer" is rendered by default
        b.wait_visible(".pf-c-console__vnc canvas")

        # make sure the log file is full - then empty it and reboot the VM - the log file should fill up again
        wait(lambda: "login as 'cirros' user." in self.machine.execute(f"cat {args['logfile']}"), delay=3)

        self.machine.execute(f"echo '' > {args['logfile']}")
        b.click("#subVmTest1-system-vnc-sendkey button")
        b.click("#ctrl-alt-Delete")
        wait(lambda: "Requesting system reboot" in self.machine.execute(f"cat {args['logfile']}"), delay=3)

    def testInlineConsoleWithUrlRoot(self, urlroot=""):
        self.testInlineConsole(urlroot="/webcon")

    def testSerialConsole(self):
        b = self.browser
        m = self.machine
        name = "vmWithSerialConsole"

        self.createVm(name, graphics='vnc', ptyconsole=True)

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

        self.goToVmPage(name)
        b.wait_in_text(f"#vm-{name}-system-state", "Running")

        b.click("#pf-c-console__type-selector")
        b.wait_visible("#pf-c-console__type-selector + .pf-c-select__menu")
        b.click("#SerialConsole button")
        b.wait_not_present("#pf-c-console__type-selector + .pf-c-select__menu")

        b.wait_in_text(f"#{name}-terminal .xterm-accessibility-tree", f"Connected to domain '{name}'")

        # In case the OS already finished booting, press Enter into the console to re-trigger the login prompt
        # Sometimes, pressing Enter one time doesn't take effect, so loop to press Enter to make sure the console has accepted it
        for _ in range(0, 60):
            b.focus(f"#{name}-terminal .xterm-accessibility-tree")
            b.key_press("\r")
            if "cirros login" in b.text(f"#{name}-terminal .xterm-accessibility-tree"):
                break
            time.sleep(1)
        # Make sure the content of console is expected
        wait(lambda: "cirros login" in b.text(f"#{name}-terminal .xterm-accessibility-tree"))

        b.click(f"#{name}-serialconsole-disconnect")
        b.wait_text(f"#{name}-terminal", "Disconnected from serial console. Click the connect button.")

        b.click(f"#{name}-serialconsole-connect")
        b.wait_in_text(f"#{name}-terminal .xterm-accessibility-tree > div:nth-child(1)", "Connected to domain")

        b.click("button:contains(Expand)")
        b.assert_pixels("#vm-vmWithSerialConsole-consoles-page", "vm-details-console-serial",
                        ignore=[".pf-c-console__vnc"], skip_layouts=["rtl"])

        # Add a second serial console
        m.execute("virsh destroy vmWithSerialConsole; virt-xml --add-device vmWithSerialConsole --console pty,target_type=virtio; virsh start vmWithSerialConsole")
        b.click("#pf-c-console__type-selector")
        b.wait_visible("#pf-c-console__type-selector + .pf-c-select__menu")
        b.click("li:contains('Serial console (console0)') button")
        b.wait(lambda: m.execute("ps aux | grep 'virsh -c qemu:///system console vmWithSerialConsole console0'"))
        b.click("#pf-c-console__type-selector")
        b.click("li:contains('Serial console (console1)') button")
        b.wait(lambda: m.execute("ps aux | grep 'virsh -c qemu:///system console vmWithSerialConsole console1'"))

        # Add multiple serial consoles
        # Remove all console firstly
        m.execute("virsh destroy vmWithSerialConsole")
        m.execute("virt-xml --remove-device vmWithSerialConsole --console all")
        # Add serial0 and console1 ~ console5
        m.execute("virt-xml vmWithSerialConsole --add-device --console pty,target.type=serial")
        m.execute("for i in {1..5};do virt-xml vmWithSerialConsole --add-device --console pty,target.type=virtio; done")
        m.execute("virsh start vmWithSerialConsole")

        for i in range(0, 6):
            b.click("#pf-c-console__type-selector")
            b.wait_visible("#pf-c-console__type-selector + .pf-c-select__menu")
            b.click(f'li:contains(\'Serial console ({"serial" if i == 0 else "console"}{i})\') button')
            b.wait(lambda: m.execute(f'ps aux | grep \'virsh -c qemu:///system console vmWithSerialConsole {"serial" if i == 0 else "console"}{i}\''))

        # disconnecting the serial console closes the pty channel
        self.allow_journal_messages("connection unexpectedly closed by peer",
                                    ".*Connection reset by peer")
        self.allow_browser_errors("Disconnection timed out.")
        self.allow_journal_messages(".* couldn't shutdown fd: Transport endpoint is not connected")
        self.allow_journal_messages("127.0.0.1:5900: couldn't read: Connection refused")

    def testBasic(self):
        b = self.browser
        name = "subVmTest1"

        self.createVm(name, graphics="vnc", ptyconsole=True)

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

        self.waitVmRow(name)
        self.goToVmPage(name)
        b.wait_in_text(f"#vm-{name}-system-state", "Running")

        # test switching console from serial to graphical
        b.wait_visible(f"#vm-{name}-consoles")
        b.wait_visible(".pf-c-console__vnc canvas")

        b.click("#pf-c-console__type-selector")
        b.wait_visible("#pf-c-console__type-selector + .pf-c-select__menu")
        b.click("#SerialConsole button")
        b.wait_not_present("#pf-c-console__type-selector + .pf-c-select__menu")

        b.wait_not_present(".pf-c-console__vnc canvas")
        b.wait_visible(f"#{name}-terminal")

        # Go back to Vnc console
        b.click("#pf-c-console__type-selector")
        b.wait_visible("#pf-c-console__type-selector + .pf-c-select__menu")
        b.click("#VncConsole button")
        b.wait_not_present("#pf-c-console__type-selector + .pf-c-select__menu")
        b.wait_visible(".pf-c-console__vnc canvas")

        # Go to the expanded console view
        b.click("button:contains(Expand)")

        # Test message is present if VM is not running
        self.performAction(name, "forceOff", checkExpectedState=False)

        b.wait_in_text("#vm-not-running-message", "start the virtual machine")

        # Test deleting VM from console page will not trigger any error
        self.performAction(name, "delete")
        b.wait_visible(f"#vm-{name}-delete-modal-dialog")
        b.click(f"#vm-{name}-delete-modal-dialog button:contains(Delete)")
        self.waitPageInit()
        self.waitVmRow(name, present=False)

        b.wait_not_present("#navbar-oops")

        self.allowed_messages.append("connection unexpectedly closed by peer")


if __name__ == '__main__':
    test_main()
