"""The gen_results_summary module generates a summary report given a
results directory.

The summary report is primarily generated using the results.xml files
in the results directory.  If there is a ltm-run-stats file in the
top-level directory, then assume that the results directory was
generated by the LTM server.  In LTM mode we sort the test suite
reports by the order the LTM server launched the VM's.

In the future, if we start using multiple VM's to run tests for a
specific file system config, we'll need to make this module smarter.

"""
import copy
import os
import sys
import time
from datetime import datetime
from junitparser import JUnitXml, Property, Properties, Failure, Error, Skipped

PREEMPTED = 'preempted'
TIMEOUT   = 'timeout'

class TestStats:
    def __init__(self):
        self.failed = 0
        self.skipped = 0
        self.error = 0
        self.total = 0

class wrapped_print:
    def __init__(self, f, label, sep):
        self.f = f
        self.label = label
        self.sep = sep
        self.first = True
        self.pos = 0

    def write(self, str):
        if self.first:
            self.f.write('  %s: ' % self.label)
            self.pos = len(self.label) + 4
            self.first = False
        else:
            self.f.write(self.sep)
        l = len(str) + len(self.sep)
        self.pos += l
        if self.pos > 76:
            self.f.write('\n    ')
            self.pos = l + 5
        self.f.write(str)

    def done(self):
        if not self.first:
            self.f.write('\n')

def get_results(dirroot):
    """Return a list of files named results.xml in a directory hierarchy"""
    for dirpath, _dirs, filenames in os.walk(dirroot):
        if 'results.xml' in filenames:
            yield dirpath + '/results.xml'

def parse_timestamp(timestamp):
    """Parse an ISO-8601-like timestamp as found in an xUnit file."""
    if timestamp == "":
        return 0
    for fmt in ('%Y-%m-%dT%H:%M:%S%z', '%Y-%m-%dT%H:%M:%S'):
        try:
            return time.mktime(datetime.strptime(timestamp, fmt).timetuple())
        except ValueError:
            pass
    raise ValueError('no valid timestamp format found')

def failed_tests(testsuite):
    """This iterator the failed tests from the testsuite."""
    for testcase in testsuite:
        if isinstance(testcase.result, Failure):
            yield testcase

def get_property(props, key):
    """Return the value of the first property with the given name"""
    if props is None:
        return None
    for prop in props:
        if prop.name == key:
            return prop.value
    return None

def get_properties(props, key):
    """An interator which returns values of properties with a given name."""
    if props is None:
        yield None
    for prop in props:
        if prop.name == key:
            yield prop.value

def remove_properties(props, key):
    """Remove properties with a given name."""
    if props is None:
        return
    for prop in props:
        if prop.name == key:
            props.remove(prop)

def print_tests(out_f, testsuite, result_type, type_label):
    """Print all of the tests which match a particular result_type"""
    wp = wrapped_print(out_f, type_label, ' ')
    for testcase in testsuite:
        if testcase.name == PREEMPTED or testcase.name == TIMEOUT:
                continue
        match = False
        for entry in testcase.result:
            if isinstance(entry, result_type):
                match = True
        if not match:
            continue
        wp.write(testcase.name)
    wp.done()

def total_tests(testsuites):
    """Print the total number of tests in an array of testsuites"""
    total = 0
    for testsuite in testsuites:
        if testsuite.tests is not None:
            total += testsuite.tests
    return total

def sum_testsuites(testsuites):
    """Summarize all of the test suite statistics"""
    runtime = 0
    tests = 0
    skipped = 0
    failures = 0
    errors = 0
    for testsuite in testsuites:
        runtime += testsuite.time
        tests += testsuite.tests
        skipped += testsuite.skipped
        failures += testsuite.failures
        errors += testsuite.errors
    return (tests, skipped, failures, errors, runtime)

def get_testsuite_stats(testsuite):
    """Aggregate stats on individual tests"""
    Stats = {}
    for test_case in testsuite:
        isFail = False
        isSkipped = False
        isError = False
        for entry in test_case.result:
            if isinstance(entry, Failure):
                isFail = True
            if isinstance(entry, Skipped):
                isSkipped = True
            if isinstance(entry, Error):
                isError = True
        if test_case.name in Stats:
            s = Stats[test_case.name]
        else:
            s = TestStats()
            Stats[test_case.name] = s
        s.total += 1
        if isFail:
            s.failed += 1
        if isSkipped:
            s.skipped += 1
        if isError:
            s.error += 1

    return Stats

