#!/usr/bin/env python3
"""SECURITE des inscriptions (corrige le 18/10/2026) : POST /api/parcours-inscription et POST /api/candidatures retrouvaient n'importe quel compte par son adresse e-mail (administrateur compris),
reecrivaient son profil (candidatures) et OUVRAIENT UNE SESSION a sa place. Maintenant : adresse connue = 409 « account_exists », ni session, ni modification, sauf pour le proprietaire deja connecte ;
adresse inconnue = compte prospect cree et connecte comme avant ; limite de debit."""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lib import call, mk_user, ok, reset_rate_limits, sql, summary

def section(t): print('\n■ ' + t); reset_rate_limits()
def prof(uid): return sql(f"SELECT CONCAT_WS('|', firstname, lastname, marque, craft, role) FROM users WHERE id = '{uid}'")
def tokens(uid): return int(sql(f"SELECT COUNT(*) FROM refresh_tokens WHERE user_id = '{uid}'"))
def session_cookie(h): return any('uluana_session' in v or 'uluana_refresh' in v for k, v in h.items() if k.lower() == 'set-cookie')
def wipe():
    e = "'sg\\_%@example.test'"; p = "'sg\\_%'"
    sql(f"DELETE FROM refresh_tokens WHERE user_id COLLATE utf8mb4_unicode_ci IN (SELECT id FROM users WHERE id LIKE {p} OR email LIKE {e} OR email LIKE 'sg\\_%@test.test')")
    sql(f"DELETE FROM candidatures WHERE email LIKE {e} OR email LIKE 'sg\\_%@test.test'; DELETE FROM user_parcours WHERE user_id IN (SELECT id FROM users WHERE id LIKE {p} OR email LIKE {e}); DELETE FROM artisan_gallery WHERE user_id IN (SELECT id FROM users WHERE id LIKE {p} OR email LIKE {e})")
    sql(f"DELETE FROM notification_prefs WHERE user_id IN (SELECT id FROM users WHERE id LIKE {p} OR email LIKE {e}); DELETE FROM users WHERE id LIKE {p} OR email LIKE {e}")
INS = lambda who, email, name='Pirate Hack', par='PAR-01': call('POST', who, '/parcours-inscription', {'name': name, 'email': email, 'marque': 'Marque pirate', 'craft': 'Metier pirate', 'parcours_id': par})
CAND = lambda who, email, name='Pirate Hack': call('POST', who, '/candidatures', {'name': name, 'email': email, 'marque': 'Marque pirate', 'craft': 'Metier pirate', 'need': 'un besoin', 'ca': '10000', 'plan': 'essentiel'})

wipe()
for uid, role in (('sg_admin', 'admin'), ('sg_pros', 'prospect'), ('sg_cli', 'client_essentiel'), ('sg_resp', 'responsable')): mk_user(uid, role, 'Prenom', 'Origine')
sql("UPDATE users SET marque = 'Marque origine', craft = 'Metier origine' WHERE id LIKE 'sg\\_%'")
BEFORE = {u: prof(u) for u in ('sg_admin', 'sg_pros', 'sg_cli', 'sg_resp')}; T0 = {u: tokens(u) for u in BEFORE}

section("Inscription à un parcours : une adresse connue n'ouvre plus de session")
for uid, label in (('sg_admin', 'un administrateur'), ('sg_resp', 'un responsable'), ('sg_cli', 'un client'), ('sg_pros', 'un prospect')):
    st, d, h = INS(None, f'{uid}@test.test')
    ok(st == 409 and d.get('code') == 'account_exists' and 'token' not in d and 'user' not in d and not session_cookie(h), f'adresse d\'{label} sans être connecté : 409, AUCUN jeton, AUCUN compte renvoyé, AUCUN cookie de session')
    ok(prof(uid) == BEFORE[uid] and tokens(uid) == T0[uid] and int(sql(f"SELECT COUNT(*) FROM user_parcours WHERE user_id = '{uid}'")) == 0, f'le compte d\'{label} est INTACT : profil identique, aucune session créée, aucun parcours assigné')
for variante in ('SG_ADMIN@TEST.TEST', '  sg_admin@test.test  ', 'Sg_Admin@Test.Test'):
    st, d, h = INS(None, variante); ok(st == 409 and 'token' not in d and not session_cookie(h), f'variante de casse ou d\'espaces ({variante!r}) : même refus')
