#!/usr/bin/python3
# -*- coding: utf-8 -*-

# This file is part of Cockpit.
#
# Copyright (C) 2015 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 argparse
import json
import os
import sys
import subprocess
from testlib_avocado.machine_core import testvm


CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
if CURRENT_DIR not in sys.path:
    sys.path.insert(1, CURRENT_DIR)


machine_test_dir = "/tmp/avocado_tests"

# this is where the avocado results on the test machine will be stored
avocado_results_dir = "/root/avocado_results"

# this is where we publish logs on the local machine
arg_attachments_dir = os.environ.get("TEST_ATTACHMENTS", None)
if not arg_attachments_dir:
    arg_attachments_dir = os.getcwd()
if not os.path.exists(arg_attachments_dir):
    os.makedirs(arg_attachments_dir)

def prepare_kdump_test(machine):
    # enable kernel kdump support
    machine.execute("grubby --update-kernel DEFAULT --args='crashkernel=128M'")
    machine.execute("systemctl enable kdump")
    machine.execute("reboot")
    machine.wait_reboot()


def prepare_avocado_tests(machine):
    """ Upload all avocado related files and create the results directory
    """
    machine.execute(command="mkdir -p " + avocado_results_dir)
    machine.upload(["avocado"], machine_test_dir)
    machine.execute(command="ls -l /tmp/avocado_tests")


def tap_output(machine):
    try:
        json_info = json.loads(machine.execute(command="cat " + os.path.join(avocado_results_dir, "latest/results.json"), quiet=True))
    except Exception as e:
        print("ERROR: Unable to fetch JSON from remote machine: " + os.path.join(avocado_results_dir, "latest/results.json"))
        print(e)
        return None
    print("\nTAP output -------------------------")
    print("1..%s" % len(json_info['tests']))
    counter = 1
    for t in json_info['tests']:
        test_status = t['status']
        test_name = os.path.basename(t['id'])
        test_log = t['logfile']
        test_time_string = "duration: %ds" % t['time']
        print("# ----------------------------------------------------------------------")
        print("# %s %s" % (test_name, test_time_string))
        print("#")
        try:
            print(machine.execute(command="cat " + test_log, quiet=True))
        except Exception as e:
            print("ERROR: Unable to fetch testlog from remote machine: " + test_log)
            print(e)
        print("# TEST LOG END -------")
        if test_status == 'PASS':
            print("ok %s %s # %s" % (counter, test_name, test_time_string))
        else:
            print("not ok %s %s # %s" % (counter, test_name, test_time_string))
        counter = counter + 1
    if len(json_info['tests']) != json_info['pass']:
        print("# %d TESTS FAILED duration: %ds" % (len(json_info['tests']) - json_info['pass'], json_info['time']))
    else:
        print("# TESTS PASSED duration: %ds" %  json_info['time'])


def run_avocado_tests(machine, avocado_tests, print_failed=True, env={}, timeout=3600):
    """ Execute avocado on the machine with the passed environment variables and run
        the specified tests. For example:
        run_avocado(machine,
                    ["checklogin-basic.py", "checklogin-raw.py"],
                    ["HUB=" + self.selenium.address, "BROWSER=firefox"]
                   )
        Return success of the tests (True: all passed, False: at least one failed)
        If 'print_failed' is True, attempt to print a list of failed tests
    """
    return_state = True
    cmd_parts = ["python3 -m avocado run",
                 "--job-results-dir " + avocado_results_dir,
                 ' '.join([machine_test_dir + os.sep + x for x in avocado_tests]),
                 ">&2"
                 ]

    # HACK override env for LC_ALL to utf, python3 avocado fails with LANG set to C
    try:
        machine.execute(" ".join(cmd_parts), timeout=timeout, environment=env)
    except subprocess.CalledProcessError:
        if print_failed:
            # try to get the list of failed tests
            try:
                failed_tests_info = machine.execute(
                    command="cat " + os.path.join(avocado_results_dir, "latest/results.json"),
                    quiet=True
                )
                machine.execute("cp -v *.png %s/ 2>/dev/null || true" % avocado_results_dir)
                machine.execute("cp -v *.html %s/ 2>/dev/null || true" % avocado_results_dir)
                failed_tests = json.loads(failed_tests_info)
                for t in failed_tests['tests']:
                    test_status = t['status']
                    if test_status != 'PASS':
                        test_id, test_name = t['id'].split("-", 1)
                        if test_name.startswith(machine_test_dir + '/'):
                            test_name = test_name[(len(machine_test_dir) + 1):]
                        fail_reason = t['fail_reason']
                        print("[" + test_status + "] " + test_name + " (" + fail_reason + ")")
            except subprocess.CalledProcessError:
                print("Unable to show avocado test result summary")
        return_state = False
    tap_output(machine)
    return return_state


