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

Starts five fixtures on 127.0.0.1 (http.server on 8080, http.server with TLS on 8443, a header-echo
HTTPS target on 8444, the Basic-auth proxy on 8888 and the logging SOCKS5 server on 1080), then runs every case from cases.py in a new
interpreter with a clean environment. For each case it records stdout, stderr (where Python prints
warnings), and what the proxy and the SOCKS server logged while the case ran.

    python3 -m venv .venv && . .venv/bin/activate
    python -m pip install -r requirements.txt
    python run_lab.py            # writes results.json; pass a file name to keep another run

Needs Python 3.14 (http.server --tls-cert) and the openssl command for the lab certificate.
"""
import json
import pathlib
import platform
import socket
import subprocess
import sys
import tempfile
import time
from importlib import metadata

from cases import CASES, PRELUDE

HERE = pathlib.Path(__file__).resolve().parent
PORTS = {"http_target": 8080, "https_target": 8443, "echo_target": 8444, "proxy": 8888, "socks": 1080}


def wait_for(port: int) -> None:
    for _ in range(100):
        try:
            socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
            return
        except OSError:
            time.sleep(0.1)
    raise SystemExit(f"nothing is listening on 127.0.0.1:{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:
    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="aiohttp-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", "subjectAltName=IP:127.0.0.1,DNS:localhost"], check=True, capture_output=True)

    proxy_log, socks_log = work / "proxy.jsonl", work / "socks.log"
    python = sys.executable
    fixtures = [
        subprocess.Popen([python, "-m", "http.server", "--bind", "127.0.0.1", "--directory", str(docroot), str(PORTS["http_target"])],
                         stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL),
        subprocess.Popen([python, "-m", "http.server", "--bind", "127.0.0.1", "--directory", str(docroot), "--tls-cert", str(cert), "--tls-key", str(key),
                          str(PORTS["https_target"])], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL),
        subprocess.Popen([python, str(HERE / "echo_target.py"), str(PORTS["echo_target"]), str(cert), str(key)]),
        subprocess.Popen([python, str(HERE / "auth_proxy.py"), str(PORTS["proxy"]), "--user", "user", "--password", "pass", "--log", str(proxy_log)],
                         stdout=subprocess.DEVNULL),
        subprocess.Popen([python, "-u", str(HERE / "socks5_server.py"), str(PORTS["socks"])], stdout=socks_log.open("w", encoding="utf-8")),
    ]
    results = []
    try:
        for port in PORTS.values():
            wait_for(port)
        # The readiness probes above reach the proxy and the SOCKS server; start the case logs after them.
        time.sleep(0.3)
        _, proxy_offset = new_lines(proxy_log, 0)
        _, socks_offset = new_lines(socks_log, 0)
        for name, env, warnings_flag, code in CASES:
            script = cwd / f"{name}.py"
            script.write_text(PRELUDE + code, encoding="utf-8")
            command = [python, *(["-W", warnings_flag] if warnings_flag else []), script.name]
            clean_env = {"PATH": "/usr/bin:/bin", "HOME": str(home), "LAB_CA": str(cert), **env}
            done = subprocess.run(command, cwd=cwd, env=clean_env, capture_output=True, text=True, timeout=60)
            time.sleep(0.2)
            proxy_lines, proxy_offset = new_lines(proxy_log, proxy_offset)
            socks_lines, socks_offset = new_lines(socks_log, socks_offset)
            results.append({
                "case": name, "env": env, "command": " ".join(["python", *command[1:]]), "code": code.strip(),
                "exit_code": done.returncode, "stdout": done.stdout.strip().splitlines(),
                "stderr": done.stderr.replace(str(cwd) + "/", "").replace(str(pathlib.Path(python).parent.parent), "<venv>").strip().splitlines(),
                "proxy_log": [json.loads(line) for line in proxy_lines], "socks_log": socks_lines,
            })
            print(f"{name}: exit {done.returncode}; proxy saw {len(proxy_lines)}, socks saw {len(socks_lines)}; {' | '.join(done.stdout.strip().splitlines())[:200]}")
    finally:
        for fixture in fixtures:
            fixture.terminate()
    versions = {package: metadata.version(package) for package in ("aiohttp", "aiohttp-socks", "python-socks", "yarl", "multidict")}
    report = {"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "python": platform.python_version(), "platform": platform.platform(),
              "versions": versions, "ports": PORTS, "prelude": PRELUDE, "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, aiohttp {versions['aiohttp']}, Python {report['python']}")


if __name__ == "__main__":
    main()
