"""Check that the Python (hunt/) and the JavaScript (web/) give the same numbers.

    python tests/parity.py            # needs node on PATH

Levels: every accuracy, confusion count and weight from run_level must match (weights to
1e-6, accuracies exactly). CSV checks: the demo file, the sklearn datasets bundled with
scikit-learn (clean data, to see the false positives) and a few awkward synthetic files;
check ids, severities, titles and the full detail text must be identical, numbers to 1e-6.
Exits non-zero on any mismatch.
"""
from __future__ import annotations

import json
import math
import os
import subprocess
import sys
import tempfile

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from hunt import checks, levels  # noqa: E402

TOL = 1e-6


def close(a, b, tol=TOL):
    if isinstance(a, bool) or isinstance(b, bool):
        return a == b
    if isinstance(a, (int, float)) and isinstance(b, (int, float)):
        if (isinstance(a, float) and math.isnan(a)) or (isinstance(b, float) and math.isnan(b)):
            return False
        return abs(a - b) <= tol * max(1.0, abs(a), abs(b))
    if isinstance(a, list) and isinstance(b, list):
        return len(a) == len(b) and all(close(x, y, tol) for x, y in zip(a, b))
    if isinstance(a, dict) and isinstance(b, dict):
        return set(a) == set(b) and all(close(a[k], b[k], tol) for k in a)
    return a == b


def diff(a, b, path="", out=None, tol=TOL):
    out = [] if out is None else out
    if isinstance(a, dict) and isinstance(b, dict):
        for k in sorted(set(a) | set(b)):
            if k not in a or k not in b:
                out.append(f"{path}.{k}: only in {'py' if k in a else 'js'}")
            else:
                diff(a[k], b[k], f"{path}.{k}", out, tol)
    elif isinstance(a, list) and isinstance(b, list):
        if len(a) != len(b):
            out.append(f"{path}: length {len(a)} vs {len(b)}")
        for i, (x, y) in enumerate(zip(a, b)):
            if not close(x, y, tol):
                diff(x, y, f"{path}[{i}]", out, tol)
                if len(out) > 20:
                    return out
    elif not close(a, b, tol):
        out.append(f"{path}: py {a!r} vs js {b!r}")
    return out


def node(*args):
    res = subprocess.run(["node", os.path.join(ROOT, "tests", "run_js.js"), *args], check=True, capture_output=True, cwd=ROOT)
    return json.loads(res.stdout.decode("utf-8"))


def py_levels():
    out = []
    for lv in levels.make_all():
        r = levels.run_level(lv)
        if "counterfactual" in r:
            for c in r["counterfactual"]:
                c.pop("before", None)
                c.pop("after", None)
        out.append(json.loads(json.dumps(r)))
    return out


def csv_cases(tmp):
    cases = [("web/demo.csv (left)", os.path.join(ROOT, "web", "demo.csv"), "left", None)]
    try:
        from sklearn import datasets
        for name, load in (("breast_cancer", datasets.load_breast_cancer), ("iris", datasets.load_iris), ("wine", datasets.load_wine)):
            d = load()
            p = os.path.join(tmp, name + ".csv")
            with open(p, "w", encoding="utf-8") as f:
                f.write(",".join(list(d.feature_names) + ["target"]) + "\n")
                for row, t in zip(d.data, d.target):
                    f.write(",".join("%r" % float(v) for v in row) + "," + str(d.target_names[t]) + "\n")
            cases.append((name + ".csv (target)", p, "target", None))
    except ImportError:
        pass
    # awkward file: quotes, tabs in names, missing values, a category column, an ISO timestamp
    p = os.path.join(tmp, "awkward.csv")
    import random
    rnd = random.Random(3)
    with open(p, "w", encoding="utf-8") as f:
        f.write('"order id",when,"city, region",amount,note,outcome\n')
        for i in range(120):
            y = rnd.random() < 0.4
            when = "2024-%02d-%02dT%02d:%02d" % (1 + i // 10, 1 + i % 28, rnd.randrange(24), rnd.randrange(60))
            city = rnd.choice(['"Paris, FR"', "Berlin", "", "N/A"])
            amount = "" if rnd.random() < 0.1 else "%.2f" % (rnd.gauss(50 + 20 * y, 15))
            f.write("%s,%s,%s,%s,%s,%s\n" % ("A%04d" % i, when, city, amount, '"he said ""ok"""', "won" if y else "lost"))
    cases.append(("awkward.csv (outcome)", p, "outcome", None))
    cases.append(("awkward.csv (outcome, time=when)", p, "outcome", "when"))
    return cases


def main():
    try:
        subprocess.run(["node", "--version"], check=True, capture_output=True)
    except (OSError, subprocess.CalledProcessError):
        print("node is not available; can't run the parity check")
        return 2
    failed = 0
    js = node("levels")
    for a, b in zip(py_levels(), js):
        for c in b.get("counterfactual", []):
            c.pop("before", None)
            c.pop("after", None)
        errs = diff(a, b)
        print(("ok   " if not errs else "FAIL ") + "level " + a["id"] + "   before %.4f after %.4f" % (a["before"]["acc"], a["after"]["acc"]))
        for e in errs[:10]:
            print("     " + e)
        failed += bool(errs)
    with tempfile.TemporaryDirectory() as tmp:
        for label, path, target, timecol in csv_cases(tmp):
            with open(path, encoding="utf-8") as f:
                cols, rows = checks.read_csv(f.read())
            py = json.loads(json.dumps(checks.analyze(cols, rows, target, timecol)))
            jsr = node("csv", path, target, *([timecol] if timecol else []))
            errs = diff(py, jsr)
            print(("ok   " if not errs else "FAIL ") + label + "   " + ", ".join(c["severity"][0] + ":" + c["id"] for c in py["checks"]))
            for e in errs[:10]:
                print("     " + e)
            failed += bool(errs)
    print("%d problem(s)" % failed)
    return 1 if failed else 0


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