def copy_avocado_logs(machine, title):
    if machine and machine.ssh_reachable:
        # check if the remote directory exists (if it doesn't, we want to quit silently)
        test_command = "if test -d '{0}'; then echo exists; fi".format(avocado_results_dir)
        if "exists" not in machine.execute(command=test_command, quiet=True):
            return

        # get the screenshots first and move them on the guest so they won't get archived
        remote_screenshots = os.path.join(avocado_results_dir, "*.png")
        remote_archive_dir = os.path.split(avocado_results_dir)[0]
        test_command_screenshots = """for f in "{0}/"*.png; do [ -e  "$f" ] && echo "exists"; break; done""".format(avocado_results_dir)
        if "exists" in machine.execute(command=test_command_screenshots, quiet=True):
            machine.download(remote_screenshots, arg_attachments_dir)

        # create a compressed archive on the remote host
        remote_archive = os.path.join(remote_archive_dir, "avocado_logs.tar.gz")
        machine.execute("cd {0} && tar czf {1} --exclude='*.png' {2}".format(remote_archive_dir, remote_archive, os.path.split(avocado_results_dir)[1]))

        # download and attach to our test
        local_archive = os.path.join(arg_attachments_dir, "{0}-{1}.avocado.tar.gz".format(machine.label, title))
        machine.download(remote_archive, local_archive)


def wait_for_selenium_running(machine, host, port=4444):
    WAIT_SELENIUM_RUNNING = """
until curl -s --connect-timeout 3 http://%s:%d >/dev/null; do
sleep 0.5;
done;
""" % (host, port)
    with testvm.Timeout(seconds=300, error_message="Timeout while waiting for selenium to start"):
        machine.execute(WAIT_SELENIUM_RUNNING)


def prepare_machines_test_env(avocado_tests, machine):
    prepare_env = False
    for test in avocado_tests:
        if "selenium-machines" in test:
            prepare_env = True
            break
    if not prepare_env:
        return

    # Ensure everything has started correctly
    machine.execute("systemctl start libvirtd.service")
    # Wait until we can get a list of domains
    machine.execute("until virsh list > /dev/null; do sleep 0.5; done")
    # Wait for the network 'default' to become active
    machine.execute("until virsh net-info default | grep Active > /dev/null; do sleep 0.5; done")
    # Enforce use of cockpit-machines instead of cockpit-machines-ovirt
    machine.execute(
        "sed -i 's/\"priority\".*$/\"priority\": 100,/' {0}".format("/usr/share/cockpit/machines/manifest.json"))
    machine.execute(
        "[ ! -e {0} ] || sed -i 's/\"priority\".*$/\"priority\": 0,/' {0}".format("/usr/share/cockpit/ovirt/manifest.json"))
    machine.execute("systemctl try-restart libvirtd")

    # Prepare image
    image_file = machine.pull("cirros")
    machine.upload([image_file], "/var/lib/libvirt/images")
    machine.execute("cd /var/lib/libvirt/images && mv {} cirros.qcow2 && chmod 644 cirros.qcow2".format(os.path.basename(image_file)))


