#!/usr/bin/env python3
"""Compare a fresh lab run with the published results, record by record.

    python3 compare.py results.json harness/out/results.json
    python3 compare.py results.json harness/out/results.json --strict
    python3 compare.py results.json harness/out/results.json --ignore-not-run   # some runtimes missing

The first file is the published results.json; the second is what your run wrote (a whole
run or a subset). Records are matched by id; only ids present in both are compared.

Behaviour fields (a difference exits 1): status, header_present, header_received,
content_encoding_served, origin_wire_body_bytes, decoded_bytes, auto_decoded, decode_check,
anomalies, and whether the client raised an error.
Detail fields (reported; with --strict a difference also exits 1): proxy_bytes_up,
proxy_bytes_down, proxy_saw_header, client_version, runtime_version, and the error text with
loopback ports and URLs masked. They move with your curl/OpenSSL/Python builds and TLS
details, so a difference there is expected on another machine.

Timestamps, durations, executable paths and ports are never compared.
Standard library only; Python 3.9+.
"""
from __future__ import annotations

import argparse
import json
import re
import sys

BEHAVIOUR = ["status", "header_present", "header_received", "content_encoding_served", "origin_wire_body_bytes",
             "decoded_bytes", "auto_decoded", "decode_check", "anomalies", "raised_error"]
DETAIL = ["proxy_bytes_up", "proxy_bytes_down", "proxy_saw_header", "client_version", "runtime_version",
          "error_text"]


def norm_error(t):
    if not t:
        return None
    t = re.sub(r"url='[^']*'", "url='…'", t)
    t = re.sub(r"(127\.0\.0\.1|localhost):\d+", r"\1:PORT", t)
    t = re.sub(r"\bport=\d+", "port=PORT", t)
    t = re.sub(r"https?://127\.0\.0\.1:PORT/\S*", "URL", t)
    t = re.sub(r"(?:~|\$[A-Z_]+|/)[^\s;'\"]*/", "PATH/", t)  # install locations differ per machine
    return t


def value(r: dict, field: str):
    if field == "raised_error":
        return bool(r.get("exception_text")) and r.get("status") == "run"
    if field == "error_text":
        return norm_error(r.get("exception_text"))
    return r.get(field)


def load(path: str) -> dict:
    try:
        with open(path, encoding="utf-8") as fh:
            doc = json.load(fh)
        return {r["id"]: r for r in doc["records"]}
    except (OSError, json.JSONDecodeError, KeyError, TypeError) as exc:
        print(f"compare: cannot read records from {path}: {exc}", file=sys.stderr)
        sys.exit(2)


def main(argv=None) -> int:
    ap = argparse.ArgumentParser(description="Compare a fresh lab run with the published results.")
    ap.add_argument("published")
    ap.add_argument("fresh")
    ap.add_argument("--strict", action="store_true", help="detail-field differences also fail")
    ap.add_argument("--ignore-not-run", action="store_true",
                    help="skip records your run marked not-run (a missing prerequisite)")
    ap.add_argument("--show", type=int, default=5, help="example ids to print per differing field")
    a = ap.parse_args(argv)

    pub, new = load(a.published), load(a.fresh)
    ids = [i for i in new if i in pub]
    skipped = [i for i in ids if a.ignore_not_run and new[i].get("status") == "not-run"]
    ids = [i for i in ids if i not in skipped]
    only_new = [i for i in new if i not in pub]
    print(f"compare: {len(ids)} records compared; {len(only_new)} only in your run; "
          f"{len(pub) - len(ids) - len(skipped)} published records not in your run"
          + (f"; {len(skipped)} not-run records skipped" if skipped else ""))
    if not ids:
        print("compare: nothing to compare (no shared record ids)", file=sys.stderr)
        return 1

    failed = False
    for tier, fields in (("behaviour", BEHAVIOUR), ("detail", DETAIL)):
        for f in fields:
            diffs = [i for i in ids if value(pub[i], f) != value(new[i], f)]
            if not diffs:
                print(f"  {tier:<9} {f:<24} identical in {len(ids)}")
                continue
            if tier == "behaviour" or a.strict:
                failed = True
            print(f"  {tier:<9} {f:<24} DIFFERS in {len(diffs)}")
            for i in diffs[:a.show]:
                print(f"      {i}: published {value(pub[i], f)!r} / yours {value(new[i], f)!r}")
    not_run = [i for i in new if new[i].get("status") == "not-run"]
    if not_run and not a.ignore_not_run:
        print(f"compare: {len(not_run)} record(s) in your run are not-run (a prerequisite was missing), e.g. "
              f"{not_run[0]}: {new[not_run[0]].get('exception_text')}")
    print("compare: " + ("DIFFERENT" if failed else "OK: same behaviour" + (" and same detail fields" if a.strict else "")))
    return 1 if failed else 0


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