def testsuite_failed(testsuite, Stats=None,
                     hard_failure=True,
                     flake=True,
                     skip=False,
                     error=True,
                     timeout=True,
                     preempt=False):
    """Returns whether a testsuite is considered failed.

    Can pass in Stats struct if you already have it.
    The additional parameters define which events are considered as failure.
    """
    if error and testsuite.errors > 0:
        return True
    if skip and testsuite.skipped > 0:
        return True
    if flake and hard_failure and testsuite.failures > 0:
        return True

    if Stats is None:
        Stats = get_testsuite_stats(testsuite)

    if preempt is True and PREEMPTED in Stats:
        return True
    if timeout is True and TIMEOUT in Stats:
        return True

    if not hard_failure and not flake:
        return False

    if testsuite.failures == 0:
        return False

    for t in Stats:
        if t == PREEMPTED or t == TIMEOUT:
            continue
        s = Stats[t]
        if s.failed > 0:
            if s.total == s.failed:
                if hard_failure:
                    return True
            else:
                if flake:
                    return True
    return False

def print_summary(out_f, testsuite, verbose):
    """Print a summary for a particular test suite

    Print the file system configuration, the number of tests run,
    skipped, and failed.  If there are any failed tests, print a list
    of the failed tests.  The output will look something like this:

    ext4/bigalloc 244 tests, 25 skipped, 5 errors, 880 seconds
       generic/219 generic/235 generic/422 generic/451 generic/456
    """
    cfg = get_property(testsuite.properties(), 'TESTCFG')
    if cfg is None:
        cfg = get_property(testsuite.properties(), 'FSTESTCFG')

    runtime = testsuite.time
    tests = testsuite.tests
    skipped = testsuite.skipped
    failures = testsuite.failures
    errors = testsuite.errors
    Stats = get_testsuite_stats(testsuite)
    preempts = 0
    if PREEMPTED in Stats:
        s = Stats[PREEMPTED]
        preempts = s.total
        errors -= s.error
        tests -= s.total
        failures -= s.failed
    # we mark timeout with a single entry as "error" testcase
    # timeout causes shutdown, so will not occur more than once
    if TIMEOUT in Stats:
        errors -= 1
        tests -= 1
    out_f.write('%s: %d tests, ' % (cfg, tests))
    if failures > 0:
        out_f.write('%d failures, ' % failures)
    if errors > 0:
        out_f.write('%d errors, ' % errors)
    if skipped > 0:
        out_f.write('%d skipped, ' % skipped)
    if preempts > 0:
        out_f.write('%d VM preempts, ' % preempts)
    out_f.write('%d seconds\n' % runtime)
    if TIMEOUT in Stats:
        out_f.write('** Test VM timed out! **\n')
        out_f.write('** This could be from a crash or test infra ' \
                    'issue and should be treated as failure**\n')
    if verbose:
        for test_case in testsuite:
            status = ''
            if test_case.name == PREEMPTED:
                out_f.write("  Test VM preempted!\n")
                continue
            if test_case.name == TIMEOUT:
                out_f.write("    Test VM timed out!\n")
                continue
            for entry in test_case.result:
                if isinstance(entry, Failure):
                    status = status + 'Failed,'
                if isinstance(entry, Skipped):
                    status = status + 'Skipped,'
                if isinstance(entry, Error):
                    status = status + 'Error,'
            if (status == ''):
                status = 'Pass'
            else:
                status = status[:-1]
            out_f.write("  %-12s %-8s %ds\n" %
                        (test_case.name, status, test_case.time))
    else:
        wp = wrapped_print(out_f, 'Failures', ' ')
        for t in Stats:
            if t == PREEMPTED or t == TIMEOUT:
                continue
            s = Stats[t]
            if s.failed == 0 or s.total != s.failed:
                continue
            wp.write(t)
        wp.done()

        wp = wrapped_print(out_f, 'Flaky', '   ')
        for t in Stats:
            if t == PREEMPTED or t == TIMEOUT:
                continue
            s = Stats[t]
            if s.failed == 0 or s.total == s.failed:
                continue
            wp.write('%s: %2.0f%% (%d/%d)' % (t, s.failed / s.total * 100,
                                              s.failed, s.total))
        wp.done()

        if errors > 0:
            print_tests(out_f, testsuite, Error, 'Errors')

    return Stats

