#!/usr/bin/env python3
"""Run the curl_cffi proxy lab on loopback and write results.json.

Starts the fixtures (python -m http.server on 127.0.0.1:8080 and 127.0.0.2:8080, the same with TLS
on port 8443, the jobs target on 8081, three Basic-auth proxies on 8888, 8889 and 8890, a logging
SOCKS5 server on 1080 and one that always fails on 1081), then runs every case from cases.py with
`python <case>.py` in a new interpreter with a clean environment. For each case it records what the
script printed and what the proxies, the SOCKS servers and the jobs target logged while it ran.

    python3 -m venv .venv && . .venv/bin/activate
    python -m pip install -r requirements.txt
    sudo sh run_lab.sh           # private /etc/hosts with the dnsprobe.example entry, then this file

Needs Python 3.14 (http.server --tls-cert), the openssl command for the lab certificate, and the
hosts entry from hosts.lab: run_lab.sh adds it in a private mount namespace.
"""
import json
import pathlib
import platform
import socket
import subprocess
import sys
import tempfile
import time
from importlib import metadata

from cases import CASES

HERE = pathlib.Path(__file__).resolve().parent
PORTS = {"http_target": 8080, "https_target": 8443, "jobs_target": 8081, "proxy": 8888, "proxy_b": 8889, "proxy_c": 8890, "socks": 1080,
         "socks_failing": 1081}
PROBE_HOST, PROBE_ADDRESS = "dnsprobe.example", "127.0.0.2"


def wait_for(host: str, port: int) -> None:
    for _ in range(100):
        try:
            socket.create_connection((host, port), timeout=0.2).close()
            return
        except OSError:
            time.sleep(0.1)
    raise SystemExit(f"nothing is listening on {host}:{port}")


def new_lines(path: pathlib.Path, offset: int) -> tuple[list[str], int]:
    text = path.read_text(encoding="utf-8") if path.exists() else ""
    return text[offset:].splitlines(), len(text)


