#!/usr/bin/python3
"""Turn a run's results.csv into report.md and summary.json.

    report.py <run-dir>
"""

import csv
import json
import os
import sys
from collections import OrderedDict

ORDER = ["FAIL", "ERROR", "TIMEOUT", "PASS", "SKIP"]
BAD = ("FAIL", "ERROR", "TIMEOUT")


def read_rows(path):
    rows = []
    with open(path, newline="") as fh:
        rd = csv.reader(fh)
        header = next(rd, None)
        for r in rd:
            if len(r) < 6:
                r = r + [""] * (6 - len(r))
            rows.append({
                "engine": r[0], "collection": r[1], "test": r[2],
                "status": r[3], "duration_s": r[4], "message": r[5],
            })
    return rows


def tally(rows):
    t = OrderedDict((s, 0) for s in ORDER)
    for r in rows:
        t[r["status"]] = t.get(r["status"], 0) + 1
    return t


def total_time(rows):
    s = 0.0
    for r in rows:
        try:
            s += float(r["duration_s"])
        except (TypeError, ValueError):
            pass
    return s


def md_table(headers, rows):
    out = ["| " + " | ".join(headers) + " |",
           "|" + "|".join(["---"] * len(headers)) + "|"]
    for r in rows:
        out.append("| " + " | ".join(str(c).replace("|", "\\|") for c in r) + " |")
    return "\n".join(out)


def main(argv):
    if len(argv) < 2:
        sys.stderr.write(__doc__)
        return 2
    run_dir = argv[1]
    csv_path = os.path.join(run_dir, "results.csv")
    if not os.path.exists(csv_path):
        sys.stderr.write("no results.csv in %s\n" % run_dir)
        return 2

    rows = read_rows(csv_path)
    env = {}
    env_path = os.path.join(run_dir, "env.txt")
    if os.path.exists(env_path):
        for line in open(env_path, errors="replace"):
            if ":" in line:
                k, _, v = line.partition(":")
                env[k.strip()] = v.strip()

    overall = tally(rows)
    engines = sorted({r["engine"] for r in rows if r["engine"]})

    summary = {
        "run_dir": os.path.abspath(run_dir),
        "environment": env,
        "totals": dict(overall),
        "total_tests": len(rows),
        "total_seconds": round(total_time(rows), 1),
        "engines": {},
        "failures": [],
    }

    lines = []
    lines.append("# Kernel test report")
    lines.append("")
    for k in ("started", "finished", "profile", "engines", "kernel", "arch",
              "os", "selinux", "secureboot", "taint_before", "taint_after",
              "cmdline"):
        if k in env:
            lines.append("- **%s**: %s" % (k, env[k]))
    lines.append("")

    # --- verdict -----------------------------------------------------------
    bad = sum(overall.get(s, 0) for s in BAD)
    if bad == 0:
        verdict = "PASS -- nothing failed"
    else:
        verdict = "FAIL -- %d test(s) failed, errored or timed out" % bad
    lines.append("## Verdict")
    lines.append("")
    lines.append("**%s**" % verdict)
    lines.append("")
    lines.append(md_table(
        ["result", "count"],
        [[s, overall.get(s, 0)] for s in ORDER if overall.get(s, 0)]))
    lines.append("")
    lines.append("%d tests in %.0f s of measured test time."
                 % (len(rows), total_time(rows)))
    lines.append("")

    # --- per engine --------------------------------------------------------
    lines.append("## By engine")
    lines.append("")
    tbl = []
    for e in engines:
        er = [r for r in rows if r["engine"] == e]
        t = tally(er)
        summary["engines"][e] = {"totals": dict(t), "tests": len(er),
                                 "seconds": round(total_time(er), 1)}
        tbl.append([e, len(er)] + [t.get(s, 0) for s in ORDER]
                   + ["%.0f" % total_time(er)])
    lines.append(md_table(["engine", "tests"] + [s.lower() for s in ORDER] + ["seconds"], tbl))
    lines.append("")

    # --- per collection ----------------------------------------------------
    lines.append("## By collection")
    lines.append("")
    tbl = []
    seen = []
    for r in rows:
        key = (r["engine"], r["collection"])
        if key not in seen:
            seen.append(key)
    for e, c in seen:
        cr = [r for r in rows if r["engine"] == e and r["collection"] == c]
        t = tally(cr)
        tbl.append([e, c or "-", len(cr)] + [t.get(s, 0) for s in ORDER])
    lines.append(md_table(["engine", "collection", "tests"] + [s.lower() for s in ORDER], tbl))
    lines.append("")

    # --- failures ----------------------------------------------------------
    failures = [r for r in rows if r["status"] in BAD]
    lines.append("## Failures")
    lines.append("")
    if not failures:
        lines.append("None.")
    else:
        lines.append(md_table(
            ["engine", "collection", "test", "result", "detail"],
            [[r["engine"], r["collection"], r["test"], r["status"],
              (r["message"] or "")[:120]] for r in failures]))
        for r in failures:
            summary["failures"].append({
                "engine": r["engine"], "collection": r["collection"],
                "test": r["test"], "status": r["status"],
                "message": r["message"],
            })
    lines.append("")

    # --- slowest -----------------------------------------------------------
    timed = []
    for r in rows:
        try:
            timed.append((float(r["duration_s"]), r))
        except (TypeError, ValueError):
            pass
    timed.sort(key=lambda x: -x[0])
    if timed[:10]:
        lines.append("## Slowest tests")
        lines.append("")
        lines.append(md_table(
            ["seconds", "engine", "collection", "test"],
            [["%.1f" % d, r["engine"], r["collection"], r["test"]]
             for d, r in timed[:10]]))
        lines.append("")

    # --- kernel log --------------------------------------------------------
    badness = []
    for root, _dirs, files in os.walk(run_dir):
        for f in files:
            if f.endswith(".badness"):
                badness.append(os.path.relpath(os.path.join(root, f), run_dir))
    lines.append("## Kernel log")
    lines.append("")
    if badness:
        lines.append("The kernel complained while these tests ran. Read these "
                     "files before trusting any result above:")
        lines.append("")
        for b in sorted(badness):
            lines.append("- `%s`" % b)
        summary["kernel_complaints"] = sorted(badness)
    else:
        lines.append("No BUG, Oops, WARNING or lockup was logged during the run.")
        summary["kernel_complaints"] = []
    lines.append("")

    summary["verdict"] = "PASS" if bad == 0 else "FAIL"
    with open(os.path.join(run_dir, "report.md"), "w") as fh:
        fh.write("\n".join(lines) + "\n")
    with open(os.path.join(run_dir, "summary.json"), "w") as fh:
        json.dump(summary, fh, indent=2, sort_keys=True)
        fh.write("\n")

    print(verdict)
    return 0 if bad == 0 else 1


if __name__ == "__main__":
    sys.exit(main(sys.argv))
