#!/usr/bin/env python3
"""Run `index.html` in a headless Firefox and report what mocha said.

Standard library only, on purpose. WebDriver is a REST protocol over HTTP, so a
runner for it needs no package -- start geckodriver, POST a session, navigate,
read the title mocha writes. Adding a client library here would put a
`requirements.txt` beside a suite whose whole argument is that it needs no
package manager.

Firefox rather than Chrome, and that is not a preference. Google publishes
`chrome-for-testing` for linux64 and no Linux arm64 build at all, and astra is
aarch64; the Chromium in apt there is the snap shim, which cuts against taking
snaps off that machine. Firefox plus geckodriver runs on both boxes, so the
suite has one story rather than two.

Usage:

    python3 crates/quasi-webview/tests/browser/run.py

Needs `firefox` on PATH and `geckodriver` on PATH or in `~/.local/bin`. Exits 0
when every test passed, 1 when any failed, and 2 when the run could not happen
-- a missing driver is not a red suite.
"""

import json
import pathlib
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request

PORT = 4444
BASE = f"http://127.0.0.1:{PORT}"
PAGE = pathlib.Path(__file__).resolve().parent / "index.html"


def driver():
    """geckodriver, wherever this machine keeps it."""
    found = shutil.which("geckodriver")
    if found:
        return found
    local = pathlib.Path.home() / ".local" / "bin" / "geckodriver"
    if local.exists():
        return str(local)
    sys.exit(
        "geckodriver not found. Put it on PATH or in ~/.local/bin:\n"
        "  https://github.com/mozilla/geckodriver/releases"
    )


def call(method, path, body=None):
    data = json.dumps(body).encode() if body is not None else None
    request = urllib.request.Request(
        BASE + path,
        data=data,
        method=method,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(request, timeout=60) as answer:
        return json.loads(answer.read() or b"{}")


def listening():
    """Wait for the driver to answer, rather than sleeping a guessed amount."""
    for _ in range(80):
        try:
            urllib.request.urlopen(BASE + "/status", timeout=2)
            return True
        except Exception:
            time.sleep(0.25)
    return False


def verdict(session):
    """Poll the title mocha writes when the run ends."""
    for _ in range(120):
        said = call("GET", f"/session/{session}/title")["value"]
        if said.startswith(("PASS", "FAIL")):
            return said
        time.sleep(0.25)
    return "TIMEOUT"


def main():
    if not shutil.which("firefox"):
        sys.exit("firefox not found on PATH.")

    gecko = subprocess.Popen(
        [driver(), "--port", str(PORT), "--log", "error"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL,
    )
    try:
        if not listening():
            sys.exit(f"geckodriver did not start listening on {PORT}.")

        opened = call(
            "POST",
            "/session",
            {
                "capabilities": {
                    "alwaysMatch": {
                        "browserName": "firefox",
                        "moz:firefoxOptions": {"args": ["-headless"]},
                    }
                }
            },
        )["value"]
        session = opened["sessionId"]
        print(f"firefox {opened['capabilities'].get('browserVersion')}, headless")

        try:
            call("POST", f"/session/{session}/url", {"url": PAGE.as_uri()})
            said = verdict(session)
            report = call(
                "POST",
                f"/session/{session}/execute/sync",
                {
                    "script": "return document.getElementById('mocha').innerText;",
                    "args": [],
                },
            )["value"]
        finally:
            call("DELETE", f"/session/{session}")
    finally:
        gecko.terminate()

    print(said)
    if not said.startswith("PASS"):
        # The rendered report, which is where a failure says what it wanted.
        print("\n".join(line for line in report.splitlines() if line.strip()))
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
