"""Hosted payment page checks over HTTP (redirects, OTP, once-only status changes, expired link).

Configuration comes from environment variables (see README.md in this folder):
  TABBY_SIM_BASE   simulator base URL            (default http://localhost:4030/tabby-sim)
  TABBY_SIM_KEY    Authorization header value    (default "Bearer sk_test_000000000", the simulator's test key)
"""
import os
import sys

import json, re, urllib.request, urllib.error, urllib.parse, uuid

BASE = os.environ.get("TABBY_SIM_BASE", "http://localhost:4030/tabby-sim")
KEY = os.environ.get("TABBY_SIM_KEY", "Bearer sk_test_000000000")


class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *a, **k):
        return None


opener = urllib.request.build_opener(NoRedirect)


def call(method, path, body=None, form=None, auth=False):
    data, headers = None, {}
    if body is not None:
        data, headers = json.dumps(body).encode(), {"content-type": "application/json"}
    if form is not None:
        data = urllib.parse.urlencode(form).encode()
    if auth:
        headers["Authorization"] = KEY
    req = urllib.request.Request(BASE + path, data=data, method=method, headers=headers)
    try:
        r = opener.open(req, timeout=15)
    except urllib.error.HTTPError as e:
        r = e
    return r.status if hasattr(r, "status") else r.code, dict(r.headers), r.read().decode()


def checkout(phone="500000001", email="otp.success@tabby.ai", urls="default"):
    if urls == "default":
        urls = {"success": "https://example.com/ok", "cancel": "https://example.com/cancel", "failure": "https://example.com/fail"}
    body = {"merchant_code": "ae", "payment": {"amount": "100.00", "currency": "AED",
            "buyer": {"phone": phone, "email": email},
            "order": {"reference_id": "T-http", "items": [{"title": "Phone", "quantity": 1, "unit_price": "100.00"}]}}}
    if urls is not None:
        body["merchant_urls"] = urls
    status, _, text = call("POST", "/api/v2/checkout", body=body, auth=True)
    d = json.loads(text)
    return d["id"], d["payment"]["id"], d


results = []


def check(name, ok, detail):
    results.append(ok)
    print(("PASS  " if ok else "FAIL  ") + name + "  ->  " + detail)


def title(html):
    m = re.search(r"<h1>(.*?)</h1>", html)
    return m.group(1) if m else "(no h1)"


# A. cancel redirect (no OTP needed)
sid, pid, _ = checkout()
st, h, _ = call("POST", f"/hpp/{sid}", form={"action": "cancel"})
check("cancel redirects to the cancel url", st == 302 and h.get("location") == f"https://example.com/cancel?payment_id={pid}", f"{st} {h.get('location')}")

# B. reject redirect
sid, pid, _ = checkout()
st, h, _ = call("POST", f"/hpp/{sid}", form={"action": "reject"})
check("reject redirects to the failure url", st == 302 and h.get("location") == f"https://example.com/fail?payment_id={pid}", f"{st} {h.get('location')}")

# C. url that already has a query -> &
sid, pid, _ = checkout(urls={"success": "https://x.example/ok?order=7", "cancel": "", "failure": ""})
st, h, _ = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
check("existing query -> uses & ", h.get("location") == f"https://x.example/ok?order=7&payment_id={pid}", f"{st} {h.get('location')}")

# D. fragment is kept after the query
sid, pid, _ = checkout(urls={"success": "https://x.example/done#thanks", "cancel": "", "failure": ""})
st, h, _ = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
check("fragment kept after the query", h.get("location") == f"https://x.example/done?payment_id={pid}#thanks", f"{st} {h.get('location')}")

# E. javascript: url is never followed
sid, pid, _ = checkout(urls={"success": "javascript:alert(1)", "cancel": "", "failure": ""})
st, h, body = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
check("javascript: url -> no redirect, result page", st == 200 and "location" not in h and title(body) == "Payment authorised", f"{st} location={h.get('location')} h1={title(body)}")

# F. no merchant_urls at all: all three null in the checkout response, approve shows the result page
sid, pid, d = checkout(urls=None)
check("missing merchant_urls -> all three keys null", d["merchant_urls"] == {"success": None, "cancel": None, "failure": None}, json.dumps(d["merchant_urls"], sort_keys=True))
st, h, body = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
check("approve without a success url -> result page, no redirect", st == 200 and "location" not in h and title(body) == "Payment authorised", f"{st} h1={title(body)}")

# G. rejected session has no hpp
sid, pid, d = checkout(phone="0500000002")
st, _, body = call("GET", f"/hpp/{sid}")
check("rejected session: no web_url in response", d["configuration"]["available_products"] == {} and d["status"] == "rejected", f"available_products={d['configuration']['available_products']} status={d['status']}")
check("rejected session: GET hpp -> 404", st == 404 and title(body) == "Checkout not found", f"{st} h1={title(body)}")

# H. unknown action
sid, pid, _ = checkout()
st, _, body = call("POST", f"/hpp/{sid}", form={"action": "nope"})
check("unknown action -> form with error, 200", st == 200 and "Unknown action." in body, f"{st} {'Unknown action.' in body}")
st, _, body = call("GET", f"/hpp/{sid}")
check("...and the payment is still open", st == 200 and 'name="otp"' in body, f"{st}")

# I. approving twice: second one is 'Already processed'
sid, pid, _ = checkout()
st1, h1, _ = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
st2, h2, body2 = call("POST", f"/hpp/{sid}", form={"action": "approve", "otp": "8888"})
check("second approve -> Already processed, no redirect", st1 == 302 and st2 == 200 and "location" not in h2 and title(body2) == "Already processed", f"first={st1} second={st2} h1={title(body2)}")
st3, _, body3 = call("POST", f"/hpp/{sid}", form={"action": "reject"})
check("reject after approve -> Already processed (never backwards)", st3 == 200 and "already authorized" in body3, f"{st3} {'already authorized' in body3}")

# J. close-tab button with the wrong OTP changes nothing
sid, pid, _ = checkout()
st, h, body = call("POST", f"/hpp/{sid}", form={"action": "approve_no_redirect", "otp": "1"})
check("approve_no_redirect + wrong OTP -> error, still open", st == 200 and "Invalid OTP" in body, f"{st}")

# K. unknown / malformed session ids
st, _, body = call("GET", f"/hpp/{uuid.uuid4()}")
check("random session id GET -> 404", st == 404, f"{st}")
st, _, body = call("POST", f"/hpp/{uuid.uuid4()}", form={"action": "approve", "otp": "8888"})
check("random session id POST -> 404", st == 404, f"{st}")
st, _, body = call("GET", "/hpp/not-a-uuid")
check("non-uuid session id GET -> 404", st == 404, f"{st}")

# L. html escaping end to end
body = {"merchant_code": "ae", "payment": {"amount": "5", "currency": "SAR", "description": "<script>alert(1)</script>",
        "buyer": {"phone": "500000001", "email": "a@b.c"}, "order": {"items": [{"title": "<b>x</b>", "quantity": 1, "unit_price": "5"}]}}}
_, _, text = call("POST", "/api/v2/checkout", body=body, auth=True)
sid = json.loads(text)["id"]
st, _, html = call("GET", f"/hpp/{sid}")
check("html is escaped on the live page", st == 200 and "<script>alert(1)</script>" not in html and "&lt;script&gt;" in html, f"{st}")

print(f"\n{sum(results)}/{len(results)} passed")
