#!/usr/bin/env python3

import argparse
import glob
import json
import os
import re
import xml.dom.minidom as md

TEST_COLLECTIONS = {
    "EUnit": "src/**/.eunit/*.xml",
    "EXUnit": "_build/integration/lib/couchdbtest/*.xml",
    "Mango": "src/mango/*.xml",
    "JavaScript": "test/javascript/*.xml",
}


def _attrs(elem):
    ret = {}
    for k, v in elem.attributes.items():
        ret[k.lower()] = v
    return ret


def _text(elem):
    rc = []
    for node in elem.childNodes:
        if node.nodeType == node.TEXT_NODE:
            rc.append(node.data)
        else:
            rc.append(self._text(node))
    return "".join(rc)


class TestCase(object):
    def __init__(self, elem):
        self.elem = elem

        attrs = _attrs(elem)

        self.name = self._name(attrs)
        self.time = float(attrs["time"])

        self.failure = False
        self._check_failure(elem, attrs)

        self.error = False
        self._check_error(elem, attrs)

        self.skipped = False
        self._check_skipped(elem, attrs)

    def _check_failure(self, elem, attrs):
        failures = elem.getElementsByTagName("failure")
        if not failures:
            return

        self.failure = True
        self.failure_msg = _text(failures[0]).strip()

    def _check_error(self, elem, attrs):
        errors = elem.getElementsByTagName("error")
        if not errors:
            return

        self.error = True
        self.error_msg = _text(errors[0]).strip()

    def _check_skipped(self, elem, attrs):
        skipped = elem.getElementsByTagName("skipped")
        if not skipped:
            return

        attrs = _attrs(skipped[0])
        self.skipped = True
        self.skipped_msg = attrs.get("message", attrs.get("type", "<unknown>"))

    def _name(self, attrs):
        klass = attrs.get("classname", "")
        if klass.startswith("Elixir."):
            klass = klass[len("Elixir.") :]
        if klass:
            return "%s - %s" % (klass, attrs["name"])
        return attrs["name"]


class TestSuite(object):
    SUITE_NAME_PATTERNS = [re.compile("module '([^']+)'"), re.compile("Elixir\.(.+)")]

    def __init__(self, elem):
        self.elem = elem

        attrs = _attrs(elem)

        self.name = self._name(attrs)

        self.time = 0.0
        if "time" in attrs:
            self.time = float(attrs["time"])

        self.num_tests = int(attrs["tests"])
        self.num_failures = int(attrs["failures"])
        self.num_errors = int(attrs["errors"])
        self.num_skipped = 0

        self.tests = []
        self.test_time = 0.0

        for t_elem in elem.getElementsByTagName("testcase"):
            self.tests.append(TestCase(t_elem))
            self.test_time += self.tests[-1].time
            if self.tests[-1].skipped:
                self.num_skipped += 1

        if self.time == 0.0 and self.test_time > 0.0:
            self.time = self.test_time

    def _name(self, attrs):
        raw_name = attrs["name"]
        for p in self.SUITE_NAME_PATTERNS:
            match = p.match(raw_name)
            if match:
                return match.group(1)
        return raw_name


class TestCollection(object):
    def __init__(self, name, pattern):
        self.name = name
        self.pattern = pattern
        self.suites = []
        self.bad_files = []

        for fname in glob.glob(pattern):
            self._load_file(fname)

    def _load_file(self, filename):
        try:
            dom = md.parse(filename)
        except:
            self.bad_files.append(filename)
            return
        for elem in dom.getElementsByTagName("testsuite"):
            self.suites.append(TestSuite(elem))


def parse_args():
    parser = argparse.ArgumentParser(description="Show test result summaries")
    parser.add_argument(
        "--ignore-failures",
        action="store_true",
        default=False,
        help="Don't display test failures",
    )
    parser.add_argument(
        "--ignore-errors",
        action="store_true",
        default=False,
        help="Don't display test errors",
    )
    parser.add_argument(
        "--ignore-skipped",
        action="store_true",
        default=False,
        help="Don't display skipped tests",
    )
    parser.add_argument(
        "--all", type=int, default=0, help="Number of rows to show for all groups"
    )
    parser.add_argument(
        "--collection",
        action="append",
        default=[],
        help="Which collection to display. May be repeated.",
    )
    parser.add_argument(
        "--suites", type=int, default=0, help="Number of suites to show"
    )
    parser.add_argument("--tests", type=int, default=0, help="Number of tests to show")
    parser.add_argument(
        "--sort",
        default="total",
        choices=["test", "fixture", "total"],
        help="Timing column to sort on",
    )
    return parser.parse_args()


