#!/usr/bin/env python3
"""Menu de bas de page de l'application mobile : lecture publique, configuration depuis /control."""
import json, os, re, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lib import ROOT, call, mk_user, ok, reset_rate_limits, sql, summary

CAT = {'dashboard': ('Accueil', 'Home', 'Accueil', 'Home', 'home'), 'fil': ('Le fil', 'The feed', 'Fil', 'Feed', 'feed'), 'notifications': ('Notifications', 'Notifications', 'Alertes', 'Alerts', 'bell'), 'collaboration': ('Collaboration', 'Collaboration', 'Projets', 'Projects', 'handshake'), 'communaute': ('Membres', 'Members', 'Membres', 'Members', 'users'),
       'parcours-liste': ('Parcours', 'Paths', 'Parcours', 'Paths', 'route'), 'developpement': ('Catalogue', 'Catalog', 'Catalogue', 'Catalog', 'grid'), 'quiz-profil': ("Où j'en suis", 'Where I stand', 'Bilan', 'Progress', 'target'), 'le-lab': ('Le Lab', 'The Lab', 'Lab', 'Lab', 'flask'), 'formules': ('Formules', 'Plans', 'Formules', 'Plans', 'tag'), 'messagerie': ('Messages', 'Messages', 'Messages', 'Messages', 'chat'), 'mon-compte': ('Mon profil', 'My profile', 'Profil', 'Profile', 'user')}
ICONS = ['home', 'feed', 'bell', 'handshake', 'users', 'route', 'grid', 'target', 'flask', 'tag', 'user', 'star', 'heart', 'plus', 'chat', 'calendar']
SEED = [('dashboard', 'home', False), ('collaboration', 'handshake', False), ('fil', 'feed', True), ('notifications', 'bell', False), ('mon-compte', 'user', False)]
def adm(who='m_admin'): return call('GET', who, '/admin-mobile-nav')
def put(body, who='m_admin'): return call('PUT', who, '/admin-mobile-nav', body)
def pub(): return call('GET', None, '/mobile-nav')
def items_of(d): return [(i['page'], i['icon'], i['featured']) for i in d['items']]
def logs(): return [tuple(r.split('\t')) for r in sql("SELECT action, COALESCE(old_value,''), COALESCE(new_value,''), changed_by FROM messaging_settings_log WHERE setting_key='mobile_nav' ORDER BY id").split('\n') if r]
def section(t): print('\n■ ' + t); reset_rate_limits()
def reset_nav():
    sql("DELETE FROM mobile_nav_items; UPDATE mobile_nav_config SET enabled = 0, updated_by = NULL WHERE id = 1; DELETE FROM messaging_settings_log WHERE setting_key = 'mobile_nav'")
    sql(open(ROOT + '/sql/2026_10_17_001_mobile_nav.sql', encoding='utf-8').read())          # rejoue la migration : recree l'amorce

sql("DELETE FROM users WHERE id LIKE 'm\\_%'"); mk_user('m_admin', 'admin', 'Alice', 'Admin'); mk_user('m_resp', 'responsable', 'Mona', 'Moreau'); mk_user('m_user', 'prospect', 'Paul', 'Petit'); reset_nav()

section("Amorce et lecture publique")
st, d, h = pub()
ok(st == 200 and d == {'enabled': False, 'items': []}, 'par défaut le menu est DÉSACTIVÉ : { enabled: false, items: [] } (rien n\'apparaît tant qu\'on ne l\'active pas)')
ok(h.get('Cache-Control') == 'public, max-age=60', 'lecture publique, mise en cache 60 s')
a = adm()[1]; ok(a['enabled'] is False and items_of(a) == SEED, 'amorce : 5 liens (accueil, collaboration, LE FIL mis en avant au centre, notifications, profil), menu désactivé')
ok(call('POST', None, '/mobile-nav', {})[0] == 405 and call('PUT', None, '/mobile-nav', {})[0] == 405 and call('DELETE', 'm_admin', '/mobile-nav')[0] == 405, 'la lecture publique refuse toute écriture (405)')
ok(call('GET', 'm_user', '/mobile-nav')[1] == {'enabled': False, 'items': []}, 'un membre connecté la lit aussi')

section("Droits sur la configuration")
ok(call('GET', None, '/admin-mobile-nav')[0] == 401 and adm('m_user')[0] == 403 and put({'enabled': True, 'items': []}, 'm_user')[0] == 403 and call('PUT', None, '/admin-mobile-nav', {'enabled': True, 'items': []})[0] == 401, 'visiteur : 401 ; membre : 403 (lecture et écriture)')
ok(adm('m_resp')[0] == 200 and adm('m_resp')[1]['can_edit'] is False and adm()[1]['can_edit'] is True, 'responsable : peut CONSULTER (can_edit faux) ; admin : can_edit vrai')
st, d, _ = put({'enabled': True, 'items': []}, 'm_resp'); ok(st == 403 and 'admin' in d['error'] and pub()[1]['enabled'] is False and adm()[1]['enabled'] is False, 'responsable qui tente de modifier : 403, rien n\'a changé')
ok(call('DELETE', 'm_admin', '/admin-mobile-nav')[0] == 405, 'méthode inconnue : 405')

