#!/usr/bin/env python3
"""@mentions dans les publications et les commentaires (CDC n° 11) : suggestions, périmètre, liste noire, visibilité, validation, notifications,
modification, suppression."""
import json, os, sys
from urllib.parse import quote
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lib import call, mk_user, ok, reset_rate_limits, sql, summary

def setting(k, v): sql(f"INSERT INTO messaging_settings (setting_key, setting_value) VALUES ('{k}', '{json.dumps(v)}') ON DUPLICATE KEY UPDATE setting_value = '{json.dumps(v)}'")
def unset(k): sql(f"DELETE FROM messaging_settings WHERE setting_key = '{k}'")
def follow(a, b): sql(f"INSERT IGNORE INTO member_follows (follower_id, followed_id) VALUES ('{a}', '{b}')")
def section(t): print('\n■ ' + t); reset_rate_limits()
def sugg(who, qs=''): st, d, _ = call('GET', who, '/mention-suggestions' + qs); return st, d
def names(d): return [x['name'] for x in d['items']]
def ids(d): return [x['id'] for x in d['items']]
def post(who, body, mentions=None, vis='all'):
    p = {'body': body, 'theme_id': THEME, 'visibility': vis}
    if mentions is not None: p['mentions'] = mentions
    return call('POST', who, '/posts', p)
def comment(who, pid, body, mentions=None, parent=None):
    p = {'body': body}
    if mentions is not None: p['mentions'] = mentions
    if parent: p['parent_id'] = parent
    return call('POST', who, f'/posts/{pid}/comments', p)
def notifs(who, qs=''): return call('GET', who, '/notifications' + qs)[1]['items']
def mrows(): return sql("SELECT CONCAT(subject_type, ':', subject_id, ':', member_id) FROM mentions ORDER BY id").split('\n') if sql("SELECT COUNT(*) FROM mentions").strip() != '0' else []
def block(a, b): call('POST', a, '/blocks', {'user_id': b, 'block': True})

def wipe(prefix):
    """Supprime TOUT ce que les membres de test ont laissé : publications, commentaires, j'aime, mentions, notifications, abonnements."""
    p = f"'{prefix}\\_%'"
    sql(f"DELETE cl FROM comment_likes cl JOIN comments c ON c.id = cl.comment_id WHERE c.user_id LIKE {p} OR c.post_id IN (SELECT id FROM posts WHERE user_id LIKE {p})")
    sql(f"DELETE FROM mentions WHERE member_id LIKE {p} OR (subject_type = 'post' AND subject_id IN (SELECT id FROM posts WHERE user_id LIKE {p})) OR (subject_type = 'comment' AND subject_id IN (SELECT id FROM comments WHERE user_id LIKE {p}))")
    sql(f"DELETE FROM comments WHERE user_id LIKE {p} OR post_id IN (SELECT id FROM posts WHERE user_id LIKE {p})")
    for t in ('post_likes', 'post_saves', 'post_media'): sql(f"DELETE FROM {t} WHERE post_id IN (SELECT id FROM posts WHERE user_id LIKE {p})")
    sql(f"DELETE FROM post_likes WHERE user_id LIKE {p}; DELETE FROM post_saves WHERE user_id LIKE {p}; DELETE FROM posts WHERE user_id LIKE {p}")
    sql(f"DELETE FROM notifications WHERE user_id LIKE {p} OR actor_id LIKE {p}; DELETE FROM member_follows WHERE follower_id LIKE {p} OR followed_id LIKE {p}")

wipe('n'); sql("DELETE FROM users WHERE id LIKE 'n\\_%'")
for uid, first, last in [('n_a', 'Alice', 'Adam'), ('n_b', 'Bruno', 'Blanc'), ('n_c', 'Chloé', 'Carré'), ('n_d', 'Dan', 'Dupont'), ('n_e', 'Eve', 'Etienne'), ('n_m1', 'Marie', 'Dupont'), ('n_m2', 'Marie', 'Durand'), ('n_s', 'Sam', 'Seul'), ('n_i', 'Ines', 'Inactive')]: mk_user(uid, 'client_essentiel', first, last)
sql("UPDATE users SET craft = 'Potier' WHERE id = 'n_b'; UPDATE users SET craft = '' WHERE id <> 'n_b' AND id LIKE 'n\\_%'")
THEME = int(sql("SELECT id FROM post_themes WHERE active = 1 AND system_key IS NULL ORDER BY id LIMIT 1").strip())
sql("DELETE FROM mentions"); sql("DELETE FROM notifications WHERE actor_id LIKE 'n\\_%' OR user_id LIKE 'n\\_%'")
sql("DELETE FROM member_follows WHERE follower_id LIKE 'n\\_%' OR followed_id LIKE 'n\\_%'"); unset('mentions_max_per_content'); unset('network_definition')
for f in ('n_b', 'n_c', 'n_d', 'n_e', 'n_m1', 'n_m2', 'n_i'): follow('n_a', f)
sql("UPDATE users SET active = 0 WHERE id = 'n_i'")