def display_failures(collections):
    failures = []
    for collection in collections:
        for suite in collection.suites:
            for test in suite.tests:
                if not test.failure:
                    continue
                failures.append((test.name, test.failure_msg))

    if not len(failures):
        return
    print("Failures")
    print("========")
    print()
    for failure in failures:
        print(failure[0])
        print("-" * len(failure[0]))
        print()
        print(failure[1])
        print()


def display_errors(collections):
    errors = []
    for collection in collections:
        for suite in collection.suites:
            for test in suite.tests:
                if not test.error:
                    continue
                errors.append((test.name, test.error_msg))

    if not len(errors):
        return
    print("Errors")
    print("======")
    print()
    for error in errors:
        print(error[0])
        print("-" * len(error[0]))
        print()
        print(error[1])
        print()


def display_skipped(collections):
    skipped = []
    for collection in collections:
        for suite in collection.suites:
            for test in suite.tests:
                if not test.skipped:
                    continue
                name = "%s - %s - %s" % (collection.name, suite.name, test.name)
                skipped.append((name, test.skipped_msg))
    if not skipped:
        return
    print("Skipped")
    print("=======")
    print()
    for row in sorted(skipped):
        print("  %s: %s" % row)
    print()


def display_table(table):
    for ridx, row in enumerate(table):
        new_row = []
        for col in row:
            if isinstance(col, float):
                new_row.append("%4.1fs" % col)
            elif isinstance(col, int):
                new_row.append("%d" % col)
            else:
                new_row.append(col)
        table[ridx] = new_row
    for row in table:
        fmt = " ".join(["%10s"] * len(row))
        print(fmt % tuple(row))


def display_collections(collections, sort):
    rows = []
    for collection in collections:
        total_time = 0.0
        test_time = 0.0
        num_tests = 0
        num_failures = 0
        num_errors = 0
        num_skipped = 0
        for suite in collection.suites:
            total_time += suite.time
            test_time += suite.test_time
            num_tests += suite.num_tests
            num_failures += suite.num_failures
            num_errors += suite.num_errors
            num_skipped += suite.num_skipped
        cols = (
            total_time,
            max(0.0, total_time - test_time),
            test_time,
            num_tests,
            num_failures,
            num_errors,
            num_skipped,
            collection.name + "        ",
        )
        rows.append(cols)

    scol = 0
    if sort == "fixture":
        scol = 1
    elif sort == "test":
        scol = 2

    def skey(row):
        return (-1.0 * row[scol], row[-1])

    rows.sort(key=skey)

    print("Collections")
    print("===========")
    print()
    headers = ["Total", "Fixture", "Test", "Count", "Failed", "Errors", "Skipped"]
    display_table([headers] + rows)
    print()


def display_suites(collections, count, sort):
    rows = []
    for collection in collections:
        for suite in collection.suites:
            cols = [
                suite.time,
                max(0.0, suite.time - suite.test_time),
                suite.test_time,
                suite.num_tests,
                suite.num_failures,
                suite.num_errors,
                suite.num_skipped,
                collection.name + " - " + suite.name,
            ]
            rows.append(cols)

    scol = 0
    if sort == "fixture":
        scol = 1
    elif sort == "test":
        scol = 2

    def skey(row):
        return (-1.0 * row[scol], row[-1])

    rows.sort(key=skey)

    rows = rows[:count]

    print("Suites")
    print("======")
    print()
    headers = ["Total", "Fixture", "Test", "Count", "Failed", "Errors", "Skipped"]
    display_table([headers] + rows)
    print()


def display_tests(collections, count):
    rows = []
    for collection in collections:
        for suite in collection.suites:
            for test in suite.tests:
                if test.failure or test.error or test.skipped:
                    continue
                fmt = "%s - %s - %s"
                display = fmt % (collection.name, suite.name, test.name)
                rows.append((test.time, display))

    def skey(row):
        return (-1.0 * row[0], row[-1])

    rows.sort(key=skey)
    rows = rows[:count]

    print("Tests")
    print("=====")
    print()
    display_table(rows)
    print()


def main():
    args = parse_args()

    if not args.collection:
        args.collection = ["eunit", "exunit", "mango", "javascript"]

    collections = []
    for name, pattern in TEST_COLLECTIONS.items():
        if name.lower() not in args.collection:
            continue
        collections.append(TestCollection(name, pattern))

    if not args.ignore_failures:
        display_failures(collections)

    if not args.ignore_errors:
        display_errors(collections)

    if not args.ignore_skipped:
        display_skipped(collections)

    display_collections(collections, args.sort)

    if args.all > 0:
        args.suites = args.all
        args.tests = args.all

    if args.suites > 0:
        display_suites(collections, args.suites, args.sort)

    if args.tests > 0:
        display_tests(collections, args.tests)


if __name__ == "__main__":
    main()