st, d, h = INS('sg_cli', 'sg_admin@test.test'); ok(st == 409 and 'token' not in d and not session_cookie(h) and prof('sg_admin') == BEFORE['sg_admin'], 'connecté comme un AUTRE compte : refusé aussi (on ne s\'inscrit pas à la place de quelqu\'un d\'autre)')
st, d, h = INS('sg_pros', 'sg_pros@test.test', name='Nouveau Nom'); ok(st == 201 and d['user']['email'] == 'sg_pros@test.test' and 'token' not in d and not session_cookie(h) and d['parcours_id'] == 'PAR-01', 'le PROPRIÉTAIRE déjà connecté s\'inscrit avec sa propre adresse : 201, son compte renvoyé, pas de nouvelle session')
ok(int(sql("SELECT COUNT(*) FROM user_parcours WHERE user_id = 'sg_pros' AND parcours_id = 'PAR-01'")) == 1 and tokens('sg_pros') == T0['sg_pros'], 'et le parcours est bien assigné, sans session créée')
st, d, h = INS(None, 'sg_new1@example.test', name='Nina Neuve'); ok(st == 201 and d.get('token') and d['user']['email'] == 'sg_new1@example.test' and d['user']['role'] == 'prospect' and session_cookie(h), 'adresse INCONNUE : comportement inchangé : compte prospect créé, connecté (jeton + cookie), parcours assigné')
nid = d['user']['id']; ok(sql(f"SELECT role FROM users WHERE id = '{nid}'") == 'prospect' and tokens(nid) == 1 and int(sql(f"SELECT COUNT(*) FROM user_parcours WHERE user_id = '{nid}'")) == 1, 'le nouveau compte est un prospect avec une seule session')
st, d2, h2 = INS(None, 'sg_new1@example.test'); ok(st == 409 and 'token' not in d2 and not session_cookie(h2), 'et la même adresse, une seconde fois, sans être connecté : désormais refusée (c\'est maintenant une adresse connue)')

section("Candidature : même protection, et le profil n'est plus réécrit")
for uid, label in (('sg_admin', 'un administrateur'), ('sg_cli', 'un client')):
    st, d, h = CAND(None, f'{uid}@test.test'); ok(st == 409 and d.get('code') == 'account_exists' and 'token' not in d and 'user' not in d and not session_cookie(h), f'candidature avec l\'adresse d\'{label}, sans être connecté : 409, aucun jeton, aucun cookie')
    ok(prof(uid) == BEFORE[uid] and tokens(uid) == T0[uid] and int(sql(f"SELECT COUNT(*) FROM candidatures WHERE email = '{uid}@test.test'")) == 0, f'le profil d\'{label} n\'est PAS réécrit (prénom, nom, marque, métier identiques), aucune session, aucune candidature enregistrée')
st, d, h = CAND('sg_pros', 'sg_admin@test.test'); ok(st == 409 and prof('sg_admin') == BEFORE['sg_admin'], 'connecté comme un autre compte : refusé, profil de l\'administrateur intact')
st, d, h = CAND('sg_cli', 'sg_cli@test.test', name='Client Renomme'); ok(st == 201 and d['user']['email'] == 'sg_cli@test.test' and 'token' not in d and not session_cookie(h) and int(sql("SELECT COUNT(*) FROM candidatures WHERE email = 'sg_cli@test.test'")) == 1, 'le PROPRIÉTAIRE connecté dépose sa candidature : 201, sans nouvelle session')
ok(sql("SELECT firstname FROM users WHERE id = 'sg_cli'") == 'Client', 'et son profil est mis à jour par lui-même, comme avant')
st, d, h = CAND(None, 'sg_new2@example.test', name='Olga Nouvelle'); ok(st == 201 and d.get('token') and d['user']['role'] == 'prospect' and session_cookie(h), 'adresse INCONNUE : candidature enregistrée, compte prospect créé et connecté, comme avant (le paiement de l\'acompte suit)')

section("Limite de débit")
reset_rate_limits(); codes = [INS(None, 'sg_admin@test.test')[0] for _ in range(22)]; ok(codes[:20] == [409] * 20 and 429 in codes[20:], f'inscription : 20 essais par heure et par adresse IP, puis 429 ({codes[19:22]})')
reset_rate_limits(); codes = [CAND(None, 'sg_admin@test.test')[0] for _ in range(22)]; ok(codes[:20] == [409] * 20 and 429 in codes[20:], f'candidature : 20 essais par heure et par adresse IP, puis 429 ({codes[19:22]})')
reset_rate_limits()
section("Contrôle du code : les autres émissions de session")
import re
src = {f: open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'api', f), encoding='utf-8').read() for f in ('parcours_inscription.php', 'candidatures.php')}
ok(all('current_user()' in s and 'account_exists' in s and '$isNewAccount' in s for s in src.values()) and all(len(re.findall(r'jwt_issue\(', s)) == 1 and re.search(r'if \(\$isNewAccount\) \{[^}]*jwt_issue', s) for s in src.values()), 'dans les deux fichiers : la garde « propriétaire connecté » existe et l\'unique émission de session est réservée à un compte tout juste créé')
wipe()
summary()