section("Migration et réglage")
ok(sql("SHOW TABLES LIKE 'mentions'").strip() == 'mentions', 'la table mentions existe')
st, d, _ = call('GET', 'n_a', '/messaging-config'); ok(st == 200, 'la configuration publique se charge toujours')
mk_user('n_admin', 'admin', 'Adm', 'In'); st, d, _ = call('GET', 'n_admin', '/admin-messaging-settings'); S = {s['key']: s for s in d['settings']}
ok('mentions_max_per_content' in S and (S['mentions_max_per_content']['min'], S['mentions_max_per_content']['max'], S['mentions_max_per_content']['default']) == (1, 20, 10) and S['mentions_max_per_content']['group'] == 'feed', 'réglage /control « mentions_max_per_content » : entier de 1 à 20, 10 par défaut, groupe « fil »')

section("Suggestions : accès et périmètre")
ok(call('GET', None, '/mention-suggestions')[0] == 401 and call('POST', 'n_a', '/mention-suggestions', {})[0] == 405, 'visiteur : 401 ; autre méthode : 405')
st, d = sugg('n_a'); ok(st == 200 and set(names(d)) == {'Bruno B.', 'Chloé C.', 'Dan D.', 'Eve E.', 'Marie D.'} and len(d['items']) == 6 and all(set(x) == {'id', 'name', 'badge'} for x in d['items']), 'sans texte : les personnes SUIVIES (5 + les deux « Marie D. »), compte inactif exclu, soi-même exclu')
ok(d['items'][0]['id'] == 'n_b' and d['items'][0]['badge'] == 'Potier' and d['items'][1]['badge'] is None, 'chaque suggestion porte { id, nom « Prénom N. », badge = spécialité (ou vide) }')
ok(names(sugg('n_a', '?q=bru')[1]) == ['Bruno B.'] and names(sugg('n_a', '?q=BRU')[1]) == ['Bruno B.'], 'recherche par début de prénom, sans tenir compte de la casse')
ok(names(sugg('n_a', '?q=chlo')[1]) == ['Chloé C.'] and names(sugg('n_a', '?q=carre')[1]) == ['Chloé C.'] and names(sugg('n_a', '?q=' + quote('Carré'))[1]) == ['Chloé C.'], 'sans tenir compte des accents (« chlo » → Chloé ; « carre » et « Carré » → par le nom de famille)')
ok(names(sugg('n_a', '?q=marie')[1]) == ['Marie D.', 'Marie D.'] and names(sugg('n_a', '?q=marie%20durand')[1]) == ['Marie D.'] and sugg('n_a', '?q=marie%20durand')[1]['items'][0]['id'] == 'n_m2', 'prénom + nom : « marie durand » ne retient que la bonne Marie')
ok(sugg('n_a', '?q=%25')[1]['items'] == [] and sugg('n_a', '?q=_')[1]['items'] == [], 'les jokers « % » et « _ » sont des caractères ordinaires : aucune suggestion (ne ramènent pas tout le monde)')
ok(sugg('n_a', '?q=' + 'x' * 80)[0] == 200, 'un texte très long est tronqué, sans erreur')
ok('Sam S.' not in names(sugg('n_a')[1]) and sugg('n_a', '?q=sam')[1]['items'] == [], 'un inconnu (ni suivi, ni auteur du fil, ni participant) n\'est JAMAIS suggéré')
sp = post('n_s', 'Je publie enfin')[1]['post']['id']; ok('Sam S.' in names(sugg('n_a', '?q=sam')[1]), 'dès qu\'il publie (auteur du fil visible) : il devient suggéré')
sql("UPDATE users SET active = 1 WHERE id = 'n_i'"); ok('Ines I.' in names(sugg('n_a', '?q=ines')[1]), 'un compte réactivé est suggéré'); sql("UPDATE users SET active = 0 WHERE id = 'n_i'")
sql("UPDATE users SET account_status = 'suspended' WHERE id = 'n_e'"); ok('Eve E.' not in names(sugg('n_a')[1]), 'un compte suspendu n\'est pas suggéré'); sql("UPDATE users SET account_status = 'active' WHERE id = 'n_e'")
many = [f'n_x{i}' for i in range(12)]
for i, u in enumerate(many): mk_user(u, 'client_essentiel', 'Zed', 'Zz'); follow('n_a', u)
ok(len(sugg('n_a', '?q=zed')[1]['items']) == 8, 'au plus 8 suggestions')
sql("DELETE FROM users WHERE id LIKE 'n\\_x%'")