section("Ce que la configuration propose")
a = adm()[1]
ok([c['page'] for c in a['catalogue']] == list(CAT) and all((c['label_fr'], c['label_en'], c['short_fr'], c['short_en'], c['icon']) == CAT[c['page']] for c in a['catalogue']), 'catalogue des pages proposables : 11 pages, avec libellé long (administration) et libellé COURT affiché sous l\'icône, en fr et en, et icône par défaut')
ok(all(0 < len(c['short_fr']) <= 9 and 0 < len(c['short_en']) <= 9 for c in a['catalogue']), 'les libellés courts font 9 caractères au plus : ils tiennent sous une icône, même à 6 liens sur un petit écran')
ok(a['icons'] == ICONS and a['max_items'] == 6, '16 icônes autorisées ; 6 liens au plus')
ok(all(('label_fr' in i and 'label_en' in i and 'short_fr' in i and 'short_en' in i) for i in a['items']) and a['items'][2]['label_fr'] == 'Le fil' and a['items'][2]['short_fr'] == 'Fil' and a['items'][3]['short_fr'] == 'Alertes', 'chaque lien porte ses libellés long et court (jamais d\'icône sans nom) : le fil s\'affiche « Fil », les notifications « Alertes »')

section("Enregistrer : ordre, icône, mis en avant")
st, d, _ = put({'enabled': True, 'items': [{'page': 'fil', 'icon': 'star', 'featured': True}, {'page': 'dashboard'}, {'page': 'notifications', 'featured': False}, {'page': 'communaute', 'icon': 'heart'}]})
ok(st == 200 and d['ok'] and d['changed'] is True and d['enabled'] is True, 'enregistré : 200, changé')
ok(items_of(d) == [('fil', 'star', True), ('dashboard', 'home', False), ('notifications', 'bell', False), ('communaute', 'heart', False)], 'l\'ORDRE donné est l\'ordre d\'affichage ; une icône absente prend celle de la page ; « mis en avant » absent = non')
ok(pub()[1] == {'enabled': True, 'items': [{'page': 'fil', 'icon': 'star', 'featured': True, 'label_fr': 'Le fil', 'label_en': 'The feed', 'short_fr': 'Fil', 'short_en': 'Feed'}, {'page': 'dashboard', 'icon': 'home', 'featured': False, 'label_fr': 'Accueil', 'label_en': 'Home', 'short_fr': 'Accueil', 'short_en': 'Home'}, {'page': 'notifications', 'icon': 'bell', 'featured': False, 'label_fr': 'Notifications', 'label_en': 'Notifications', 'short_fr': 'Alertes', 'short_en': 'Alerts'}, {'page': 'communaute', 'icon': 'heart', 'featured': False, 'label_fr': 'Membres', 'label_en': 'Members', 'short_fr': 'Membres', 'short_en': 'Members'}]}, 'la lecture publique reflète TOUT DE SUITE le changement (pages, icônes, ordre, mis en avant, libellés)')
st, d, _ = put({'enabled': False, 'items': [{'page': 'fil'}]}); ok(st == 200 and pub()[1] == {'enabled': False, 'items': []} and items_of(adm()[1]) == [('fil', 'feed', False)], 'désactiver : la lecture publique ne montre RIEN, mais la configuration est conservée pour l\'admin')
put({'enabled': True, 'items': [{'page': p} for p in ('dashboard', 'fil', 'collaboration', 'communaute', 'developpement', 'mon-compte')]}); ok(len(pub()[1]['items']) == 6, 'six liens : accepté (le maximum)')
st, d, _ = put({'enabled': True, 'items': [{'page': 'fil', 'featured': True}, {'page': 'dashboard', 'featured': True}]}); ok(st == 200 and [i['featured'] for i in pub()[1]['items']] == [True, True], 'plusieurs liens peuvent être mis en avant (c\'est à l\'admin de juger)')
st, d, _ = put({'enabled': True, 'items': []}); ok(st == 200 and pub()[1] == {'enabled': True, 'items': []}, 'aucun lien : accepté (l\'interface n\'affichera rien)')
st, d, _ = put({'enabled': True, 'items': [{'page': 'fil', 'featured': 1}, {'page': 'dashboard', 'featured': 0}, {'page': 'communaute', 'icone_inconnue': 'x', 'extra': 5}]}); ok(st == 200 and items_of(adm()[1]) == [('fil', 'feed', True), ('dashboard', 'home', False), ('communaute', 'users', False)], '1 / 0 acceptés pour « mis en avant » ; les clés inconnues sont ignorées')