def print_property_line(out_f, props, key):
    """Print a line containing the given property."""
    value = get_property(props, key)
    if value is not None and value != "":
        out_f.write('%-10s %s\n' % (key + ':', value))

def print_properties(out_f, props, key):
    """Print multiple property lines."""
    for value in get_properties(props, key):
        out_f.write('%-10s %s\n' % (key + ':', value))

def print_header(out_f, props):
    """Print the header of the report."""
    print_property_line(out_f, props, 'TESTRUNID')
    print_property_line(out_f, props, 'KERNEL')
    print_property_line(out_f, props, 'CMDLINE')
    print_property_line(out_f, props, 'CPUS')
    print_property_line(out_f, props, 'MEM')
    print_property_line(out_f, props, 'MNTOPTS')
    out_f.write('\n')

def print_trailer(out_f, props):
    """Print the trailer of the report."""
    out_f.write('\n')
    print_property_line(out_f, props, 'FSTESTIMG')
    print_property_line(out_f, props, 'FSTESTPRJ')
    print_properties(out_f, props, 'FSTESTVER')
    print_property_line(out_f, props, 'FSTESTCFG')
    print_property_line(out_f, props, 'FSTESTSET')
    print_property_line(out_f, props, 'FSTESTEXC')
    print_property_line(out_f, props, 'FSTESTOPT')
    print_property_line(out_f, props, 'GCE ID')

def check_for_ltm(results_dir, props):
    """Check to see if the results directory was created by the LTM and
    adjust the properties accordingly.  Returns true if we are in LTM
    mode.
    """
    try:
        out_f = open(os.path.join(results_dir, 'ltm-run-stats'))
        for line in out_f:
            key, value = line.split(': ', 1)
            value = value.rstrip('\n').strip('"')
            remove_properties(props, key)
            props.add_property(Property(key, value))
        out_f.close()
        remove_properties(props, 'GCE ID')
        remove_properties(props, 'FSTESTCFG')
        return True
    except IOError:
        return False

def gen_results_summary(results_dir, output_fn=None, merge_fn=None,
                        verbose=False, verbosity_threshold=30,
                        fail_fn=None):
    """Scan a results directory and generate a summary file"""
    reports = []
    combined = JUnitXml()
    nr_files = 0
    out_f = sys.stdout

    for filename in get_results(results_dir):
        reports.append(JUnitXml.fromfile(filename))

    if len(reports) == 0:
        return 0

    if output_fn is not None:
        out_f = open(output_fn, "w")

    props = copy.deepcopy(reports[0].child(Properties))

    ltm = check_for_ltm(results_dir, props)

    print_header(out_f, props)

    sort_by = lambda ts: parse_timestamp(ts.timestamp)
    if ltm:
        sort_by = lambda ts: ts.hostname

    if total_tests(reports) < verbosity_threshold:
        verbose = True

    for testsuite in sorted(reports, key=sort_by):
        stats = print_summary(out_f, testsuite, verbose)

        if fail_fn is not None and not os.path.exists(fail_fn):
            if testsuite_failed(testsuite, Stats=stats,
                                    hard_failure=True, flake=False,
                                    skip=False, error=True,
                                    timeout=True, preempt=False):
                # touch file
                fail_fd = os.open(fail_fn, os.O_RDWR | os.O_CREAT)
                os.close(fail_fd)

        combined.add_testsuite(testsuite)
        nr_files += 1

    out_f.write('Totals: %d tests, %d skipped, %d failures, %d errors, %ds\n' \
                % sum_testsuites(reports))

    print_trailer(out_f, props)

    if merge_fn is not None:
        combined.update_statistics()
        combined.write(merge_fn + '.new')
        if os.path.exists(merge_fn):
            os.rename(merge_fn, merge_fn + '.bak')
        os.rename(merge_fn + '.new', merge_fn)

    return nr_files

def tests_have_errors(results_dir):
    """Scan a results directory and return true if any tests errored out"""

    for filename in get_results(results_dir):
        ts = JUnitXml.fromfile(filename)

        if ts.errors > 0:
            return True
    return False
