"""Drive headless Chrome over CDP to check the app actually boots."""
import base64, json, secrets, socket, struct, subprocess, sys, time, urllib.request

URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8081/"
CHROME = ("/tmp/claude-0/-root-alhang/4ab098a2-a126-4502-b30c-c3d7b59e8765/scratchpad/"
          "chs/chrome-headless-shell-linux64/chrome-headless-shell")
PORT = 9222


class WS:
    def __init__(self, url):
        _, rest = url.split("://", 1)
        hostport, path = rest.split("/", 1)
        host, port = hostport.split(":")
        self.sock = socket.create_connection((host, int(port)), timeout=20)
        key = base64.b64encode(secrets.token_bytes(16)).decode()
        self.sock.sendall((
            "GET /%s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
            "Sec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n\r\n"
            % (path, hostport, key)).encode())
        buf = b""
        while b"\r\n\r\n" not in buf:
            buf += self.sock.recv(4096)
        self.buf = buf.split(b"\r\n\r\n", 1)[1]
        self.next_id = 0

    def send(self, obj):
        p = json.dumps(obj).encode()
        m = secrets.token_bytes(4)
        n = len(p)
        h = bytearray([0x81])
        if n < 126:
            h.append(0x80 | n)
        elif n < 65536:
            h.append(0x80 | 126); h += struct.pack(">H", n)
        else:
            h.append(0x80 | 127); h += struct.pack(">Q", n)
        h += m
        self.sock.sendall(bytes(h) + 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()
            self.sock.settimeout(left)
            c = self.sock.recv(1 << 16)
            if not c:
                raise ConnectionError()
            self.buf += c
        out, self.buf = self.buf[:n], self.buf[n:]
        return out

    def recv(self, timeout=20):
        dl = time.monotonic() + timeout
        while True:
            hdr = self._need(2, dl)
            op, n = hdr[0] & 0x0F, hdr[1] & 0x7F
            if n == 126:
                n = struct.unpack(">H", self._need(2, dl))[0]
            elif n == 127:
                n = struct.unpack(">Q", self._need(8, dl))[0]
            data = self._need(n, dl) if n else b""
            if op == 0x8:
                raise ConnectionError("closed")
            if op in (0x1, 0x2):
                return json.loads(data.decode("utf-8", "replace"))

    def call(self, method, params=None, timeout=25):
        self.next_id += 1
        mid = self.next_id
        self.send({"id": mid, "method": method, "params": params or {}})
        dl = time.monotonic() + timeout
        while True:
            msg = self.recv(max(1, dl - time.monotonic()))
            if msg.get("id") == mid:
                return msg
            EVENTS.append(msg)


EVENTS = []

proc = subprocess.Popen(
    [CHROME, "--remote-debugging-port=%d" % PORT, "--no-sandbox", "--disable-gpu",
     "--disable-dev-shm-usage", "--window-size=1280,900", "about:blank"],
    stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
    target = None
    for _ in range(50):
        time.sleep(0.4)
        try:
            with urllib.request.urlopen("http://127.0.0.1:%d/json" % PORT, timeout=3) as r:
                tabs = json.loads(r.read())
            pages = [t for t in tabs if t.get("type") == "page"]
            if pages:
                target = pages[0]
                break
        except Exception:
            continue
    if not target:
        print("could not reach devtools"); raise SystemExit(2)

    ws = WS(target["webSocketDebuggerUrl"])
    ws.call("Runtime.enable")
    ws.call("Log.enable")
    ws.call("Network.enable")
    ws.call("Page.enable")
    # The app ships its crash to Sentry -- intercept that POST and read it,
    # since the error boundary hides the error from the console entirely.
    ws.call("Fetch.enable", {"patterns": [{"urlPattern": "*sentry.io*"}]})
    ws.call("Debugger.enable")
    # The app's error boundary swallows the crash and ships it to a reporter
    # that isn't here, so nothing reaches console. Pause on ALL exceptions.
    ws.call("Debugger.setPauseOnExceptions", {"state": "all"})
    import os as _os
    tok = _os.environ.get("INJECT_TOKEN")
    if tok:
        # This is exactly how the client persists it: JSON-encoded under
        # localStorage "token" (its inline FAST CONNECT script JSON.parses it).
        ws.call("Page.addScriptToEvaluateOnNewDocument",
                {"source": 'try{localStorage.setItem("token",%s)}catch(e){}'
                           % json.dumps(json.dumps(tok))})
    print("navigating to", URL)
    ws.call("Page.navigate", {"url": URL})

    # let the SPA boot: it code-splits, so several chunk loads follow
    deadline = time.monotonic() + 60
    while time.monotonic() < deadline:
        try:
            msg = ws.recv(2)
        except TimeoutError:
            continue
        except ConnectionError:
            break
        EVENTS.append(msg)
        if msg.get("method") == "Debugger.paused":
            ws.next_id += 1
            ws.send({"id": ws.next_id, "method": "Debugger.resume", "params": {}})
        if msg.get("method") == "Fetch.requestPaused":
            rid = msg["params"]["requestId"]
            try:
                body = ws.call("Fetch.getRequestPostData", {"requestId": rid})
                raw = body.get("result", {}).get("postData", "")
                print("\n=== SENTRY CRASH REPORT ===")
                try:
                    rep = json.loads(raw)
                    for exc in (rep.get("exception") or {}).get("values", []):
                        print("  type :", exc.get("type"))
                        print("  value:", exc.get("value"))
                        frames = (exc.get("stacktrace") or {}).get("frames", [])
                        for f in frames[-6:]:
                            print("    at %s (%s:%s)" % (
                                f.get("function"), str(f.get("filename"))[-40:],
                                f.get("lineno")))
                    if rep.get("message"):
                        print("  message:", str(rep["message"])[:300])
                except ValueError:
                    print(raw[:1500])
                print("=== END REPORT ===\n")
            except Exception as exc:
                print("could not read sentry body:", exc)
            ws.next_id += 1
            ws.send({"id": ws.next_id, "method": "Fetch.failRequest",
                     "params": {"requestId": rid, "errorReason": "Aborted"}})

    def ev(js):
        r = ws.call("Runtime.evaluate",
                    {"expression": js, "returnByValue": True, "awaitPromise": False})
        return r.get("result", {}).get("result", {}).get("value")

    print("\n--- page state ---")
    print("title      :", ev("document.title"))
    print("mount kids :", ev("document.getElementById('app-mount') ? document.getElementById('app-mount').children.length : 'NO MOUNT'"))
    print("body chars :", ev("document.body.innerText.length"))
    text = ev("document.body.innerText.slice(0,600)")
    print("visible text:")
    print((text or "").strip()[:600])
    print("classes    :", ev("document.getElementById('app-mount') && document.getElementById('app-mount').firstElementChild ? document.getElementById('app-mount').firstElementChild.className : ''"))

    print("\n--- console errors ---")
    errs = 0
    for e in EVENTS:
        if e.get("method") == "Log.entryAdded":
            entry = e["params"]["entry"]
            if entry.get("level") in ("error",):
                errs += 1
                if errs <= 12:
                    print("  [%s] %s" % (entry.get("source"), entry.get("text", "")[:220]))
        if e.get("method") == "Runtime.exceptionThrown":
            errs += 1
            det = e["params"]["exceptionDetails"]
            desc = (det.get("exception") or {}).get("description") or det.get("text")
            if errs <= 12:
                print("  [exception] %s" % str(desc)[:260])
    print("total errors:", errs)

    print("\n--- thrown exceptions (including caught) ---")
    seen = set()
    for e in EVENTS:
        if e.get("method") != "Debugger.paused":
            continue
        data = e["params"].get("data") or {}
        desc = data.get("description") or data.get("value") or data.get("className") or ""
        frames = e["params"].get("callFrames") or []
        loc = ""
        if frames:
            f = frames[0]
            loc = "%s:%s" % (f.get("url", "?").split("/")[-1],
                             f.get("location", {}).get("lineNumber"))
        first = str(desc).split("\n")[0][:200]
        if (first, loc) in seen:
            continue
        seen.add((first, loc))
        print("  %s\n      @ %s" % (first, loc))
        for f in frames[1:4]:
            print("        <- %s (%s)" % (f.get("functionName") or "(anon)",
                                          f.get("url", "?").split("/")[-1]))
    print("distinct exceptions:", len(seen))

    print("\n--- LAST 8 throws, chronological, full stacks ---")
    paused = [e for e in EVENTS if e.get("method") == "Debugger.paused"]
    print("total paused events:", len(paused))
    for e in paused[-8:]:
        data = e["params"].get("data") or {}
        desc = str(data.get("description") or data.get("value")
                   or data.get("className") or "")
        print("  >>", "\n     ".join(desc.split("\n")[:6])[:600])
        for f in (e["params"].get("callFrames") or [])[:5]:
            print("        at %s (%s:%s)" % (
                f.get("functionName") or "(anon)",
                f.get("url", "?").split("/")[-1],
                f.get("location", {}).get("lineNumber")))

    post = _os.environ.get("POST_EVAL")
    if post:
        print("\n--- post-load eval ---")
        r = ws.call("Runtime.evaluate", {"expression": post, "returnByValue": True,
                                         "awaitPromise": True}, timeout=40)
        res = r.get("result", {})
        if res.get("exceptionDetails"):
            print("  EXCEPTION:", json.dumps(res["exceptionDetails"])[:400])
        print("  ->", json.dumps(res.get("result", {}).get("value"))[:800])

    print("\n--- attachment rendering ---")
    print("inline <img> from /attachments/:",
          ev("document.querySelectorAll('img[src*=\"/attachments/\"]').length"))
    print("<a download> links          :",
          ev("document.querySelectorAll('a[download]').length"))
    print("anchors to /attachments/     :",
          ev("document.querySelectorAll('a[href*=\"/attachments/\"]').length"))
    print("img natural sizes            :",
          ev("Array.from(document.querySelectorAll('img[src*=\"/attachments/\"]'))"
             ".map(function(i){return i.naturalWidth+'x'+i.naturalHeight}).join(', ')"))

    print("\n--- console API calls (React logs boundary errors here) ---")
    n = 0
    for e in EVENTS:
        if e.get("method") != "Runtime.consoleAPICalled":
            continue
        p = e["params"]
        if p.get("type") not in ("error", "warning", "assert"):
            continue
        n += 1
        if n <= 12:
            parts = []
            for arg in p.get("args", [])[:4]:
                parts.append(str(arg.get("value") or arg.get("description")
                                 or arg.get("className") or "")[:400])
            print("  [%s] %s" % (p["type"], " | ".join(parts))[:900])
    print("console error/warn count:", n)

    print("\n--- exceptions with no description (raw data) ---")
    shown = 0
    for e in EVENTS:
        if e.get("method") != "Debugger.paused":
            continue
        data = e["params"].get("data") or {}
        if data.get("description"):
            continue
        shown += 1
        if shown <= 10:
            print("  ", json.dumps(data)[:400])
    print("no-description throws:", shown)

    print("\n--- all network requests ---")
    for e in EVENTS:
        if e.get("method") == "Network.responseReceived":
            r = e["params"]["response"]
            print("  %-4s %-9s %s" % (r.get("status"), e["params"].get("type"),
                                      r["url"][:120]))

    want = _os.environ.get("BODY_OF")
    if want:
        print("\n--- response body for %s ---" % want)
        for e in EVENTS:
            if e.get("method") != "Network.responseReceived":
                continue
            resp = e["params"]["response"]
            if want not in resp["url"]:
                continue
            print("  url    :", resp["url"])
            print("  status :", resp["status"], resp.get("mimeType"))
            try:
                r = ws.call("Network.getResponseBody",
                            {"requestId": e["params"]["requestId"]}, timeout=20)
                body = r.get("result", {}).get("result", {}).get("body")
                print("  body   :", (body or "")[:700])
            except Exception as exc:
                print("  body   : could not read (%s)" % exc)

    print("\n--- failed requests ---")
    fails = 0
    for e in EVENTS:
        if e.get("method") == "Network.loadingFailed":
            fails += 1
            if fails <= 10:
                print("  ", e["params"].get("errorText"), e["params"].get("type"))
        if e.get("method") == "Network.responseReceived":
            resp = e["params"]["response"]
            if resp.get("status", 200) >= 400:
                fails += 1
                if fails <= 10:
                    print("  HTTP", resp["status"], resp["url"][:140])
    print("total failures:", fails)
finally:
    proc.terminate()
