# Copyright 2024 sysmocom - s.f.m.c. GmbH
# SPDX-License-Identifier: GPL-3.0-or-later
import atexit
import copy
import glob
import logging
import os
import os.path
import shlex
import shutil
import subprocess
import testenv
import testenv.cmd
import testenv.testenv_cfg
import time

builddir_env = {}
testsuite_proc = None


def update_deps():
    logging.info("Updating osmo-ttcn3-hacks/deps")
    testenv.cmd.run(["make", "deps"], cwd=testenv.ttcn3_hacks_dir)


def prepare_testsuite_dir():
    testsuite_dir = f"{testenv.ttcn3_hacks_dir}/{testenv.args.testsuite}"
    logging.info(f"Generating links and Makefile for {testenv.args.testsuite}")
    testenv.cmd.run(["./gen_links.sh"], cwd=testsuite_dir, env=builddir_env)
    testenv.cmd.run("USE_CCACHE=1 ./regen_makefile.sh", cwd=testsuite_dir, env=builddir_env)


def init():
    global builddir_env

    atexit.register(stop)
    update_deps()

    titan_version, _ = testenv.testenv_cfg.get_titan_version_first_cfg()
    ttcn3_dir = f"/opt/eclipse-titan-{titan_version}"

    if testenv.args.podman or os.path.exists(ttcn3_dir):
        cache_dir = "podman" if testenv.args.podman else "host"
        suffix = testenv.cmd.distro_cache_suffix()
        builddir = os.path.join(testenv.args.cache, cache_dir, f"titan-{titan_version}{suffix}")
        path_old = testenv.cmd.generate_env(podman=testenv.args.podman)["PATH"]
        builddir_env = {
            "BUILDDIR": builddir,
            "TTCN3_DIR": ttcn3_dir,
            "PATH": f"{ttcn3_dir}/bin:{path_old}",
        }

    prepare_testsuite_dir()


def build():
    titan_version, titan_reason = testenv.testenv_cfg.get_titan_version_first_cfg()
    logging.info(f"Building testsuite (eclipse-titan {titan_version}, {titan_reason})")

    env = copy.copy(builddir_env)
    env["PARALLEL_MAKE"] = f"-j{testenv.testenv_cfg.get_titan_make_job_count()}"

    testenv.cmd.run(["make", testenv.args.testsuite], cwd=testenv.ttcn3_hacks_dir, env=env)


def is_running(pid):
    # Check if a process is still running, or if it is dead / a zombie. We
    # can't just use proc.poll() because this gets called from another thread.
    cmdline = f"/proc/{pid}/cmdline"
    if not os.path.exists(cmdline):
        return False

    # The cmdline file is empty if it is a zombie
    with open(cmdline) as f:
        return f.read() != ""


def merge_log_files(cfg):
    section_data = cfg["testsuite"]
    cwd = os.path.join(testenv.testdir.testdir, "testsuite")

    logging.info("Merging log files")
    log_merge = os.path.join(testenv.ttcn3_hacks_dir, "log_merge.sh")
    # stdout of this script is very verbose, making it harder to see the output
    # that matters (tests failed or not), so redirect it to /dev/null
    cmd = f"{shlex.quote(log_merge)} {shlex.quote(section_data['program'])} --rm >/dev/null"
    testenv.cmd.run(cmd, cwd=cwd, env=builddir_env)


def format_log_files(cfg):
    cwd = os.path.join(testenv.testdir.testdir, "testsuite")

    logging.info("Formatting log files")
    cmd = os.path.join(testenv.data_dir, "scripts/log_format.sh")
    testenv.cmd.run(cmd, cwd=cwd, env=builddir_env)


def get_junit_logs(topdir):
    pattern = os.path.join(topdir, "**", "junit-*.log")
    return glob.glob(pattern, recursive=True)


