# resulttool - report test results in JUnit XML format
#
# Copyright (c) 2024, Siemens AG.
#
# SPDX-License-Identifier: GPL-2.0-only
#

import os
import xml.etree.ElementTree as ET
import resulttool.resultutils as resultutils


DEFAULT_JUNIT_FILE = "junit.xml"


class PtestSummary:
    """Collected infromation of one ptest suite

    Collect the information of many ptests of a ptest suite such as ptestresults.APtest.test_foo1,
    ptestresults.APtest.test_foo2, ... as one testcase APtest. This can be merged into information
    from ptestresult.sections.
    """
    def __init__(self):
        self.tests = 0
        self.ERROR = []
        self.FAILED = []
        self.SKIPPED = []

    def add_status(self, ptest_testcase, status):
        self.tests += 1
        if status == "FAILED":
            self.FAILED.append(ptest_testcase)
        elif status == "ERROR":
            self.ERROR.append(ptest_testcase)
        elif status == "SKIPPED":
            self.SKIPPED.append(ptest_testcase)

    @property
    def status(self):
        """Normalize the status of many ptests to one status of the ptest suite"""
        if len(self.ERROR) > 0:
            return "ERROR"
        if len(self.FAILED) > 0:
            return "FAILED"
        if len(self.SKIPPED) == self.tests:
            return "SKIPPED"
        return "SUCCESS"

    @property
    def log_summary(self):
        """Return a summary of the ptest suite"""
        summary_parts = []

        if self.ERROR:
            summary_parts.append("ERROR:" + os.linesep)
            summary_parts.append(os.linesep.join(["- " + s for s in self.ERROR]) + os.linesep)

        if self.FAILED:
            summary_parts.append("FAILED:" + os.linesep)
            summary_parts.append(os.linesep.join(["- " + s for s in self.FAILED]) + os.linesep)

        if self.SKIPPED:
            summary_parts.append("SKIPPED:" + os.linesep)
            summary_parts.append(os.linesep.join(["- " + s for s in self.SKIPPED]) + os.linesep)

        return "".join(summary_parts) if summary_parts else "No failures or errors"


def create_testcase(testsuite, testcase_dict, status, status_message, status_text=None, system_out=None):
    """Create a junit testcase node"""
    testcase_node = ET.SubElement(testsuite, "testcase", testcase_dict)

    print("%s -> %s status: %s" % (testcase_dict["classname"], testcase_dict["name"], status))

    se = None
    if status == "SKIPPED":
        se = ET.SubElement(testcase_node, "skipped", message = status_message.replace('\n', ' ') if status_message else None)
    elif status == "FAILED":
        se = ET.SubElement(testcase_node, "failure")
        se = ET.SubElement(testcase_node, "system-out").text = (status_message or "") + os.linesep + (system_out or "")
    elif status == "ERROR":
        se = ET.SubElement(testcase_node, "error")
        se = ET.SubElement(testcase_node, "system-out").text = (status_message or "") + os.linesep + (system_out or "")