section("Suggestions : liste noire dans les DEUX sens")
block('n_a', 'n_b'); ok('Bruno B.' not in names(sugg('n_a')[1]), 'Alice a bloqué Bruno : il n\'est pas suggéré à Alice')
block('n_c', 'n_a'); ok('Chloé C.' not in names(sugg('n_a')[1]), 'Chloé a bloqué Alice : elle n\'est pas suggérée à Alice non plus')
ok('Alice A.' not in names(sugg('n_c')[1]) and 'Alice A.' not in names(sugg('n_b')[1]), 'et Alice n\'est suggérée ni à Chloé ni à Bruno')
call('POST', 'n_a', '/blocks', {'user_id': 'n_b', 'block': False}); call('POST', 'n_c', '/blocks', {'user_id': 'n_a', 'block': False}); ok({'Bruno B.', 'Chloé C.'} <= set(names(sugg('n_a')[1])), 'débloqués : de nouveau suggérés')

section("Suggestions : fil de discussion et visibilité")
pid = post('n_a', 'Une publication', vis='all')[1]['post']['id']; comment('n_s', pid, 'Bravo')
ok(names(sugg('n_e', f'?post={pid}&q=sam')[1]) == ['Sam S.'], 'sur un fil : les participants (ici un commentateur que personne ne suit) sont suggérés')
st, d = sugg('n_e', f'?post={pid}'); ok(d['items'][0]['id'] == 'n_a' and d['items'][1]['id'] == 'n_s', 'les participants du fil passent EN PREMIER (l\'auteur, puis les commentateurs)')
ok(sugg('n_e', '?post=999999')[0] == 404 and sugg('n_e', '?post=abc')[0] == 422 and sugg('n_e', '?post=0')[0] == 422, 'publication inconnue : 404 ; identifiant invalide : 422')
follow('n_a', 'n_b'); netp = post('n_a', 'Pour mon réseau', vis='network')[1]['post']['id']
sql("DELETE FROM member_follows WHERE follower_id = 'n_a' AND followed_id = 'n_c'")
ok(sugg('n_c', f'?post={netp}')[0] == 404, 'une publication « mon réseau » que le lecteur ne peut pas voir : 404 (jamais « refusée pour telle raison »)')
follow('n_b', 'n_c'); follow('n_b', 'n_d'); st, d = sugg('n_b', f'?post={netp}'); ok(st == 200 and 'Chloé C.' not in names(d) and 'Dan D.' in names(d), 'sur une publication « mon réseau » : seulement les personnes qui peuvent la VOIR (Chloé n\'est plus suivie par Alice : non suggérée)')
follow('n_a', 'n_c')
st, d = sugg('n_a', '?visibility=network&q=chlo'); ok(names(d) == ['Chloé C.'], 'nouvelle publication « mon réseau » : on suggère les personnes que l\'auteur suit')
sql("DELETE FROM member_follows WHERE follower_id = 'n_a' AND followed_id = 'n_c'"); ok(sugg('n_a', '?visibility=network&q=chlo')[1]['items'] == [] and names(sugg('n_a', '?visibility=all&q=chlo')[1]) == [] , 'Chloé n\'est plus suivie : ni suggérée en « mon réseau » ni (suivi retiré, sans publication) en « tous »'); follow('n_a', 'n_c')
ok(sugg('n_a', '?visibility=zzz&q=bru')[0] == 200, 'une visibilité inconnue vaut « tous »')