def cat_junit_logs():
    tool = "cat"

    if testenv.args.podman or shutil.which("source-highlight"):
        colors = os.environ.get("TESTENV_SOURCE_HIGHLIGHT_COLORS", "esc256")
        tool = f"source-highlight -f {shlex.quote(colors)} -s xml -i"

    for path in get_junit_logs(testenv.testdir.testdir_topdir):
        cmd = f"echo && {tool} {shlex.quote(path)} && echo"
        logging.info(f"Showing {os.path.relpath(path, testenv.testdir.testdir_topdir)}")
        testenv.cmd.run(cmd)


def check_testsuite_successful(loop_count=None):
    topdir = testenv.testdir.testdir_topdir
    if loop_count is not None:
        topdir = os.path.join(topdir, f"loop-{loop_count}")

    ret = True

    for path in get_junit_logs(topdir):
        for match_str in ["failures='0'", "errors='0'"]:
            cmd = ["grep", "-q", match_str, path]
            if testenv.cmd.run(cmd, check=False).returncode:
                ret = False
                break
        if not ret:
            break

    return ret


def run(cfg):
    global testsuite_proc

    section_data = cfg["testsuite"]

    cwd = os.path.join(testenv.testdir.testdir, "testsuite")
    start_testsuite = os.path.join(testenv.ttcn3_hacks_dir, "start-testsuite.sh")
    suite = os.path.join(testenv.ttcn3_hacks_dir, testenv.args.testsuite, section_data["program"])
    suite = os.path.relpath(suite, testenv.ttcn3_hacks_dir)

    env = copy.copy(builddir_env)
    env["TTCN3_PCAP_PATH"] = os.path.join(testenv.testdir.testdir, "testsuite")

    # Let ttcn3-tcpdump-stop.sh retrieve talloc reports
    host, port = testenv.testenv_cfg.get_vty_host_port(cfg)
    if port:
        env["OSMO_SUT_HOST"] = host
        env["OSMO_SUT_PORT"] = port

    env = testenv.cmd.generate_env(env, testenv.args.podman)

    cmd = [start_testsuite, suite, section_data["config"]]

    test_arg = testenv.args.test
    if test_arg:
        if "." in test_arg:
            cmd += [test_arg]
        else:
            cmd += [f"{section_data['program']}.{test_arg}"]

    titan_version, titan_reason = testenv.testenv_cfg.get_titan_version_first_cfg()
    logging.info(f"Running testsuite (eclipse-titan {titan_version}, {titan_reason})")

    if testenv.podman.is_running():
        testsuite_proc = testenv.podman.exec_cmd_background(cmd, cwd=cwd, env=env)
    else:
        logging.debug(f"+ {cmd}")
        testsuite_proc = subprocess.Popen(cmd, cwd=cwd, env=env)

    # Ensure all daemons run until the testsuite stops
    while True:
        time.sleep(1)

        if not is_running(testsuite_proc.pid):
            if testenv.args.podman and not testenv.podman.is_running():
                raise testenv.NoTraceException("podman container crashed!")
            logging.debug("Testsuite is done")
            stop()
            break

        testenv.daemons.check_if_crashed()

    merge_log_files(cfg)
    format_log_files(cfg)

    if testenv.args.bisect and not check_testsuite_successful():
        raise testenv.NoTraceException("Testsuite failed!")


def get_current_test():
    path = os.path.join(testenv.testdir.testdir, "testsuite/.current_test")
    try:
        with open(path, "r") as h:
            return h.readline().rstrip()
    except:  # noqa
        # File may not exist, e.g. if test was stopped
        return None


def wait_until_test_stopped():
    path = os.path.join(testenv.testdir.testdir, "testsuite/.current_test")

    logging.debug("Waiting until test has stopped...")

    for i in range(0, 1200):
        time.sleep(0.1)
        if not os.path.exists(path):
            return

    raise testenv.NoTraceError("Timeout in wait_until_test_stopped()")


def stop():
    global testsuite_proc

    if testsuite_proc:
        logging.info(f"Stopping testsuite ({testsuite_proc.pid})")
        testenv.daemons.kill(testsuite_proc.pid)
        testsuite_proc = None