def run_avocado(avocado_tests, verbose, browser, download_logs, sit):
    use_selenium = (browser != 'none')
    image = os.environ.get("TEST_OS", "fedora-30")
    network = testvm.VirtNetwork()
    machine = testvm.VirtMachine(verbose=verbose, networking=network.host(), image=image)
    selenium = windows = None

    if use_selenium:
        if 'edge' in browser:
            windows = testvm.VirtMachine(image='windows-10', memory_mb=4096, cpus=2, verbose=verbose, networking=network.host())
        else:
            selenium = testvm.VirtMachine(image="services", verbose=verbose, networking=network.host())

    try:
        # just start the machine without waiting for it to boot, get the ip address later
        machine.start()
        if selenium:
            # actually wait here, because starting selenium takes a while
            selenium.pull(selenium.image_file)
            selenium.start()
            selenium.wait_boot()
            selenium.set_address("10.111.112.10/20")

            # start selenium on the server
            selenium.upload(["avocado/selenium_start.sh"], "/root")
            selenium.execute(command="/root/selenium_start.sh")
        elif windows:
            selenium = windows
            selenium.pull(selenium.image_file)
            selenium.start()

        # apply all preparation instructions
        machine.wait_boot()
        prepare_kdump_test(machine)
        prepare_avocado_tests(machine)
        prepare_machines_test_env(avocado_tests, machine)

        machine.set_address("10.111.113.1/20")
        machine.start_cockpit()
        test_timeout = 3600
        if selenium and 'edge' in browser:
            machine.dhcp_server(range=['10.111.112.10', '10.111.112.10'])
            # win-10 image is achingly slow
            test_timeout = 7200

        machine.execute("adduser test")
        machine.execute("echo superhardpasswordtest5554 | passwd --stdin test")
        machine.execute("usermod -a -G wheel test")
        machine.execute("echo 'test        ALL=(ALL)       NOPASSWD: ALL' >> /etc/sudoers")
        machine.execute("sed -i 's/^Defaults.*requiretty/#&/g' /etc/sudoers")
        machine.execute("echo 'Defaults !requiretty' >> /etc/sudoers")

        if selenium:
            wait_for_selenium_running(machine, "10.111.112.10")

        # Now actually run the tests
        selenium_address = "10.111.112.10" if selenium else ""
        env = {"HUB": selenium_address, "GUEST": "10.111.113.1", "BROWSER": browser}
        success = run_avocado_tests(machine, avocado_tests, env=env, timeout=test_timeout)

        if not success:
            copy_avocado_logs(machine, "FAILED")
        elif download_logs:
            copy_avocado_logs(machine, "PASSED")

        if not success and sit:
            sys.stderr.write(machine.diagnose() + "\nPress RET to continue...\n")
            sys.stdin.readline()

        return success
    finally:
        machine.kill()
        if selenium:
            selenium.kill()


def main():
    parser = argparse.ArgumentParser(description='Run Cockpit Avocado test(s)')
    parser.add_argument('-v', '--verbose', dest="verbosity", action='store_true',
                        help='Verbose output')
    parser.add_argument('-q', '--quick', action='store_true', help='Build faster')
    parser.add_argument("-b", "--browser", choices=['none', 'firefox', 'chrome', 'edge'],
                    default='none',
                    help="selenium browser choice - in case of none, selenium isn't started")
    parser.add_argument("-s", "--selenium-tests", dest='selenium_tests', action='store_true',
                        help="Run known browser/selenium tests")
    parser.add_argument("-t", "--tests", dest='regular_tests', action='store_true',
                        help="Run known regular (non-selenium) tests")
    parser.add_argument("-l", "--logs", dest='download_logs', action='store_true',
                        help="Always download avocado logs, even on success")
    parser.add_argument("--sit", dest='sit', action='store_true',
                        help="Sit and wait after test failure")
    parser.add_argument('tests', nargs='*')

    opts = parser.parse_args()

    regular_tests = ["checklogin-basic.py", "checklogin-raw.py"]
    browser_tests = [
        "selenium-base.py",
        "selenium-navigate.py",
        "selenium-storage.py",
        "selenium-sosreport.py",
        "selenium-dashboard.py",
        "selenium-tuned.py",
        "selenium-network.py",
        "selenium-hwinfo.py",
        "selenium-kdump.py",
    ]
    machines_tests = [
        "selenium-machines-basic.py",
        "selenium-machines-consoles.py",
        "selenium-machines-disks.py",
        "selenium-machines-networks.py",
        "selenium-machines-nonroot.py",
        "selenium-machines-storage-pool.py",
        "selenium-machines-usage.py",
        "selenium-machines-vcpu.py"
    ]

    if opts.regular_tests:
        opts.tests += regular_tests
    if opts.selenium_tests:
        opts.tests += browser_tests
        opts.tests += machines_tests

    # if we don't have a browser but are supposed to run selenium related tests, fail
    if opts.browser == 'none':
        for t in browser_tests:
            if t in opts.tests:
                sys.stderr.write("Unable to run test {0} because browser isn't set.\n".format(t))
                return 1

    if len(opts.tests) == 0:
        sys.stderr.write("No tests specified.\n")
        return 0

    if run_avocado(opts.tests, opts.verbosity, opts.browser, opts.download_logs, opts.sit):
        return 0
    else:
        return 1


if __name__ == '__main__':
    sys.exit(main())
