"""Execute the Node.js and Go verify snippets against the loopback lab.

Same method as labcore/snippets.py (which runs the curl, httpx and Requests
snippets): each snippet file is published as written, with PROXY_URL and
https://example.com/ as placeholders. For the lab run:
  * the literal text "https://example.com/" is replaced by a loopback https URL
    (the only change to the published file);
  * PROXY_URL points at the counting proxy (CONNECT). The Node snippets are run
    the way their "Run:" comment says: NODE_USE_ENV_PROXY=1 HTTPS_PROXY=$PROXY_URL;
  * trust in the lab CA: NODE_EXTRA_CA_CERTS for Node. The Go snippet is
    compiled together with one extra, unpublished file, lab_trust.go (its full
    text is in each .run.txt), whose init() puts the lab CA into
    http.DefaultTransport's RootCAs before the snippet clones that transport, so
    the run does not depend on how Go treats SSL_CERT_FILE. (go1.27.1 on macOS
    honours SSL_CERT_FILE unless GODEBUG=x509sslcertoverrideplatform=0, which is
    also the default for a main module that declares go 1.26 or older.)
    Verification stays on in every run.
Printed values are then checked against what the origin received and sent.

  python -m nodego.snippets --state STATE --out-dir OUT/snippets
"""

from __future__ import annotations

import argparse
import datetime as _dt
import json
import os
import re
import shutil
import subprocess
import sys
import tempfile

from labcore import bodies
from labcore.origin import BodyStore, Origin, OriginLog
from labcore.proxy import CountingProxy, ProxyLog
from labcore.redact import redact

HARNESS = bodies.HARNESS_DIR
SNIP_DIR = os.path.join(HARNESS, "snippets")
PLACEHOLDER = "https://example.com/"
GO_TOOLCHAIN = "go1.27.1"

LAB_TRUST_GO = '''// LAB ONLY, not part of the published snippet. It adds the lab's local CA to
// http.DefaultTransport before the snippet clones it, so trust does not depend on
// the Go version or GODEBUG (go1.27.1 on macOS would also honour SSL_CERT_FILE,
// unless GODEBUG=x509sslcertoverrideplatform=0). Verification stays on.
package main

import (
	"crypto/tls"
	"crypto/x509"
	"net/http"
	"os"
)

func init() {
	pem, err := os.ReadFile(os.Getenv("LAB_CA_FILE"))
	if err != nil {
		panic(err)
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(pem) {
		panic("no certificates in LAB_CA_FILE")
	}
	http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{RootCAs: pool}
}
'''

RUNS = [
    *[{"tag": f"snippet-node-fetch-node{v.split('.')[0]}", "file": "node-fetch-verify.mjs", "kind": "node", "node": v}
      for v in ("22.23.3", "24.21.0", "26.10.0")],
    *[{"tag": f"snippet-node-fetch-bytes-node{v.split('.')[0]}", "file": "node-fetch-bytes.mjs", "kind": "node",
       "node": v} for v in ("22.23.3", "24.21.0", "26.10.0")],
    *[{"tag": f"snippet-node-http-node{v.split('.')[0]}", "file": "node-http-verify.mjs", "kind": "node", "node": v}
      for v in ("22.23.3", "24.21.0", "26.10.0")],
    {"tag": "snippet-go", "file": "go-verify.go", "kind": "go"},
]


def utcnow() -> str:
    return _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")


def go_env(state: str) -> dict:
    gs = os.path.join(state, "go")
    return {"GOTOOLCHAIN": GO_TOOLCHAIN, "GOENV": "off", "GOCACHE": os.path.join(gs, "cache"),
            "GOPATH": os.path.join(gs, "path"), "GOMODCACHE": os.path.join(gs, "modcache"), "GOFLAGS": "-modcacherw"}


