"""
Test de bout en bout via HTTP (comme un vrai navigateur), sur une base neuve.
  python3 tests/http_test.py http://127.0.0.1:8100
"""
import concurrent.futures as cf
import datetime as dt
import html
import re
import sys
import urllib.parse
import urllib.request
from http.cookiejar import CookieJar

BASE = sys.argv[1].rstrip("/")
step = 0


def ok(msg):
    global step
    step += 1
    print(f"  ✓ {step}. {msg}")


class Client:
    def __init__(self):
        self.jar = CookieJar()
        self.op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self.jar))

    def get(self, path):
        r = self.op.open(BASE + path)
        return r.status, r.read().decode()

    def post(self, path, data, form_page=None):
        page = self.get(form_page or path)[1]
        csrf = re.search(r'name="csrf" value="([^"]+)"', page).group(1)
        body = urllib.parse.urlencode({"csrf": csrf, **data}, doseq=True).encode()
        r = self.op.open(BASE + path, body)
        return r.geturl(), r.read().decode()


def flash(page):
    m = re.search(r'class="flash flash-(ok|error)"[^>]*>\s*(.*?)\s*(<code|</div>)', page, re.S)
    return (m.group(1), html.unescape(m.group(2)).strip()) if m else (None, "")


def secret(page):
    m = re.search(r'<code class="secret">([^<]+)</code>', page)
    return m.group(1) if m else None


def expect(page, kind, contains=""):
    k, msg = flash(page)
    assert k == kind and contains in msg, f"attendu {kind} « {contains} », obtenu {k} « {msg} »"


def future(minutes):
    return (dt.datetime.now() + dt.timedelta(minutes=minutes)).strftime("%Y-%m-%dT%H:%M")


def balance(c):
    page = c.get("/portefeuille.php")[1]
    return int(re.search(r'class="big num accent"[^>]*>([\d  ]+)', page).group(1).replace(" ", "").replace(" ", ""))


print("\n— Installation et comptes —")
admin = Client()
url, page = admin.post("/installation.php", {"username": "timeo", "password": "motdepasse-solide", "confirm": "motdepasse-solide"})
expect(page, "ok", "Site installé")
assert admin.get("/installation.php")[0] == 200 and "Connexion" not in admin.get("/")[1][:0]
ok("Installation : compte admin créé et connecté")

intrus = Client()
st, page = intrus.get("/installation.php")
assert "Créer le compte admin" not in page
ok("La page d'installation disparaît une fois le site installé")

players = {}
for name in ["alice", "bob", "carl"]:
    _, page = admin.post("/admin/joueurs.php", {"action": "invite"})
    code = secret(page)
    c = Client()
    _, page = c.post("/inscription.php", {"code": code, "username": name, "password": "motdepasse-solide", "confirm": "motdepasse-solide"})
    expect(page, "ok", "Bienvenue")
    players[name] = c
_, page = Client().post("/inscription.php", {"code": code, "username": "eve", "password": "motdepasse-solide", "confirm": "motdepasse-solide"})
expect(page, "error", "déjà utilisé")
ok("Invitations : 3 joueurs inscrits, code réutilisé refusé")

bad = Client()
for _ in range(5):
    _, page = bad.post("/connexion.php", {"username": "alice", "password": "mauvais"})
    expect(page, "error", "Identifiants incorrects")
_, page = bad.post("/connexion.php", {"username": "alice", "password": "motdepasse-solide"})
expect(page, "error", "Trop de tentatives")
ok("Connexion : 5 échecs puis blocage, même avec le bon mot de passe")

print("\n— Cycle d'un pari —")
alice, bob, carl = players["alice"], players["bob"], players["carl"]
url, page = alice.post("/proposer.php", {"title": "À quelle heure Bob s'endort ?", "options": "Avant minuit\nAprès minuit",
                                          "criteria": "Dernier message sur le groupe", "subject_id": "", "closes_at": "", "description": ""})
