"""Drive the toy with a real HTTP client and report what it makes of each framing.

Everything here exists to make the result reproducible. A fixed port so no
port number leaks into the transcript, a connect-poll instead of a sleep so
there is no start-up race, `curl -s` so the progress meter stays out, a 1s
`--max-time` so the timeout cases finish, and -- deliberately -- no elapsed
seconds printed anywhere, because that is the one field that differs between
otherwise byte-identical runs.

    python3 demo.py
"""
import os
import platform
import shutil
import socket
import subprocess
import sys
import tempfile
import time

HERE = os.path.dirname(os.path.abspath(__file__))
SERVER = os.path.join(HERE, "http_server.py")
PORT = 8127
MAX_TIME = "1"
URL = "http://127.0.0.1:%d/hello" % PORT


def require(condition, message):
    if not condition:
        sys.exit("demo.py: " + message)


def port_is_free(port):
    sock = socket.socket()
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        sock.bind(("127.0.0.1", port))
        return True
    except OSError:
        return False
    finally:
        sock.close()


def start_server(mode, after, bufsize=4096):
    """Start the toy in a subprocess and wait until it actually accepts."""
    proc = subprocess.Popen(
        [sys.executable, SERVER, "--mode", mode, "--after", after,
         "--port", str(PORT), "--bufsize", str(bufsize)],
        stderr=subprocess.PIPE, text=True)
    deadline = time.monotonic() + 5.0
    while time.monotonic() < deadline:
        probe = socket.socket()
        try:
            probe.connect(("127.0.0.1", PORT))
            return proc
        except OSError:
            time.sleep(0.01)
        finally:
            probe.close()
    proc.kill()
    sys.exit("demo.py: server never came up on port %d" % PORT)


def stop_server(proc):
    proc.kill()
    _, err = proc.communicate()
    return err


def curl(*args):
    """Run curl, silenced. Returns (stdout, stderr, exit code)."""
    done = subprocess.run(["curl", "-s", "--max-time", MAX_TIME] + list(args),
                          capture_output=True, text=True)
    return done.stdout, done.stderr, done.returncode


def case(mode, after, *extra):
    proc = start_server(mode, after)
    out, _, code = curl(*(extra or (URL,)))
    return out, code, stop_server(proc)


def rule(title):
    print("\n" + "=" * 74)
    print(title)
    print("=" * 74)


# --- 0. banner -------------------------------------------------------------

def banner():
    rule("0. banner -- the environment these results are valid for")
    version = subprocess.run(["curl", "--version"], capture_output=True,
                             text=True).stdout.splitlines()[0]
    print("  client   : %s" % version)
    print("  python   : %s" % sys.version.split("\n")[0])
    print("  platform : %s  %s" % (platform.platform(), platform.machine()))
    print("  transport: TCP over loopback, 127.0.0.1:%d" % PORT)
    print("  invoked  : curl -s --max-time %s" % MAX_TIME)
    print("\n  Exit codes below are curl's opinion, not the protocol's. The recv")
    print("  counts in section 5 are loopback numbers. Nothing here observed a")
    print("  TCP segment boundary.")


# --- 1. the pair -----------------------------------------------------------

def the_pair():
    rule("1. the pair -- same body, same server, framing changed")
    for mode in ("correct", "none", "short"):
        out, code, _ = case(mode, "hold")
        print("  mode=%-8s exit=%-3d stdout=%r" % (mode, code, out))
    print("\n  'none' printed all 27 bytes and failed. 'short' printed 10 bytes")
    print("  and succeeded. The exit code reports on the framing, not the body.")


# --- 2. the full matrix ----------------------------------------------------

MODES = ["correct", "none", "short", "long", "chunked", "chunked-noterm",
         "close-header-only", "http10", "both", "both-te-first"]


def matrix():
    rule("2. every framing, held open vs. closed after the response")
    print("  %-18s %-6s %-5s %s" % ("mode", "after", "exit", "stdout"))
    print("  %-18s %-6s %-5s %s" % ("-" * 18, "-" * 6, "-" * 5, "-" * 29))
    for mode in MODES:
        for after in ("hold", "fin"):
            out, code, _ = case(mode, after)
            print("  %-18s %-6s %-5d %r" % (mode, after, code, out))
    print("\n  Read the 'fin' column: every framing works once the socket closes.")
    print("  Framing is only load-bearing because the connection is meant to")
    print("  survive the response.")


# --- 3. curl's own account of the truncation -------------------------------

def excess_trace():
    rule("3. what curl says about 'short', in full (curl -sv, unfiltered)")
    proc = start_server("short", "keepalive")
    done = subprocess.run(
        ["curl", "-sv", "--max-time", MAX_TIME, URL],
        capture_output=True, text=True)
    stop_server(proc)
    for line in done.stderr.splitlines():
        print("  " + line)
    print("  [exit %d, stdout %r]" % (done.returncode, done.stdout))


# --- 4. the keep-alive consequence -----------------------------------------

def keep_alive():
    rule("4. two requests down one connection, with Content-Length: 10")
    print("  (server log below; connection 1 is always start_server's readiness")
    print("   probe, which connects and closes without sending a request)")
    for mode in ("correct", "short"):
        proc = start_server(mode, "keepalive")
        base = "http://127.0.0.1:%d/" % PORT
        out, _, code = curl(base + "a", base + "b")
        err = stop_server(proc)
        print("  mode=%-8s exit=%-3d stdout=%r" % (mode, code, out))
        for line in err.splitlines():
            print("      | " + line)


# --- 5. is the 4096 the wire's number, or mine? ----------------------------

def recv_split():
    rule("5. the same 20000-byte upload, read with three buffer sizes")
    handle, path = tempfile.mkstemp(prefix="http-from-sockets-", suffix=".bin")
    os.write(handle, b"x" * 20000)
    os.close(handle)
    try:
        for bufsize in (4096, 1024, 65536):
            proc = start_server("correct", "fin", bufsize=bufsize)
            out, _, code = curl("--data-binary", "@" + path,
                                "http://127.0.0.1:%d/upload" % PORT)
            err = stop_server(proc)
            sizes = [ln for ln in err.splitlines() if "recv sizes" in ln][0]
            print("  bufsize=%-5d curl exit=%d" % (bufsize, code))
            print("     %s" % sizes.strip())
    finally:
        os.unlink(path)
    print("\n  Same bytes on the wire every time. The only thing that changed is")
    print("  the integer this program passes to recv(). This is recv's buffer")
    print("  bound; nothing here observed a TCP segment.")


def main():
    require(shutil.which("curl"),
            "this demo needs curl on PATH -- the whole point is a client "
            "someone else wrote (see section 7 of the commentary)")
    require(port_is_free(PORT),
            "port %d is busy; free it (lsof -i :%d) and re-run" % (PORT, PORT))
    banner()
    the_pair()
    matrix()
    excess_trace()
    keep_alive()
    recv_split()
    print()


if __name__ == "__main__":
    main()