section("Validation")
put({'enabled': True, 'items': [{'page': 'fil', 'featured': True}]}); before = adm()[1]; n_logs = len(logs())
bad = [({'enabled': 'oui', 'items': []}, 'enabled', 'enabled pas un booléen'), ({'items': []}, 'enabled', 'enabled absent'), ({'enabled': True}, 'items', 'items absent'), ({'enabled': True, 'items': 'fil'}, 'items', 'items pas une liste'), ({'enabled': True, 'items': {'a': 1}}, 'items', 'items est un objet'),
       ({'enabled': True, 'items': [{'page': p} for p in ('dashboard', 'fil', 'collaboration', 'communaute', 'developpement', 'mon-compte', 'formules')]}, 'items', '7 liens'),
       ({'enabled': True, 'items': [{'page': 'inconnue'}]}, 'items.0.page', 'page inconnue'), ({'enabled': True, 'items': [{'page': '../etc/passwd'}]}, 'items.0.page', 'page qui ressemble à un chemin'), ({'enabled': True, 'items': [{'icon': 'home'}]}, 'items.0.page', 'page absente'), ({'enabled': True, 'items': [{'page': 5}]}, 'items.0.page', 'page pas du texte'),
       ({'enabled': True, 'items': [{'page': 'fil'}, {'page': 'fil'}]}, 'items.1.page', 'même page deux fois'), ({'enabled': True, 'items': [{'page': 'fil', 'icon': 'inconnue'}]}, 'items.0.icon', 'icône inconnue'), ({'enabled': True, 'items': [{'page': 'fil', 'icon': '<svg>'}]}, 'items.0.icon', 'icône qui est du HTML'), ({'enabled': True, 'items': [{'page': 'fil', 'icon': 3}]}, 'items.0.icon', 'icône pas du texte'),
       ({'enabled': True, 'items': [{'page': 'fil', 'featured': 'oui'}]}, 'items.0.featured', 'mis en avant pas un booléen'), ({'enabled': True, 'items': ['fil']}, 'items.0', 'un lien qui n\'est pas un objet'), ({'enabled': True, 'items': [{'page': 'fil'}, {'page': 'nope'}, {'page': 'fil'}]}, 'items.1.page', 'plusieurs erreurs dans la liste')]
for body, key, why in bad:
    st, d, _ = put(body); ok(st == 422 and key in d['errors'], f'refusé (422, « {key} ») : {why}')
st, d, _ = put({'enabled': True, 'items': [{'page': 'fil'}, {'page': 'nope'}, {'page': 'fil'}]}); ok({'items.1.page', 'items.2.page'} <= set(d['errors']), 'toutes les erreurs de la liste sont renvoyées d\'un coup')
ok(adm()[1]['items'] == before['items'] and adm()[1]['enabled'] == before['enabled'] and len(logs()) == n_logs, 'après tous ces refus : la configuration est INTACTE (atomique) et rien n\'est journalisé')
ok(call('PUT', 'm_admin', '/admin-mobile-nav', raw=b'{pas du json')[0] == 422 and put([])[0] == 422, 'corps invalide ou vide : refusé')

section("Journal")
reset_nav(); put({'enabled': True, 'items': [{'page': 'fil', 'featured': True}, {'page': 'dashboard'}]}); L = logs()
ok(len(L) == 1 and L[0][0] == 'set' and L[0][3] == 'm_admin' and json.loads(L[0][1])['enabled'] is False and json.loads(L[0][2]) == {'enabled': True, 'items': [{'page': 'fil', 'icon': 'feed', 'featured': True}, {'page': 'dashboard', 'icon': 'home', 'featured': False}]}, 'journal : qui, ancienne configuration (menu désactivé, 5 liens), nouvelle configuration')
put({'enabled': True, 'items': [{'page': 'fil', 'featured': True}, {'page': 'dashboard'}]}); ok(len(logs()) == 1, 'renvoyer la MÊME configuration : rien n\'est écrit ni journalisé (changed faux)'); ok(put({'enabled': True, 'items': [{'page': 'fil', 'featured': True}, {'page': 'dashboard'}]})[1]['changed'] is False, 'la réponse le dit : changed = false')
put({'enabled': True, 'items': [{'page': 'dashboard'}, {'page': 'fil', 'featured': True}]}); ok(len(logs()) == 2, 'changer seulement l\'ORDRE est un changement, journalisé')
section("Résistance")
sql("UPDATE mobile_nav_items SET icon = '<script>' WHERE page_key = 'fil'; INSERT INTO mobile_nav_items (page_key, icon, featured, sort_order) VALUES ('page_retiree', 'home', 0, 99)")
ok([i['icon'] for i in pub()[1]['items'] if i['page'] == 'fil'] == ['feed'] and 'page_retiree' not in [i['page'] for i in pub()[1]['items']], 'une icône invalide en base retombe sur celle de la page ; une page qui n\'est plus au catalogue n\'est plus servie')
ok(re.search(r'\^mobile-nav\$\s+mobile-nav\.php', open(ROOT + '/api/.htaccess', encoding='utf-8').read()) and re.search(r'\^admin-mobile-nav\$\s+admin-mobile-nav\.php', open(ROOT + '/api/.htaccess', encoding='utf-8').read()), 'api/.htaccess : une règle par endpoint')
reset_nav(); sql("DELETE FROM users WHERE id LIKE 'm\\_%'"); summary()