expect(page, "ok", "Proposition envoyée")
mid = int(re.search(r"id=(\d+)", url).group(1))
admin_page = admin.get("/admin/")[1]
ids = dict((html.unescape(n), v) for v, n in re.findall(r'<option value="(\d+)"[^>]*>([^<]+)</option>', admin_page))
_, page = admin.post("/admin/", {"market_id": mid, "title": "À quelle heure Bob s'endort ?", "options": "Avant minuit\nAprès minuit",
                                 "criteria": "Dernier message", "arbiter_id": ids["carl"], "subject_id": ids["bob"],
                                 "closes_at": future(2), "max_stake": "300"})
expect(page, "ok", "ouvert aux mises")
ok("Proposition par un joueur, validation par l'admin (arbitre carl, sujet bob)")

page = alice.get(f"/pari.php?id={mid}")[1]
opts = re.findall(r'name="option_id" value="(\d+)"', page)
_, page = alice.post(f"/pari.php?id={mid}", {"action": "bet", "option_id": opts[0], "amount": "100"})
expect(page, "ok", "Mise de 100")
_, page = admin.post(f"/pari.php?id={mid}", {"action": "bet", "option_id": opts[1], "amount": "100"})
expect(page, "ok")
assert "tu ne peux pas miser" in bob.get(f"/pari.php?id={mid}")[1]
_, page = bob.post(f"/pari.php?id={mid}", {"action": "bet", "option_id": opts[0], "amount": "10"}, form_page="/proposer.php")
expect(page, "error", "sujet")
_, page = carl.post(f"/pari.php?id={mid}", {"action": "bet", "option_id": opts[0], "amount": "10"}, form_page="/proposer.php")
expect(page, "error", "arbitre")
ok("Mises OK ; le sujet et l'arbitre sont bloqués côté serveur (même en forçant le formulaire)")

# Formulaire sans jeton CSRF → refusé
r = alice.op.open(BASE + f"/pari.php?id={mid}", urllib.parse.urlencode({"action": "bet", "option_id": opts[0], "amount": "5"}).encode())
expect(r.read().decode(), "error", "Formulaire expiré")
ok("Requête sans jeton CSRF refusée")

print("\n— Mises simultanées —")
big_url, page = admin.post("/proposer.php", {"title": "Test de concurrence", "options": "Oui\nNon", "criteria": "Constat de test",
                                             "subject_id": "", "closes_at": "", "description": ""})
big = int(re.search(r"id=(\d+)", big_url).group(1))
admin.post("/admin/", {"market_id": big, "title": "Test de concurrence", "options": "Oui\nNon", "criteria": "Constat de test",
                       "arbiter_id": ids["carl"], "subject_id": "", "closes_at": future(60), "max_stake": "100000"})
page = bob.get(f"/pari.php?id={big}")[1]
csrf = re.search(r'name="csrf" value="([^"]+)"', page).group(1)
opt = re.search(r'name="option_id" value="(\d+)"', page).group(1)
before = balance(bob)


def bet(_):
    body = urllib.parse.urlencode({"csrf": csrf, "action": "bet", "option_id": opt, "amount": "150"}).encode()
    return flash(bob.op.open(BASE + f"/pari.php?id={big}", body).read().decode())


with cf.ThreadPoolExecutor(20) as ex:
    list(ex.map(bet, range(20)))
# On compte les mises réellement enregistrées (les messages « flash » partagent le même cookie
# entre les 20 requêtes parallèles, ils ne sont donc pas fiables pour compter).
accepted = bob.get(f"/pari.php?id={big}")[1].count('accent">bob</strong>')
after = balance(bob)
assert accepted == before // 150 and after == before - 150 * accepted and after >= 0, (accepted, before, after)
ok(f"20 mises envoyées en même temps : {accepted} acceptées, solde {before} → {after}, jamais négatif")

print("\n— Pages —")
for path in ["/", "/grilles.php", "/proposer.php", "/classement.php", "/portefeuille.php", f"/pari.php?id={mid}",
             "/admin/", "/admin/joueurs.php", "/admin/grille.php"]:
    st, page = admin.get(path)
    assert st == 200 and "Fatal" not in page and "Warning" not in page, path
st, page = alice.get("/admin/")
assert "Tableau de bord" not in page
assert "Comptes équilibrés" in admin.get("/admin/")[1]
ok("Toutes les pages s'affichent, l'admin est inaccessible aux joueurs, comptes équilibrés")

print(f"\n✅ {step} vérifications HTTP OK\n")