section("Publier avec des mentions")
reset_rate_limits(); n0 = len(notifs('n_d'))
st, d, _ = post('n_a', 'Salut @Dan D. et @Eve E. !', ['n_d', 'n_e'])
ok(st == 201 and [m['id'] for m in d['post']['mentions']] == ['n_d', 'n_e'] and [m['name'] for m in d['post']['mentions']] == ['Dan D.', 'Eve E.'], 'publier avec 2 mentions : 201, la publication renvoyée liste { id, nom }')
P1 = d['post']['id']; ok(mrows() == [f'post:{P1}:n_d', f'post:{P1}:n_e'], 'la table mentions dit qui est mentionné (par identifiant)')
nd = notifs('n_d'); ok(len(nd) == n0 + 1 and nd[0]['type'] == 'mention' and nd[0]['kind'] == 'post' and nd[0]['actor']['id'] == 'n_a' and nd[0]['post_id'] == P1, 'Dan reçoit une notification « mention » (kind post, auteur Alice, post_id) qui mène à la publication')
ok(any(n['type'] == 'mention' for n in notifs('n_e')) and not any(n['type'] == 'mention' for n in notifs('n_b')) and not any(n['type'] == 'mention' for n in notifs('n_a')), 'Eve aussi ; Bruno (non mentionné) et Alice (l\'auteur) : rien')
st, d, _ = call('GET', 'n_b', f'/posts/{P1}'); ok([m['name'] for m in d['post']['mentions']] == ['Dan D.', 'Eve E.'], 'un autre lecteur voit la liste des mentions pour les surligner')
st, d, _ = call('GET', 'n_b', '/posts'); ok(any(x['id'] == P1 and len(x['mentions']) == 2 for x in d['items']) and all('mentions' in x for x in d['items']), 'chaque publication du fil porte « mentions » (vide si aucune)')

section("Publier : refus (rien n'est enregistré)")
def refused(body, mentions, key, why, vis='all', who='n_a'):
    c0 = int(sql("SELECT COUNT(*) FROM posts")); st, d, _ = post(who, body, mentions, vis); reset_rate_limits()
    ok(st == 422 and key in d.get('errors', {}) and int(sql("SELECT COUNT(*) FROM posts")) == c0 and int(sql("SELECT COUNT(*) FROM mentions")) == len(mrows()), f'refusé (422, « {key} ») et rien d\'enregistré : {why}')
n_mrows = len(mrows())
refused('Salut Bruno', ['n_b'], 'mentions.0', 'le texte ne contient pas « @Bruno B. » (pas de mention cachée)')
refused('Salut @bruno b.', ['n_b'], 'mentions.0', 'la casse compte : « @bruno b. » n\'est pas « @Bruno B. »')
refused('Salut @Bruno', ['n_b'], 'mentions.0', 'le prénom seul ne suffit pas : il faut le nom affiché complet')
refused('Salut @Bruno B.x', ['n_b'], 'mentions.0', 'une lettre colle au nom : « @Bruno B.x » ne le mentionne pas')
refused('Salut @Alice A.', ['n_a'], 'mentions.0', 'on ne se mentionne pas soi-même')
refused('Salut @Ines I.', ['n_i'], 'mentions.0', 'un compte inactif ne peut pas être mentionné')
refused('Salut @Inconnu X.', ['n_inconnu'], 'mentions.0', 'identifiant inconnu')
refused('Salut @Bruno B. @Bruno B.', ['n_b', 'n_b'], 'mentions.1', 'le même membre deux fois')
refused('Salut @Marie D. et @Marie D.', ['n_m1', 'n_m2'], 'mentions.1', 'deux membres au même nom affiché (« Marie D. ») : on ne saurait pas les distinguer')
refused('Salut', 'n_b', 'mentions', 'une chaîne au lieu d\'une liste'); refused('Salut', {'a': 'n_b'}, 'mentions', 'un objet au lieu d\'une liste')
refused('Salut @Bruno B.', [5], 'mentions.0', 'un identifiant qui n\'est pas du texte'); refused('Salut @Bruno B.', [''], 'mentions.0', 'un identifiant vide')
mk_user('n_far', 'client_essentiel', 'Loin', 'Lointain'); refused('Salut @Loin L.', ['n_far'], 'mentions.0', 'un membre hors périmètre (ni suivi, ni auteur du fil, ni participant)')
block('n_a', 'n_b'); refused('Salut @Bruno B.', ['n_b'], 'mentions.0', 'un membre que j\'ai bloqué'); call('POST', 'n_a', '/blocks', {'user_id': 'n_b', 'block': False})
block('n_b', 'n_a'); refused('Salut @Bruno B.', ['n_b'], 'mentions.0', 'un membre qui m\'a bloqué (le blocage vaut dans les DEUX sens)'); call('POST', 'n_b', '/blocks', {'user_id': 'n_a', 'block': False})
setting('mentions_max_per_content', 2); refused('@Bruno B. @Chloé C. @Dan D.', ['n_b', 'n_c', 'n_d'], 'mentions', '3 mentions quand le réglage en autorise 2'); st, d, _ = post('n_a', '@Bruno B. @Chloé C.', ['n_b', 'n_c']); ok(st == 201, '2 mentions : accepté (le maximum réglé)'); unset('mentions_max_per_content'); reset_rate_limits()
many_ids = [f'n_q{i}' for i in range(11)]; body = ' '.join(f'@Qq{i} Q.' for i in range(11)); refused(body, many_ids, 'mentions', '11 mentions avec le réglage par défaut (10)')
ok(len(mrows()) >= n_mrows, 'aucune mention parasite après tous ces refus')

