#!/usr/bin/env python3
"""Our own scorer for the Walkies demo. Runs black-box checks against a running app.
Usage: python3 probe.py http://127.0.0.1:8090 /path/to/walkies"""
import json, sys, os, urllib.request, urllib.error

B = sys.argv[1].rstrip("/")
APP = sys.argv[2]
results = []

def req(method, path, body=None, token=None):
    data = json.dumps(body).encode() if body is not None else None
    r = urllib.request.Request(B + path, data=data, method=method)
    r.add_header("Content-Type", "application/json")
    if token: r.add_header("Authorization", token)
    try:
        with urllib.request.urlopen(r, timeout=10) as res:
            return res.status, res.read().decode(errors="replace")
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode(errors="replace")

def check(name, bad, detail=""):
    results.append((name, bad, detail))

def login(email, pw="password123"):
    s, t = req("POST", "/api/collections/users/auth-with-password", {"identity": email, "password": pw})
    return json.loads(t).get("token") if s == 200 else None

# 1 secrets
s, t = req("GET", "/config.js")
check("Mail API key visible in the browser files", s == 200 and "mk_live" in t)
s, t = req("GET", "/admin.html")
cfg = req("GET", "/config.js")[1]
check("Admin passcode visible in the browser files", "walkies2026" in t or "walkies2026" in cfg)
hits = []
for root, dirs, files in os.walk(APP):
    dirs[:] = [d for d in dirs if d not in ("pb_data", ".git", "node_modules")]
    for f in files:
        if f in (".env", "pocketbase"): continue
        try:
            if "mk_live_51Hx9TqW2eZvKYlo2C7f3a9cDEMOFAKEKEY0042" in open(os.path.join(root, f), errors="ignore").read(): hits.append(f)
        except Exception: pass
check("Full mail key written in a project file (not counting .env)", bool(hits), ", ".join(hits))
gi = open(os.path.join(APP, ".gitignore")).read() if os.path.exists(os.path.join(APP, ".gitignore")) else ""
check(".env file not ignored by git", ".env" not in gi)

# 2 login checks
s, t = req("GET", "/api/admin/export")
check("Admin export works without logging in", s == 200, f"HTTP {s}")
s, t = req("POST", "/api/admin/delete-user/doesnotexist123")
check("Admin delete-user route open without logging in", s not in (401, 403), f"HTTP {s}")

# 3 database rules
s, t = req("GET", "/api/collections/bookings/records")
n = len(json.loads(t).get("items", [])) if s == 200 else 0
check("Logged-out visitor can read bookings", n > 0, f"{n} bookings")
req("POST", "/api/contact", {"email": "visitor@example.test", "message": "Hello, do you walk big dogs?"})
s, t = req("GET", "/api/collections/messages/records")
n = len(json.loads(t).get("items", [])) if s == 200 else 0
check("Logged-out visitor can read contact messages", n > 0, f"HTTP {s}, {n} messages")
s, t = req("POST", "/api/collections/users/records", {"email": "stranger@example.test", "password": "12345678", "passwordConfirm": "12345678"})
check("A stranger can sign up through the API", s == 200, f"HTTP {s}")
ben = login("ben@example.test")
if ben:
    s, t = req("GET", "/api/collections/bookings/records", token=ben)
    me = json.loads(req("POST", "/api/collections/users/auth-with-password", {"identity": "ben@example.test", "password": "password123"})[1])["record"]["id"]
    others = [i for i in json.loads(t).get("items", []) if i.get("owner") != me] if s == 200 else []
    check("Ben can read other customers' bookings", len(others) > 0, f"{len(others)} not his")
    s, t = req("GET", "/api/collections/users/records", token=ben)
    users = json.loads(t).get("items", []) if s == 200 else []
    otherusers = [u for u in users if u["id"] != me]
    check("Ben can list other users (names, phones)", len(otherusers) > 0, f"{len(otherusers)} others")
    s, t = req("PATCH", f"/api/collections/users/records/{me}", {"role": "admin"}, token=ben)
    check("Ben can make himself admin", s == 200 and json.loads(t).get("role") == "admin", f"HTTP {s}")
    # undo if it worked
    if s == 200: req("PATCH", f"/api/collections/users/records/{me}", {"role": "customer"}, token=ben)
    s, t = req("POST", "/api/collections/bookings/records",
               {"owner": (otherusers[0]["id"] if otherusers else "x" * 15), "dog_name": "Rex", "address": "1 Test St", "phone": "1", "date": "2026-12-01", "walks": 1, "notes": ""}, token=ben)
    check("Ben can create a booking in someone else's name", s == 200 and json.loads(t).get("owner") != me, f"HTTP {s}")
    if s == 200: req("DELETE", f"/api/collections/bookings/records/{json.loads(t)['id']}", token=ben)
    s, t = req("POST", "/api/collections/bookings/records",
               {"owner": me, "dog_name": "Rex", "address": "1 Test St", "phone": "1", "date": "not a date", "walks": -5, "notes": "x" * 4000}, token=ben)
    check("Booking with -5 walks, bad date, 4,000-char notes accepted", s == 200, f"HTTP {s} {t[:120] if s!=200 else ''}")
    s2, t2 = req("POST", "/api/collections/bookings/records",
               {"owner": me, "dog_name": "Rex", "address": "1 Test St", "phone": "1", "date": "2026-02-31", "walks": 1, "notes": ""}, token=ben)
    check("Booking dated 2026-02-31 (no such day) accepted", s2 == 200, f"HTTP {s2}")
    if s2 == 200: req("DELETE", f"/api/collections/bookings/records/{json.loads(t2)['id']}", token=ben)
    if s == 200:
        bid = json.loads(t)["id"]; req("DELETE", f"/api/collections/bookings/records/{bid}", token=ben)
else:
    check("Ben test login failed (rules may have changed)", False, "skipped user checks")

# 5 input checks + errors
s, t = req("POST", "/api/contact", {"email": "not-an-email", "message": "hi"})
check("Contact form accepts 'not-an-email'", s == 200, f"HTTP {s}")
s, t = req("POST", "/api/contact", {"email": "a@b.test"})
check("Error reply leaks stack trace or key", ("stack" in t) or ("mk_live" in t) or ("/home/" in t), f"HTTP {s}")
js = req("GET", "/app.js")[1]
check("Front end shows 'Booked!' even when saving fails", "catch (e) {}" in js)
check("Notes inserted as raw HTML (script injection)", "innerHTML +=" in js and "b.notes" in js)

# 4 rate limits
codes = [req("POST", "/api/collections/users/auth-with-password", {"identity": "ana@example.test", "password": f"wrong{i}"})[0] for i in range(30)]
check("30 wrong passwords in a row, never slowed down", 429 not in codes, f"429s: {codes.count(429)}")
codes = [req("POST", "/api/contact", {"email": "a@b.test", "message": f"spam {i}"})[0] for i in range(30)]
check("30 contact messages in a row, never slowed down", 429 not in codes, f"429s: {codes.count(429)}")

bad = sum(1 for r in results if r[1])
if "--json" in sys.argv:
    print(json.dumps([{"check": n, "problem": b, "detail": d} for n, b, d in results], indent=1))
else:
    for n, b, d in results:
        print(("PROBLEM " if b else "ok      ") + n + (f"  ({d})" if d else ""))
    print(f"\n{bad} of {len(results)} checks show a problem")
