import base64, json, secrets, socket, ssl, struct, time, urllib.request

H = "misc.mickai.me"
BASE = "https://%s/api" % H
UA = ("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/45.0.2454.85 Safari/537.36")
results = []


def E(cond, label):
    results.append(bool(cond))
    print(("  PASS  " if cond else "  FAIL  ") + label, flush=True)


def api(method, path, body=None, token=None):
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, method=method)
    req.add_header("User-Agent", UA)
    if data:
        req.add_header("Content-Type", "application/json")
    if token:
        req.add_header("Authorization", token)
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            raw = resp.read()
            return resp.status, (json.loads(raw) if raw else None)
    except urllib.error.HTTPError as e:
        raw = e.read()
        return e.code, (json.loads(raw) if raw else None)


class Sock:
    def __init__(self):
        raw = socket.create_connection((H, 443), timeout=20)
        self.s = ssl.create_default_context().wrap_socket(raw, server_hostname=H)
        key = base64.b64encode(secrets.token_bytes(16)).decode()
        self.s.sendall((
            "GET /gateway HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\n"
            "Upgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\n"
            "Sec-WebSocket-Version: 13\r\n\r\n" % (H, UA, key)).encode())
        self.s.settimeout(20)
        self.buf = b""
        while b"\r\n\r\n" not in self.buf:
            chunk = self.s.recv(4096)
            if not chunk:
                break
            self.buf += chunk
        head, self.buf = self.buf.split(b"\r\n\r\n", 1)
        self.status = head.split(b"\r\n")[0]

    def send(self, obj):
        p = json.dumps(obj).encode()
        m = secrets.token_bytes(4)
        n = len(p)
        hdr = bytearray([0x81])
        # Anything >=126 needs an extended length field; packing it into the
        # 7-bit field desynchronises the stream.
        if n < 126:
            hdr.append(0x80 | n)
        elif n < 65536:
            hdr.append(0x80 | 126)
            hdr += struct.pack(">H", n)
        else:
            hdr.append(0x80 | 127)
            hdr += struct.pack(">Q", n)
        hdr += m
        self.s.sendall(bytes(hdr) + bytes(b ^ m[i % 4] for i, b in enumerate(p)))

    def _need(self, n, deadline):
        while len(self.buf) < n:
            left = deadline - time.monotonic()
            if left <= 0:
                raise TimeoutError("timeout waiting for %d bytes" % n)
            self.s.settimeout(left)
            chunk = self.s.recv(65536)
            if not chunk:
                raise ConnectionError("closed")
            self.buf += chunk
        out, self.buf = self.buf[:n], self.buf[n:]
        return out

    def recv(self, timeout=20):
        deadline = time.monotonic() + timeout
        hdr = self._need(2, deadline)
        op, n = hdr[0] & 0x0F, hdr[1] & 0x7F
        if n == 126:
            n = struct.unpack(">H", self._need(2, deadline))[0]
        elif n == 127:
            n = struct.unpack(">Q", self._need(8, deadline))[0]
        data = self._need(n, deadline) if n else b""
        if op == 0x8:
            return {"t": "__close__", "code": struct.unpack(">H", data[:2])[0]}
        return json.loads(data.decode())


tag = secrets.token_hex(3)
status, user = api("POST", "/auth/register",
                   {"username": "livecheck", "email": "live%s@x.dev" % tag,
                    "password": "hunter22"})
E(status == 200 and user.get("token"), "register through Cloudflare")
token = user["token"]

status, gw = api("GET", "/gateway")
E(status == 200 and gw["url"] == "wss://%s/gateway" % H, "gateway discovery -> %s" % gw["url"])

ws = Sock()
E(b"101" in ws.status, "wss upgrade through Cloudflare (%s)" % ws.status.decode())
ws.send({"op": 2, "d": {"token": token, "properties": {"$os": "Linux"}, "v": 3}})
pkt = ws.recv()
E(pkt.get("t") == "READY", "READY frame received over wss")
E(pkt["d"]["heartbeat_interval"] == 41250, "heartbeat_interval rides inside READY")
E(pkt["d"]["user"]["username"] == "livecheck", "READY carries the right user")

status, guild = api("POST", "/guilds", {"name": "Live Check", "region": "us-west"}, token)
E(status == 201, "create guild through Cloudflare")
E(ws.recv().get("t") == "GUILD_CREATE", "GUILD_CREATE pushed over wss")

channel = [c for c in guild["channels"] if c["type"] == "text"][0]
status, msg = api("POST", "/channels/%s/messages" % channel["id"],
                  {"content": "hello from 2015", "mentions": [], "nonce": "42",
                   "tts": False}, token)
E(status == 200, "post message through Cloudflare")
pkt = ws.recv()
E(pkt.get("t") == "MESSAGE_CREATE" and pkt["d"]["nonce"] == "42",
  "MESSAGE_CREATE pushed back with nonce intact")

print()
print("LIVE PATH OK" if all(results) else "LIVE PATH HAS FAILURES", flush=True)