section("« Mon réseau » : le mentionné doit pouvoir VOIR la publication")
sql("DELETE FROM member_follows WHERE follower_id = 'n_a'"); follow('n_a', 'n_b'); follow('n_a', 'n_d'); sql("DELETE FROM member_follows WHERE follower_id = 'n_c'")
follow('n_s', 'n_a'); st, d, _ = post('n_a', 'Pour @Sam S. seulement', ['n_s'], 'network'); ok(st == 422, 'Sam n\'est pas suivi par Alice : il ne verrait pas une publication « mon réseau » : refusé')
reset_rate_limits(); st, d, _ = post('n_a', 'Pour @Bruno B. seulement', ['n_b'], 'network'); ok(st == 201 and d['post']['mentions'][0]['id'] == 'n_b', 'Bruno est suivi par Alice : il verra la publication, la mention est acceptée')
NP = d['post']['id']; ok(any(n['type'] == 'mention' and n['post_id'] == NP for n in notifs('n_b')), 'et il est notifié')

section("Commentaires avec mentions")
reset_rate_limits(); sql("DELETE FROM member_follows WHERE follower_id = 'n_a'"); [follow('n_a', x) for x in ('n_b', 'n_c', 'n_d', 'n_e')]; follow('n_c', 'n_d'); follow('n_b', 'n_d'); follow('n_b', 'n_c')
st, d, _ = post('n_a', 'Un fil pour discuter'); CP = d['post']['id']; comment('n_b', CP, 'Premier')
before = len(notifs('n_d')); st, d, _ = comment('n_c', CP, 'Regardez @Dan D. et @Alice A. !', ['n_d', 'n_a'])
ok(st == 201 and [m['name'] for m in d['comment']['mentions']] == ['Dan D.', 'Alice A.'], 'commenter avec 2 mentions (dont l\'auteur de la publication) : 201, le commentaire renvoyé les liste')
CM = d['comment']['id']; nd = notifs('n_d'); ok(len(nd) == before + 1 and nd[0]['type'] == 'mention' and nd[0]['kind'] == 'comment' and nd[0]['post_id'] == CP, 'Dan reçoit une notification « mention » de type commentaire, qui mène à la publication')
na = [n for n in notifs('n_a') if n['post_id'] == CP and n['actor'] and n['actor']['id'] == 'n_c']; ok(sorted(n['type'] for n in na) == ['mention'], 'Alice, auteur de la publication ET mentionnée : UNE seule notification (la mention), pas aussi « a commenté votre publication »')
st, d, _ = comment('n_c', CP, 'Sans mention'); ok(st == 201 and d['comment']['mentions'] == [] and any(n['type'] == 'comment' for n in notifs('n_a')), 'un commentaire sans mention : la notification « commentaire » habituelle part')
st, d, _ = comment('n_c', CP, 'Pour @Sam S.', ['n_s']); ok(st == 201 and d['comment']['mentions'][0]['id'] == 'n_s', 'un auteur du fil visible (Sam a publié) peut être mentionné même sans être suivi')
st, d, _ = comment('n_c', CP, 'Pour @Loin L.', ['n_far']); ok(st == 422 and 'mentions.0' in d['errors'], 'un membre hors périmètre : refusé'); ok(int(sql(f"SELECT COUNT(*) FROM comments WHERE post_id = {CP}")) == 4, 'et le commentaire refusé n\'est pas enregistré')
block('n_c', 'n_d'); st, d, _ = comment('n_c', CP, 'Pour @Dan D.', ['n_d']); ok(st == 422 and 'mentions.0' in d['errors'], 'sur la publication d\'un TIERS : on ne mentionne pas un membre qu\'on a bloqué'); call('POST', 'n_c', '/blocks', {'user_id': 'n_d', 'block': False})
block('n_d', 'n_c'); st, d, _ = comment('n_c', CP, 'Pour @Dan D.', ['n_d']); ok(st == 422 and 'mentions.0' in d['errors'], 'ni un membre qui nous a bloqué (dans les DEUX sens)'); call('POST', 'n_d', '/blocks', {'user_id': 'n_c', 'block': False})
st, d, _ = comment('n_c', CP, 'Pour @Dan D. de nouveau', ['n_d']); ok(st == 201, 'débloqués : la mention de Dan est de nouveau acceptée')
replies = lambda: len([n for n in notifs('n_b') if n['type'] == 'comment' and n['kind'] == 'reply' and n['post_id'] == CP]); r0 = replies()
st, d, _ = comment('n_c', CP, 'Pour @Bruno B.', ['n_b'], parent=int(sql(f"SELECT id FROM comments WHERE post_id = {CP} ORDER BY id LIMIT 1")))
ok(st == 201 and replies() == r0 and notifs('n_b')[0]['type'] == 'mention' and notifs('n_b')[0]['kind'] == 'comment', 'une réponse qui MENTIONNE l\'auteur du commentaire parent : une notification « mention », pas aussi « a répondu à votre commentaire »')
reset_rate_limits(); sql("DELETE FROM member_follows WHERE follower_id = 'n_a' AND followed_id = 'n_c'"); st, d, _ = call('POST', 'n_c', f'/posts/{netp}/comments', {'body': 'Pour @Dan D.', 'mentions': ['n_d']}); ok(st == 404, 'commenter (avec mention) une publication « mon réseau » qu\'on ne voit pas : 404'); follow('n_a', 'n_c')
pn = post('n_a', 'Réseau', vis='network')[1]['post']['id']; sql("DELETE FROM member_follows WHERE follower_id = 'n_a' AND followed_id = 'n_e'"); st, d, _ = comment('n_b', pn, 'Pour @Eve E.', ['n_e'])
ok(st in (404, 422) and (st == 404 or 'mentions.0' in d['errors']), 'sur une publication « mon réseau » : on ne mentionne pas quelqu\'un qui ne la verrait pas'); follow('n_a', 'n_e')