def check(run: dict, stdout: str, entries: list[dict]) -> tuple[bool, list[str]]:
    lines = [l for l in stdout.splitlines() if l.strip()]
    if len(entries) != 1:
        return False, [f"expected 1 origin request, got {len(entries)}"]
    e = entries[0]
    got_ae, served, wire, ident = e["accept_encoding"], e.get("content_encoding"), e["bytes_written"], e["identity_bytes"]
    notes = [f"origin received Accept-Encoding {got_ae!r}; sent {wire} B with Content-Encoding {served!r} "
             f"(identity body {ident} B)"]
    ok = True
    if run["file"] == "node-fetch-verify.mjs":
        m1 = re.fullmatch(r"sent (?:accept-encoding: (.*)|no Accept-Encoding)", lines[0]) if lines else None
        m2 = re.fullmatch(r"content-encoding (\S+) decoded bytes (\d+)", lines[1]) if len(lines) > 1 else None
        if not (m1 and m2 and len(lines) == 2):
            return False, notes + [f"unexpected output shape: {lines!r}"]
        sent = m1.group(1)
        ok &= sent == got_ae
        notes.append(f"printed sent={sent!r} vs origin received {got_ae!r} -> {'match' if sent == got_ae else 'MISMATCH'}")
        ce = None if m2.group(1) == "null" else m2.group(1)
        good = ce == served and int(m2.group(2)) == ident
        ok &= good
        notes.append(f"printed content-encoding={m2.group(1)} decoded bytes={m2.group(2)} vs origin {served!r}, "
                     f"identity {ident} B -> {'match' if good else 'MISMATCH'}")
    elif run["file"] == "node-fetch-bytes.mjs":
        m = re.fullmatch(r"content-encoding (\S+) wire body bytes (\S+) decoded bytes (\d+)", lines[0]) if len(lines) == 1 else None
        if not m:
            return False, notes + [f"unexpected output shape: {lines!r}"]
        ce = None if m.group(1) == "null" else m.group(1)
        good = ce == served and m.group(2) == str(wire) and int(m.group(3)) == ident
        ok &= good
        notes.append(f"printed content-encoding={m.group(1)} wire body bytes (encodedBodySize)={m.group(2)} "
                     f"decoded bytes={m.group(3)} vs origin {served!r}, {wire} B written, identity {ident} B -> "
                     f"{'match' if good else 'MISMATCH'}")
    elif run["file"] == "node-http-verify.mjs":
        m1 = re.fullmatch(r"sent accept-encoding (.*)", lines[0]) if lines else None
        m2 = re.fullmatch(r"content-encoding (\S+) body bytes (\d+)", lines[1]) if len(lines) > 1 else None
        if not (m1 and m2 and len(lines) == 2):
            return False, notes + [f"unexpected output shape: {lines!r}"]
        sent = None if m1.group(1) == "(none)" else m1.group(1)
        ok &= sent == got_ae
        notes.append(f"printed sent={m1.group(1)!r} vs origin received {got_ae!r} -> {'match' if sent == got_ae else 'MISMATCH'}")
        ce = None if m2.group(1) == "(none)" else m2.group(1)
        good = ce == served and int(m2.group(2)) == wire
        ok &= good
        notes.append(f"printed content-encoding={m2.group(1)} body bytes={m2.group(2)} vs origin {served!r}, "
                     f"{wire} B written -> {'match' if good else 'MISMATCH'}")
    else:
        m = re.fullmatch(r'uncompressed=(true|false) content-encoding="(.*)" content-length=(-?\d+) decoded-bytes=(\d+)',
                         lines[0]) if len(lines) == 1 else None
        if not m:
            return False, notes + [f"unexpected output shape: {lines!r}"]
        unc = m.group(1) == "true"
        exp_unc = served == "gzip" and got_ae == "gzip"
        good = (unc == exp_unc and int(m.group(4)) == ident and int(m.group(3)) == (-1 if unc else wire)
                and m.group(2) == ("" if unc else (served or "")))
        ok &= good
        notes.append(f"printed uncompressed={m.group(1)} content-encoding={m.group(2)!r} content-length={m.group(3)} "
                     f"decoded-bytes={m.group(4)}; expected uncompressed={str(exp_unc).lower()} (origin got {got_ae!r}, "
                     f"served {served!r}), decoded {ident} B -> {'match' if good else 'MISMATCH'}")
    return ok, notes