def main() -> None:
    try:
        resolved = socket.gethostbyname(PROBE_HOST)
    except OSError:
        resolved = None
    if resolved != PROBE_ADDRESS:
        raise SystemExit(f"{PROBE_HOST} resolves to {resolved}, not {PROBE_ADDRESS}: run this through run_lab.sh or add hosts.lab to /etc/hosts")
    for name, port in PORTS.items():
        try:
            socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
        except OSError:
            continue
        raise SystemExit(f"port {port} ({name}) is already in use")

    work = pathlib.Path(tempfile.mkdtemp(prefix="curl-cffi-proxy-lab-"))
    docroot, home, cwd = work / "docroot", work / "home", work / "cwd"
    for directory in (docroot, home, cwd):
        directory.mkdir()
    (docroot / "index.html").write_text("lab target ok\n", encoding="utf-8")
    cert, key = work / "cert.pem", work / "key.pem"
    subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2", "-keyout", str(key), "-out", str(cert),
                    "-subj", "/CN=localhost", "-addext", f"subjectAltName=IP:127.0.0.1,IP:{PROBE_ADDRESS},DNS:localhost,DNS:{PROBE_HOST}"],
                   check=True, capture_output=True)

    proxy_log, socks_log, failing_socks_log, target_log = work / "proxy.jsonl", work / "socks.log", work / "socks-failing.log", work / "target.jsonl"
    python = sys.executable
    quiet = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL}

    def target(address: str, port: int, tls: bool) -> subprocess.Popen:
        extra = ["--tls-cert", str(cert), "--tls-key", str(key)] if tls else []
        return subprocess.Popen([python, "-m", "http.server", "--bind", address, "--directory", str(docroot), *extra, str(port)], **quiet)

    def proxy(port: int, password: str) -> subprocess.Popen:
        return subprocess.Popen([python, str(HERE / "auth_proxy.py"), str(port), "--user", "user", "--password", password, "--log", str(proxy_log)],
                                stdout=subprocess.DEVNULL)

    fixtures = [
        target("127.0.0.1", PORTS["http_target"], False),
        target(PROBE_ADDRESS, PORTS["http_target"], False),
        target("127.0.0.1", PORTS["https_target"], True),
        target(PROBE_ADDRESS, PORTS["https_target"], True),
        subprocess.Popen([python, str(HERE / "jobs_target.py"), str(PORTS["jobs_target"]), "--log", str(target_log)], stdout=subprocess.DEVNULL),
        proxy(PORTS["proxy"], "pass"),
        proxy(PORTS["proxy_b"], "pass"),
        proxy(PORTS["proxy_c"], "p@ss:w/rd"),
        subprocess.Popen([python, "-u", str(HERE / "socks5_server.py"), str(PORTS["socks"])], stdout=socks_log.open("w", encoding="utf-8")),
        subprocess.Popen([python, "-u", str(HERE / "socks5_server.py"), str(PORTS["socks_failing"]), "--reply", "1"],
                         stdout=failing_socks_log.open("w", encoding="utf-8")),
    ]
    venv = str(pathlib.Path(python).parent.parent)
    results = []
    try:
        for port in PORTS.values():
            wait_for("127.0.0.1", port)
        for name in ("http_target", "https_target"):
            wait_for(PROBE_ADDRESS, PORTS[name])
        # The readiness probes above reach the fixtures; start the per-case logs after them.
        time.sleep(0.3)
        _, proxy_offset = new_lines(proxy_log, 0)
        _, socks_offset = new_lines(socks_log, 0)
        _, failing_offset = new_lines(failing_socks_log, 0)
        _, target_offset = new_lines(target_log, 0)
        for name, env, code in CASES:
            script = cwd / f"{name}.py"
            script.write_text(code, encoding="utf-8")
            clean_env = {"PATH": "/usr/bin:/bin", "HOME": str(home), "PYTHONPATH": str(HERE), "LAB_CA": str(cert), **env}
            done = subprocess.run([python, script.name], cwd=cwd, env=clean_env, capture_output=True, text=True, timeout=120)
            time.sleep(0.2)
            proxy_lines, proxy_offset = new_lines(proxy_log, proxy_offset)
            socks_lines, socks_offset = new_lines(socks_log, socks_offset)
            failing_lines, failing_offset = new_lines(failing_socks_log, failing_offset)
            target_lines, target_offset = new_lines(target_log, target_offset)

            def clean(text: str) -> list[str]:
                return text.replace(str(cwd) + "/", "").replace(venv, "<venv>").replace(str(HERE), "<lab>").replace(str(work), "<work>").strip().splitlines()

            results.append({
                "case": name, "env": env, "command": f"python {script.name}", "code": code.strip(), "exit_code": done.returncode,
                "stdout": clean(done.stdout), "stderr": clean(done.stderr),
                "proxy_log": [json.loads(line) for line in proxy_lines], "socks_log": socks_lines, "failing_socks_log": failing_lines,
                "target_log": [json.loads(line) for line in target_lines],
            })
            print(f"{name}: exit {done.returncode}; proxies saw {len(proxy_lines)}, socks saw {len(socks_lines) + len(failing_lines)}, "
                  f"jobs target saw {len(target_lines)}; {' | '.join(done.stdout.strip().splitlines())[:330]}")
    finally:
        for fixture in fixtures:
            fixture.terminate()

    import curl_cffi

    versions = {"curl_cffi": metadata.version("curl_cffi"), "libcurl": curl_cffi.__curl_version__, "cffi": metadata.version("cffi"),
                "certifi": metadata.version("certifi")}
    report = {"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "python": platform.python_version(), "platform": platform.platform(),
              "versions": versions, "ports": PORTS, "hosts": f"{PROBE_ADDRESS} {PROBE_HOST}",
              "lab_common": (HERE / "lab_common.py").read_text(encoding="utf-8"), "cases": results}
    output = HERE / (sys.argv[1] if len(sys.argv) > 1 else "results.json")
    output.write_text(json.dumps(report, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
    print(f"wrote {output.name}: {len(results)} cases, curl_cffi {versions['curl_cffi']}, {versions['libcurl'].split()[0]}, Python {report['python']}")


if __name__ == "__main__":
    main()