section("Modifier : seules les NOUVELLES mentions notifient")
reset_rate_limits(); st, d, _ = post('n_a', 'Bonjour @Bruno B.', ['n_b']); E1 = d['post']['id']; nb = lambda: len([n for n in notifs('n_b') if n['type'] == 'mention' and n['post_id'] == E1])
ok(nb() == 1, 'départ : Bruno a une notification')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour @Bruno B. et @Chloé C.', 'mentions': ['n_b', 'n_c']}); ok(st == 200 and [m['id'] for m in d['post']['mentions']] == ['n_b', 'n_c'], 'ajouter Chloé : 200, la liste est à jour')
ok(nb() == 1 and any(n['type'] == 'mention' and n['post_id'] == E1 for n in notifs('n_c')), 'Chloé est notifiée ; Bruno N\'est PAS renotifié')
call('POST', 'n_b', '/notifications', {'action': 'read_all'}); st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour @Bruno B. et @Chloé C. !!', 'mentions': ['n_b', 'n_c']}); ok(nb() == 1 and not [n for n in notifs('n_b') if not n['read']], 'renvoyer les mêmes mentions : personne n\'est renotifié')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour @Bruno B.', 'mentions': ['n_b']}); ok([m['id'] for m in d['post']['mentions']] == ['n_b'] and mrows().count(f'post:{E1}:n_c') == 0, 'retirer Chloé (liste sans elle) : sa mention disparaît')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour @Bruno B. encore'}); ok([m['id'] for m in d['post']['mentions']] == ['n_b'], 'modifier le texte SANS liste : la mention dont « @Nom » figure encore est conservée')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour tout le monde'}); ok(d['post']['mentions'] == [] and mrows().count(f'post:{E1}:n_b') == 0, 'modifier le texte SANS liste et sans « @Nom » : la mention est retirée')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'body': 'Bonjour @Dan D.', 'mentions': ['n_x']}); ok(st == 422 and 'mentions.0' in d['errors'], 'une mention invalide à la modification : 422')
st, d, _ = call('PUT', 'n_a', f'/posts/{E1}', {'mentions': ['n_b']}); ok(st == 422 and 'mentions.0' in d['errors'], 'liste seule, sans que le texte contienne le nom : refusée')
st, d, _ = post('n_a', 'Pour tous @Dan D.', ['n_d']); E2 = d['post']['id']; sql("DELETE FROM member_follows WHERE follower_id = 'n_a' AND followed_id = 'n_d'")
st, d, _ = call('PUT', 'n_a', f'/posts/{E2}', {'visibility': 'network'}); ok(st == 200 and d['post']['mentions'] == [], 'passer en « mon réseau » : une personne mentionnée qui ne verrait plus la publication est retirée (sans notification)'); follow('n_a', 'n_d')
st, d, _ = post('n_a', 'Pour tous @Dan D.', ['n_d']); E3 = d['post']['id']; st, d, _ = call('PUT', 'n_a', f'/posts/{E3}', {'visibility': 'network'}); ok(d['post']['mentions'][0]['id'] == 'n_d', 'passer en « mon réseau » : une personne suivie par l\'auteur reste mentionnée')
reset_rate_limits(); follow('n_b', 'n_d'); st, d, _ = comment('n_b', CP, 'Salut @Alice A.', ['n_a']); C1 = d['comment']['id']
st, d, _ = call('PUT', 'n_b', f'/posts/{CP}/comments/{C1}', {'body': 'Salut @Alice A. et @Dan D.', 'mentions': ['n_a', 'n_d']}); ok(st == 200 and [m['id'] for m in d['comment']['mentions']] == ['n_a', 'n_d'], 'modifier un commentaire en ajoutant une mention : 200')
st, d, _ = call('PUT', 'n_b', f'/posts/{CP}/comments/{C1}', {'body': 'Salut'}); ok(d['comment']['mentions'] == [], 'modifier un commentaire sans liste ni « @Nom » : mentions retirées')