def main(argv=None):
    ap = argparse.ArgumentParser()
    ap.add_argument("--state", required=True)
    ap.add_argument("--out-dir", required=True)
    a = ap.parse_args(argv)
    try:
        store = BodyStore(bodies.load_bodies(a.state))
        ca = bodies.ca_paths(a.state)
    except bodies.LabSetupError as exc:
        print(f"snippets-node-go: {exc}", file=sys.stderr)
        return 2
    os.makedirs(a.out_dir, exist_ok=True)
    olog, plog = OriginLog(), ProxyLog()
    origin = Origin(store, olog, ca["server_cert"], ca["server_key"]).start()
    proxy = CountingProxy(0, plog).start()
    work = tempfile.mkdtemp(prefix="lab-snippets-node-go-")
    summary = {"label": "SYNTHETIC loopback run of the published Node.js and Go verify snippets", "run_utc": utcnow(),
               "placeholder_replaced": PLACEHOLDER, "runs": []}
    failures = 0
    for f in sorted({r["file"] for r in RUNS}):
        shutil.copyfile(os.path.join(SNIP_DIR, f), os.path.join(a.out_dir, f))
    rt = os.path.join(a.state, "runtimes")
    base_env = {k: v for k, v in os.environ.items()
                if k.lower() not in {"http_proxy", "https_proxy", "all_proxy", "no_proxy", "ssl_cert_file",
                                     "node_extra_ca_certs", "node_use_env_proxy", "node_tls_reject_unauthorized",
                                     "node_options", "gotoolchain", "goflags"}}
    for run in RUNS:
        src = open(os.path.join(SNIP_DIR, run["file"]), encoding="utf-8").read()
        if src.count(PLACEHOLDER) != 1:
            print(f"snippets-node-go: {run['file']} must contain {PLACEHOLDER} exactly once", file=sys.stderr)
            return 2
        url = f"{origin.https_base}/t/{run['tag']}/negotiate/fixture"
        body = src.replace(PLACEHOLDER, url)
        record = {"tag": run["tag"], "snippet": run["file"], "t_utc": utcnow(), "loopback_url": url}
        env = dict(base_env, PROXY_URL=proxy.url)
        rundir = os.path.join(work, run["tag"])
        os.makedirs(rundir)
        exe_file = os.path.join(rundir, run["file"])
        open(exe_file, "w", encoding="utf-8").write(body)
        extra_files = {}
        if run["kind"] == "node":
            node = os.path.join(rt, f"node-{run['node']}", "bin", "node")
            if not os.path.exists(node):
                record.update(status="not-run", reason=f"prerequisite missing: {node} (run ./setup.sh)")
                summary["runs"].append(record)
                failures += 1
                print(f"snippets-node-go: {run['tag']}: NOT RUN: {record['reason']}", file=sys.stderr)
                continue
            env.update(NODE_USE_ENV_PROXY="1", HTTPS_PROXY=proxy.url, NODE_EXTRA_CA_CERTS=ca["ca"])
            env_desc = ("PROXY_URL=<counting proxy>; NODE_USE_ENV_PROXY=1 HTTPS_PROXY=$PROXY_URL (as the Run: comment "
                        "says); NODE_EXTRA_CA_CERTS=<lab CA ca.pem> (verification on)")
            argv = [node, exe_file]
            record["interpreter"] = node
            record["client_version"] = subprocess.run(
                [node, "-p", "`Node.js ${process.version} (undici ${process.versions.undici}, "
                             "OpenSSL ${process.versions.openssl}) ${process.execPath}`"],
                capture_output=True, text=True).stdout.strip()
        else:
            gobin = shutil.which("go")
            if not gobin:
                record.update(status="not-run", reason="prerequisite missing: go not found on PATH")
                summary["runs"].append(record)
                failures += 1
                print(f"snippets-node-go: {run['tag']}: NOT RUN: {record['reason']}", file=sys.stderr)
                continue
            open(os.path.join(rundir, "lab_trust.go"), "w", encoding="utf-8").write(LAB_TRUST_GO)
            extra_files["lab_trust.go"] = LAB_TRUST_GO
            benv = dict(base_env, **go_env(a.state))
            exe = os.path.join(rundir, "go-verify")
            b = subprocess.run([gobin, "build", "-trimpath", "-o", exe, run["file"], "lab_trust.go"], cwd=rundir,
                               env=benv, capture_output=True, text=True)
            if b.returncode != 0:
                record.update(status="not-run", reason=f"go build failed: {b.stderr.strip()[-500:]}")
                summary["runs"].append(record)
                failures += 1
                print(f"snippets-node-go: {run['tag']}: NOT RUN: {record['reason']}", file=sys.stderr)
                continue
            env.update(LAB_CA_FILE=ca["ca"])
            env_desc = ("PROXY_URL=<counting proxy>; LAB_CA_FILE=<lab CA ca.pem>, read only by the unpublished "
                        "lab_trust.go (verification on)")
            argv = [exe]
            record["interpreter"] = f"go build ({GO_TOOLCHAIN}, GOTOOLCHAIN pinned) of {run['file']} + lab_trust.go"
            gv = subprocess.run([gobin, "version", exe], capture_output=True, text=True).stdout.strip()
            record["client_version"] = "go-verify binary: " + gv.rsplit(": ", 1)[-1]
        proxy.tag = run["tag"]
        proc = subprocess.run(argv, capture_output=True, text=True, env=env, cwd=rundir, timeout=60)
        proxy.wait_idle(5)
        proxy.tag = ""
        entries = [e for e in olog.for_cell(run["tag"]) if e.get("body_id")]
        conns, _ = plog.for_tag(run["tag"])
        ok, notes = check(run, proc.stdout, entries) if proc.returncode == 0 else (False, ["non-zero exit"])
        failures += 0 if ok else 1
        record.update(status="run", exit_code=proc.returncode, stdout=proc.stdout, stderr=proc.stderr,
                      consistent_with_origin=ok, checks=notes, environment=env_desc,
                      proxy={"connections": len(conns), "modes": sorted({c.get('mode') for c in conns}),
                             "bytes_up": sum(c.get("bytes_up", 0) for c in conns),
                             "bytes_down": sum(c.get("bytes_down", 0) for c in conns)})
        if extra_files:
            record["unpublished_lab_files"] = extra_files
        summary["runs"].append(record)
        with open(os.path.join(a.out_dir, f"{run['tag']}.run.txt"), "w", encoding="utf-8") as fh:
            text = (f"# {run['file']} executed {record['t_utc']} (SYNTHETIC loopback run)\n"
                    f"# interpreter: {record['interpreter']}\n# client: {record['client_version']}\n"
                    f"# only change to the published file: '{PLACEHOLDER}' -> '{url}'\n"
                    f"# environment: {env_desc}\n# exit code: {proc.returncode}\n\n"
                    f"--- stdout ---\n{proc.stdout}--- stderr ---\n{proc.stderr}\n"
                    f"--- checks against the origin log ---\n" + "\n".join(notes) +
                    f"\nconsistent_with_origin: {ok}\nproxy: {json.dumps(record['proxy'])}\n")
            for name, content in extra_files.items():
                text += f"\n--- unpublished lab file compiled with the snippet: {name} ---\n{content}"
            fh.write(redact(text))
        print(f"snippets-node-go: {run['tag']}: exit {proc.returncode}, consistent_with_origin={ok}: "
              f"{proc.stdout.strip()!r}")
    origin.stop()
    proxy.stop()
    shutil.rmtree(work, ignore_errors=True)
    with open(os.path.join(a.out_dir, "snippets-node-go-results.json"), "w", encoding="utf-8") as fh:
        fh.write(redact(json.dumps(summary, indent=2)) + "\n")
    return 1 if failures else 0


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