def junit_tree(testresults, test_log_dir=None):
    """Create a JUnit XML tree from testresults

    Generates a tuple of the XML tree and a dictionary of ptest log files.
    The dictionary contains the path where the log file is located as key and the log content as value.
    The log file path is test_log_dir/ptest_name.log.
    """
    test_logfiles = {}
    testsuites_node = ET.Element("testsuites")

    image_errors = image_failures = image_skipped = image_tests = image_total_time = 0
    ptest_errors = ptest_failures = ptest_skipped = ptest_tests = ptest_total_time = 0
    total_errors = total_failures = total_skipped = total_tests = total_time = 0

    for _, run_name, _, results in resultutils.test_run_results(testresults):
        test_run_testsuite = ET.SubElement(testsuites_node, "testsuite", name=run_name)

        # Handle all image tests but skip all ptests related sections
        imagetest_testsuite = ET.SubElement(test_run_testsuite, "testsuite", name="Image Tests")

        ptest_summarys = {}

        for result_id, result in results.items():
            if result_id.startswith("ptestresult.sections") or result_id.startswith("ptestresult.rawlogs"):
                continue

            if result_id.startswith("ptestresult."):
                ptest_name = result_id.split(".", 3)[1]
                test_case = result_id.split(".", 3)[2]
                if ptest_name not in ptest_summarys:
                    ptest_summarys[ptest_name] = PtestSummary()
                ptest_summarys[ptest_name].add_status(test_case, result["status"])
            else:
                image_total_time += int(result["duration"])
                image_tests += 1
                status = result["status"]
                if status == "FAILED":
                    image_failures += 1
                elif status == "ERROR":
                    image_errors += 1
                elif status == "SKIPPED":
                    image_skipped += 1

                testcase_dict = {
                    "name": result_id,
                    "classname": "testimage",
                    "time": str(result["duration"]),
                }
                create_testcase(
                    imagetest_testsuite,
                    testcase_dict,
                    status,
                    result.get("log", None))

        imagetest_testsuite.set("errors", str(image_errors))
        imagetest_testsuite.set("failures", str(image_failures))
        imagetest_testsuite.set("skipped", str(image_skipped))
        imagetest_testsuite.set("tests", str(image_tests))
        imagetest_testsuite.set("time", str(image_total_time))

        # Handle all ptest related sections
        if "ptestresult.sections" in results:
            ptest_testsuite = ET.SubElement(test_run_testsuite, "testsuite", name="Package Tests")

            for ptest_name, result in results["ptestresult.sections"].items():
                testcase_dict = {
                    "name": ptest_name,
                    "classname": "ptest",
                    "time": str(result["duration"]),
                }

                exitcode = result.get("exitcode")
                log = result.get("log")
                system_out = None
                if log:
                    if test_log_dir is not None:
                        test_log_file = os.path.join(test_log_dir, ptest_name + ".log")
                        system_out = f"[[ATTACHMENT|{test_log_file}]]"
                        test_logfiles[test_log_file] = log
                    else:
                        system_out = log

                # Determine status and log summary
                if ptest_name in ptest_summarys:
                    status = ptest_summarys[ptest_name].status
                    log_summary = ptest_summarys[ptest_name].log_summary
                else:
                    # When there is no detailed result for the ptest, we assume it was skipped or errored
                    status = "SKIPPED" if exitcode in (None, "0") else "ERROR"
                    print("Warning: ptest %s has no detailed results, marking as %s" % (ptest_name, status))
                    log_summary = log if log else "No log available."

                create_testcase(ptest_testsuite,
                                testcase_dict,
                                status,
                                log_summary,
                                system_out=system_out)

                ptest_total_time += int(result["duration"])
                ptest_tests += 1
                if status == "FAILED":
                    ptest_failures += 1
                elif status == "ERROR":
                    ptest_errors += 1
                elif status == "SKIPPED":
                    ptest_skipped += 1

            ptest_testsuite.set("errors", str(ptest_errors))
            ptest_testsuite.set("failures", str(ptest_failures))
            ptest_testsuite.set("skipped", str(ptest_skipped))
            ptest_testsuite.set("tests", str(ptest_tests))
            ptest_testsuite.set("time", str(ptest_total_time))

        total_errors += image_errors + ptest_errors
        total_failures += image_failures + ptest_failures
        total_skipped += image_skipped + ptest_skipped
        total_tests += image_tests + ptest_tests
        total_time += image_total_time + ptest_total_time

    testsuites_node.set("errors", str(total_errors))
    testsuites_node.set("failures", str(total_failures))
    testsuites_node.set("skipped", str(total_skipped))
    testsuites_node.set("tests", str(total_tests))
    testsuites_node.set("time", str(total_time))

    ptest_success = ptest_tests - ptest_errors - ptest_failures - ptest_skipped
    image_success = image_tests - image_errors - image_failures - image_skipped
    total_success = total_tests - total_errors - total_failures - total_skipped

    print("ptest     -> tests: %d, success: %d, error: %d, failures: %d, skipped: %d, time: %d" %
              (ptest_tests, ptest_success, ptest_errors, ptest_failures, ptest_skipped, ptest_total_time))

    print("testimage -> tests: %d, success: %d, error: %d, failures: %d, skipped: %d, time: %d" %
            (image_tests, image_success, image_errors, image_failures, image_skipped, image_total_time))

    print("total     -> tests: %d, success: %d, error: %d, failures: %d, skipped: %d, time: %d" %
            (total_tests, total_success, total_errors, total_failures, total_skipped, total_time))
    tree = ET.ElementTree(testsuites_node)
    return tree, test_logfiles


def junit(args, logger):
    if args.junit_xml_path is not None:
        junit_xml_path = args.junit_xml_path
    else:
        junit_xml_path = os.path.join(os.path.dirname(args.json_file), DEFAULT_JUNIT_FILE)
    logger.debug("Generating JUnit XML report from %s" % args.json_file)
    testresults = resultutils.load_resultsdata(args.json_file, configmap=resultutils.store_map)

    # dump ptest logs to a file in a subdirectory where the junit.xml is located
    test_log_dir = None
    if args.attach_log_files:
        test_log_dir = "test-logs"
        junit_dir_abs = os.path.dirname(os.path.abspath(junit_xml_path))
        test_log_dir_abs = os.path.join(junit_dir_abs, test_log_dir)
        if not os.path.exists(test_log_dir_abs):
            os.makedirs(test_log_dir_abs)

    tree, test_logfiles = junit_tree(testresults, test_log_dir)

    for test_logfile, log in test_logfiles.items():
        with open(os.path.join(junit_dir_abs, test_logfile), "w") as f:
            f.write(log)

    tree.write(junit_xml_path, encoding="UTF-8", xml_declaration=True)
    logger.info("Saved JUnit XML report as %s" % junit_xml_path)


def register_commands(subparsers):
    """Register subcommands from this plugin"""
    parser_build = subparsers.add_parser("junit",
        help="create test report in JUnit XML format",
        description="generate unit test report in JUnit XML format based on the latest test results in the testresults.json.",
        group="analysis"
    )
    parser_build.set_defaults(func=junit)

    # If BUILDDIR is set, use the default path for the testresults.json and junit.xml
    # Otherwise require the user to provide the path to the testresults.json
    help_json_file = "json file should point to the testresults.json"
    help_junit_xml_path = "junit xml path allows setting the path of the generated test report."
    try:
        builddir = os.environ["BUILDDIR"]
    except KeyError:
        builddir = None
    if builddir:
        log_path = os.path.join(builddir, "tmp", "log", "oeqa")
        parser_build.add_argument("json_file", nargs="?",
            default=os.path.join(log_path, "testresults.json"),
            help=help_json_file + " (default: %(default)s)")
        parser_build.add_argument("-j", "--junit_xml_path",
            default=os.path.join(log_path, DEFAULT_JUNIT_FILE),
            help=help_junit_xml_path + " (default: %(default)s)")
    else:
        parser_build.add_argument("json_file", help=help_json_file)
        parser_build.add_argument("-j", "--junit_xml_path", nargs="?",
            help=help_junit_xml_path + " (default: junit.xml in the same folder as the testresults.json)")

    parser_build.add_argument("-a", "--attach-log-files", action="store_true", default=False,
        help="Write the log files to subfolder in the same folder as the junit.xml and reference them in the junit.xml")