section("Supprimer")
reset_rate_limits(); follow('n_b', 'n_c'); st, d, _ = post('n_a', 'À supprimer @Dan D.', ['n_d']); D1 = d['post']['id']; comment('n_b', D1, 'Et @Chloé C.', ['n_c']); DC = int(sql(f"SELECT id FROM comments WHERE post_id = {D1}"))
ok(f'post:{D1}:n_d' in mrows() and f'comment:{DC}:n_c' in mrows() and any(n['post_id'] == D1 for n in notifs('n_d')), 'départ : mention de publication, mention de commentaire, notification')
call('DELETE', 'n_b', f'/posts/{D1}/comments/{DC}'); ok(f'comment:{DC}:n_c' not in mrows() and f'post:{D1}:n_d' in mrows(), 'supprimer le commentaire : sa mention part, celle de la publication reste')
call('DELETE', 'n_a', f'/posts/{D1}'); ok(not [r for r in mrows() if f':{D1}:' in r] and not any(n['post_id'] == D1 for n in notifs('n_d')), 'supprimer la publication : ses mentions ET les notifications de mention partent')

section("Notifications : préférences et liste noire")
reset_rate_limits(); call('PUT', 'n_d', '/notification-prefs', {'types': {'mention': {'in_app': False}}}); before = len(notifs('n_d')); st, d, _ = post('n_a', 'Encore @Dan D.', ['n_d']); ok(st == 201 and len(notifs('n_d')) == before and [m['id'] for m in d['post']['mentions']] == ['n_d'], 'Dan a désactivé les notifications de mention : la mention existe, pas de notification')
call('PUT', 'n_d', '/notification-prefs', {'types': {'mention': {'in_app': True}}})
reset_rate_limits(); block('n_d', 'n_a'); nbefore = len(notifs('n_d')); st, d, _ = post('n_a', 'Après blocage @Dan D.', ['n_d']); ok(st == 422 and 'mentions.0' in d['errors'] and len(notifs('n_d')) == nbefore, 'Dan a bloqué Alice : elle ne peut plus le mentionner, et il ne reçoit rien')
call('POST', 'n_d', '/blocks', {'user_id': 'n_a', 'block': False})

wipe('n'); sql("DELETE FROM users WHERE id LIKE 'n\\_%'"); unset('mentions_max_per_content'); unset('network_definition'); summary()
