# ============================================================================
# [LIVRE] JAVASCRIPT - GUIDE ULTRA-DÉTAILLÉ POUR ÉTUDIANTS EN GÉNIE LOGICIEL
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER JAVASCRIPT DE ZÉRO À EXPERT
#
# Ce guide est organisé en 5 parties progressives :
#
# PARTIE 1 : FONDAMENTAUX (js_partie1.txt)
# - Chapitre 0 : Introduction à JavaScript
# - Chapitre 1 : Variables, Types et Opérateurs
# - Chapitre 2 : Structures de Contrôle
# - Chapitre 3 : Fonctions
# - Chapitre 4 : Tableaux et Objets
#
# PARTIE 2 : PROGRAMMATION ORIENTÉE OBJET (js_partie2.txt)
# - Chapitre 5 : Classes et Prototypes
# - Chapitre 6 : Héritage et Polymorphisme
# - Chapitre 7 : Modules ES6
# - Chapitre 8 : Gestion d'Erreurs
#
# PARTIE 3 : ASYNCHRONE ET DOM (js_partie3.txt)
# - Chapitre 9  : Asynchrone - Callbacks, Promises, Async/Await
# - Chapitre 10 : Manipulation du DOM
# - Chapitre 11 : Événements
# - Chapitre 12 : Fetch API et AJAX
#
# PARTIE 4 : FONCTIONNALITÉS AVANCÉES (js_partie4.txt)
# - Chapitre 13 : Closures et Scope
# - Chapitre 14 : Programmation Fonctionnelle
# - Chapitre 15 : Iterators, Generators, Symbols
# - Chapitre 16 : WeakMap, WeakSet, Proxy, Reflect
# - Chapitre 17 : Expressions Régulières
#
# PARTIE 5 : ÉCOSYSTÈME ET BONNES PRATIQUES (js_partie5.txt)
# - Chapitre 18 : Node.js et npm
# - Chapitre 19 : Testing (Jest)
# - Chapitre 20 : Outils (Webpack, Babel, ESLint)
# - Chapitre 21 : Patterns de Conception
# - Chapitre 22 : Performance et Sécurité
# - Chapitre 23 : Best Practices Finales
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~25-30 heures
# [DOCS] PRÉREQUIS : Logique algorithmique de base
#
# ============================================================================
#
# [IDEE] COMMENT UTILISER CE GUIDE :
# 1. Lisez les parties dans l'ordre
# 2. Testez TOUS les exemples dans la console du navigateur ou Node.js
# 3. Faites les exercices pratiques à la fin de chaque chapitre
# 4. Consultez le corrigé APRÈS avoir essayé
# 5. Créez vos propres projets
#
# [OUTILS] OUTILS NÉCESSAIRES :
# - Navigateur moderne (Chrome, Firefox, Edge)
# - VS Code (recommandé)
# - Node.js (https://nodejs.org)
# - Extension Live Server pour VS Code
#
# ============================================================================

// ============================================================================
// [LIVRE] JAVASCRIPT - PARTIE 1 : FONDAMENTAUX
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 0 : Introduction à JavaScript
// - Chapitre 1 : Variables, Types et Opérateurs
// - Chapitre 2 : Structures de Contrôle
// - Chapitre 3 : Fonctions
// - Chapitre 4 : Tableaux et Objets
//
// [TEMPS] TEMPS : ~6-8 heures
// [DOCS] PRÉREQUIS : Aucun
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 0 : INTRODUCTION À JAVASCRIPT
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est JavaScript et son rôle
[OK] Comment JavaScript s'exécute
[OK] La différence entre JavaScript côté client et serveur
[OK] Comment configurer votre environnement
[OK] Vos premiers "Hello World"
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QUE JAVASCRIPT ?
// ----------------------------------------------------------------------------

/*
DÉFINITION SIMPLE

JavaScript (JS) est un langage de programmation qui permet d'ajouter
de l'interactivité aux pages web. C'est le seul langage qui s'exécute
nativement dans les navigateurs web.

[IDEE] LES 3 PILIERS DU WEB :

HTML    -> Structure       (Le squelette)
CSS     -> Style           (L'apparence)
JS      -> Comportement    (L'interactivité)

ANALOGIE [ACCUEIL] :
HTML = Murs, planchers (structure)
CSS  = Peinture, décoration (style)
JS   = Électricité, portes automatiques (comportement)


[DOCS] HISTORIQUE RAPIDE :

1995 -> Créé par Brendan Eich chez Netscape (en 10 jours !)
1997 -> Standardisé sous le nom ECMAScript (ES)
2009 -> Node.js : JavaScript côté serveur
2015 -> ES6/ES2015 : Grande modernisation (const, let, classes, etc.)
2016+ -> Mises à jour annuelles (ES2016, ES2017, ..., ES2024)


[REFLEXION] POURQUOI APPRENDRE JAVASCRIPT ?

[OK] LE SEUL LANGAGE DU NAVIGATEUR
   Impossible de faire du web sans JS

[OK] POLYVALENT
   - Front-end (React, Vue, Angular)
   - Back-end (Node.js, Express)
   - Mobile (React Native)
   - Desktop (Electron)
   - Data Science (TensorFlow.js)

[OK] POPULAIRE
   #1 des langages sur GitHub et Stack Overflow

[OK] GRANDE COMMUNAUTÉ
   - npmjs.com : 2 millions+ de packages
   - Stack Overflow : Millions de réponses

[OK] PAYANT
   Développeur JS parmi les mieux payés


[WEB] JS CÔTÉ CLIENT vs CÔTÉ SERVEUR :

CLIENT (Navigateur) :
- Interaction utilisateur
- Animation DOM
- Validation formulaires
- Appels API
- Stockage local (localStorage)

SERVEUR (Node.js) :
- API REST
- Base de données
- Authentification
- Traitement fichiers
- Microservices
*/


// ----------------------------------------------------------------------------
// [OUTILS] CONFIGURER L'ENVIRONNEMENT
// ----------------------------------------------------------------------------

/*
OPTION 1 : CONSOLE DU NAVIGATEUR (Immédiat, aucune installation)

1. Ouvrez Chrome ou Firefox
2. Appuyez sur F12 (ou Ctrl+Shift+I)
3. Cliquez sur l'onglet "Console"
4. Tapez directement du JavaScript !

OPTION 2 : FICHIER HTML + JS (Recommandé pour apprendre)

Structure minimale :
*/

// index.html
/*
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Ma page JS</title>
</head>
<body>
    <h1>Hello World</h1>

    <!-- Script à la FIN du body (meilleure pratique) -->
    <script src="app.js"></script>
</body>
</html>
*/

// app.js
console.log("Hello World depuis app.js !");

/*
OPTION 3 : NODE.JS (Pour JS côté serveur)

1. Téléchargez Node.js sur https://nodejs.org
2. Vérifiez installation :
   node --version    -> v20.x.x
   npm --version     -> 10.x.x

3. Créez hello.js :
   console.log("Hello depuis Node.js !");

4. Exécutez :
   node hello.js


OPTION 4 : REPLIT / CODEPEN (En ligne, aucune installation)
- https://replit.com
- https://codepen.io


[IDEE] RECOMMANDATION POUR DÉBUTER :
1. VS Code + Extension "Live Server"
2. Un fichier index.html
3. Un fichier app.js
4. La console du navigateur pour tester rapidement
*/


// ----------------------------------------------------------------------------
// [DEMARRAGE] PREMIERS PAS : HELLO WORLD
// ----------------------------------------------------------------------------

// 1. Console (le plus utilisé pour debug)
console.log("Hello, World!");
console.log("Je suis développeur JavaScript !");

// 2. Types de messages console
console.log("Message normal");
console.error("Message d'erreur (rouge)");
console.warn("Message d'avertissement (orange)");
console.info("Message d'information (bleu)");

// 3. Affichage dans la page (rarement utilisé en prod)
document.write("Hello depuis document.write!");  // [ATTENTION] Éviter en prod

// 4. Pop-up (alertes) - utile pour tester mais déconseillé en prod
alert("Hello World en pop-up !");

// 5. Console avec plusieurs valeurs
console.log("Nom:", "Alice", "Age:", 25);

// 6. Variables dans console.log
let nom = "Alice";
let age = 25;
console.log(`Bonjour, je suis ${nom} et j'ai ${age} ans.`);
//           ^ Template literal (backticks `)


// ----------------------------------------------------------------------------
// [SPEECH_BALLOON] COMMENTAIRES
// ----------------------------------------------------------------------------

// Commentaire sur une seule ligne

/*
   Commentaire
   sur plusieurs
   lignes
*/

/**
 * Documentation JSDoc (pour fonctions)
 * @param {string} nom - Le nom de la personne
 * @param {number} age - L'âge de la personne
 * @returns {string} Message de salutation
 */
function saluer(nom, age) {
    return `Bonjour ${nom}, vous avez ${age} ans.`;
}


// ============================================================================
// [GUIDE] CHAPITRE 1 : VARIABLES, TYPES ET OPÉRATEURS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Déclarer des variables avec var, let, const
[OK] Comprendre les types de données primitifs
[OK] Utiliser les opérateurs arithmétiques, logiques, comparaison
[OK] Comprendre la coercition de types
[OK] Utiliser les template literals
*/


// ----------------------------------------------------------------------------
// [PACKAGE] VARIABLES : var, let, const
// ----------------------------------------------------------------------------

/*
POURQUOI 3 FAÇONS DE DÉCLARER ?

JavaScript a évolué :
var   -> ES5 (ancien, problèmes de scope)
let   -> ES6 (moderne, variable modifiable)
const -> ES6 (moderne, constante)


[IDEE] RÈGLE SIMPLE :
1. Utilisez TOUJOURS const par défaut
2. Si la valeur change -> let
3. N'utilisez JAMAIS var (sauf code legacy)
*/

// [OK] CONST : Valeur qui ne change pas
const PI = 3.14159;
const MAX_USERS = 100;
const APP_NAME = "MonApp";

// [OK] LET : Valeur qui peut changer
let score = 0;
let compteur = 0;
let nom = "Alice";

score = 10;      // [OK] OK, let est modifiable
// PI = 3.14;   // [X] TypeError: Assignment to constant variable

// [X] VAR : Ancien, problèmes de scope (à éviter)
var ancienStyle = "éviter !";

/*
[IDEE] DIFFÉRENCES CLÉS

1. SCOPE (portée)
   var   -> Function scope (visible dans toute la fonction)
   let   -> Block scope (visible dans le bloc {})
   const -> Block scope (visible dans le bloc {})

2. HOISTING (levage)
   var   -> Levé au début de la fonction, initialisé à undefined
   let   -> Levé mais pas initialisé (Temporal Dead Zone)
   const -> Levé mais pas initialisé (Temporal Dead Zone)

3. RÉASSIGNATION
   var   -> Oui
   let   -> Oui
   const -> Non (mais objet/tableau mutable !)
*/

// EXEMPLE SCOPE :
function exempleScope() {
    if (true) {
        var varVariable = "var visible partout dans la fonction";
        let letVariable = "let visible uniquement ici";
        const constVariable = "const visible uniquement ici";
    }
    console.log(varVariable);    // [OK] "var visible..."
    // console.log(letVariable); // [X] ReferenceError
}

// [ATTENTION] CONST avec objets : La RÉFÉRENCE est constante, pas le CONTENU
const user = { nom: "Alice" };
user.nom = "Bob";          // [OK] OK ! On modifie la propriété
user.age = 30;             // [OK] OK ! On ajoute une propriété
// user = { nom: "Bob" }; // [X] On ne peut pas réassigner user

console.log(user); // { nom: "Bob", age: 30 }

// Même chose avec les tableaux
const fruits = ["pomme", "banane"];
fruits.push("cerise"); // [OK] OK !
// fruits = [];         // [X] Erreur


// ----------------------------------------------------------------------------
// [DESIGN] TYPES DE DONNÉES PRIMITIFS
// ----------------------------------------------------------------------------

/*
JavaScript a 7 types primitifs :
1. number    -> Nombres (entiers ET décimaux)
2. string    -> Chaînes de caractères
3. boolean   -> true / false
4. null      -> Absence intentionnelle de valeur
5. undefined -> Valeur non assignée
6. symbol    -> Identifiant unique (ES6)
7. bigint    -> Très grands entiers (ES2020)

+ 1 type référence :
8. object    -> Objets, Tableaux, Fonctions
*/

// 1. NUMBER
const entier = 42;
const decimal = 3.14;
const negatif = -10;
const exponentiel = 1e6;  // 1,000,000

// Valeurs spéciales des nombres
console.log(Infinity);       // Infini
console.log(-Infinity);      // Moins infini
console.log(NaN);            // Not a Number
console.log(0 / 0);         // NaN
console.log(1 / 0);         // Infinity
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

// Opérations avec NaN
console.log(NaN === NaN);    // false ! (NaN n'est pas égal à lui-même)
console.log(isNaN(NaN));     // true
console.log(isNaN("abc"));   // true (tente conversion)
console.log(Number.isNaN(NaN));  // true (plus strict, recommandé)

// 2. STRING
const simple = 'Guillemets simples';
const double = "Guillemets doubles";
const template = `Template literal avec ${nom}`; // <- Recommandé !

// Caractères d'échappement
console.log("Ligne 1\nLigne 2");     // \n = saut de ligne
console.log("Tab\there");            // \t = tabulation
console.log("Il a dit \"Bonjour\""); // \" = guillemet
console.log("C:\\Users\\Alice");     // \\ = backslash

// 3. BOOLEAN
const vrai = true;
const faux = false;

// 4. NULL vs UNDEFINED
let pasDefini;           // undefined (pas de valeur assignée)
let vide = null;         // null (absence intentionnelle)

console.log(typeof undefined); // "undefined"
console.log(typeof null);      // "object" <- Bug historique de JS !

// 5. SYMBOL (ES6)
const sym1 = Symbol("description");
const sym2 = Symbol("description");
console.log(sym1 === sym2); // false ! Chaque Symbol est unique

// 6. BIGINT (ES2020)
const grandNombre = 9007199254740992n; // Le n à la fin = BigInt
console.log(typeof grandNombre); // "bigint"


// ----------------------------------------------------------------------------
// [RECHERCHE] L'OPÉRATEUR typeof
// ----------------------------------------------------------------------------

console.log(typeof 42);           // "number"
console.log(typeof "hello");      // "string"
console.log(typeof true);         // "boolean"
console.log(typeof undefined);    // "undefined"
console.log(typeof null);         // "object" <- BUG historique !
console.log(typeof Symbol());     // "symbol"
console.log(typeof 42n);          // "bigint"
console.log(typeof {});           // "object"
console.log(typeof []);           // "object" (pas "array" !)
console.log(typeof function(){}); // "function"

// Pour détecter un tableau :
console.log(Array.isArray([]));   // true [OK]


// ----------------------------------------------------------------------------
// [SYNC] CONVERSION DE TYPES (COERCITION)
// ----------------------------------------------------------------------------

/*
JavaScript peut convertir automatiquement les types.
C'est puissant mais peut causer des bugs surprenants !
*/

// CONVERSIONS EXPLICITES (recommandées [OK])

// -> Number
Number("42");        // 42
Number("3.14");      // 3.14
Number("abc");       // NaN
Number(true);        // 1
Number(false);       // 0
Number(null);        // 0
Number(undefined);   // NaN
Number("");          // 0

parseInt("42px");    // 42 (extrait le nombre)
parseFloat("3.14m"); // 3.14

// -> String
String(42);          // "42"
String(true);        // "true"
String(null);        // "null"
String(undefined);   // "undefined"
(42).toString();     // "42"
(255).toString(16);  // "ff" (hexadécimal)
(8).toString(2);     // "1000" (binaire)

// -> Boolean
Boolean(0);           // false
Boolean("");          // false
Boolean(null);        // false
Boolean(undefined);   // false
Boolean(NaN);         // false
Boolean(false);       // false
// TOUT LE RESTE = true !
Boolean(1);           // true
Boolean("hello");     // true
Boolean([]);          // true (tableau vide = truthy !)
Boolean({});          // true (objet vide = truthy !)

/*
[IDEE] VALEURS FALSY (converties en false) :
0, "", null, undefined, NaN, false

TOUT LE RESTE = TRUTHY !
*/

// COERCITION IMPLICITE (automatique, attention !)
console.log("5" + 3);       // "53" (string + number = string)
console.log("5" - 3);       // 2 (string - number = number !)
console.log("5" * "3");     // 15 (number !)
console.log(true + 1);      // 2
console.log(false + 1);     // 1
console.log(null + 1);      // 1
console.log(undefined + 1); // NaN


// ----------------------------------------------------------------------------
// [CONFIG] OPÉRATEURS
// ----------------------------------------------------------------------------

// 1. ARITHMÉTIQUES
console.log(10 + 3);  // 13 (addition)
console.log(10 - 3);  // 7  (soustraction)
console.log(10 * 3);  // 30 (multiplication)
console.log(10 / 3);  // 3.333... (division)
console.log(10 % 3);  // 1  (modulo = reste)
console.log(2 ** 8);  // 256 (puissance, ES2016)

// 2. ASSIGNATION
let x = 10;
x += 5;   // x = x + 5 = 15
x -= 3;   // x = x - 3 = 12
x *= 2;   // x = x * 2 = 24
x /= 4;   // x = x / 4 = 6
x **= 2;  // x = x ** 2 = 36
x %= 10;  // x = x % 10 = 6

// 3. INCRÉMENTATION/DÉCRÉMENTATION
let i = 0;
i++;      // Post-incrémentation : utilise puis incrémente
++i;      // Pré-incrémentation : incrémente puis utilise
i--;      // Post-décrémentation
--i;      // Pré-décrémentation

// Différence pré/post
let a = 5;
console.log(a++); // 5 (utilise la valeur, PUIS incrémente)
console.log(a);   // 6

let b = 5;
console.log(++b); // 6 (incrémente D'ABORD, puis utilise)
console.log(b);   // 6

// 4. COMPARAISON
/*
[ATTENTION] DIFFÉRENCE CRITIQUE :
== (égalité loose) : Compare les VALEURS avec coercition de types
=== (égalité stricte) : Compare VALEUR ET TYPE (TOUJOURS UTILISER CECI)
*/

console.log(5 == "5");   // true [X] (coercition !)
console.log(5 === "5");  // false [OK] (types différents)
console.log(5 !== "5");  // true [OK]
console.log(null == undefined);  // true <- Exception notable
console.log(null === undefined); // false

console.log(5 > 3);   // true
console.log(5 < 3);   // false
console.log(5 >= 5);  // true
console.log(5 <= 4);  // false

// 5. LOGIQUES
console.log(true && false);  // false (AND : les deux doivent être vrais)
console.log(true || false);  // true  (OR : au moins un vrai)
console.log(!true);          // false (NOT : inverse)

// Court-circuit (Short-circuit evaluation)
// && : S'arrête au PREMIER faux
// || : S'arrête au PREMIER vrai
const result1 = null && "jamais évalué";     // null
const result2 = "Alice" || "jamais évalué";  // "Alice"

// Utilisation pratique :
const username = null;
const display = username || "Anonyme";  // "Anonyme" si username est falsy
console.log(display); // "Anonyme"

// 6. OPÉRATEUR NULLISH COALESCING (??) - ES2020
// Seulement null et undefined (pas 0, "", false)
const valeur = null ?? "défaut";    // "défaut"
const zero = 0 ?? "défaut";        // 0 (car 0 n'est pas null/undefined !)
const texte = "" ?? "défaut";      // "" (car "" n'est pas null/undefined !)

// 7. OPTIONAL CHAINING (?.) - ES2020
const obj = { a: { b: { c: 42 } } };
console.log(obj?.a?.b?.c);    // 42
console.log(obj?.x?.y?.z);    // undefined (pas d'erreur !)
// Sans ?. : obj.x.y.z -> TypeError: Cannot read property 'y' of undefined

// 8. OPÉRATEUR TERNAIRE
const ageUtilisateur = 20;
const acces = ageUtilisateur >= 18 ? "Adulte" : "Mineur";
console.log(acces); // "Adulte"
//   condition      ? si_vrai   : si_faux

// 9. NULLISH ASSIGNMENT ??= (ES2021)
let config = null;
config ??= "valeur par défaut"; // Assigne seulement si null/undefined

// 10. LOGICAL ASSIGNMENT ||= &&=
let a2 = 0;
a2 ||= 42;  // Assigne si falsy
console.log(a2); // 42


// ----------------------------------------------------------------------------
// [NOTE] TEMPLATE LITERALS (ES6)
// ----------------------------------------------------------------------------

const prenom = "Alice";
const annee = 2024;
const score2 = 95.5;

// Ancienne façon (concaténation) [X]
const msg1 = "Bonjour " + prenom + " ! Année " + annee;

// Template literal [OK]
const msg2 = `Bonjour ${prenom} ! Année ${annee}`;

// Expressions dans templates
const msg3 = `Score: ${score2} / 100 = ${(score2 / 100 * 100).toFixed(0)}%`;

// Multi-lignes
const multi = `
    Ligne 1
    Ligne 2
    Ligne 3
`;

// Fonction dans template
const majuscules = `${prenom.toUpperCase()} a ${annee - 1999} ans`;

// Template imbriqué
const items = [1, 2, 3];
const html = `<ul>${items.map(i => `<li>${i}</li>`).join('')}</ul>`;


// ----------------------------------------------------------------------------
// [NOMBRE] LES MÉTHODES DES NOMBRES (Number)
// ----------------------------------------------------------------------------

const num = 3.14159;

// Formatage
console.log(num.toFixed(2));       // "3.14" (arrondi 2 décimales, retourne string)
console.log(num.toPrecision(4));   // "3.142" (4 chiffres significatifs)
console.log(num.toString());       // "3.14159"
console.log(num.toString(16));     // "3.243f..." (hexadécimal)

// Méthodes statiques
console.log(Number.isInteger(42));      // true
console.log(Number.isInteger(42.5));    // false
console.log(Number.isFinite(Infinity)); // false
console.log(Number.isNaN(NaN));         // true
console.log(Number.parseInt("42px"));   // 42
console.log(Number.parseFloat("3.14m"));// 3.14

// Math object
console.log(Math.round(4.6));   // 5
console.log(Math.floor(4.9));   // 4 (arrondi vers le bas)
console.log(Math.ceil(4.1));    // 5 (arrondi vers le haut)
console.log(Math.trunc(4.9));   // 4 (enlève décimales)
console.log(Math.abs(-5));      // 5
console.log(Math.max(1, 5, 3)); // 5
console.log(Math.min(1, 5, 3)); // 1
console.log(Math.pow(2, 8));    // 256
console.log(Math.sqrt(16));     // 4
console.log(Math.cbrt(27));     // 3 (racine cubique)
console.log(Math.PI);           // 3.14159...
console.log(Math.E);            // 2.71828...

// Nombre aléatoire
const random = Math.random();            // [0, 1[
const alea1_10 = Math.floor(Math.random() * 10) + 1; // [1, 10]


// ----------------------------------------------------------------------------
// [NOTE] LES MÉTHODES DES CHAÎNES (String)
// ----------------------------------------------------------------------------

const str = "  Hello, World!  ";
const str2 = "JavaScript est incroyable !";

// Propriété
console.log(str.length);         // 17

// Accès aux caractères
console.log(str[0]);             // " " (espace)
console.log(str.charAt(2));      // "H"
console.log(str.charCodeAt(2));  // 72 (code ASCII)

// Recherche
console.log(str2.indexOf("est")); // 11 (-1 si pas trouvé)
console.log(str2.includes("JavaScript")); // true
console.log(str2.startsWith("Java"));     // true
console.log(str2.endsWith("!"));          // true

// Modification (retournent TOUJOURS une nouvelle string !)
console.log(str.trim());          // "Hello, World!" (enlève espaces)
console.log(str.trimStart());     // "Hello, World!  "
console.log(str.trimEnd());       // "  Hello, World!"
console.log(str2.toLowerCase());  // "javascript est incroyable !"
console.log(str2.toUpperCase());  // "JAVASCRIPT EST INCROYABLE !"
console.log(str2.replace("est", "est vraiment")); // Première occurrence
console.log(str2.replaceAll("!", ".")); // Toutes les occurrences

// Extraction
console.log(str2.slice(0, 10));   // "JavaScript" (de 0 à 9)
console.log(str2.slice(-1));      // "!" (depuis la fin)
console.log(str2.substring(0, 4)); // "Java"

// Découpage
console.log(str2.split(" "));   // ["JavaScript", "est", "incroyable", "!"]
console.log("1,2,3".split(",")); // ["1", "2", "3"]

// Répétition et remplissage
console.log("ha".repeat(3));         // "hahaha"
console.log("5".padStart(3, "0"));   // "005"
console.log("5".padEnd(3, "0"));     // "500"

// Correspondance regex
const email = "user@example.com";
console.log(email.match(/@/));       // Tableau avec infos du match
console.log(email.search(/@/));      // 4 (index)


// ============================================================================
// [GUIDE] CHAPITRE 2 : STRUCTURES DE CONTRÔLE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser if/else, switch
[OK] Maîtriser les boucles for, while, do-while
[OK] Utiliser break, continue
[OK] Comprendre for...in et for...of
[OK] Gérer les cas d'erreur avec try/catch
*/


// ----------------------------------------------------------------------------
// [MELANGE] CONDITIONS : if / else if / else
// ----------------------------------------------------------------------------

const noteEtudiant = 75;

// Structure de base
if (noteEtudiant >= 90) {
    console.log("A - Excellent !");
} else if (noteEtudiant >= 80) {
    console.log("B - Bien");
} else if (noteEtudiant >= 70) {
    console.log("C - Satisfaisant");
} else if (noteEtudiant >= 60) {
    console.log("D - Passable");
} else {
    console.log("F - Insuffisant");
}

// Conditions sans accolades (seulement pour une seule instruction)
if (noteEtudiant >= 60) console.log("Passé !");

// Opérateur ternaire
const mention = noteEtudiant >= 60 ? "Passé" : "Échoué";

// Ternaires imbriqués (lisibilité réduite, utiliser avec parcimonie)
const grade = noteEtudiant >= 90 ? "A"
             : noteEtudiant >= 80 ? "B"
             : noteEtudiant >= 70 ? "C"
             : noteEtudiant >= 60 ? "D"
             : "F";


// ----------------------------------------------------------------------------
// [MELANGE] SWITCH STATEMENT
// ----------------------------------------------------------------------------

const jour = "Lundi";

switch (jour) {
    case "Lundi":
    case "Mardi":
    case "Mercredi":
    case "Jeudi":
    case "Vendredi":
        console.log("Jour de semaine");
        break; // [ATTENTION] OBLIGATOIRE pour ne pas "tomber" au case suivant

    case "Samedi":
    case "Dimanche":
        console.log("Week-end !");
        break;

    default:
        console.log("Jour inconnu");
}

/*
[IDEE] SWITCH vs IF/ELSE

Switch :
[OK] Lisible pour beaucoup de cas
[OK] Compare avec === (strict)
[ATTENTION] N'oubliez pas break !
[ATTENTION] Seulement pour valeurs exactes

If/Else :
[OK] Conditions complexes (>, <, etc.)
[OK] Pas de risque de "fall-through"
*/


// ----------------------------------------------------------------------------
// [SYNC] BOUCLES
// ----------------------------------------------------------------------------

// 1. FOR (connaissez le nombre d'itérations)
for (let i = 0; i < 5; i++) {
    console.log(`Itération ${i}`);
}
// Itération 0, 1, 2, 3, 4

// Syntaxe : for (initialisation; condition; mise_à_jour)

// Décrémentation
for (let i = 10; i > 0; i--) {
    console.log(i);
}

// Pas de 2
for (let i = 0; i <= 10; i += 2) {
    console.log(i); // 0, 2, 4, 6, 8, 10
}

// 2. WHILE (condition inconnue à l'avance)
let count = 0;
while (count < 5) {
    console.log(`Count: ${count}`);
    count++;
}

// [ATTENTION] BOUCLE INFINIE (à éviter !)
// while (true) { ... }  <- Sans break, ça ne s'arrête pas !

// 3. DO...WHILE (s'exécute AU MOINS UNE FOIS)
let n = 10;
do {
    console.log(`n = ${n}`); // Affiché même si condition fausse au départ
    n--;
} while (n > 5);

// 4. FOR...OF (itérer sur des valeurs : tableaux, strings, etc.)
const fruits = ["pomme", "banane", "cerise"];

for (const fruit of fruits) {
    console.log(fruit); // pomme, banane, cerise
}

// Avec strings
for (const lettre of "Hello") {
    console.log(lettre); // H, e, l, l, o
}

// Avec index (using entries())
for (const [index, fruit] of fruits.entries()) {
    console.log(`${index}: ${fruit}`); // 0: pomme, 1: banane, 2: cerise
}

// 5. FOR...IN (itérer sur les CLÉS d'un objet)
const personne = { nom: "Alice", age: 25, ville: "Paris" };

for (const cle in personne) {
    console.log(`${cle}: ${personne[cle]}`);
}
// nom: Alice, age: 25, ville: Paris

/*
[ATTENTION] FOR...IN vs FOR...OF

for...in -> Itère sur les CLÉS (propriétés) d'un objet
for...of -> Itère sur les VALEURS d'un itérable (tableau, string, etc.)

N'utilisez PAS for...in sur les tableaux (comportement imprévisible)
Utilisez for...of pour les tableaux, for...in pour les objets !
*/


// ----------------------------------------------------------------------------
// [BLACK_RIGHT-POINTING_DOUBLE_TRIANGLE_WITH_VERTICAL_BAR] BREAK ET CONTINUE
// ----------------------------------------------------------------------------

// BREAK : Sortir de la boucle
for (let i = 0; i < 10; i++) {
    if (i === 5) break; // Sort de la boucle à i=5
    console.log(i); // 0, 1, 2, 3, 4
}

// CONTINUE : Passer à l'itération suivante
for (let i = 0; i < 10; i++) {
    if (i % 2 === 0) continue; // Saute les pairs
    console.log(i); // 1, 3, 5, 7, 9
}

// LABELS (pour boucles imbriquées)
outer: for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
        if (j === 1) break outer; // Sort de la boucle EXTERNE
        console.log(`${i}, ${j}`);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 3 : FONCTIONS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Déclarer des fonctions de plusieurs façons
[OK] Utiliser paramètres et valeurs de retour
[OK] Comprendre les paramètres par défaut et rest
[OK] Utiliser l'opérateur spread
[OK] Comprendre les arrow functions
[OK] Connaître les fonctions d'ordre supérieur
*/


// ----------------------------------------------------------------------------
// [PACKAGE] DÉCLARATIONS DE FONCTIONS
// ----------------------------------------------------------------------------

// 1. FUNCTION DECLARATION (hoistée - peut être utilisée avant déclaration)
function saluer(prenom) {
    return `Bonjour, ${prenom} !`;
}
console.log(saluer("Alice")); // Peut être appelée avant la définition !

// 2. FUNCTION EXPRESSION (pas hoistée)
const calculer = function(a, b) {
    return a + b;
};
console.log(calculer(3, 4)); // 7

// 3. ARROW FUNCTION - ES6 (syntaxe concise)
const multiplier = (a, b) => a * b;

// Arrow function avec corps
const diviser = (a, b) => {
    if (b === 0) return null;
    return a / b;
};

// Formes des arrow functions
const simple1 = x => x * 2;          // Un paramètre, pas de ()
const simple2 = () => 42;             // Zéro paramètre, () obligatoire
const simple3 = (x, y) => x + y;     // Plusieurs paramètres
const simple4 = x => ({ valeur: x }); // Retourner un objet (parenthèses !)

// 4. ANONYMOUS FUNCTION
setTimeout(function() {
    console.log("Exécuté après 1 seconde");
}, 1000);

// 5. IMMEDIATELY INVOKED FUNCTION EXPRESSION (IIFE)
(function() {
    const scope = "Je suis dans un scope isolé";
    console.log(scope);
})();

// Arrow IIFE
(() => {
    console.log("Arrow IIFE !");
})();


// ----------------------------------------------------------------------------
// [LISTE] PARAMÈTRES
// ----------------------------------------------------------------------------

// Paramètres standards
function add(a, b) {
    return a + b;
}

// Paramètres par défaut (ES6)
function creerUser(nom, age = 18, role = "user") {
    return { nom, age, role };
}
console.log(creerUser("Alice"));          // { nom: "Alice", age: 18, role: "user" }
console.log(creerUser("Bob", 25));        // { nom: "Bob", age: 25, role: "user" }
console.log(creerUser("Admin", 30, "admin")); // { nom: "Admin", age: 30, role: "admin" }

// Paramètre REST (...) - collecte les arguments restants
function somme(...nombres) {
    return nombres.reduce((total, n) => total + n, 0);
}
console.log(somme(1, 2, 3));          // 6
console.log(somme(1, 2, 3, 4, 5));    // 15

function log(niveau, ...messages) {
    console.log(`[${niveau}]`, ...messages);
}
log("INFO", "Connexion", "Alice", "à", "Paris");

// Arguments object (ancien style, préférer rest params)
function ancienStyle() {
    console.log(arguments); // Objet Arguments (pas un vrai tableau)
}

// DESTRUCTURING DANS LES PARAMÈTRES
function afficherUser({ nom, age, ville = "Inconnue" }) {
    console.log(`${nom}, ${age} ans, ${ville}`);
}
afficherUser({ nom: "Alice", age: 25, ville: "Paris" });

function premier([a, b, c]) {
    console.log(a, b, c);
}
premier([1, 2, 3]);


// ----------------------------------------------------------------------------
// [OBJECTIF] VALEURS DE RETOUR
// ----------------------------------------------------------------------------

// Une fonction sans return retourne undefined
function sanRetour() {
    console.log("Hello");
    // retourne undefined implicitement
}

// Retourner plusieurs valeurs via un objet
function divisionEtReste(a, b) {
    return {
        quotient: Math.floor(a / b),
        reste: a % b
    };
}

const { quotient, reste } = divisionEtReste(17, 5);
console.log(quotient, reste); // 3, 2

// Retourner plusieurs valeurs via un tableau
function minMax(tableau) {
    return [Math.min(...tableau), Math.max(...tableau)];
}

const [min, max] = minMax([3, 1, 4, 1, 5, 9, 2, 6]);
console.log(min, max); // 1, 9


// ----------------------------------------------------------------------------
// [WATER_WAVE] OPÉRATEUR SPREAD (...)
// ----------------------------------------------------------------------------

// Étendre un tableau
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// Copier un tableau
const copy = [...arr1]; // Copie superficielle

// Étendre dans des arguments
console.log(Math.max(...arr1)); // 3 (équivalent à Math.max(1, 2, 3))

// Étendre un objet (ES2018)
const defaults = { couleur: "bleu", taille: "M" };
const custom = { ...defaults, couleur: "rouge" }; // Surcharge couleur
console.log(custom); // { couleur: "rouge", taille: "M" }

// Copier un objet
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }


// ----------------------------------------------------------------------------
// [CIBLE] ARROW FUNCTIONS - DÉTAILS IMPORTANTS
// ----------------------------------------------------------------------------

/*
DIFFÉRENCES CRUCIALES avec les fonctions classiques :

1. PAS DE LEUR PROPRE 'this'
   Arrow functions héritent 'this' de leur scope extérieur

2. PAS D'OBJET 'arguments'
   Utiliser les rest params (...args)

3. NE PEUVENT PAS ÊTRE CONSTRUCTEUR
   Pas de new avec arrow functions

4. PAS DE HOISTING
   (comme function expressions)
*/

// Exemple : this dans les arrow functions
class Timer {
    constructor() {
        this.secondes = 0;
    }

    demarrer() {
        // [OK] Arrow function : this = l'instance du Timer
        setInterval(() => {
            this.secondes++;
            console.log(this.secondes);
        }, 1000);

        // [X] Function classique : this = undefined (strict mode) ou window
        // setInterval(function() {
        //     this.secondes++; // Erreur ou NaN !
        // }, 1000);
    }
}


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 1 : Fondamentaux et Fonctions
// ----------------------------------------------------------------------------

/*
EXERCICE 1.1 : Calculatrice basique

Créez une fonction calculatrice(a, op, b) qui :
- Accepte deux nombres et un opérateur (+, -, *, /, %)
- Retourne le résultat
- Lance une erreur si l'opérateur est inconnu
- Retourne null pour division par zéro

Exemples :
calculatrice(10, "+", 5) -> 15
calculatrice(10, "/", 0) -> null
calculatrice(10, "^", 2) -> throw Error("Opérateur inconnu : ^")
*/

// CORRIGÉ 1.1 :
function calculatrice(a, op, b) {
    switch (op) {
        case "+": return a + b;
        case "-": return a - b;
        case "*": return a * b;
        case "/": return b === 0 ? null : a / b;
        case "%": return b === 0 ? null : a % b;
        default: throw new Error(`Opérateur inconnu : ${op}`);
    }
}

console.log(calculatrice(10, "+", 5));  // 15
console.log(calculatrice(10, "/", 0));  // null
try {
    calculatrice(10, "^", 2);
} catch(e) {
    console.log(e.message); // "Opérateur inconnu : ^"
}


/*
EXERCICE 1.2 : FizzBuzz

Créez une fonction fizzBuzz(n) qui retourne un tableau de 1 à n où :
- Les multiples de 3 sont remplacés par "Fizz"
- Les multiples de 5 sont remplacés par "Buzz"
- Les multiples de 3 ET 5 sont remplacés par "FizzBuzz"

fizzBuzz(15) -> [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
*/

// CORRIGÉ 1.2 :
function fizzBuzz(n) {
    const result = [];
    for (let i = 1; i <= n; i++) {
        if (i % 15 === 0) result.push("FizzBuzz");
        else if (i % 3 === 0) result.push("Fizz");
        else if (i % 5 === 0) result.push("Buzz");
        else result.push(i);
    }
    return result;
}

console.log(fizzBuzz(15));


/*
EXERCICE 1.3 : Palindrome

Créez une fonction estPalindrome(str) qui :
- Retourne true si la string est un palindrome
- Ignore casse et espaces
- Exemples : "racecar" -> true, "A man a plan a canal Panama" -> true
*/

// CORRIGÉ 1.3 :
function estPalindrome(str) {
    const nettoye = str.toLowerCase().replace(/\s/g, "");
    const inverse = nettoye.split("").reverse().join("");
    return nettoye === inverse;
}

console.log(estPalindrome("racecar")); // true
console.log(estPalindrome("hello"));  // false
console.log(estPalindrome("A man a plan a canal Panama")); // true


// ============================================================================
// [GUIDE] CHAPITRE 4 : TABLEAUX ET OBJETS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer et manipuler des tableaux
[OK] Utiliser les méthodes essentielles des tableaux (map, filter, reduce, etc.)
[OK] Créer et manipuler des objets
[OK] Utiliser la déstructuration
[OK] Comprendre les méthodes d'objets (Object.keys, values, entries)
[OK] Utiliser les méthodes fonctionnelles
*/


// ----------------------------------------------------------------------------
// [DOCS] TABLEAUX (Arrays)
// ----------------------------------------------------------------------------

// CRÉATION
const vide = [];
const nombres = [1, 2, 3, 4, 5];
const mixte = [1, "hello", true, null, { a: 1 }];
const matrice = [[1, 2], [3, 4], [5, 6]];

// new Array (peu utilisé)
const arr = new Array(5);          // [undefined x5]
const arr2 = new Array(1, 2, 3);   // [1, 2, 3]
const arr3 = Array.of(5);          // [5] (pas Array(5) !)
const arr4 = Array.from("hello");  // ['h', 'e', 'l', 'l', 'o']
const arr5 = Array.from({length: 5}, (_, i) => i + 1); // [1, 2, 3, 4, 5]

// ACCÈS
console.log(nombres[0]);     // 1 (premier)
console.log(nombres[4]);     // 5 (dernier)
console.log(nombres.at(-1)); // 5 (dernier, ES2022)
console.log(nombres.at(-2)); // 4 (avant-dernier)

// PROPRIÉTÉS
console.log(nombres.length); // 5

// MODIFICATION
nombres[0] = 10;             // Modifier
nombres.push(6);             // Ajouter à la fin -> retourne nouvelle longueur
nombres.unshift(0);          // Ajouter au début -> retourne nouvelle longueur
nombres.pop();               // Supprimer le dernier -> retourne élément supprimé
nombres.shift();             // Supprimer le premier -> retourne élément supprimé

// SPLICE (modification au milieu)
const couleurs = ["rouge", "vert", "bleu"];
couleurs.splice(1, 0, "orange");      // Insérer à l'index 1
// ["rouge", "orange", "vert", "bleu"]
couleurs.splice(1, 1);                // Supprimer 1 élément à l'index 1
// ["rouge", "vert", "bleu"]
couleurs.splice(1, 1, "jaune");       // Remplacer

// RECHERCHE
const animaux = ["chat", "chien", "oiseau", "chat"];
console.log(animaux.indexOf("chien"));      // 1
console.log(animaux.lastIndexOf("chat"));   // 3
console.log(animaux.includes("oiseau"));    // true
console.log(animaux.find(a => a.length > 4)); // "chien"
console.log(animaux.findIndex(a => a.length > 4)); // 1

// COPIE ET EXTRACTION
const copie = nombres.slice();       // Copie complète
const partie = nombres.slice(1, 3); // Éléments 1 et 2
const copie2 = [...nombres];        // Copie avec spread (préféré)

// COMBINAISON
const a = [1, 2, 3];
const b = [4, 5, 6];
const fusion = a.concat(b);    // [1, 2, 3, 4, 5, 6]
const fusion2 = [...a, ...b];  // Même résultat, préféré

// APLATISSEMENT
const nested = [1, [2, 3], [4, [5, 6]]];
console.log(nested.flat());     // [1, 2, 3, 4, [5, 6]] (un niveau)
console.log(nested.flat(2));    // [1, 2, 3, 4, 5, 6] (deux niveaux)
console.log(nested.flat(Infinity)); // [1, 2, 3, 4, 5, 6] (tous niveaux)

// TRI
const nums = [3, 1, 4, 1, 5, 9, 2, 6];
nums.sort();             // [ATTENTION] Tri lexicographique par défaut !
// [1, 1, 2, 3, 4, 5, 6, 9] <- Correct ici, mais :
// [10, 2, 1].sort() -> [1, 10, 2] <- FAUX !

nums.sort((a, b) => a - b); // Tri numérique croissant [OK]
nums.sort((a, b) => b - a); // Tri numérique décroissant [OK]

// Tri de strings
const noms = ["Charlie", "Alice", "Bob"];
noms.sort(); // ["Alice", "Bob", "Charlie"] [OK]
noms.sort((a, b) => a.localeCompare(b)); // Pour accents

// CONVERSION
console.log([1, 2, 3].join(", ")); // "1, 2, 3"
console.log([1, 2, 3].join(""));   // "123"

// VÉRIFICATION
console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray("hello"));   // false


// ----------------------------------------------------------------------------
// [RAPIDE] MÉTHODES FONCTIONNELLES DES TABLEAUX (LES PLUS IMPORTANTES !)
// ----------------------------------------------------------------------------

/*
Ces méthodes NE MODIFIENT PAS le tableau original.
Elles retournent un NOUVEAU tableau (sauf reduce, some, every, etc.)

[IDEE] MÉTHODES ESSENTIELLES :
map()    -> Transformer chaque élément
filter() -> Garder certains éléments
reduce() -> Réduire à une valeur
find()   -> Trouver un élément
some()   -> Au moins un élément satisfait
every()  -> Tous les éléments satisfont
forEach()-> Itérer (sans retour)
flatMap() -> map + flat
*/

const etudiants = [
    { nom: "Alice", note: 85, matiere: "Math" },
    { nom: "Bob",   note: 72, matiere: "Physique" },
    { nom: "Claire", note: 91, matiere: "Math" },
    { nom: "David", note: 68, matiere: "Chimie" },
    { nom: "Eve",   note: 95, matiere: "Math" }
];

// MAP : Transformer chaque élément -> retourne NOUVEAU tableau même taille
const noms2 = etudiants.map(e => e.nom);
// ["Alice", "Bob", "Claire", "David", "Eve"]

const notesDoublees = etudiants.map(e => ({
    ...e,
    note: e.note * 2  // Ne devrait pas dépasser 100, mais pour l'exemple
}));

// MAP avec index
const numerotes = etudiants.map((e, i) => `${i + 1}. ${e.nom}`);

// FILTER : Garder certains éléments -> retourne NOUVEAU tableau (peut être vide)
const meilleurs = etudiants.filter(e => e.note >= 80);
// [Alice, Claire, Eve]

const matheux = etudiants.filter(e => e.matiere === "Math");
// [Alice, Claire, Eve]

// Chaîner map et filter
const nomsMathReussants = etudiants
    .filter(e => e.matiere === "Math" && e.note >= 80)
    .map(e => e.nom);
// ["Alice", "Claire", "Eve"]

// REDUCE : Réduire à une valeur unique
const totalNotes = etudiants.reduce((acc, e) => acc + e.note, 0);
console.log(totalNotes); // 411

const moyenne = totalNotes / etudiants.length;
console.log(moyenne.toFixed(1)); // "82.2"

// Reduce pour grouper par matière
const parMatiere = etudiants.reduce((acc, e) => {
    if (!acc[e.matiere]) acc[e.matiere] = [];
    acc[e.matiere].push(e);
    return acc;
}, {});
/*
{
  Math: [Alice, Claire, Eve],
  Physique: [Bob],
  Chimie: [David]
}
*/

// FIND : Premier élément qui satisfait la condition
const premier90 = etudiants.find(e => e.note >= 90);
// Claire (premier trouvé)

// FINDINDEX : Index du premier élément
const indexPremier90 = etudiants.findIndex(e => e.note >= 90);
// 2

// SOME : Au moins un élément satisfait ? (retourne boolean)
const aDesExcellents = etudiants.some(e => e.note >= 95);
// true (Eve)

// EVERY : Tous les éléments satisfont ? (retourne boolean)
const tousPassent = etudiants.every(e => e.note >= 60);
// true

// FOREACH : Itérer sans retour (effets de bord)
etudiants.forEach((e, i) => {
    console.log(`${i}: ${e.nom} - ${e.note}`);
});

// FLATMAP : map + flat (un niveau)
const mots = ["Hello World", "Foo Bar"];
const lettres = mots.flatMap(m => m.split(" "));
// ["Hello", "World", "Foo", "Bar"]


// ----------------------------------------------------------------------------
// [CONSTRUCTION] OBJETS (Objects)
// ----------------------------------------------------------------------------

// CRÉATION
const obj = {};           // Objet vide
const obj2 = new Object(); // Équivalent, moins courant

// Objet avec propriétés
const car = {
    marque: "Toyota",
    modele: "Corolla",
    annee: 2022,
    couleur: "blanc",
    kilometrage: 15000,

    // Méthode
    decrire() {
        return `${this.marque} ${this.modele} (${this.annee})`;
    },

    // Getter
    get age() {
        return new Date().getFullYear() - this.annee;
    },

    // Setter
    set couleurVoiture(c) {
        this.couleur = c.toLowerCase();
    }
};

// ACCÈS AUX PROPRIÉTÉS
console.log(car.marque);        // "Toyota" (dot notation)
console.log(car["modele"]);     // "Corolla" (bracket notation)
console.log(car.decrire());     // "Toyota Corolla (2022)"
console.log(car.age);          // Calculé par le getter

// Bracket notation pour propriétés dynamiques
const prop = "annee";
console.log(car[prop]); // 2022

// MODIFICATION
car.couleur = "rouge";
car["kilometrage"] = 20000;
car.couleurVoiture = "BLEU"; // Via setter

// AJOUT
car.nbrPortes = 4;
car.demarrer = function() { console.log("Vroom !"); };

// SUPPRESSION
delete car.couleur;

// VÉRIFICATION
console.log("marque" in car);          // true
console.log(car.hasOwnProperty("marque")); // true
console.log(car.marque !== undefined); // true (moins fiable)

// ENUMÉRATION DES PROPRIÉTÉS
const user = { nom: "Alice", age: 25, ville: "Paris" };

Object.keys(user);      // ["nom", "age", "ville"] (clés)
Object.values(user);    // ["Alice", 25, "Paris"] (valeurs)
Object.entries(user);   // [["nom", "Alice"], ["age", 25], ["ville", "Paris"]]

// Itérer
for (const [cle, valeur] of Object.entries(user)) {
    console.log(`${cle}: ${valeur}`);
}

// MÉTHODES D'OBJET UTILES
// Object.assign : Fusionner objets
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const result = Object.assign(target, source); // target modifié
// { a: 1, b: 4, c: 5 }

// Object.freeze : Rendre immuable
const config = Object.freeze({ debug: false, version: "1.0" });
// config.debug = true; // Silencieusement ignoré en non-strict, erreur en strict

// Object.seal : Empêche ajout/suppression mais permet modification
const sealed = Object.seal({ x: 1, y: 2 });
sealed.x = 10;     // [OK] OK
// sealed.z = 3;   // [X] Ignoré
// delete sealed.x;// [X] Ignoré

// SHORTHAND PROPERTIES (ES6)
const nomVar = "Alice";
const ageVar = 25;
const userObj = { nomVar, ageVar };
// équivalent à { nomVar: nomVar, ageVar: ageVar }
// { nomVar: "Alice", ageVar: 25 }

// COMPUTED PROPERTIES (ES6)
const key = "dynamique";
const objDyn = { [key]: "valeur", [`${key}2`]: "valeur2" };
// { dynamique: "valeur", dynamique2: "valeur2" }


// ----------------------------------------------------------------------------
// [DEVERROUILLE] DÉSTRUCTURATION (TRÈS IMPORTANT EN JS MODERNE !)
// ----------------------------------------------------------------------------

// DÉSTRUCTURATION D'OBJET
const { nom: nomEtudiant, age: ageEtudiant, ville = "Inconnue" } = user;
//      ^ renommage                                 ^ valeur défaut

console.log(nomEtudiant); // "Alice"
console.log(ageEtudiant); // 25
console.log(ville);        // "Inconnue" (non défini dans user)

// Déstructuration imbriquée
const entreprise = {
    nom: "TechCorp",
    adresse: {
        rue: "123 Main St",
        ville: "Paris",
        code: "75001"
    },
    employes: ["Alice", "Bob"]
};

const { nom: nomEntreprise, adresse: { ville: villeEntreprise } } = entreprise;
console.log(nomEntreprise); // "TechCorp"
console.log(villeEntreprise); // "Paris"

// DÉSTRUCTURATION DE TABLEAU
const [premier, deuxieme, ...reste] = [1, 2, 3, 4, 5];
console.log(premier);  // 1
console.log(deuxieme); // 2
console.log(reste);    // [3, 4, 5]

// Ignorer des éléments
const [, second, , quatrieme] = [1, 2, 3, 4];
console.log(second);   // 2
console.log(quatrieme);// 4

// Swap de variables (élégant !)
let x2 = 1, y2 = 2;
[x2, y2] = [y2, x2];
console.log(x2, y2); // 2, 1

// DÉSTRUCTURATION DANS LES PARAMÈTRES
function afficherProfil({ nom, age, role = "user" }) {
    console.log(`${nom} (${age}) - ${role}`);
}


// ----------------------------------------------------------------------------
// [WORLD_MAP] MAP ET SET (Structures de données ES6)
// ----------------------------------------------------------------------------

// MAP : Association clé -> valeur (clés peuvent être de N'IMPORTE QUEL type)
const mapEx = new Map();
mapEx.set("nom", "Alice");
mapEx.set("age", 25);
mapEx.set(1, "numérique");
mapEx.set({ id: 1 }, "objet comme clé");

mapEx.get("nom");     // "Alice"
mapEx.has("age");     // true
mapEx.size;           // 4
mapEx.delete("age");
mapEx.clear();        // Vider tout

// Itérer sur Map
const scores = new Map([["Alice", 95], ["Bob", 87], ["Eve", 92]]);
for (const [nom, score] of scores) {
    console.log(`${nom}: ${score}`);
}
scores.keys();      // Iterator des clés
scores.values();    // Iterator des valeurs
scores.entries();   // Iterator des paires

// Convertir Map <-> Object
const mapToObj = Object.fromEntries(scores);
const objToMap = new Map(Object.entries(user));

// SET : Collection de valeurs UNIQUES
const setEx = new Set([1, 2, 3, 2, 1]); // {1, 2, 3} (doublons supprimés !)
setEx.add(4);
setEx.has(2);    // true
setEx.delete(1);
setEx.size;      // 3

// Supprimer doublons d'un tableau (pattern classique !)
const avecDoublons = [1, 2, 2, 3, 3, 3, 4];
const sansDoublons = [...new Set(avecDoublons)]; // [1, 2, 3, 4]


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 2 : Tableaux et Objets
// ----------------------------------------------------------------------------

/*
EXERCICE 2.1 : Manipulation de tableau d'objets

Vous avez une liste de produits. Créez des fonctions pour :
a) Filtrer par catégorie
b) Calculer le prix moyen
c) Trouver le produit le moins cher
d) Grouper par catégorie
*/

const produits = [
    { id: 1, nom: "Laptop", prix: 999, categorie: "Electronique" },
    { id: 2, nom: "Souris", prix: 29, categorie: "Electronique" },
    { id: 3, nom: "Bureau", prix: 349, categorie: "Mobilier" },
    { id: 4, nom: "Chaise", prix: 249, categorie: "Mobilier" },
    { id: 5, nom: "Clavier", prix: 79, categorie: "Electronique" }
];

// CORRIGÉ 2.1 :

// a) Filtrer par catégorie
const filtrerParCategorie = (prods, cat) =>
    prods.filter(p => p.categorie === cat);

// b) Prix moyen
const prixMoyen = (prods) => {
    const total = prods.reduce((sum, p) => sum + p.prix, 0);
    return (total / prods.length).toFixed(2);
};

// c) Produit le moins cher
const moinsCher = (prods) =>
    prods.reduce((min, p) => p.prix < min.prix ? p : min);

// d) Grouper par catégorie
const grouperParCategorie = (prods) =>
    prods.reduce((acc, p) => {
        (acc[p.categorie] = acc[p.categorie] || []).push(p);
        return acc;
    }, {});

console.log(filtrerParCategorie(produits, "Electronique"));
console.log(prixMoyen(produits));
console.log(moinsCher(produits));
console.log(grouperParCategorie(produits));


/*
EXERCICE 2.2 : Système de notes

Créez une fonction analyserNotes(etudiants) qui retourne :
{
  moyenne: number,
  meilleur: { nom, note },
  pire: { nom, note },
  repartition: { A: n, B: n, C: n, D: n, F: n }
}
*/

// CORRIGÉ 2.2 :
function analyserNotes(etudiants) {
    const notes = etudiants.map(e => e.note);
    const moyenne = notes.reduce((s, n) => s + n, 0) / notes.length;

    const meilleur = etudiants.reduce((best, e) =>
        e.note > best.note ? e : best
    );
    const pire = etudiants.reduce((worst, e) =>
        e.note < worst.note ? e : worst
    );

    const getGrade = note => {
        if (note >= 90) return "A";
        if (note >= 80) return "B";
        if (note >= 70) return "C";
        if (note >= 60) return "D";
        return "F";
    };

    const repartition = etudiants.reduce((acc, e) => {
        const grade = getGrade(e.note);
        acc[grade] = (acc[grade] || 0) + 1;
        return acc;
    }, {});

    return {
        moyenne: parseFloat(moyenne.toFixed(2)),
        meilleur: { nom: meilleur.nom, note: meilleur.note },
        pire: { nom: pire.nom, note: pire.note },
        repartition
    };
}

console.log(analyserNotes(etudiants));


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 1
// ============================================================================

/*
CE QUE VOUS AVEZ APPRIS :

Chapitre 0 - Introduction :
[OK] JS : Le seul langage du navigateur
[OK] Environnement : VS Code + Node.js + Chrome DevTools
[OK] console.log() pour debug

Chapitre 1 - Variables, Types, Opérateurs :
[OK] const (préféré), let, var (éviter)
[OK] 7 types primitifs + object
[OK] typeof, coercition de types
[OK] ===  (strict) vs == (loose)
[OK] Opérateur ??, ?., ternaire
[OK] Template literals `${variable}`

Chapitre 2 - Contrôle :
[OK] if/else if/else
[OK] switch/case/default
[OK] for, while, do-while
[OK] for...of (valeurs), for...in (clés)
[OK] break, continue

Chapitre 3 - Fonctions :
[OK] function declaration (hoistée)
[OK] function expression
[OK] Arrow function =>
[OK] Paramètres par défaut, rest (...)
[OK] Spread operator (...)
[OK] Destructuring dans paramètres

Chapitre 4 - Tableaux et Objets :
[OK] map(), filter(), reduce(), find(), some(), every()
[OK] Object.keys(), values(), entries()
[OK] Destructuring [] et {}
[OK] Spread operator pour copie/fusion
[OK] Map et Set ES6


[CLE] POINTS CLÉS À RETENIR :

1. TOUJOURS === (jamais ==)
2. TOUJOURS const (sinon let, jamais var)
3. Arrow functions pour les callbacks
4. Préférer map/filter/reduce aux boucles for
5. Utiliser la déstructuration
6. Template literals (backticks) pour les strings


-> PROCHAINE ÉTAPE : PARTIE 2 - POO et Modules
*/

// ============================================================================
// [LIVRE] JAVASCRIPT - PARTIE 2 : PROGRAMMATION ORIENTÉE OBJET
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 5 : Classes et Prototypes
// - Chapitre 6 : Héritage et Polymorphisme
// - Chapitre 7 : Modules ES6
// - Chapitre 8 : Gestion d'Erreurs
//
// [TEMPS] TEMPS : ~5-6 heures
// [DOCS] PRÉREQUIS : Partie 1 complétée
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 5 : CLASSES ET PROTOTYPES
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le système de prototypes de JS
[OK] Créer des classes (ES6)
[OK] Utiliser constructeurs, méthodes, propriétés
[OK] Getters, setters, méthodes statiques
[OK] Champs privés (#)
[OK] Comprendre 'this'
*/


// ----------------------------------------------------------------------------
// [ANALYSE] PROTOTYPES : LA BASE DE TOUT
// ----------------------------------------------------------------------------

/*
JAVASCRIPT EST UN LANGAGE À PROTOTYPES

Contrairement à Java, Python, etc. qui ont des classes "vraies",
JavaScript utilisait à l'origine un système de PROTOTYPES.

Les "classes" ES6 sont du SUCRE SYNTAXIQUE sur les prototypes.
Comprendre les prototypes = comprendre JS en profondeur.


[IDEE] LE PROTOTYPE CHAIN

Chaque objet a un [[Prototype]] (ancêtre).
Quand on accède à une propriété :
1. JS cherche dans l'objet
2. Si pas trouvé -> cherche dans le prototype
3. Si pas trouvé -> cherche dans le prototype du prototype
4. ... jusqu'à null

C'est la "Prototype Chain" !
*/

// Objet simple
const animal = {
    respirer() {
        console.log("Je respire");
    }
};

const chien = {
    aboyer() {
        console.log("Ouaf !");
    }
};

// Définir le prototype de chien = animal
Object.setPrototypeOf(chien, animal);

chien.aboyer();   // "Ouaf !" (méthode propre)
chien.respirer(); // "Je respire" (héritée du prototype !)

// Vérifications
console.log(chien.hasOwnProperty("aboyer"));  // true (méthode propre)
console.log(chien.hasOwnProperty("respirer")); // false (héritée)
console.log(Object.getPrototypeOf(chien) === animal); // true

// Chaîne de prototypes
// chien -> animal -> Object.prototype -> null

/*
AVANT ES6 : Fonctions Constructeur
*/

// Vieille façon (comprendre pour lire ancien code)
function Person(nom, age) {
    this.nom = nom;
    this.age = age;
}

Person.prototype.saluer = function() {
    return `Bonjour, je suis ${this.nom}`;
};

const alice = new Person("Alice", 25);
console.log(alice.saluer()); // "Bonjour, je suis Alice"


// ----------------------------------------------------------------------------
// [CLASSICAL_BUILDING] CLASSES ES6 (SYNTAXE MODERNE)
// ----------------------------------------------------------------------------

class Animal {
    // 1. Champs de classe (ES2022) - déclarés avant le constructeur
    espece = "inconnue";       // Public
    #energie = 100;            // Privé (# = private !)
    static compteur = 0;       // Static public

    // 2. Constructeur
    constructor(nom, age) {
        this.nom = nom;        // Propriété d'instance
        this.age = age;
        Animal.compteur++;     // Incrémente compteur static
    }

    // 3. Méthodes d'instance
    manger(aliment) {
        this.#energie += 20;
        return `${this.nom} mange ${aliment}. Énergie: ${this.#energie}`;
    }

    seReposer() {
        this.#energie = 100;
        return `${this.nom} se repose. Énergie restaurée !`;
    }

    // 4. Getter
    get info() {
        return `${this.nom} (${this.age} ans)`;
    }

    // 5. Setter
    set nomAnimal(valeur) {
        if (typeof valeur !== "string") {
            throw new TypeError("Le nom doit être une string");
        }
        this.nom = valeur.trim();
    }

    // 6. Méthode statique (appartient à la CLASSE, pas aux instances)
    static creerGeneral(nom) {
        return new Animal(nom, 0);
    }

    static getCompteur() {
        return Animal.compteur;
    }

    // 7. Méthode privée
    #calculerSante() {
        return this.#energie > 50 ? "Bonne santé" : "Fatigué";
    }

    bilan() {
        return `${this.nom}: ${this.#calculerSante()} (${this.#energie}%)`;
    }

    // 8. toString (bonne pratique)
    toString() {
        return `Animal[${this.nom}, ${this.age}ans]`;
    }
}

// Utilisation
const chat = new Animal("Felix", 3);
console.log(chat.manger("croquettes")); // Felix mange croquettes. Énergie: 120
console.log(chat.info);                 // "Felix (3 ans)" (getter)
chat.nomAnimal = "Félix le chat";       // Setter
console.log(chat.bilan());

// Méthode statique
const bebe = Animal.creerGeneral("BabyAnimal");
console.log(Animal.getCompteur()); // 2

// Privé innaccessible de l'extérieur
// chat.#energie;          // SyntaxError
// chat.#calculerSante();  // SyntaxError


// ----------------------------------------------------------------------------
// [LOGIQUE] COMPRENDRE 'this'
// ----------------------------------------------------------------------------

/*
'this' est un des concepts les plus CONFUS de JavaScript.
Sa valeur dépend du CONTEXTE D'APPEL.

RÈGLES :
1. Dans un objet/méthode  -> 'this' = l'objet
2. Appel libre (fonction) -> 'this' = undefined (strict) ou global
3. Arrow function          -> 'this' = contexte lexical (parent)
4. Event handler (DOM)     -> 'this' = l'élément déclencheur
5. new                     -> 'this' = nouvel objet créé
6. call/apply/bind         -> 'this' = ce qu'on passe
*/

// 1. Dans une méthode
const objet = {
    nom: "Alice",
    saluer() {
        console.log(`Bonjour, ${this.nom}`); // this = objet
    }
};
objet.saluer(); // "Bonjour, Alice"

// 2. Perte de contexte (PIÈGE CLASSIQUE !)
const saluer = objet.saluer;
// saluer(); // this = undefined (ou window en non-strict)

// 3. Arrow function préserve le 'this'
class Timer {
    constructor() {
        this.ticks = 0;
    }

    start() {
        // [OK] Arrow function : this = instance de Timer
        this.interval = setInterval(() => {
            this.ticks++;
            console.log(this.ticks);
        }, 1000);
    }

    stop() {
        clearInterval(this.interval);
    }
}

// 4. call, apply, bind
function direBonjour(salutation, ponctuation) {
    return `${salutation} ${this.nom}${ponctuation}`;
}

const personne = { nom: "Alice" };

// call : appelle immédiatement, args séparés
console.log(direBonjour.call(personne, "Bonjour", "!")); // "Bonjour Alice!"

// apply : appelle immédiatement, args en tableau
console.log(direBonjour.apply(personne, ["Hello", "."])); // "Hello Alice."

// bind : retourne une NOUVELLE fonction liée
const direBonjou = direBonjour.bind(personne, "Salut");
console.log(direBonjou("?")); // "Salut Alice?"


// ============================================================================
// [GUIDE] CHAPITRE 6 : HÉRITAGE ET POLYMORPHISME
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser extends pour l'héritage
[OK] Utiliser super pour accéder au parent
[OK] Comprendre le polymorphisme
[OK] Mixins (composition)
[OK] instanceof
*/


// ----------------------------------------------------------------------------
// [LIEN] HÉRITAGE AVEC extends
// ----------------------------------------------------------------------------

class Vehicule {
    #vitesse = 0;
    static nombreVehicules = 0;

    constructor(marque, modele, annee) {
        this.marque = marque;
        this.modele = modele;
        this.annee = annee;
        Vehicule.nombreVehicules++;
    }

    accelerer(km) {
        this.#vitesse += km;
        return `${this.marque} accélère à ${this.#vitesse} km/h`;
    }

    freiner() {
        this.#vitesse = 0;
        return `${this.marque} s'arrête`;
    }

    get vitesse() { return this.#vitesse; }

    decrire() {
        return `${this.marque} ${this.modele} (${this.annee})`;
    }

    toString() {
        return `[Vehicule: ${this.decrire()}]`;
    }
}

// Héritage simple
class Voiture extends Vehicule {
    constructor(marque, modele, annee, nbrPortes = 4) {
        super(marque, modele, annee); // [ATTENTION] OBLIGATOIRE avant this !
        this.nbrPortes = nbrPortes;
        this.estDemarree = false;
    }

    demarrer() {
        this.estDemarree = true;
        return `${this.marque} démarre... Vroom !`;
    }

    // Override de la méthode parent (polymorphisme)
    decrire() {
        return `${super.decrire()} - ${this.nbrPortes} portes`;
        //      ^ super.methode() pour appeler la méthode parent
    }
}

// Héritage multi-niveau
class VoitureElectrique extends Voiture {
    #batterie = 100;

    constructor(marque, modele, annee, autonomie) {
        super(marque, modele, annee, 4);
        this.autonomie = autonomie;
    }

    charger(pourcentage) {
        this.#batterie = Math.min(100, this.#batterie + pourcentage);
        return `${this.marque} chargée à ${this.#batterie}%`;
    }

    accelerer(km) {
        this.#batterie -= km * 0.2;
        return `${super.accelerer(km)} (Batterie: ${this.#batterie.toFixed(0)}%)`;
    }

    decrire() {
        return `${super.decrire()} [RAPIDE] (${this.autonomie}km autonomie)`;
    }
}

// Utilisation
const voiture = new Voiture("Renault", "Clio", 2022);
const tesla = new VoitureElectrique("Tesla", "Model 3", 2023, 500);

console.log(voiture.demarrer());      // "Renault démarre..."
console.log(voiture.decrire());       // "Renault Clio (2022) - 4 portes"
console.log(tesla.decrire());         // "Tesla Model 3 (2023) - 4 portes [RAPIDE] (500km autonomie)"
console.log(tesla.accelerer(30));     // "Tesla accélère à 30 km/h (Batterie: 94%)"

// INSTANCEOF
console.log(tesla instanceof VoitureElectrique); // true
console.log(tesla instanceof Voiture);            // true
console.log(tesla instanceof Vehicule);           // true
console.log(tesla instanceof Animal);             // false


// ----------------------------------------------------------------------------
// [MODULE] POLYMORPHISME EN PRATIQUE
// ----------------------------------------------------------------------------

/*
Le polymorphisme = même interface, comportements différents
*/

class Forme {
    constructor(couleur) {
        this.couleur = couleur;
    }

    // Méthode à redéfinir dans les classes filles
    aire() {
        throw new Error("La méthode aire() doit être implémentée");
    }

    perimetre() {
        throw new Error("La méthode perimetre() doit être implémentée");
    }

    decrire() {
        return `${this.constructor.name} ${this.couleur}: aire=${this.aire().toFixed(2)}, périmètre=${this.perimetre().toFixed(2)}`;
    }
}

class Cercle extends Forme {
    constructor(rayon, couleur) {
        super(couleur);
        this.rayon = rayon;
    }

    aire() { return Math.PI * this.rayon ** 2; }
    perimetre() { return 2 * Math.PI * this.rayon; }
}

class Rectangle extends Forme {
    constructor(largeur, hauteur, couleur) {
        super(couleur);
        this.largeur = largeur;
        this.hauteur = hauteur;
    }

    aire() { return this.largeur * this.hauteur; }
    perimetre() { return 2 * (this.largeur + this.hauteur); }
}

class Triangle extends Forme {
    constructor(base, hauteur, couleur) {
        super(couleur);
        this.base = base;
        this.hauteur = hauteur;
        this.hypotenuse = Math.sqrt(base ** 2 + hauteur ** 2);
    }

    aire() { return (this.base * this.hauteur) / 2; }
    perimetre() { return this.base + this.hauteur + this.hypotenuse; }
}

// POLYMORPHISME EN ACTION
const formes = [
    new Cercle(5, "rouge"),
    new Rectangle(4, 6, "bleu"),
    new Triangle(3, 4, "vert")
];

// Même appel, comportements différents selon la classe !
formes.forEach(f => console.log(f.decrire()));

// Aire totale (polymorphisme)
const aireTotal = formes.reduce((sum, f) => sum + f.aire(), 0);
console.log(`Aire totale: ${aireTotal.toFixed(2)}`);


// ----------------------------------------------------------------------------
// [DESIGN] MIXINS (COMPOSITION)
// ----------------------------------------------------------------------------

/*
JavaScript ne supporte pas l'héritage multiple.
Les MIXINS permettent d'ajouter des comportements depuis plusieurs sources.

PRINCIPE : "Composition over Inheritance"
Au lieu de hériter d'une classe, on "mélange" des comportements.
*/

// Mixin serializable
const Serializable = (Base) => class extends Base {
    toJSON() {
        return JSON.stringify(this);
    }

    static fromJSON(json) {
        return Object.assign(new this(), JSON.parse(json));
    }
};

// Mixin avec timestamp
const Timestamped = (Base) => class extends Base {
    constructor(...args) {
        super(...args);
        this.createdAt = new Date().toISOString();
        this.updatedAt = new Date().toISOString();
    }

    update(data) {
        Object.assign(this, data);
        this.updatedAt = new Date().toISOString();
        return this;
    }
};

// Mixin validable
const Validatable = (Base) => class extends Base {
    validate() {
        const errors = [];
        if (this.validateRules) {
            this.validateRules.forEach(({ field, required, minLength }) => {
                if (required && !this[field]) {
                    errors.push(`${field} est requis`);
                }
                if (minLength && this[field]?.length < minLength) {
                    errors.push(`${field} doit faire au moins ${minLength} caractères`);
                }
            });
        }
        return errors;
    }
};

// Classe de base simple
class BaseModel {
    constructor(data = {}) {
        Object.assign(this, data);
    }
}

// Composer les mixins
class User extends Serializable(Timestamped(Validatable(BaseModel))) {
    validateRules = [
        { field: "email", required: true },
        { field: "nom", required: true, minLength: 2 }
    ];

    constructor(data) {
        super(data);
    }
}

const user2 = new User({ nom: "Alice", email: "alice@ex.com" });
console.log(user2.toJSON());
console.log(user2.validate()); // []


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 3 : POO
// ----------------------------------------------------------------------------

/*
EXERCICE 3.1 : Système bancaire

Créez un système avec :
- CompteBase : solde, titulaire, méthodes basiques
- CompteCourant : découvert autorisé
- CompteEpargne : taux d'intérêt, plafond
- ComptePremium (extends CompteCourant) : cashback sur dépôts

Chaque compte doit :
- Permettre dépôt et retrait
- Vérifier les limites
- Historique des transactions
*/

// CORRIGÉ 3.1 :

class CompteBase {
    #solde;
    #historique = [];

    constructor(titulaire, soldeInitial = 0) {
        this.titulaire = titulaire;
        this.#solde = soldeInitial;
        this.#enregistrer("ouverture", soldeInitial);
    }

    #enregistrer(type, montant) {
        this.#historique.push({
            type,
            montant,
            date: new Date().toISOString(),
            solde: this.#solde
        });
    }

    deposer(montant) {
        if (montant <= 0) throw new Error("Le montant doit être positif");
        this.#solde += montant;
        this.#enregistrer("dépôt", montant);
        return `Dépôt de ${montant}€. Nouveau solde: ${this.#solde}€`;
    }

    retirer(montant) {
        if (montant <= 0) throw new Error("Le montant doit être positif");
        if (!this.#verifierRetrait(montant)) {
            throw new Error("Fonds insuffisants");
        }
        this.#solde -= montant;
        this.#enregistrer("retrait", -montant);
        return `Retrait de ${montant}€. Nouveau solde: ${this.#solde}€`;
    }

    // Méthode à surcharger dans les sous-classes
    #verifierRetrait(montant) {
        return this.#solde >= montant;
    }

    get solde() { return this.#solde; }
    get historique() { return [...this.#historique]; }

    toString() {
        return `[${this.constructor.name}] ${this.titulaire}: ${this.#solde}€`;
    }
}

class CompteCourant extends CompteBase {
    constructor(titulaire, soldeInitial = 0, decouvertMax = 500) {
        super(titulaire, soldeInitial);
        this.decouvertMax = decouvertMax;
    }

    retirer(montant) {
        if (this.solde - montant < -this.decouvertMax) {
            throw new Error(`Découvert max (${this.decouvertMax}€) dépassé`);
        }
        return super.retirer(montant);
    }
}

class CompteEpargne extends CompteBase {
    constructor(titulaire, soldeInitial = 0, tauxAnnuel = 0.02, plafond = 22950) {
        super(titulaire, soldeInitial);
        this.tauxAnnuel = tauxAnnuel;
        this.plafond = plafond;
    }

    deposer(montant) {
        if (this.solde + montant > this.plafond) {
            throw new Error(`Plafond (${this.plafond}€) dépassé`);
        }
        return super.deposer(montant);
    }

    calculerInterets() {
        const interets = this.solde * this.tauxAnnuel;
        return super.deposer(interets);
    }
}

// Test
const courant = new CompteCourant("Alice", 1000);
console.log(courant.deposer(500));     // 1500€
console.log(courant.retirer(1800));    // -300€ (découvert)
console.log(`${courant}`);


// ============================================================================
// [GUIDE] CHAPITRE 7 : MODULES ES6
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre pourquoi les modules sont essentiels
[OK] Exporter (export) des fonctions, classes, variables
[OK] Importer (import) ce dont vous avez besoin
[OK] Import/Export par défaut vs nommé
[OK] Import dynamique
[OK] Modules CommonJS (Node.js)
*/


// ----------------------------------------------------------------------------
// [PACKAGE] POURQUOI LES MODULES ?
// ----------------------------------------------------------------------------

/*
PROBLÈMES SANS MODULES :

1. Variables globales (conflits de noms)
2. Dépendances implicites
3. Ordre d'inclusion important
4. Code difficile à maintenir

AVEC MODULES :
[OK] Encapsulation
[OK] Réutilisabilité
[OK] Dépendances explicites
[OK] Pas de pollution globale
*/

// ----------------------------------------------------------------------------
// [SORTIE] EXPORT (Exporter)
// ----------------------------------------------------------------------------

// FICHIER : utils.js

// EXPORTS NOMMÉS (plusieurs exports par fichier)

// Export de variable
export const PI = 3.14159;
export const VERSION = "1.0.0";

// Export de fonction
export function additionner(a, b) {
    return a + b;
}

export const multiplier = (a, b) => a * b;

// Export de classe
export class Calculatrice {
    constructor() {
        this.historique = [];
    }

    calculer(a, op, b) {
        let result;
        switch(op) {
            case "+": result = a + b; break;
            case "-": result = a - b; break;
            case "*": result = a * b; break;
            case "/": result = b !== 0 ? a / b : null; break;
        }
        this.historique.push({ a, op, b, result });
        return result;
    }
}

// Export groupé à la fin
function formater(n) { return n.toFixed(2); }
function arrondir(n) { return Math.round(n); }
// export { formater, arrondir };  // Décommentez dans vrai module

// Renommage à l'export
// export { formater as format, arrondir as round };


// EXPORT PAR DÉFAUT (un seul par fichier)
// export default class MathUtils { ... }
// OU
// const MathUtils = { ... };
// export default MathUtils;


// ----------------------------------------------------------------------------
// [ENTREE] IMPORT (Importer)
// ----------------------------------------------------------------------------

// FICHIER : main.js

// Import nommé
// import { PI, additionner, Calculatrice } from './utils.js';

// Import avec renommage
// import { additionner as add, multiplier as mul } from './utils.js';

// Import de tout
// import * as Utils from './utils.js';
// Utils.additionner(2, 3);

// Import par défaut
// import MathUtils from './mathUtils.js';

// Import par défaut + nommé
// import MathUtils, { PI, additionner } from './utils.js';

// Import dynamique (lazy loading)
/*
async function chargerModule() {
    const module = await import('./utils.js');
    console.log(module.additionner(2, 3));
}
*/


// ----------------------------------------------------------------------------
// [PACKAGE] MODULES COMMONJS (NODE.JS) - SYSTÈME LEGACY
// ----------------------------------------------------------------------------

/*
Node.js utilisait CommonJS avant les modules ES6.
Vous verrez souvent require() et module.exports dans le code existant.
*/

// COMMONJS EXPORT (utils.cjs)
// const PI = 3.14159;
// function ajouter(a, b) { return a + b; }
// module.exports = { PI, ajouter };
// OU pour export par défaut :
// module.exports = ajouter;

// COMMONJS IMPORT (main.cjs)
// const { PI, ajouter } = require('./utils.cjs');
// const ajouter = require('./utils.cjs'); // import par défaut

/*
[IDEE] DIFFÉRENCES CJS vs ESM :

CommonJS (require)     | ES Modules (import)
──────────────────────|─────────────────────
Synchrone              | Asynchrone
Runtime                | Compilation
Partiel                | Top-level seulement
Tout fichier .js       | .mjs ou "type":"module"
*/


// ============================================================================
// [GUIDE] CHAPITRE 8 : GESTION D'ERREURS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser try/catch/finally
[OK] Créer des erreurs personnalisées
[OK] Propager et gérer les erreurs
[OK] Gestion d'erreurs asynchrones
[OK] Types d'erreurs natifs
*/


// ----------------------------------------------------------------------------
// [ALERTE] TRY / CATCH / FINALLY
// ----------------------------------------------------------------------------

/*
STRUCTURE :
try {
    // Code qui peut échouer
} catch (erreur) {
    // Traitement de l'erreur
} finally {
    // Toujours exécuté (avec ou sans erreur)
}
*/

// Exemple basique
function diviser(a, b) {
    try {
        if (b === 0) throw new Error("Division par zéro impossible");
        return a / b;
    } catch (err) {
        console.error(`Erreur: ${err.message}`);
        return null;
    } finally {
        console.log("Opération de division terminée"); // Toujours exécuté
    }
}

console.log(diviser(10, 2));  // 5, puis "Opération terminée"
console.log(diviser(10, 0));  // Erreur, null, puis "Opération terminée"


// ----------------------------------------------------------------------------
// [SCENARIO] TYPES D'ERREURS NATIFS
// ----------------------------------------------------------------------------

/*
JavaScript a plusieurs types d'erreurs intégrés :
*/

// ReferenceError : Variable non déclarée
try {
    console.log(variableNonDeclaree);
} catch (e) {
    console.log(e instanceof ReferenceError); // true
    console.log(e.name);    // "ReferenceError"
    console.log(e.message); // "variableNonDeclaree is not defined"
}

// TypeError : Type incorrect
try {
    null.toString();
} catch (e) {
    console.log(e instanceof TypeError); // true
}

// SyntaxError : Syntaxe invalide (généralement au parsing)
try {
    eval("const x = {"); // SyntaxError
} catch (e) {
    console.log(e instanceof SyntaxError); // true
}

// RangeError : Valeur hors limites
try {
    new Array(-1); // RangeError: Invalid array length
} catch (e) {
    console.log(e instanceof RangeError); // true
}

// URIError
try {
    decodeURIComponent("%");
} catch (e) {
    console.log(e instanceof URIError); // true
}

// EvalError (rare)
// AggregateError (ES2021 - Promise.any)


// ----------------------------------------------------------------------------
// [OUTILS] ERREURS PERSONNALISÉES
// ----------------------------------------------------------------------------

// Créer ses propres types d'erreurs
class AppError extends Error {
    constructor(message, code, statusCode = 500) {
        super(message);         // Appeler le constructeur parent
        this.name = "AppError"; // Surcharger le nom
        this.code = code;
        this.statusCode = statusCode;
        this.timestamp = new Date().toISOString();

        // Capturer la stack trace (Node.js)
        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, AppError);
        }
    }
}

class ValidationError extends AppError {
    constructor(champ, message) {
        super(message, "VALIDATION_ERROR", 400);
        this.name = "ValidationError";
        this.champ = champ;
    }
}

class NotFoundError extends AppError {
    constructor(ressource, id) {
        super(`${ressource} avec l'id ${id} introuvable`, "NOT_FOUND", 404);
        this.name = "NotFoundError";
        this.ressource = ressource;
    }
}

class AuthError extends AppError {
    constructor(message = "Non autorisé") {
        super(message, "AUTH_ERROR", 401);
        this.name = "AuthError";
    }
}

// Utilisation
function trouverUser(id) {
    const users = { 1: { nom: "Alice" }, 2: { nom: "Bob" } };

    if (!Number.isInteger(id)) {
        throw new ValidationError("id", "L'id doit être un entier");
    }

    const user = users[id];
    if (!user) {
        throw new NotFoundError("User", id);
    }

    return user;
}

function gererErreur(fn, ...args) {
    try {
        return fn(...args);
    } catch (err) {
        if (err instanceof ValidationError) {
            console.error(`Validation: ${err.champ} - ${err.message}`);
        } else if (err instanceof NotFoundError) {
            console.error(`${err.ressource} #${err.id} introuvable`);
        } else if (err instanceof AppError) {
            console.error(`Erreur app [${err.code}]: ${err.message}`);
        } else {
            console.error(`Erreur inattendue: ${err.message}`);
            throw err; // Re-lancer si on ne sait pas gérer
        }
        return null;
    }
}

console.log(gererErreur(trouverUser, 1));     // { nom: "Alice" }
console.log(gererErreur(trouverUser, 99));    // Erreur NotFound
console.log(gererErreur(trouverUser, "abc")); // Erreur Validation


// ----------------------------------------------------------------------------
// [RAPIDE] GESTION D'ERREURS ASYNCHRONES
// ----------------------------------------------------------------------------

// AVEC PROMISES
function fetchData(url) {
    return fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error(`HTTP Error: ${response.status}`);
            }
            return response.json();
        })
        .catch(err => {
            console.error("Erreur fetch:", err.message);
            return null;
        });
}

// AVEC ASYNC/AWAIT
async function chargerDonnees(id) {
    try {
        const response = await fetch(`https://api.example.com/users/${id}`);

        if (!response.ok) {
            throw new NotFoundError("User", id);
        }

        const data = await response.json();
        return data;

    } catch (err) {
        if (err instanceof NotFoundError) {
            console.log(`User ${id} non trouvé`);
            return null;
        }

        // Autres erreurs (réseau, etc.)
        console.error("Erreur réseau:", err.message);
        throw err; // Re-lancer
    } finally {
        console.log(`Chargement user ${id} terminé`);
    }
}

// PROMISE TOUTES EN PARALLÈLE avec gestion d'erreurs
async function chargerTout() {
    const ids = [1, 2, 3, 99]; // 99 n'existe pas

    // allSettled : attend toutes les promises, y compris les erreurs
    const resultats = await Promise.allSettled(
        ids.map(id => chargerDonnees(id))
    );

    resultats.forEach((resultat, i) => {
        if (resultat.status === "fulfilled") {
            console.log(`User ${ids[i]}: succès`);
        } else {
            console.log(`User ${ids[i]}: erreur - ${resultat.reason.message}`);
        }
    });
}


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 4 : POO Avancée et Modules
// ----------------------------------------------------------------------------

/*
EXERCICE 4.1 : Mini Jeu RPG

Créez un système de jeu RPG avec :
- Personnage (base)
  - Guerrier (extend)
  - Mage (extend)
  - Archer (extend)
- Chaque classe a ses attaques spéciales
- Gestion HP, magie, niveau
*/

// CORRIGÉ 4.1 :

class Personnage {
    #hp;
    #hpMax;
    #mort = false;

    constructor(nom, hpMax, force) {
        this.nom = nom;
        this.#hpMax = hpMax;
        this.#hp = hpMax;
        this.force = force;
        this.niveau = 1;
        this.xp = 0;
    }

    attaquer(cible) {
        if (this.#mort) throw new Error(`${this.nom} est mort !`);
        const degats = this.force + Math.floor(Math.random() * 10);
        const msg = cible.recevoirDegats(degats);
        return `${this.nom} attaque ${cible.nom}: ${msg}`;
    }

    recevoirDegats(degats) {
        this.#hp = Math.max(0, this.#hp - degats);
        if (this.#hp === 0) {
            this.#mort = true;
            return `${this.nom} subit ${degats} dégâts et meurt !`;
        }
        return `${this.nom} subit ${degats} dégâts (${this.#hp}/${this.#hpMax} HP)`;
    }

    soigner(montant) {
        if (this.#mort) return `${this.nom} est mort et ne peut être soigné`;
        this.#hp = Math.min(this.#hpMax, this.#hp + montant);
        return `${this.nom} récupère ${montant} HP (${this.#hp}/${this.#hpMax})`;
    }

    gagnerXP(xp) {
        this.xp += xp;
        if (this.xp >= this.niveau * 100) {
            this.xp = 0;
            this.niveau++;
            this.force += 2;
            return `[BRAVO] ${this.nom} passe niveau ${this.niveau} !`;
        }
        return `${this.nom} gagne ${xp} XP`;
    }

    get estMort() { return this.#mort; }
    get hpActuel() { return this.#hp; }
    get hpMax() { return this.#hpMax; }

    status() {
        return `${this.nom} (Niv.${this.niveau}) - HP: ${this.#hp}/${this.#hpMax} - Force: ${this.force}`;
    }
}

class Guerrier extends Personnage {
    #bouclierActif = false;

    constructor(nom) {
        super(nom, 150, 20);
        this.type = "Guerrier [COMBAT]";
    }

    attaqueFurieuse(cible) {
        const degats = this.force * 2.5;
        return `[COMBAT] Attaque furieuse ! ${cible.recevoirDegats(Math.floor(degats))}`;
    }

    activerBouclier() {
        this.#bouclierActif = true;
        return `[SECURITE] ${this.nom} active son bouclier !`;
    }

    recevoirDegats(degats) {
        const reduction = this.#bouclierActif ? Math.floor(degats * 0.5) : degats;
        this.#bouclierActif = false; // Bouclier utilisé
        return super.recevoirDegats(reduction);
    }
}

class Mage extends Personnage {
    #mana;
    #manaMax;

    constructor(nom) {
        super(nom, 80, 8);
        this.#manaMax = 100;
        this.#mana = 100;
        this.type = "Mage [CRYSTAL_BALL]";
    }

    bouleDesFeu(cible) {
        const cout = 30;
        if (this.#mana < cout) return "Pas assez de mana !";
        this.#mana -= cout;
        const degats = this.force * 4;
        return `[HOT] Boule de feu ! ${cible.recevoirDegats(Math.floor(degats))} (Mana: ${this.#mana})`;
    }

    mediter() {
        this.#mana = Math.min(this.#manaMax, this.#mana + 50);
        return `[DIZZY_SYMBOL] ${this.nom} médite. Mana: ${this.#mana}/${this.#manaMax}`;
    }

    get mana() { return this.#mana; }
}

// Test combat
const guerrier = new Guerrier("Thor");
const mage = new Mage("Gandalf");

console.log(guerrier.status());
console.log(mage.status());

console.log(mage.bouleDesFeu(guerrier));
console.log(guerrier.attaqueFurieuse(mage));
console.log(guerrier.activerBouclier());
console.log(mage.attaquer(guerrier));  // Bouclier réduit les dégâts


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 2
// ============================================================================

/*
CE QUE VOUS AVEZ APPRIS :

Chapitre 5 - Classes :
[OK] Prototypes (la base de tout JS)
[OK] class, constructor, méthodes
[OK] Champs publics/privés (#)
[OK] Getters/Setters
[OK] Méthodes statiques
[OK] 'this' et ses pièges
[OK] call/apply/bind

Chapitre 6 - Héritage :
[OK] extends (héritage)
[OK] super() (constructeur parent)
[OK] super.methode() (méthode parent)
[OK] Polymorphisme
[OK] instanceof
[OK] Mixins (composition)

Chapitre 7 - Modules :
[OK] export nommé, export default
[OK] import sélectif, import *
[OK] Import dynamique
[OK] CommonJS (require/module.exports)

Chapitre 8 - Erreurs :
[OK] try/catch/finally
[OK] Types d'erreurs natifs
[OK] Erreurs personnalisées (extends Error)
[OK] Gestion async (try/await/catch)
[OK] Promise.allSettled


[CLE] POINTS CLÉS :

1. Les classes JS sont du sucre syntaxique sur les prototypes
2. # pour les propriétés privées (ES2022)
3. Arrow functions pour préserver 'this'
4. Toujours créer des erreurs personnalisées
5. try/catch pour toutes les operations asynchrones
6. Préférer la composition (mixins) à l'héritage profond


-> PROCHAINE ÉTAPE : PARTIE 3 - Asynchrone et DOM
*/

// ============================================================================
// [LIVRE] JAVASCRIPT - PARTIE 3 : ASYNCHRONE ET DOM
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 9  : Asynchrone - Callbacks, Promises, Async/Await
// - Chapitre 10 : Manipulation du DOM
// - Chapitre 11 : Événements
// - Chapitre 12 : Fetch API et AJAX
//
// [TEMPS] TEMPS : ~6-8 heures
// [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 9 : PROGRAMMATION ASYNCHRONE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre le modèle d'exécution de JS (Event Loop)
[OK] Utiliser les callbacks (et leurs problèmes)
[OK] Créer et chaîner des Promises
[OK] Utiliser async/await
[OK] Gérer les erreurs asynchrones
[OK] Utiliser Promise.all, allSettled, race, any
*/


// ----------------------------------------------------------------------------
// [SYNC] L'EVENT LOOP - COMMENT JS FONCTIONNE
// ----------------------------------------------------------------------------

/*
JavaScript est SINGLE-THREADED (un seul fil d'exécution).
Mais il peut gérer des opérations ASYNCHRONES grâce à l'Event Loop.

COMPOSANTS :
┌────────────────────────────────────────────┐
│            JavaScript Engine               │
│  ┌───────────────┐  ┌──────────────────┐   │
│  │  Call Stack   │  │    Heap (Memory) │   │
│  │  (pile d'appel)│  │   (Objets, etc.) │   │
│  └───────────────┘  └──────────────────┘   │
└────────────────────────────────────────────┘

┌───────────────────────────────────────────────┐
│              Web APIs (Browser)               │
│  setTimeout, fetch, DOM events, etc.          │
└───────────────────────────────────────────────┘

┌─────────────┐    ┌─────────────────────────┐
│  Task Queue │    │  Microtask Queue         │
│  (Callbacks)│    │  (Promises, queueMicrotask)│
└─────────────┘    └─────────────────────────┘


ORDRE D'EXÉCUTION :
1. Call Stack (code synchrone)
2. Microtask Queue (Promises - PRIORITAIRE)
3. Task Queue (setTimeout, events, etc.)


EXEMPLE CONCRET :
*/

console.log("1"); // Synchrone

setTimeout(() => console.log("2"), 0); // Task Queue (même si 0ms !)

Promise.resolve().then(() => console.log("3")); // Microtask Queue

console.log("4"); // Synchrone

// RÉSULTAT : 1, 4, 3, 2
// Explication :
// 1. "1" -> Call Stack
// 4. "4" -> Call Stack
// 3. "3" -> Microtask Queue (avant setTimeout !)
// 2. "2" -> Task Queue (dernier)


// ----------------------------------------------------------------------------
// [TEL] CALLBACKS (ANCIEN STYLE - COMPRENDRE LE "CALLBACK HELL")
// ----------------------------------------------------------------------------

/*
Les callbacks étaient la seule solution asynchrone au début.
Problème : Le "Callback Hell" (aussi appelé "Pyramid of Doom")
*/

// [X] Callback Hell (difficile à lire et maintenir)
function chargerDonneesCallback(userId, callback) {
    setTimeout(() => {
        const user = { id: userId, nom: "Alice" };

        // Callback interne
        chargerPostsCallback(userId, (posts) => {

            // Callback encore plus imbriqué
            chargerCommentsCallback(posts[0].id, (comments) => {

                // Et encore...
                callback({
                    user,
                    posts,
                    premiersCommentaires: comments
                });
            });
        });
    }, 100);
}

function chargerPostsCallback(userId, callback) {
    setTimeout(() => {
        callback([{ id: 1, titre: "Post 1" }, { id: 2, titre: "Post 2" }]);
    }, 100);
}

function chargerCommentsCallback(postId, callback) {
    setTimeout(() => {
        callback([{ id: 1, texte: "Commentaire 1" }]);
    }, 100);
}

// [X] Utilisation horrible
chargerDonneesCallback(1, (data) => {
    console.log(data); // Reçoit les données
    // Mais imaginez gérer les erreurs à chaque niveau... [!]
});


// ----------------------------------------------------------------------------
// * PROMISES (MODERNE)
// ----------------------------------------------------------------------------

/*
Une Promise est un objet représentant une opération asynchrone.
États possibles :
- PENDING (en attente)
- FULFILLED (résolue - succès)
- REJECTED (rejetée - erreur)


SYNTAXE :
new Promise((resolve, reject) => {
    // Opération asynchrone
    // Si succès : resolve(valeur)
    // Si erreur : reject(erreur)
});
*/

// Créer une promise
function attendreMS(ms) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(`Attendu ${ms}ms`), ms);
    });
}

function fetchUser(id) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const users = { 1: { nom: "Alice" }, 2: { nom: "Bob" } };
            const user = users[id];

            if (user) {
                resolve(user); // Succès
            } else {
                reject(new Error(`User ${id} non trouvé`)); // Erreur
            }
        }, 300);
    });
}

// CONSOMMER UNE PROMISE avec .then / .catch / .finally
fetchUser(1)
    .then(user => {
        console.log("Succès:", user.nom);
        return user; // Passer à la prochaine .then
    })
    .then(user => {
        console.log("Second then:", user);
    })
    .catch(err => {
        console.error("Erreur:", err.message);
    })
    .finally(() => {
        console.log("Toujours exécuté");
    });

// CHAÎNER DES PROMISES
function fetchPosts(userId) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve([
                { id: 1, titre: "Post 1", userId },
                { id: 2, titre: "Post 2", userId }
            ]);
        }, 200);
    });
}

// [OK] Enchaînement propre (vs callback hell)
fetchUser(1)
    .then(user => {
        console.log(`User: ${user.nom}`);
        return fetchPosts(user.id); // Retourner une promise
    })
    .then(posts => {
        console.log(`Posts: ${posts.length}`);
        return posts[0].id;
    })
    .then(postId => {
        console.log(`Premier post ID: ${postId}`);
    })
    .catch(err => {
        // Un seul catch pour TOUTE la chaîne !
        console.error("Erreur quelque part:", err.message);
    });


// PROMISES EN PARALLÈLE
const p1 = fetchUser(1);
const p2 = fetchUser(2);

// Promise.all : Attend TOUTES les promises - Échoue si une seule échoue
Promise.all([p1, p2])
    .then(([user1, user2]) => {
        console.log(user1.nom, user2.nom); // Alice Bob
    })
    .catch(err => console.error("Une promise a échoué:", err.message));

// Promise.allSettled : Attend toutes - Ne rate jamais
Promise.allSettled([fetchUser(1), fetchUser(99)])
    .then(results => {
        results.forEach((r, i) => {
            if (r.status === "fulfilled") {
                console.log(`Promise ${i}: Succès -`, r.value);
            } else {
                console.log(`Promise ${i}: Erreur -`, r.reason.message);
            }
        });
    });

// Promise.race : La PREMIÈRE qui se résout (ou rejette) gagne
const rapide = new Promise(res => setTimeout(() => res("rapide"), 100));
const lente  = new Promise(res => setTimeout(() => res("lente"), 500));

Promise.race([rapide, lente]).then(gagnant => {
    console.log("Gagnant:", gagnant); // "rapide"
});

// Promise.any : La PREMIÈRE qui RÉUSSIT gagne (ignore les échecs)
const fail1 = Promise.reject(new Error("Erreur 1"));
const fail2 = Promise.reject(new Error("Erreur 2"));
const ok = Promise.resolve("Succès !");

Promise.any([fail1, fail2, ok])
    .then(val => console.log(val)) // "Succès !"
    .catch(err => console.error(err)); // AggregateError si toutes échouent

// Créer une Promise résolue/rejetée immédiatement
const resolved = Promise.resolve("Valeur");
const rejected = Promise.reject(new Error("Erreur"));


// ----------------------------------------------------------------------------
// [RAPIDE] ASYNC / AWAIT (LA MEILLEURE FAÇON !)
// ----------------------------------------------------------------------------

/*
async/await est du SUCRE SYNTAXIQUE sur les Promises.
Il rend le code asynchrone lisible comme du code synchrone.

RÈGLES :
- async devant function -> retourne toujours une Promise
- await ne peut être utilisé QUE dans une fonction async
- await "attend" que la Promise se résolve
*/

// Exemple basique
async function getMessage() {
    const result = await attendreMS(500);
    return result; // Retourné dans une Promise automatiquement
}

// Équivalent à :
function getMessagePromise() {
    return attendreMS(500).then(result => result);
}

// RÉÉCRITURE DU CALLBACK HELL avec async/await
async function chargerToutesLesDonnees(userId) {
    try {
        // Séquentiel (l'un après l'autre)
        const user = await fetchUser(userId);
        console.log(`User: ${user.nom}`);

        const posts = await fetchPosts(userId);
        console.log(`Posts: ${posts.length}`);

        return { user, posts };

    } catch (err) {
        console.error("Erreur:", err.message);
        throw err; // Re-lancer si nécessaire
    }
}

// Appel
chargerToutesLesDonnees(1).then(data => console.log("Données:", data));

// PARALLEL avec async/await
async function chargerEnParallele(userIds) {
    try {
        // Lancer toutes les promises EN MÊME TEMPS
        const promises = userIds.map(id => fetchUser(id));
        const users = await Promise.all(promises);

        console.log("Tous les users:", users.map(u => u.nom));
        return users;

    } catch (err) {
        console.error("Une requête a échoué:", err.message);
        return [];
    }
}

// [ATTENTION] PIÈGE COURANT : await dans forEach
async function mauvaisExemple(ids) {
    // [X] NE FONCTIONNE PAS comme prévu !
    ids.forEach(async (id) => {
        const user = await fetchUser(id); // Await ignoré !
        console.log(user);
    });
}

async function bonExemple(ids) {
    // [OK] for...of avec await (séquentiel)
    for (const id of ids) {
        const user = await fetchUser(id);
        console.log(user);
    }

    // [OK] Promise.all (parallèle)
    const users = await Promise.all(ids.map(id => fetchUser(id)));
    console.log(users);
}

// TIMEOUT avec async/await
function avecTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error(`Timeout après ${ms}ms`)), ms)
    );
    return Promise.race([promise, timeout]);
}

async function fetchAvecTimeout() {
    try {
        const data = await avecTimeout(fetchUser(1), 1000);
        return data;
    } catch (err) {
        console.error(err.message);
    }
}


// ============================================================================
// [GUIDE] CHAPITRE 10 : MANIPULATION DU DOM
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Sélectionner des éléments DOM
[OK] Modifier le contenu et les attributs
[OK] Créer, insérer et supprimer des éléments
[OK] Modifier les styles et classes
[OK] Parcourir l'arbre DOM
[OK] Utiliser les templates HTML
*/

/*
NOTE : Le DOM n'existe que dans le navigateur.
Ces exemples fonctionnent avec une page HTML.

Structure HTML de référence pour les exemples :

<!DOCTYPE html>
<html>
<body>
    <div id="app">
        <h1 class="titre principal">Hello World</h1>
        <ul id="liste">
            <li class="item">Item 1</li>
            <li class="item">Item 2</li>
            <li class="item">Item 3</li>
        </ul>
        <button id="btn">Cliquer</button>
        <input id="input" type="text" value="Initial">
        <p id="para">Paragraphe</p>
    </div>
</body>
</html>
*/


// ----------------------------------------------------------------------------
// [RECHERCHE] SÉLECTIONNER DES ÉLÉMENTS
// ----------------------------------------------------------------------------

// MÉTHODES MODERNES (utiliser ces-ci [OK])

// querySelector : Premier élément correspondant
const app = document.querySelector("#app");
const h1 = document.querySelector("h1");
const premierItem = document.querySelector(".item");
const input = document.querySelector("input[type='text']");

// querySelectorAll : Tous les éléments correspondants (NodeList)
const items = document.querySelectorAll(".item");
const liens = document.querySelectorAll("a[href^='https']");

// Convertir NodeList en tableau
const itemsArray = Array.from(items);
const itemsArray2 = [...items]; // Spread

// Itérer
items.forEach(item => console.log(item.textContent));
// OU
for (const item of items) {
    console.log(item.textContent);
}

// MÉTHODES CLASSIQUES (toujours valides)
const byId = document.getElementById("liste");
const byClass = document.getElementsByClassName("item"); // HTMLCollection (live !)
const byTag = document.getElementsByTagName("li");       // HTMLCollection (live !)


// ----------------------------------------------------------------------------
// [NOTE] MODIFIER LE CONTENU
// ----------------------------------------------------------------------------

const para = document.querySelector("#para");

// textContent : Texte brut (sécurisé [OK])
para.textContent = "Nouveau texte";
console.log(para.textContent); // "Nouveau texte"

// innerHTML : HTML complet (attention XSS !)
para.innerHTML = "<strong>Texte en gras</strong>";

// innerText : Visible text (comme textContent mais aware du CSS)
console.log(para.innerText);

// [ATTENTION] Danger XSS avec innerHTML
const userInput = "<script>alert('XSS')</script>";
// para.innerHTML = userInput; // [X] DANGEREUX !
para.textContent = userInput; // [OK] Sécurisé (affiche comme texte)

// FORMULAIRES
const champInput = document.querySelector("#input");
champInput.value = "Nouvelle valeur";
console.log(champInput.value);

// Checkbox
const checkbox = document.querySelector("input[type='checkbox']");
if (checkbox) {
    checkbox.checked = true;
    console.log(checkbox.checked); // true
}


// ----------------------------------------------------------------------------
// [DESIGN] MODIFIER LES ATTRIBUTS
// ----------------------------------------------------------------------------

const btn = document.querySelector("#btn");
const img = document.querySelector("img");

// setAttribute / getAttribute / removeAttribute / hasAttribute
btn.setAttribute("disabled", "");       // Désactiver le bouton
btn.setAttribute("data-id", "42");      // Attribut data
console.log(btn.getAttribute("id"));    // "btn"
btn.removeAttribute("disabled");         // Supprimer
console.log(btn.hasAttribute("id"));    // true

// Propriétés directes (plus rapide pour propriétés courantes)
btn.id = "nouveauBouton";
btn.title = "Cliquez ici !";

if (img) {
    img.src = "nouvelle-image.jpg";
    img.alt = "Description";
}

// DATA ATTRIBUTES
const element = document.querySelector("[data-id]");
if (element) {
    // Via dataset (convertit kebab-case en camelCase)
    console.log(element.dataset.id);          // "42"

    element.dataset.userId = "100";            // Crée data-user-id="100"
    delete element.dataset.id;                // Supprime data-id
}


// ----------------------------------------------------------------------------
// [SCENARIO] MODIFIER LES CLASSES ET STYLES
// ----------------------------------------------------------------------------

const div = document.querySelector("#app");

// CLASSES (classList - méthode recommandée)
div.classList.add("actif");               // Ajouter une classe
div.classList.remove("inactif");          // Supprimer une classe
div.classList.toggle("visible");          // Toggle (ajoute si absent, supprime si présent)
div.classList.toggle("actif", false);     // Force suppression
div.classList.replace("ancien", "nouveau"); // Remplacer
console.log(div.classList.contains("actif")); // Vérifier
console.log(div.classList.length);         // Nombre de classes
console.log(div.className);               // Chaîne de toutes les classes

// Itérer
for (const classe of div.classList) {
    console.log(classe);
}

// STYLES INLINE
div.style.color = "red";
div.style.backgroundColor = "blue";  // camelCase !
div.style.fontSize = "16px";
div.style.marginTop = "20px";

// Supprimer un style inline
div.style.removeProperty("color");
div.style.color = ""; // Aussi valide

// Lire les styles CALCULÉS (inclut CSS externe)
const computedStyle = window.getComputedStyle(div);
console.log(computedStyle.color);
console.log(computedStyle.fontSize);

// [OK] Mieux : modifier via classes CSS
// div.classList.add("actif"); // Plutôt que div.style.color = "red"


// ----------------------------------------------------------------------------
// + CRÉER ET INSÉRER DES ÉLÉMENTS
// ----------------------------------------------------------------------------

// Créer un élément
const newItem = document.createElement("li");
newItem.textContent = "Nouvel item";
newItem.classList.add("item");

// Insérer dans le DOM
const liste = document.querySelector("#liste");

liste.appendChild(newItem);             // À la FIN
liste.prepend(newItem);                 // Au DÉBUT
liste.insertBefore(newItem, liste.firstChild); // Avant un élément

// insertAdjacentElement / insertAdjacentHTML (ES6, flexible)
const cible = document.querySelector("#btn");
const newBtn = document.createElement("button");
newBtn.textContent = "Nouveau bouton";

cible.insertAdjacentElement("beforebegin", newBtn); // Avant l'élément
cible.insertAdjacentElement("afterbegin", newBtn);  // Premier enfant
cible.insertAdjacentElement("beforeend", newBtn);   // Dernier enfant
cible.insertAdjacentElement("afterend", newBtn);    // Après l'élément

// insertAdjacentHTML (performant pour HTML)
cible.insertAdjacentHTML("afterend", "<button>HTML direct</button>");

// APPEND (multiple éléments ou strings)
const fragment = document.createDocumentFragment();
["Item A", "Item B", "Item C"].forEach(texte => {
    const li = document.createElement("li");
    li.textContent = texte;
    fragment.appendChild(li);
});
liste.appendChild(fragment); // Un seul reflow/repaint !

// Cloner un élément
const clone = newItem.cloneNode(true); // true = deep clone (avec enfants)


// ----------------------------------------------------------------------------
// [SUPPRIMER] SUPPRIMER DES ÉLÉMENTS
// ----------------------------------------------------------------------------

// Moderne (ES2019)
newItem.remove(); // Supprimer directement

// Classique (compatible plus ancien)
// liste.removeChild(newItem);


// ----------------------------------------------------------------------------
// [ARBRE] PARCOURIR L'ARBRE DOM
// ----------------------------------------------------------------------------

const appEl = document.querySelector("#app");

// Navigation
console.log(appEl.parentNode);          // Parent
console.log(appEl.parentElement);       // Parent (élément uniquement)
console.log(appEl.children);            // Éléments enfants (HTMLCollection)
console.log(appEl.childNodes);          // Tous nœuds enfants (texte inclus)
console.log(appEl.firstElementChild);   // Premier enfant élément
console.log(appEl.lastElementChild);    // Dernier enfant élément
console.log(appEl.nextElementSibling);  // Frère suivant
console.log(appEl.previousElementSibling); // Frère précédent


// ============================================================================
// [GUIDE] CHAPITRE 11 : ÉVÉNEMENTS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ajouter et supprimer des event listeners
[OK] Comprendre le modèle d'événements (bubbling/capturing)
[OK] Utiliser event.preventDefault() et stopPropagation()
[OK] Event delegation
[OK] Événements personnalisés
[OK] Les types d'événements courants
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] AJOUTER DES ÉVÉNEMENTS
// ----------------------------------------------------------------------------

const button = document.querySelector("#btn");

// MÉTHODE RECOMMANDÉE : addEventListener
function handleClick(event) {
    console.log("Cliqué !", event);
    console.log("Target:", event.target);
    console.log("Cible:", event.currentTarget);
}

button.addEventListener("click", handleClick);

// Supprimer l'écouteur (MÊME référence de fonction obligatoire !)
button.removeEventListener("click", handleClick);

// Options
button.addEventListener("click", handleClick, {
    once: true,      // Exécuté UNE SEULE fois puis supprimé
    capture: false,  // Phase de capture (défaut: false = bubbling)
    passive: true    // Améliore les performances (scroll events)
});

// [X] MÉTHODES À ÉVITER
button.onclick = () => console.log("onClick"); // Remplace l'existant !
// <button onclick="maFonction()"> // Mélange HTML/JS, difficile à déboguer


// ----------------------------------------------------------------------------
// [ROUGE] L'OBJET EVENT
// ----------------------------------------------------------------------------

document.addEventListener("click", (event) => {
    // Propriétés communes
    console.log(event.type);          // "click"
    console.log(event.target);        // L'élément cliqué
    console.log(event.currentTarget); // L'élément avec le listener
    console.log(event.timeStamp);     // Timestamp de l'événement
    console.log(event.bubbles);       // Est-ce qu'il bubble ?
    console.log(event.cancelable);    // Peut être annulé ?

    // Propriétés souris
    console.log(event.clientX, event.clientY); // Position dans le viewport
    console.log(event.pageX, event.pageY);     // Position dans la page
    console.log(event.button);                 // 0=gauche, 1=milieu, 2=droite
    console.log(event.ctrlKey, event.shiftKey, event.altKey); // Modificateurs
});

// Keyboard events
document.addEventListener("keydown", (event) => {
    console.log(event.key);     // "a", "Enter", "ArrowUp", etc.
    console.log(event.code);    // "KeyA", "Enter", "ArrowUp"
    console.log(event.ctrlKey); // true si Ctrl pressé
});

// Formulaires
const form = document.querySelector("form");
if (form) {
    form.addEventListener("submit", (event) => {
        event.preventDefault(); // Empêcher rechargement de la page !
        const formData = new FormData(form);
        console.log(Object.fromEntries(formData));
    });
}


// ----------------------------------------------------------------------------
// [WATER_WAVE] BUBBLING ET CAPTURING
// ----------------------------------------------------------------------------

/*
PROPAGATION D'ÉVÉNEMENTS :

Quand un événement se produit sur un élément :
1. CAPTURING (descendant) : De window vers l'élément cible
2. TARGET : Sur l'élément cible
3. BUBBLING (montant) : De l'élément cible vers window

HTML :
<div id="outer">
    <div id="middle">
        <button id="inner">Cliquer</button>
    </div>
</div>

Cliquer sur le bouton -> L'événement "bulle" vers outer
*/

const outer = document.querySelector("#outer");
const middle = document.querySelector("#middle");
const inner = document.querySelector("#inner");

if (outer && middle && inner) {
    outer.addEventListener("click", () => console.log("outer"));
    middle.addEventListener("click", () => console.log("middle"));
    inner.addEventListener("click", () => console.log("inner"));

    // Cliquer sur inner -> "inner", "middle", "outer" (bubbling)

    // stopPropagation : Arrêter la propagation
    inner.addEventListener("click", (e) => {
        e.stopPropagation(); // "middle" et "outer" ne verront pas l'event
    });

    // stopImmediatePropagation : Arrêter même les autres listeners sur l'élément
    inner.addEventListener("click", (e) => {
        e.stopImmediatePropagation();
    });
}


// ----------------------------------------------------------------------------
// [OBJECTIF] EVENT DELEGATION (PATTERN IMPORTANT !)
// ----------------------------------------------------------------------------

/*
Au lieu d'ajouter un listener sur CHAQUE élément,
on l'ajoute sur le PARENT et on vérifie event.target.

AVANTAGES :
[OK] Un seul listener (performance)
[OK] Fonctionne pour éléments ajoutés dynamiquement
[OK] Moins de mémoire
*/

// [X] MAUVAIS : Un listener par élément
document.querySelectorAll(".item").forEach(item => {
    item.addEventListener("click", () => console.log(item.textContent));
});

// [OK] BON : Event delegation
const listeParent = document.querySelector("#liste");
if (listeParent) {
    listeParent.addEventListener("click", (event) => {
        const item = event.target.closest(".item"); // Trouver le bon ancêtre
        if (item) {
            console.log("Item cliqué:", item.textContent);
        }
    });
}

// Exemple pratique : Todo list dynamique
function creerTodoApp(container) {
    container.innerHTML = `
        <input type="text" id="todoInput" placeholder="Ajouter une tâche">
        <button id="addBtn">Ajouter</button>
        <ul id="todoList"></ul>
    `;

    const todoInput = container.querySelector("#todoInput");
    const addBtn = container.querySelector("#addBtn");
    const todoList = container.querySelector("#todoList");

    // Ajouter tâche
    function ajouterTodo() {
        const texte = todoInput.value.trim();
        if (!texte) return;

        const li = document.createElement("li");
        li.innerHTML = `
            <span>${texte}</span>
            <button class="delete" data-action="delete">×</button>
            <button class="done" data-action="done">[OK]</button>
        `;
        todoList.appendChild(li);
        todoInput.value = "";
    }

    addBtn.addEventListener("click", ajouterTodo);
    todoInput.addEventListener("keypress", (e) => {
        if (e.key === "Enter") ajouterTodo();
    });

    // [OK] Event delegation pour les boutons dynamiques
    todoList.addEventListener("click", (e) => {
        const action = e.target.dataset.action;
        const li = e.target.closest("li");

        if (action === "delete") li.remove();
        if (action === "done") li.classList.toggle("done");
    });
}


// ----------------------------------------------------------------------------
// [DESIGN] ÉVÉNEMENTS PERSONNALISÉS
// ----------------------------------------------------------------------------

// Créer un événement custom
const userConnected = new CustomEvent("userConnected", {
    detail: { username: "Alice", role: "admin" },
    bubbles: true,
    cancelable: true
});

// Écouter l'événement
document.addEventListener("userConnected", (e) => {
    console.log(`Utilisateur ${e.detail.username} connecté (${e.detail.role})`);
});

// Déclencher l'événement
document.dispatchEvent(userConnected);

// Pattern Publish/Subscribe avec EventTarget
class EventBus extends EventTarget {
    emit(type, data) {
        this.dispatchEvent(new CustomEvent(type, { detail: data }));
    }

    on(type, handler) {
        this.addEventListener(type, (e) => handler(e.detail));
        return this; // Pour chaining
    }

    off(type, handler) {
        this.removeEventListener(type, handler);
        return this;
    }
}

const bus = new EventBus();

bus.on("messageEnvoye", ({ expediteur, texte }) => {
    console.log(`${expediteur}: ${texte}`);
});

bus.emit("messageEnvoye", { expediteur: "Alice", texte: "Bonjour !" });


// ============================================================================
// [GUIDE] CHAPITRE 12 : FETCH API ET AJAX
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Utiliser l'API Fetch
[OK] Faire des requêtes GET, POST, PUT, DELETE
[OK] Envoyer du JSON, des formulaires
[OK] Gérer les headers
[OK] Créer un client HTTP réutilisable
[OK] Stratégies de retry et timeout
*/


// ----------------------------------------------------------------------------
// [WEB] FETCH API (MODERNE, RECOMMANDÉ)
// ----------------------------------------------------------------------------

const API_URL = "https://jsonplaceholder.typicode.com";

// GET : Récupérer des données
async function getUsers() {
    try {
        const response = await fetch(`${API_URL}/users`);

        // Vérifier si la requête a réussi
        if (!response.ok) {
            throw new Error(`HTTP Error: ${response.status} ${response.statusText}`);
        }

        // Parser la réponse selon le type
        const users = await response.json();  // JSON
        // const text = await response.text(); // Texte
        // const blob = await response.blob(); // Fichier binaire

        console.log("Headers:", response.headers.get("content-type"));
        return users;

    } catch (err) {
        console.error("Erreur fetch:", err.message);
        throw err;
    }
}

// POST : Créer une ressource
async function createPost(data) {
    try {
        const response = await fetch(`${API_URL}/posts`, {
            method: "POST",
            headers: {
                "Content-Type": "application/json",
                "Authorization": "Bearer mon-token"
            },
            body: JSON.stringify(data)
        });

        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        return await response.json();
    } catch (err) {
        console.error("Erreur création:", err.message);
    }
}

// PUT : Modifier complètement
async function updateUser(id, data) {
    const response = await fetch(`${API_URL}/users/${id}`, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data)
    });
    return response.json();
}

// PATCH : Modifier partiellement
async function patchUser(id, data) {
    const response = await fetch(`${API_URL}/users/${id}`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(data)
    });
    return response.json();
}

// DELETE : Supprimer
async function deletePost(id) {
    const response = await fetch(`${API_URL}/posts/${id}`, {
        method: "DELETE"
    });
    return response.ok;
}


// ----------------------------------------------------------------------------
// [USINE] CLIENT HTTP RÉUTILISABLE
// ----------------------------------------------------------------------------

class HttpClient {
    constructor(baseURL, defaultOptions = {}) {
        this.baseURL = baseURL;
        this.defaultOptions = defaultOptions;
        this.token = null;
    }

    setToken(token) {
        this.token = token;
        return this;
    }

    async #request(endpoint, options = {}) {
        const url = `${this.baseURL}${endpoint}`;

        const headers = {
            "Content-Type": "application/json",
            ...(this.token && { Authorization: `Bearer ${this.token}` }),
            ...options.headers
        };

        const config = {
            ...this.defaultOptions,
            ...options,
            headers
        };

        // Body automatiquement stringifié
        if (config.body && typeof config.body === "object") {
            config.body = JSON.stringify(config.body);
        }

        const response = await fetch(url, config);

        if (!response.ok) {
            const error = new Error(`HTTP Error: ${response.status}`);
            error.status = response.status;
            try {
                error.data = await response.json();
            } catch {}
            throw error;
        }

        // Retourner null pour 204 No Content
        if (response.status === 204) return null;

        return response.json();
    }

    get(endpoint, params = {}) {
        const query = new URLSearchParams(params).toString();
        const url = query ? `${endpoint}?${query}` : endpoint;
        return this.#request(url);
    }

    post(endpoint, body) {
        return this.#request(endpoint, { method: "POST", body });
    }

    put(endpoint, body) {
        return this.#request(endpoint, { method: "PUT", body });
    }

    patch(endpoint, body) {
        return this.#request(endpoint, { method: "PATCH", body });
    }

    delete(endpoint) {
        return this.#request(endpoint, { method: "DELETE" });
    }
}

// Utilisation
const api = new HttpClient("https://jsonplaceholder.typicode.com");

async function exempleAPI() {
    // GET avec paramètres
    const users = await api.get("/users", { _limit: 5 });

    // POST
    const newPost = await api.post("/posts", {
        title: "Mon post",
        body: "Contenu",
        userId: 1
    });

    // DELETE
    const deleted = await api.delete("/posts/1");
}


// ----------------------------------------------------------------------------
// [SYNC] STRATÉGIES AVANCÉES
// ----------------------------------------------------------------------------

// RETRY (réessayer en cas d'erreur)
async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            if (response.ok) return response.json();

            // Ne pas réessayer pour les erreurs client (4xx)
            if (response.status >= 400 && response.status < 500) {
                throw new Error(`Erreur client: ${response.status}`);
            }

            throw new Error(`Erreur serveur: ${response.status}`);

        } catch (err) {
            console.log(`Tentative ${attempt}/${maxRetries} échouée: ${err.message}`);

            if (attempt === maxRetries) throw err;

            // Attendre avant de réessayer (backoff exponentiel)
            await new Promise(res => setTimeout(res, delay * attempt));
        }
    }
}

// TIMEOUT
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);

    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        return response.json();
    } catch (err) {
        if (err.name === "AbortError") {
            throw new Error(`Timeout après ${timeout}ms`);
        }
        throw err;
    } finally {
        clearTimeout(id);
    }
}

// CACHE SIMPLE
class CachedFetch {
    #cache = new Map();
    #ttl;

    constructor(ttl = 60000) { // 1 minute par défaut
        this.#ttl = ttl;
    }

    async get(url) {
        const cached = this.#cache.get(url);

        if (cached && Date.now() - cached.timestamp < this.#ttl) {
            console.log("Cache hit:", url);
            return cached.data;
        }

        console.log("Cache miss:", url);
        const data = await fetch(url).then(r => r.json());

        this.#cache.set(url, { data, timestamp: Date.now() });
        return data;
    }

    clear() {
        this.#cache.clear();
    }
}

const cachedFetch = new CachedFetch(30000);


// ----------------------------------------------------------------------------
// [COURS] EXERCICE PRATIQUE 5 : Asynchrone et Fetch
// ----------------------------------------------------------------------------

/*
EXERCICE 5.1 : Application météo simulée

Créez une application qui :
1. Simule une API météo avec des Promises
2. Récupère la météo de plusieurs villes en parallèle
3. Gère les erreurs proprement
4. Affiche les résultats formatés
*/

// CORRIGÉ 5.1 :

// Simulation API météo
function getMeteo(ville) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const donnees = {
                Paris: { temp: 15, meteo: "Nuageux", humidite: 70 },
                Lyon: { temp: 18, meteo: "Ensoleillé", humidite: 45 },
                Marseille: { temp: 22, meteo: "Ensoleillé", humidite: 55 }
            };

            if (!donnees[ville]) {
                reject(new Error(`Ville ${ville} non trouvée`));
            } else {
                resolve({ ville, ...donnees[ville] });
            }
        }, Math.random() * 1000 + 200);
    });
}

async function appMeteo(villes) {
    console.log(`Récupération météo pour: ${villes.join(", ")}`);

    const resultats = await Promise.allSettled(
        villes.map(v => getMeteo(v))
    );

    const rapport = resultats.map((r, i) => {
        if (r.status === "fulfilled") {
            const { ville, temp, meteo, humidite } = r.value;
            return `[OK] ${ville}: ${temp}°C, ${meteo}, Humidité: ${humidite}%`;
        } else {
            return `[X] ${villes[i]}: ${r.reason.message}`;
        }
    });

    console.log("\n[GRAPHIQUE] RAPPORT MÉTÉO :");
    rapport.forEach(line => console.log(line));

    // Statistiques sur les villes réussies
    const succes = resultats
        .filter(r => r.status === "fulfilled")
        .map(r => r.value);

    if (succes.length > 0) {
        const tempMoy = succes.reduce((s, v) => s + v.temp, 0) / succes.length;
        console.log(`\n[HAUSSE] Température moyenne: ${tempMoy.toFixed(1)}°C`);
    }
}

appMeteo(["Paris", "Lyon", "Marseille", "Bordeaux"]);


/*
EXERCICE 5.2 : DOM Interactif - Liste de contacts

Créez une application de contacts avec :
1. Affichage d'une liste de contacts
2. Ajout d'un nouveau contact
3. Suppression d'un contact
4. Filtre/recherche en temps réel
5. Sauvegarde dans localStorage
*/

// CORRIGÉ 5.2 :

class ContactsApp {
    #contacts = [];
    #container;

    constructor(containerId) {
        this.#container = document.getElementById(containerId);
        if (!this.#container) return;

        this.#charger();
        this.#rendu();
    }

    #charger() {
        const saved = localStorage.getItem("contacts");
        this.#contacts = saved ? JSON.parse(saved) : [
            { id: 1, nom: "Alice Dupont", email: "alice@ex.com", tel: "0612345678" },
            { id: 2, nom: "Bob Martin", email: "bob@ex.com", tel: "0698765432" }
        ];
    }

    #sauvegarder() {
        localStorage.setItem("contacts", JSON.stringify(this.#contacts));
    }

    #ajouter(contact) {
        contact.id = Date.now();
        this.#contacts.push(contact);
        this.#sauvegarder();
        this.#rendu();
    }

    #supprimer(id) {
        this.#contacts = this.#contacts.filter(c => c.id !== id);
        this.#sauvegarder();
        this.#rendu();
    }

    #filtrer(terme) {
        const lower = terme.toLowerCase();
        return this.#contacts.filter(c =>
            c.nom.toLowerCase().includes(lower) ||
            c.email.toLowerCase().includes(lower)
        );
    }

    #rendu(contacts = this.#contacts) {
        this.#container.innerHTML = `
            <h2>[LISTE] Contacts (${contacts.length})</h2>
            <input id="search" type="text" placeholder="Rechercher...">
            <form id="addForm">
                <input name="nom" placeholder="Nom" required>
                <input name="email" type="email" placeholder="Email" required>
                <input name="tel" placeholder="Téléphone">
                <button type="submit">Ajouter</button>
            </form>
            <ul id="contactList">
                ${contacts.map(c => `
                    <li data-id="${c.id}">
                        <strong>${c.nom}</strong>
                        <span>${c.email}</span>
                        <span>${c.tel || "N/A"}</span>
                        <button data-action="delete">Supprimer</button>
                    </li>
                `).join("")}
            </ul>
        `;

        // Event listeners
        document.getElementById("search").addEventListener("input", (e) => {
            const filtered = this.#filtrer(e.target.value);
            const list = document.getElementById("contactList");
            list.innerHTML = filtered.map(c => `
                <li data-id="${c.id}">
                    <strong>${c.nom}</strong>
                    <span>${c.email}</span>
                    <button data-action="delete">Supprimer</button>
                </li>
            `).join("");
        });

        document.getElementById("addForm").addEventListener("submit", (e) => {
            e.preventDefault();
            const formData = new FormData(e.target);
            this.#ajouter(Object.fromEntries(formData));
        });

        // Event delegation
        document.getElementById("contactList").addEventListener("click", (e) => {
            if (e.target.dataset.action === "delete") {
                const id = parseInt(e.target.closest("li").dataset.id);
                this.#supprimer(id);
            }
        });
    }
}

// Pour utiliser dans une vraie page HTML :
// <div id="app"></div>
// new ContactsApp("app");


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 3
// ============================================================================

/*
CE QUE VOUS AVEZ APPRIS :

Chapitre 9 - Asynchrone :
[OK] Event Loop (call stack, microtask, task queue)
[OK] Callbacks (et leurs problèmes)
[OK] Promises (.then/.catch/.finally)
[OK] Promise.all, allSettled, race, any
[OK] async/await (la meilleure façon)
[OK] Gestion d'erreurs asynchrones

Chapitre 10 - DOM :
[OK] querySelector, querySelectorAll
[OK] textContent, innerHTML, value
[OK] setAttribute, classList
[OK] createElement, appendChild, remove
[OK] insertAdjacentHTML, DocumentFragment

Chapitre 11 - Événements :
[OK] addEventListener, removeEventListener
[OK] L'objet event (target, preventDefault)
[OK] Bubbling et capturing
[OK] Event delegation (pattern important !)
[OK] CustomEvent

Chapitre 12 - Fetch API :
[OK] fetch() GET, POST, PUT, PATCH, DELETE
[OK] Headers, body JSON
[OK] Client HTTP réutilisable
[OK] Retry et timeout
[OK] AbortController


[CLE] POINTS CLÉS :

1. Toujours utiliser async/await avec try/catch
2. Event delegation pour listes dynamiques
3. Vérifier response.ok après fetch()
4. Ne jamais bloquer le thread principal
5. AbortController pour timeout des requêtes


-> PROCHAINE ÉTAPE : PARTIE 4 - Fonctionnalités Avancées
*/

// ============================================================================
// [LIVRE] JAVASCRIPT - PARTIE 4 : FONCTIONNALITÉS AVANCÉES
// ============================================================================
//
// [OBJECTIF] CETTE PARTIE COUVRE :
// - Chapitre 13 : Closures et Portée Avancée
// - Chapitre 14 : Programmation Fonctionnelle
// - Chapitre 15 : Iterators, Generators et Symbols
// - Chapitre 16 : Proxy, Reflect et WeakRef
// - Chapitre 17 : Expressions Régulières (RegEx)
//
// [TEMPS] TEMPS : ~8-10 heures
// [DOCS] PRÉREQUIS : Parties 1, 2 et 3 complétées
// ============================================================================


// ============================================================================
// [GUIDE] CHAPITRE 13 : CLOSURES ET PORTÉE AVANCÉE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est une closure et pourquoi ça existe
[OK] Portée lexicale (lexical scope) en profondeur
[OK] Utiliser closures pour l'encapsulation
[OK] Module pattern avec closures
[OK] Pièges courants avec closures dans les boucles
[OK] Memoization avec closures
*/


// ----------------------------------------------------------------------------
// [REFLEXION] QU'EST-CE QU'UNE CLOSURE ?
// ----------------------------------------------------------------------------

/*
DÉFINITION

Une closure = une fonction qui "se souvient" de son environnement lexical
même après que la fonction parente a terminé son exécution.

ANALOGIE [SCHOOL_SATCHEL]

Imaginez une fonction comme un étudiant avec un sac à dos.
Le sac à dos contient toutes les variables accessibles au moment de la création.
Même si l'étudiant quitte l'école (contexte d'exécution terminé),
il garde son sac à dos avec tout ce qu'il y avait dedans.
*/

// Exemple fondamental
function createCounter() {
    // Cette variable est dans la portée de createCounter
    let count = 0;      // <- Variable "capturée" par la closure

    // Cette fonction INTERNE forme une closure
    // Elle "capture" count dans son sac à dos
    return function() {
        count++;         // Accès à count via closure
        return count;
    };
}

const counter1 = createCounter();
const counter2 = createCounter();

console.log(counter1()); // -> 1
console.log(counter1()); // -> 2
console.log(counter1()); // -> 3
console.log(counter2()); // -> 1 (count séparé !)
console.log(counter2()); // -> 2

/*
[IDEE] COMMENT ÇA MARCHE ?

1. createCounter() est appelé
2. count = 0 est créé dans sa portée
3. La fonction interne est créée et CAPTURE count
4. createCounter() retourne la fonction interne
5. createCounter() termine -> mais count n'est PAS supprimé !
   Pourquoi ? Parce que la fonction interne y fait référence
6. La fonction interne peut encore accéder et modifier count


PORTÉE LEXICALE vs PORTÉE DYNAMIQUE

JavaScript utilise PORTÉE LEXICALE :
-> La portée est définie par la POSITION dans le code source
-> Pas par l'ordre d'appel des fonctions
*/

// Démonstration portée lexicale
const name = "Global";

function outer() {
    const name = "Outer";

    function inner() {
        // inner "voit" le name de outer (là où elle EST DÉFINIE)
        // Pas le name global (là où elle POURRAIT être appelée)
        console.log(name); // -> "Outer"
    }

    inner();
}

outer();

// Preuve : même si on appelle inner depuis ailleurs
function executeFunction(fn) {
    const name = "Execute"; // Cette variable n'affecte PAS inner
    fn();                   // inner voit toujours "Outer"
}

function outer2() {
    const name = "Outer2";

    function inner2() {
        console.log(name); // Toujours "Outer2" (portée lexicale)
    }

    executeFunction(inner2); // -> "Outer2", pas "Execute"
}

outer2();


// ----------------------------------------------------------------------------
// [RECHERCHE] CLOSURES EN PRATIQUE
// ----------------------------------------------------------------------------

/*
1. ENCAPSULATION (données privées)

JavaScript n'avait pas de classes avant ES6
Les closures permettaient d'encapsuler des données privées
*/

function createPerson(nom, age) {
    // Ces variables sont "privées" (inaccessibles de l'extérieur)
    let _nom = nom;     // Convention _ = "privé"
    let _age = age;

    return {
        // Méthodes publiques qui accèdent aux variables privées via closure
        getNom() {
            return _nom;
        },
        getAge() {
            return _age;
        },
        vieillir() {
            _age++;
            console.log(`${_nom} a maintenant ${_age} ans`);
        },
        changerNom(nouveauNom) {
            if (typeof nouveauNom !== 'string' || nouveauNom.length < 2) {
                throw new Error("Nom invalide");
            }
            _nom = nouveauNom;
        }
    };
}

const alice = createPerson("Alice", 25);
alice.vieillir();              // -> "Alice a maintenant 26 ans"
console.log(alice.getAge());   // -> 26
console.log(alice._age);       // -> undefined (vraiment privé !)

/*
2. FACTORY FUNCTIONS (usines de fonctions)

Créer des fonctions avec comportement configurable
*/

function createMultiplier(facteur) {
    // facteur est capturé par closure
    return function(nombre) {
        return nombre * facteur;
    };
}

const doubler = createMultiplier(2);
const tripler = createMultiplier(3);
const par10 = createMultiplier(10);

console.log(doubler(5));   // -> 10
console.log(tripler(5));   // -> 15
console.log(par10(5));     // -> 50

// Exemple plus complexe : créer des validateurs
function createValidator(min, max, regex) {
    return function(valeur) {
        if (typeof valeur !== 'string') return false;
        if (valeur.length < min || valeur.length > max) return false;
        if (regex && !regex.test(valeur)) return false;
        return true;
    };
}

const validateUsername = createValidator(3, 20, /^[a-zA-Z0-9_]+$/);
const validateEmail = createValidator(5, 100, /^[^\s@]+@[^\s@]+\.[^\s@]+$/);
const validateBio = createValidator(0, 500);

console.log(validateUsername("alice_123")); // -> true
console.log(validateUsername("a"));         // -> false (trop court)
console.log(validateEmail("alice@ex.com")); // -> true

/*
3. MEMOIZATION (mise en cache des résultats)

Optimisation : ne recalculer que si nécessaire
*/

function memoize(fn) {
    const cache = new Map(); // Capturé par closure

    return function(...args) {
        const key = JSON.stringify(args);

        if (cache.has(key)) {
            console.log(`[CACHE] Résultat pour ${key}`);
            return cache.get(key);
        }

        console.log(`[CALCUL] Calcul pour ${key}`);
        const result = fn(...args);
        cache.set(key, result);
        return result;
    };
}

// Fonction Fibonacci non-optimisée (très lente pour grands nombres)
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

// Fibonacci memoizé
const fibMemo = memoize(function fibonacci_memo(n) {
    if (n <= 1) return n;
    return fibMemo(n - 1) + fibMemo(n - 2);
});

console.log(fibMemo(40)); // Calcule
console.log(fibMemo(40)); // Retourne depuis cache immédiatement


// ----------------------------------------------------------------------------
// [ATTENTION] PIÈGE CLASSIQUE : CLOSURES DANS LES BOUCLES
// ----------------------------------------------------------------------------

/*
C'est l'un des bugs les plus courants en JavaScript !
*/

// [X] BUG CLASSIQUE
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i); // Affiche toujours 5 !
    }, i * 100);
}
// Sortie : 5 5 5 5 5

/*
[IDEE] POURQUOI ?

var a portée de FONCTION (pas de bloc)
Il n'y a qu'UN SEUL i partagé entre toutes les callbacks
Quand les callbacks s'exécutent, la boucle est finie et i = 5


SOLUTIONS :

1. Utiliser let (portée de bloc)
*/

for (let i = 0; i < 5; i++) {
    // Avec let, chaque itération crée un NOUVEAU binding i
    setTimeout(function() {
        console.log(i); // Affiche 0, 1, 2, 3, 4
    }, i * 100);
}

// 2. IIFE (Immediately Invoked Function Expression)
for (var i = 0; i < 5; i++) {
    (function(j) {
        // j est capturé par closure avec sa valeur à cet instant
        setTimeout(function() {
            console.log(j); // -> 0, 1, 2, 3, 4
        }, j * 100);
    })(i); // On passe i immédiatement
}

// 3. bind()
function logValue(j) {
    console.log(j);
}

for (var i = 0; i < 5; i++) {
    setTimeout(logValue.bind(null, i), i * 100);
}


// ----------------------------------------------------------------------------
// [CONSTRUCTION] MODULE PATTERN
// ----------------------------------------------------------------------------

/*
Pattern fondamental pour organiser le code
Basé sur closures + IIFE
*/

const BancaireModule = (function() {
    // Variables "privées" (inaccessibles de l'extérieur)
    let solde = 0;
    const historique = [];
    const PIN = "1234";

    // Fonctions "privées"
    function enregistrerTransaction(type, montant) {
        historique.push({
            type,
            montant,
            date: new Date().toISOString(),
            soldeApres: solde
        });
    }

    // API publique (retournée)
    return {
        deposer(montant, pin) {
            if (pin !== PIN) {
                throw new Error("PIN incorrect");
            }
            if (montant <= 0) {
                throw new Error("Montant invalide");
            }
            solde += montant;
            enregistrerTransaction("depot", montant);
            return `Dépôt de ${montant}€ effectué. Solde: ${solde}€`;
        },

        retirer(montant, pin) {
            if (pin !== PIN) {
                throw new Error("PIN incorrect");
            }
            if (montant > solde) {
                throw new Error("Solde insuffisant");
            }
            solde -= montant;
            enregistrerTransaction("retrait", montant);
            return `Retrait de ${montant}€ effectué. Solde: ${solde}€`;
        },

        consulterSolde(pin) {
            if (pin !== PIN) {
                throw new Error("PIN incorrect");
            }
            return solde;
        },

        afficherHistorique(pin) {
            if (pin !== PIN) {
                throw new Error("PIN incorrect");
            }
            return [...historique]; // Copie pour éviter modification externe
        }
    };
})();

console.log(BancaireModule.deposer(1000, "1234")); // -> "Dépôt de 1000€ effectué. Solde: 1000€"
console.log(BancaireModule.consulterSolde("1234")); // -> 1000
console.log(BancaireModule.solde);                  // -> undefined (privé !)


// ============================================================================
// [COURS] EXERCICE PRATIQUE : SYSTÈME DE SCORE AVEC CLOSURES
// ============================================================================

/*
OBJECTIF : Créer un système de scoring pour un jeu

CAHIER DES CHARGES :
1. createScoreSystem(nomJoueur, pointsDepart = 0) -> Retourne objet avec :
   - addPoints(n) -> Ajouter n points
   - removePoints(n) -> Retirer n points (minimum 0)
   - getScore() -> Retourner score actuel
   - getHistory() -> Historique des changements
   - reset() -> Remettre à zéro
   - getStats() -> { total, moyenne, max, min } des changements
2. Les données doivent être VRAIMENT privées
3. Pas de score négatif possible

SOLUTION :
*/

function createScoreSystem(nomJoueur, pointsDepart = 0) {
    // Données privées
    let score = Math.max(0, pointsDepart);
    const historique = [];
    const debut = Date.now();

    function enregistrer(type, valeur, ancienScore) {
        historique.push({
            type,
            valeur,
            ancienScore,
            nouveauScore: score,
            timestamp: Date.now() - debut
        });
    }

    return {
        joueur: nomJoueur,

        addPoints(n) {
            if (n <= 0) throw new Error("Valeur positive requise");
            const ancien = score;
            score += n;
            enregistrer("gain", n, ancien);
            return this;
        },

        removePoints(n) {
            if (n <= 0) throw new Error("Valeur positive requise");
            const ancien = score;
            score = Math.max(0, score - n);
            enregistrer("perte", n, ancien);
            return this;
        },

        getScore() {
            return score;
        },

        getHistory() {
            return historique.map(entry => ({...entry}));
        },

        reset() {
            const ancien = score;
            score = 0;
            enregistrer("reset", ancien, ancien);
            return this;
        },

        getStats() {
            if (historique.length === 0) return null;
            const changements = historique.map(h => h.valeur);
            return {
                total: changements.length,
                somme: changements.reduce((a, b) => a + b, 0),
                moyenne: changements.reduce((a, b) => a + b, 0) / changements.length,
                max: Math.max(...changements),
                min: Math.min(...changements)
            };
        }
    };
}

// Test
const joueur = createScoreSystem("Alice", 100);
joueur.addPoints(50).addPoints(30).removePoints(20);
console.log(joueur.getScore()); // -> 160
console.log(joueur.getStats()); // { total: 3, somme: 100, moyenne: 33.3, max: 50, min: 20 }
console.log(joueur.score);      // -> undefined (vraiment privé !)


// ============================================================================
// [GUIDE] CHAPITRE 14 : PROGRAMMATION FONCTIONNELLE
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Principes de la programmation fonctionnelle
[OK] Fonctions pures et effets de bord
[OK] Composition de fonctions
[OK] Currying et application partielle
[OK] Immutabilité et immuable patterns
[OK] Foncteurs et Monades (concepts de base)
*/


// ----------------------------------------------------------------------------
// [CALCUL] FONCTIONS PURES
// ----------------------------------------------------------------------------

/*
DÉFINITION

Une fonction pure :
1. Retourne TOUJOURS le même résultat pour les mêmes arguments
2. N'a AUCUN effet de bord (pas de modification externe)
*/

// [OK] Fonction PURE
function add(a, b) {
    return a + b; // Toujours même résultat pour mêmes params
}

function calculerTaxe(prix, tauxTVA) {
    return prix * (1 + tauxTVA); // Pure, prévisible
}

// [X] Fonctions IMPURES
let total = 0;
function addImpure(a) {
    total += a; // [ATTENTION] EFFET DE BORD : modifie variable externe
    return total;
}

function getTemps() {
    return Date.now(); // [ATTENTION] Résultat différent à chaque appel
}

function afficher(message) {
    console.log(message); // [ATTENTION] Effet de bord : E/S
}

/*
[IDEE] POURQUOI LES FONCTIONS PURES ?

[OK] Testables : On sait toujours ce qu'elles font
[OK] Prévisibles : Pas de surprises
[OK] Composables : On peut les combiner facilement
[OK] Mémoizables : Résultat cacheable
[OK] Parallélisables : Pas de race conditions
*/


// ----------------------------------------------------------------------------
// [LIEN] COMPOSITION DE FONCTIONS
// ----------------------------------------------------------------------------

/*
La composition = appliquer plusieurs fonctions en séquence
Résultat d'une fonction -> entrée de la suivante
*/

// Fonctions simples et pures
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const removeSpaces = str => str.replace(/\s+/g, '_');
const addPrefix = prefix => str => `${prefix}_${str}`;

// COMPOSITION MANUELLE
function processUsername(input) {
    return removeSpaces(
        toLowerCase(
            trim(input)
        )
    );
}

// PIPE (de gauche à droite, plus lisible)
function pipe(...fns) {
    return function(value) {
        return fns.reduce((acc, fn) => fn(acc), value);
    };
}

// COMPOSE (de droite à gauche, mathématiquement correct)
function compose(...fns) {
    return function(value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

const processInput = pipe(
    trim,
    toLowerCase,
    removeSpaces
);

console.log(processInput("  Hello World  ")); // -> "hello_world"

// Composition avec données
const users = [
    { name: "Alice", age: 25, active: true, salary: 50000 },
    { name: "Bob", age: 30, active: false, salary: 60000 },
    { name: "Charlie", age: 22, active: true, salary: 45000 },
    { name: "Diana", age: 35, active: true, salary: 70000 }
];

// Fonctions réutilisables
const filterActive = arr => arr.filter(u => u.active);
const sortByAge = arr => [...arr].sort((a, b) => a.age - b.age);
const getNames = arr => arr.map(u => u.name);
const takeFst = n => arr => arr.slice(0, n);

// Composition pour obtenir les 2 plus jeunes actifs
const getTopActiveUsers = pipe(
    filterActive,
    sortByAge,
    takeFst(2),
    getNames
);

console.log(getTopActiveUsers(users)); // -> ["Charlie", "Alice"]


// ----------------------------------------------------------------------------
// [CURRY_AND_RICE] CURRYING
// ----------------------------------------------------------------------------

/*
CURRYING = Transformer une fonction multi-arguments
en séquence de fonctions à un seul argument

f(a, b, c) -> f(a)(b)(c)

POURQUOI ?
- Application partielle (configurer sans tout spécifier)
- Composition plus facile
- Réutilisabilité
*/

// Fonction normale
function multiply(a, b) {
    return a * b;
}

// Version curryfiée
function curriedMultiply(a) {
    return function(b) {
        return a * b;
    };
}

// Ou avec arrow functions
const curriedMultiply2 = a => b => a * b;

// Utilisation
const double = curriedMultiply(2);   // Application partielle !
const triple = curriedMultiply(3);

console.log(double(5));  // -> 10
console.log(triple(5));  // -> 15

// Utilitaire curry() générique
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            // Assez d'arguments -> appeler directement
            return fn(...args);
        }
        // Pas assez -> retourner fonction qui attend le reste
        return function(...moreArgs) {
            return curried(...args, ...moreArgs);
        };
    };
}

const curriedAdd = curry((a, b, c) => a + b + c);

console.log(curriedAdd(1)(2)(3));    // -> 6
console.log(curriedAdd(1, 2)(3));    // -> 6
console.log(curriedAdd(1)(2, 3));    // -> 6
console.log(curriedAdd(1, 2, 3));    // -> 6

// Applications pratiques
const curriedFilter = curry((predicate, arr) => arr.filter(predicate));
const curriedMap = curry((fn, arr) => arr.map(fn));

const filterAdults = curriedFilter(user => user.age >= 18);
const getNames2 = curriedMap(user => user.name);

const adults = filterAdults(users);
const names = getNames2(adults);
console.log(names); // -> ["Alice", "Bob", "Charlie", "Diana"]


// ----------------------------------------------------------------------------
// [ICE_CUBE] IMMUTABILITÉ
// ----------------------------------------------------------------------------

/*
PRINCIPE : Ne jamais modifier les données originales
Toujours créer de nouvelles versions
*/

// [X] MUTATION (mauvais)
function ajouterItemMutation(panier, item) {
    panier.push(item); // Modifie panier original !
    return panier;
}

// [OK] IMMUTABLE
function ajouterItem(panier, item) {
    return [...panier, item]; // Nouveau tableau
}

function retirerItem(panier, index) {
    return [...panier.slice(0, index), ...panier.slice(index + 1)];
}

function modifierQuantite(panier, index, nouvelleQuantite) {
    return panier.map((item, i) =>
        i === index ? { ...item, quantite: nouvelleQuantite } : item
    );
}

// Exemple avec objets imbriqués
const etatInitial = {
    utilisateur: { nom: "Alice", preferences: { theme: "dark" } },
    commandes: []
};

// [X] Mutation profonde
function updateThemeMutation(state, theme) {
    state.utilisateur.preferences.theme = theme; // MUTATION !
    return state;
}

// [OK] Immutable deep update
function updateTheme(state, theme) {
    return {
        ...state,                                    // Copie state
        utilisateur: {
            ...state.utilisateur,                    // Copie utilisateur
            preferences: {
                ...state.utilisateur.preferences,    // Copie préferences
                theme                                // Nouveau theme
            }
        }
    };
}

const nouvelEtat = updateTheme(etatInitial, "light");
console.log(etatInitial.utilisateur.preferences.theme); // -> "dark" (inchangé!)
console.log(nouvelEtat.utilisateur.preferences.theme);  // -> "light"


// ----------------------------------------------------------------------------
// [SCENARIO] FUNCTORS ET CONCEPTS FONCTIONNELS AVANCÉS
// ----------------------------------------------------------------------------

/*
MAYBE MONAD simplifié

Gérer null/undefined sans if partout
*/

class Maybe {
    constructor(value) {
        this._value = value;
    }

    static of(value) {
        return new Maybe(value);
    }

    isNothing() {
        return this._value === null || this._value === undefined;
    }

    // map : transformer la valeur si elle existe
    map(fn) {
        if (this.isNothing()) {
            return Maybe.of(null); // Propagation du "vide"
        }
        return Maybe.of(fn(this._value));
    }

    // getOrElse : extraire avec valeur par défaut
    getOrElse(defaultValue) {
        return this.isNothing() ? defaultValue : this._value;
    }

    // chain/flatMap : map qui retourne un Maybe
    chain(fn) {
        if (this.isNothing()) return this;
        return fn(this._value);
    }

    toString() {
        return this.isNothing() ? "Maybe(null)" : `Maybe(${this._value})`;
    }
}

// Utilisation
const user = { profil: { adresse: { ville: "Paris" } } };

// Sans Maybe (beaucoup de null checks)
const villeManuelle = user &&
    user.profil &&
    user.profil.adresse &&
    user.profil.adresse.ville || "Inconnue";

// Avec Maybe (chaînable, propre)
const ville = Maybe.of(user)
    .map(u => u.profil)
    .map(p => p.adresse)
    .map(a => a.ville)
    .getOrElse("Inconnue");

console.log(ville); // -> "Paris"

// Avec objet manquant
const villeManquante = Maybe.of(null)
    .map(u => u.profil)
    .map(p => p.adresse)
    .map(a => a.ville)
    .getOrElse("Inconnue"); // -> "Inconnue" (pas d'erreur !)

console.log(villeManquante); // -> "Inconnue"


// ============================================================================
// [COURS] EXERCICE PRATIQUE : PIPELINE DE TRAITEMENT DE DONNÉES
// ============================================================================

/*
OBJECTIF : Traiter un dataset d'employés avec programmation fonctionnelle

DONNÉES :
*/
const employes = [
    { id: 1, nom: "Alice", departement: "Tech", salaire: 55000, actif: true, annees: 5 },
    { id: 2, nom: "Bob", departement: "RH", salaire: 45000, actif: true, annees: 3 },
    { id: 3, nom: "Charlie", departement: "Tech", salaire: 60000, actif: false, annees: 8 },
    { id: 4, nom: "Diana", departement: "Tech", salaire: 70000, actif: true, annees: 10 },
    { id: 5, nom: "Eve", departement: "RH", salaire: 50000, actif: true, annees: 6 },
];

/*
TÂCHES (avec fonctions pures et composition) :
1. Filtrer les actifs
2. Calculer prime (5% par année d'ancienneté)
3. Augmenter salaire de 10%
4. Grouper par département
5. Calculer statistiques par département

SOLUTION :
*/

// Fonctions pures réutilisables
const filtrerActifs = employes => employes.filter(e => e.actif);

const ajouterPrime = employes => employes.map(e => ({
    ...e,
    prime: e.salaire * 0.05 * e.annees
}));

const augmenterSalaire = pct => employes => employes.map(e => ({
    ...e,
    salaire: Math.round(e.salaire * (1 + pct / 100))
}));

const grouperPar = cle => employes => employes.reduce((groupes, employe) => ({
    ...groupes,
    [employe[cle]]: [...(groupes[employe[cle]] || []), employe]
}), {});

const calculerStats = groupes => {
    return Object.entries(groupes).reduce((stats, [dept, employes]) => {
        const salaires = employes.map(e => e.salaire);
        return {
            ...stats,
            [dept]: {
                count: employes.length,
                salaireMoyen: Math.round(salaires.reduce((a, b) => a + b, 0) / salaires.length),
                salaireMax: Math.max(...salaires),
                salaireMin: Math.min(...salaires)
            }
        };
    }, {});
};

// Pipeline de traitement
const analyseRH = pipe(
    filtrerActifs,
    ajouterPrime,
    augmenterSalaire(10),
    grouperPar("departement"),
    calculerStats
);

const rapport = analyseRH(employes);
console.log(JSON.stringify(rapport, null, 2));
/*
-> {
  "Tech": { count: 2, salaireMoyen: 68750, salaireMax: 77000, salaireMin: 60500 },
  "RH": { count: 2, salaireMoyen: 52250, salaireMax: 55000, salaireMin: 49500 }
}
*/


// ============================================================================
// [GUIDE] CHAPITRE 15 : ITERATORS, GENERATORS ET SYMBOLS
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Protocole Iterable/Iterator
[OK] Créer des objets itérables personnalisés
[OK] Generators (function*)
[OK] Generators asynchrones (async function*)
[OK] Symbols et leur utilité
[OK] Bien-known Symbols
*/


// ----------------------------------------------------------------------------
// [SYNC] PROTOCOLE ITERABLE
// ----------------------------------------------------------------------------

/*
COMMENT ÇA MARCHE ?

for...of utilise le protocole iterable :
1. Cherche Symbol.iterator sur l'objet
2. Appelle Symbol.iterator() -> retourne un iterator
3. Répète : iterator.next() -> { value: ..., done: ... }
4. S'arrête quand done === true
*/

// Créer un objet iterable personnalisé
const range = {
    from: 1,
    to: 5,

    // Rendre iterable avec Symbol.iterator
    [Symbol.iterator]() {
        let current = this.from;
        const last = this.to;

        return {
            next() {
                if (current <= last) {
                    return { value: current++, done: false };
                }
                return { value: undefined, done: true };
            }
        };
    }
};

// Maintenant on peut utiliser for...of
for (const num of range) {
    console.log(num); // 1, 2, 3, 4, 5
}

// Et le spread operator
console.log([...range]); // -> [1, 2, 3, 4, 5]

// Classe iterable plus complète
class Range {
    constructor(start, end, step = 1) {
        this.start = start;
        this.end = end;
        this.step = step;
    }

    [Symbol.iterator]() {
        let current = this.start;
        const { end, step } = this;

        return {
            next() {
                if (current <= end) {
                    const value = current;
                    current += step;
                    return { value, done: false };
                }
                return { done: true };
            },

            // Rendre l'iterator lui-même iterable (optionnel mais bon)
            [Symbol.iterator]() { return this; }
        };
    }
}

const pairs = new Range(0, 10, 2);
console.log([...pairs]); // -> [0, 2, 4, 6, 8, 10]

for (const n of new Range(1, 5)) {
    console.log(n); // 1, 2, 3, 4, 5
}


// ----------------------------------------------------------------------------
// [RAPIDE] GENERATORS
// ----------------------------------------------------------------------------

/*
GENERATOR = Fonction qui peut SUSPENDRE et REPRENDRE son exécution

Syntaxe : function* (avec asterisque)
yield : Suspendre et retourner une valeur
*/

// Generator simple
function* counting() {
    yield 1; // Suspendre, retourner 1
    yield 2; // Reprendre, suspendre, retourner 2
    yield 3; // Reprendre, suspendre, retourner 3
    // fin -> done: true
}

const gen = counting();
console.log(gen.next()); // -> { value: 1, done: false }
console.log(gen.next()); // -> { value: 2, done: false }
console.log(gen.next()); // -> { value: 3, done: false }
console.log(gen.next()); // -> { value: undefined, done: true }

// Utiliser dans for...of
for (const val of counting()) {
    console.log(val); // 1, 2, 3
}

// Generator infini !
function* naturels() {
    let n = 1;
    while (true) {
        yield n++;
    }
}

// Prendre N premiers éléments d'un generator infini
function take(n, iterable) {
    const result = [];
    for (const val of iterable) {
        result.push(val);
        if (result.length >= n) break;
    }
    return result;
}

console.log(take(5, naturels())); // -> [1, 2, 3, 4, 5]
console.log(take(10, naturels())); // -> [1, 2, ..., 10]

// Generator pour séquence Fibonacci
function* fibonacci() {
    let [a, b] = [0, 1];
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

console.log(take(8, fibonacci())); // -> [0, 1, 1, 2, 3, 5, 8, 13]

// COMMUNICATION BIDIRECTIONNELLE avec generators
function* calculatrice() {
    let resultat = 0;

    while (true) {
        // yield retourne resultat ET reçoit une valeur via next(valeur)
        const operation = yield resultat;

        if (!operation) break;

        if (operation.type === 'add') resultat += operation.valeur;
        if (operation.type === 'sub') resultat -= operation.valeur;
        if (operation.type === 'mul') resultat *= operation.valeur;
        if (operation.type === 'div') resultat /= operation.valeur;
    }
}

const calc = calculatrice();
calc.next(); // Démarrer (premier yield)

console.log(calc.next({ type: 'add', valeur: 10 }).value); // -> 10
console.log(calc.next({ type: 'add', valeur: 5 }).value);  // -> 15
console.log(calc.next({ type: 'mul', valeur: 2 }).value);  // -> 30
console.log(calc.next({ type: 'sub', valeur: 8 }).value);  // -> 22

// yield* : Déléguer à un autre generator
function* generatorA() {
    yield 1;
    yield 2;
}

function* generatorB() {
    yield 'a';
    yield* generatorA(); // Déléguer à generatorA
    yield 'b';
}

console.log([...generatorB()]); // -> ['a', 1, 2, 'b']


// ----------------------------------------------------------------------------
// - SYMBOLS
// ----------------------------------------------------------------------------

/*
Symbol = Type primitif unique et immutable
Utilité principale : Propriétés d'objet uniques (pas de collisions)
*/

// Créer des symbols
const sym1 = Symbol('description');
const sym2 = Symbol('description');

console.log(sym1 === sym2); // -> false (TOUJOURS unique !)
console.log(typeof sym1);   // -> 'symbol'
console.log(sym1.description); // -> 'description'

// Utiliser comme clé de propriété
const ID = Symbol('id');
const user = {
    name: "Alice",
    [ID]: 12345 // Clé symbol
};

console.log(user.name); // -> "Alice"
console.log(user[ID]);  // -> 12345
console.log(Object.keys(user)); // -> ["name"] (ID symbol invisible !)

// WELL-KNOWN SYMBOLS : comportements personnalisables
class Longueur {
    constructor(valeur, unite) {
        this.valeur = valeur;
        this.unite = unite;
    }

    // Symbol.toPrimitive : Contrôler la conversion en primitif
    [Symbol.toPrimitive](hint) {
        if (hint === 'number') return this.valeur;
        if (hint === 'string') return `${this.valeur} ${this.unite}`;
        return this.valeur; // default
    }

    // Symbol.iterator : Rendre iterable
    *[Symbol.iterator]() {
        for (let i = 1; i <= this.valeur; i++) {
            yield i;
        }
    }
}

const distance = new Longueur(5, "km");
console.log(`Distance : ${distance}`); // -> "Distance : 5 km"
console.log(distance * 2);             // -> 10
console.log([...distance]);            // -> [1, 2, 3, 4, 5]


// ============================================================================
// [GUIDE] CHAPITRE 16 : PROXY, REFLECT ET WEAKREF
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer et utiliser des Proxy
[OK] Les différentes traps (pièges) disponibles
[OK] Reflect API
[OK] Cas d'usage pratiques (validation, logging, réactif)
[OK] WeakRef et FinalizationRegistry
*/


// ----------------------------------------------------------------------------
// [MIRROR] PROXY
// ----------------------------------------------------------------------------

/*
Proxy = Intercepter et personnaliser les opérations sur un objet

new Proxy(cible, gestionnaire)
- cible : L'objet à proxifier
- gestionnaire : Objet avec "traps" (intercepteurs)
*/

// Proxy basique avec logging
const handler = {
    // get : Intercepter lecture de propriété
    get(target, prop, receiver) {
        console.log(`[GET] ${String(prop)}`);
        return Reflect.get(target, prop, receiver);
    },

    // set : Intercepter écriture de propriété
    set(target, prop, value, receiver) {
        console.log(`[SET] ${String(prop)} = ${value}`);
        return Reflect.set(target, prop, value, receiver);
    }
};

const obj = new Proxy({}, handler);
obj.name = "Alice"; // -> [SET] name = Alice
obj.age = 25;       // -> [SET] age = 25
console.log(obj.name); // -> [GET] name -> "Alice"

// VALIDATION avec Proxy
function createValidatedObject(schema) {
    return new Proxy({}, {
        set(target, prop, value) {
            if (prop in schema) {
                const { type, min, max, required } = schema[prop];

                // Validation type
                if (typeof value !== type) {
                    throw new TypeError(`${prop} doit être de type ${type}`);
                }

                // Validation min (string = longueur, number = valeur)
                if (min !== undefined) {
                    const taille = type === 'string' ? value.length : value;
                    if (taille < min) {
                        throw new RangeError(`${prop} trop petit (min: ${min})`);
                    }
                }

                // Validation max
                if (max !== undefined) {
                    const taille = type === 'string' ? value.length : value;
                    if (taille > max) {
                        throw new RangeError(`${prop} trop grand (max: ${max})`);
                    }
                }
            }

            target[prop] = value;
            return true;
        },

        get(target, prop) {
            if (prop in schema && schema[prop].required && !(prop in target)) {
                throw new Error(`Propriété requise manquante: ${prop}`);
            }
            return target[prop];
        }
    });
}

const userSchema = {
    nom: { type: 'string', min: 2, max: 50, required: true },
    age: { type: 'number', min: 0, max: 150 },
    email: { type: 'string', min: 5 }
};

const userProxy = createValidatedObject(userSchema);

userProxy.nom = "Alice";     // [OK] OK
userProxy.age = 25;          // [OK] OK

try {
    userProxy.nom = "A";     // [X] Trop court
} catch (e) {
    console.log(e.message);  // "nom trop petit (min: 2)"
}

try {
    userProxy.age = "25";    // [X] Mauvais type
} catch (e) {
    console.log(e.message);  // "age doit être de type number"
}

// RÉACTIVITÉ avec Proxy (comme Vue.js !)
function reactive(obj, onChange) {
    return new Proxy(obj, {
        set(target, prop, value) {
            const oldValue = target[prop];
            target[prop] = value;

            if (oldValue !== value) {
                onChange(prop, value, oldValue);
            }

            return true;
        }
    });
}

const state = reactive(
    { compteur: 0, nom: "Alice" },
    (prop, newVal, oldVal) => {
        console.log(`[CHANGEMENT] ${prop}: ${oldVal} -> ${newVal}`);
        // Ici on pourrait re-render le DOM !
    }
);

state.compteur++;            // -> [CHANGEMENT] compteur: 0 -> 1
state.compteur++;            // -> [CHANGEMENT] compteur: 1 -> 2
state.nom = "Bob";           // -> [CHANGEMENT] nom: Alice -> Bob


// ----------------------------------------------------------------------------
// [WINDOW] REFLECT
// ----------------------------------------------------------------------------

/*
Reflect = API pour opérations réflexives sur les objets
Complément naturel de Proxy
*/

const cible = { nom: "Alice", age: 25 };

// Reflect.get(cible, prop) équivalent à cible[prop]
console.log(Reflect.get(cible, 'nom'));    // -> "Alice"

// Reflect.set(cible, prop, val) équivalent à cible[prop] = val
Reflect.set(cible, 'age', 26);
console.log(cible.age);                    // -> 26

// Reflect.has(cible, prop) équivalent à 'prop' in cible
console.log(Reflect.has(cible, 'nom'));    // -> true

// Reflect.deleteProperty(cible, prop) équivalent à delete cible[prop]
Reflect.deleteProperty(cible, 'age');
console.log(cible.age);                    // -> undefined

// Reflect.ownKeys(cible) = toutes les clés propres
const symKey = Symbol('sym');
const objAvecSym = { nom: "Alice", [symKey]: "sym value" };
console.log(Reflect.ownKeys(objAvecSym)); // -> ["nom", Symbol(sym)]

// Dans un Proxy, toujours utiliser Reflect pour la comportement par défaut
const proxyPropre = new Proxy(cible, {
    get(target, prop, receiver) {
        // log...
        return Reflect.get(target, prop, receiver); // Comportement normal
    }
});


// ============================================================================
// [GUIDE] CHAPITRE 17 : EXPRESSIONS RÉGULIÈRES (REGEX)
// ============================================================================

/*
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer et comprendre les RegEx
[OK] Classes de caractères, quantificateurs
[OK] Groupes de capture et backreferences
[OK] Flags (g, i, m, s, u, v)
[OK] Lookahead et Lookbehind
[OK] Méthodes String avec RegEx
[OK] Cas d'usage pratiques
*/


// ----------------------------------------------------------------------------
// [TEXTE] BASES DES EXPRESSIONS RÉGULIÈRES
// ----------------------------------------------------------------------------

/*
CRÉER UNE REGEX
*/

// Syntaxe littérale (recommandée)
const regex1 = /hello/;

// Constructeur (pour patterns dynamiques)
const pattern = "hello";
const regex2 = new RegExp(pattern, 'i');

/*
CLASSES DE CARACTÈRES

.       n'importe quel caractère (sauf newline)
\d      chiffre [0-9]
\D      non-chiffre
\w      mot [a-zA-Z0-9_]
\W      non-mot
\s      espace (espaces, tabs, newlines)
\S      non-espace
[abc]   un de ces caractères
[^abc]  aucun de ces caractères
[a-z]   plage de caractères
*/

// Exemples
const isChiffre = /\d+/;
const isMot = /\w+/;

console.log(isChiffre.test("123"));    // -> true
console.log(isChiffre.test("abc"));    // -> false

/*
QUANTIFICATEURS

*       0 ou plus
+       1 ou plus
?       0 ou 1 (optionnel)
{n}     exactement n
{n,}    n ou plus
{n,m}   entre n et m
*/

const regex3 = /\d{4}/; // Exactement 4 chiffres
console.log(regex3.test("2024"));   // -> true
console.log(regex3.test("202"));    // -> false

/*
ANCRES

^       Début de chaîne (ou ligne avec flag m)
$       Fin de chaîne
\b      Limite de mot
\B      Non-limite de mot
*/

const debutParA = /^Alice/;
console.log(debutParA.test("Alice est là"));   // -> true
console.log(debutParA.test("Bonjour Alice"));  // -> false

/*
FLAGS (DRAPEAUX)

i       Insensible à la casse
g       Global (toutes les correspondances)
m       Multi-ligne (^ et $ pour chaque ligne)
s       DotAll (. correspond aussi aux newlines)
u       Unicode
d       Indices (groupes avec position)
*/


// ----------------------------------------------------------------------------
// [OBJECTIF] GROUPES ET CAPTURES
// ----------------------------------------------------------------------------

// Groupe de capture
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/;
const match = "2024-12-18".match(dateRegex);

console.log(match[0]); // -> "2024-12-18" (correspondance complète)
console.log(match[1]); // -> "2024" (groupe 1)
console.log(match[2]); // -> "12" (groupe 2)
console.log(match[3]); // -> "18" (groupe 3)

// Groupes NOMMÉS (plus lisible)
const dateRegexNommee = /(?<annee>\d{4})-(?<mois>\d{2})-(?<jour>\d{2})/;
const m = "2024-12-18".match(dateRegexNommee);
console.log(m.groups.annee);  // -> "2024"
console.log(m.groups.mois);   // -> "12"
console.log(m.groups.jour);   // -> "18"

// Groupe NON-CAPTURANT (?:...)
const emailRegex = /(?:https?:\/\/)?[\w-]+\.[\w.-]+/;
// Le (?:https?:\/\/)? est optionnel et ne capture pas


// ----------------------------------------------------------------------------
// [EYES] LOOKAHEAD ET LOOKBEHIND
// ----------------------------------------------------------------------------

/*
LOOKAHEAD (?=...) : "suivi de" (sans capturer)
NEGATIVE LOOKAHEAD (?!...) : "pas suivi de"
LOOKBEHIND (?<=...) : "précédé de"
NEGATIVE LOOKBEHIND (?<!...) : "pas précédé de"
*/

// Prix : nombre suivi de "€" mais ne pas inclure "€"
const prix = /\d+(?=€)/;
console.log("100€".match(prix)?.[0]);    // -> "100"
console.log("100$".match(prix)?.[0]);    // -> undefined

// Mot précédé de "Monsieur " ou "Madame "
const titre = /(?<=Monsieur |Madame )\w+/;
console.log("Bonjour Madame Dupont".match(titre)?.[0]); // -> "Dupont"

// Validation password forte
const strongPassword = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/;
console.log(strongPassword.test("Abc123!@"));    // -> true
console.log(strongPassword.test("password"));    // -> false (pas de chiffre)
console.log(strongPassword.test("Password1"));   // -> false (pas de spécial)


// ----------------------------------------------------------------------------
// [OUTILS] MÉTHODES AVEC REGEX
// ----------------------------------------------------------------------------

const texte = "Le chat saute et le chien court";

// test() : Retourne boolean
console.log(/chat/.test(texte));     // -> true

// match() : Retourne correspondance (sans g) ou tableau (avec g)
const mots = texte.match(/\b\w{4}\b/g); // Mots de 4 lettres
console.log(mots); // -> ["chat", "saute", "chien"]

// matchAll() : Iterator de toutes correspondances avec groupes
const dateTexte = "Naissance: 2000-05-15, Mariage: 2024-06-20";
const dates = [...dateTexte.matchAll(/(\d{4})-(\d{2})-(\d{2})/g)];
dates.forEach(m => console.log(`Année: ${m[1]}, Mois: ${m[2]}, Jour: ${m[3]}`));

// search() : Retourne index ou -1
console.log(texte.search(/chien/));  // -> 20 (position)

// replace() : Remplacer
console.log(texte.replace(/chat|chien/g, "animal"));
// -> "Le animal saute et le animal court"

// replace() avec callback
const resultat = texte.replace(/\b\w+\b/g, mot => mot.toUpperCase());
console.log(resultat); // -> "LE CHAT SAUTE ET LE CHIEN COURT"

// replaceAll() : Remplacer tout (sans flag g)
console.log("ha-ha-ha".replaceAll("-", " ")); // -> "ha ha ha"

// split() avec regex
const mots2 = "un,deux;trois quatre".split(/[,;\s]+/);
console.log(mots2); // -> ["un", "deux", "trois", "quatre"]


// ----------------------------------------------------------------------------
// [LISTE] PATTERNS UTILES
// ----------------------------------------------------------------------------

/*
COLLECTION DE REGEX PRATIQUES
*/

const REGEX = {
    // Validation email
    email: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,

    // Numéro de téléphone français
    telFr: /^(?:\+33|0033|0)[1-9](?:\d{2}){4}$/,

    // Code postal français
    codePostal: /^\d{5}$/,

    // URL
    url: /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/,

    // Mot de passe fort
    motDePasse: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/,

    // Numéro de carte bancaire (basique)
    carteBancaire: /^\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}$/,

    // Adresse IP v4
    ipv4: /^(\d{1,3}\.){3}\d{1,3}$/,

    // Slug d'URL
    slug: /^[a-z0-9-]+$/,

    // Couleur hexadécimale
    hexColor: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/,

    // Date YYYY-MM-DD
    date: /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/,

    // Heure HH:MM
    heure: /^([01]\d|2[0-3]):([0-5]\d)$/,

    // Entier positif
    entierPositif: /^\d+$/,

    // Nombre décimal
    decimal: /^-?\d+(\.\d+)?$/,
};

// Utilisation
function valider(type, valeur) {
    return REGEX[type]?.test(valeur) ?? false;
}

console.log(valider('email', 'alice@example.com'));   // -> true
console.log(valider('email', 'not-an-email'));        // -> false
console.log(valider('date', '2024-12-18'));            // -> true
console.log(valider('hexColor', '#FF5733'));           // -> true
console.log(valider('hexColor', '#GGG'));              // -> false


// ============================================================================
// [COURS] EXERCICE PRATIQUE FINAL : PARSER DE LOG
// ============================================================================

/*
OBJECTIF : Analyser des fichiers de log avec RegEx et fonctions avancées

FORMAT DES LOGS :
[2024-12-18 14:30:00] [ERROR] Module:Auth - Message d'erreur
[2024-12-18 14:31:15] [INFO] Module:DB - Connexion établie
[2024-12-18 14:32:00] [WARN] Module:Cache - Cache expiré

TÂCHES :
1. Extraire toutes les entrées du log
2. Filtrer par niveau (ERROR, WARN, INFO)
3. Grouper par module
4. Statistiques par niveau

SOLUTION :
*/

const logTexte = `
[2024-12-18 14:30:00] [ERROR] Module:Auth - Authentification échouée
[2024-12-18 14:31:15] [INFO] Module:DB - Connexion établie
[2024-12-18 14:32:00] [WARN] Module:Cache - Cache expiré
[2024-12-18 14:33:00] [ERROR] Module:Auth - Token invalide
[2024-12-18 14:34:00] [INFO] Module:API - Requête traitée
[2024-12-18 14:35:00] [ERROR] Module:DB - Connexion perdue
`.trim();

// Parser avec RegEx et groupes nommés
const LOG_REGEX = /\[(?<date>\d{4}-\d{2}-\d{2}) (?<heure>\d{2}:\d{2}:\d{2})\] \[(?<niveau>\w+)\] Module:(?<module>\w+) - (?<message>.*)/g;

function parseLogs(texte) {
    return [...texte.matchAll(LOG_REGEX)].map(m => ({
        date: m.groups.date,
        heure: m.groups.heure,
        niveau: m.groups.niveau,
        module: m.groups.module,
        message: m.groups.message
    }));
}

function analyserLogs(logs) {
    const filtrerParNiveau = niveau => logs.filter(l => l.niveau === niveau);
    const grouperParModule = logs => logs.reduce((groupes, log) => ({
        ...groupes,
        [log.module]: [...(groupes[log.module] || []), log]
    }), {});

    return {
        total: logs.length,
        parNiveau: {
            errors: filtrerParNiveau('ERROR'),
            warnings: filtrerParNiveau('WARN'),
            infos: filtrerParNiveau('INFO')
        },
        parModule: grouperParModule(logs),
        stats: {
            errors: filtrerParNiveau('ERROR').length,
            warnings: filtrerParNiveau('WARN').length,
            infos: filtrerParNiveau('INFO').length
        }
    };
}

const logs = parseLogs(logTexte);
const analyse = analyserLogs(logs);

console.log(`Total: ${analyse.total} entrées`);
console.log(`Erreurs: ${analyse.stats.errors}`);
console.log(`Avertissements: ${analyse.stats.warnings}`);
console.log(`Modules avec erreurs:`,
    analyse.parNiveau.errors.map(l => l.module)
);
// -> Total: 6 entrées, Erreurs: 3, Avertissements: 1
// -> Modules avec erreurs: ["Auth", "Auth", "DB"]


// ============================================================================
// [DOCS] RÉCAPITULATIF PARTIE 4
// ============================================================================

/*
[BRAVO] FÉLICITATIONS ! PARTIE 4 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 13 : Closures
[OK] Portée lexicale
[OK] Variables capturées
[OK] Encapsulation des données
[OK] Factory functions
[OK] Memoization
[OK] Module pattern
[OK] Piège des boucles avec var

Chapitre 14 : Programmation Fonctionnelle
[OK] Fonctions pures
[OK] Immutabilité
[OK] Composition (pipe/compose)
[OK] Currying et application partielle
[OK] Maybe monad basique

Chapitre 15 : Iterators, Generators, Symbols
[OK] Protocole iterable (Symbol.iterator)
[OK] Generators (function*, yield)
[OK] Generators infinis
[OK] Communication bidirectionnelle
[OK] Symbols et well-known symbols

Chapitre 16 : Proxy et Reflect
[OK] Créer des Proxy
[OK] Traps (get, set, ...)
[OK] Validation automatique
[OK] Réactivité (comme Vue.js)
[OK] Reflect API

Chapitre 17 : RegEx
[OK] Syntaxe complète
[OK] Classes, quantificateurs, ancres
[OK] Groupes de capture (nommés)
[OK] Lookahead/lookbehind
[OK] Méthodes String avec RegEx
[OK] Collection de patterns utiles


-> PROCHAINE ÉTAPE : PARTIE 5 (js_partie5.txt)

La Partie 5 couvrira :
- Node.js et npm
- Testing avec Jest
- Outils (Webpack, Babel, ESLint)
- Patterns de conception (Design Patterns)
- Performance et sécurité
- Best practices finales

[RAPIDE] Continuez ! Vous êtes presque expert !
*/

// ============================================================================
// FIN DE PARTIE 4 - SUITE DANS js_partie5.txt
// ============================================================================

// ============================================================================
// JAVASCRIPT - PARTIE 5 : ÉCOSYSTÈME ET BONNES PRATIQUES
// ============================================================================
// Chapitres 18 à 23
// Durée estimée : 6-8 heures
// Prérequis : Parties 1, 2, 3 et 4
// ============================================================================

/*
SOMMAIRE DE CETTE PARTIE :
  Chapitre 18 : Node.js et npm
  Chapitre 19 : Testing avec Jest
  Chapitre 20 : Outils modernes (Webpack, Babel, ESLint, Prettier)
  Chapitre 21 : Design Patterns en JavaScript
  Chapitre 22 : Performance et Sécurité
  Chapitre 23 : Bonnes Pratiques et Architecture
*/


// ============================================================================
// CHAPITRE 18 : NODE.JS ET NPM
// ============================================================================

/*
POURQUOI Node.js ?
  - JavaScript côté serveur (même langage front + back)
  - Non-bloquant par nature (idéal pour I/O)
  - npm : le plus grand registre de paquets au monde
  - Utilisé par Netflix, LinkedIn, Uber, PayPal...

QUAND utiliser Node.js ?
  - APIs REST
  - Applications temps réel (chat, streaming)
  - Microservices
  - Scripts d'automatisation
  - Outils CLI
*/

// ─────────────────────────────────────────────────────────────────────────────
// 18.1 - MODULES INTÉGRÉS (BUILT-IN)
// ─────────────────────────────────────────────────────────────────────────────

// Module 'fs' (File System) - opérations sur les fichiers
const fs = require('fs');
const fsPromises = require('fs').promises; // version async/await

// Lire un fichier (synchrone - bloque le thread !)
// const contenu = fs.readFileSync('fichier.txt', 'utf8');

// Lire un fichier (asynchrone - recommandé)
async function lireFichier(chemin) {
  try {
    const contenu = await fsPromises.readFile(chemin, 'utf8');
    console.log(contenu);
    return contenu;
  } catch (err) {
    console.error(`Erreur lecture: ${err.message}`);
    throw err;
  }
}

// Écrire un fichier
async function ecrireFichier(chemin, donnees) {
  try {
    await fsPromises.writeFile(chemin, donnees, 'utf8');
    console.log('Fichier écrit avec succès');
  } catch (err) {
    console.error(`Erreur écriture: ${err.message}`);
    throw err;
  }
}

// Opérations sur les répertoires
async function operationsRepertoires() {
  // Créer un répertoire (avec parents si besoin)
  await fsPromises.mkdir('./mon-dossier/sous-dossier', { recursive: true });

  // Lister le contenu d'un répertoire
  const fichiers = await fsPromises.readdir('./');
  console.log('Fichiers:', fichiers);

  // Supprimer un fichier
  await fsPromises.unlink('./fichier-temp.txt');

  // Renommer / déplacer
  await fsPromises.rename('./ancien-nom.txt', './nouveau-nom.txt');

  // Informations sur un fichier
  const stats = await fsPromises.stat('./mon-fichier.txt');
  console.log('Taille:', stats.size, 'octets');
  console.log('Modifié le:', stats.mtime);
  console.log('Est un fichier ?', stats.isFile());
  console.log('Est un dossier ?', stats.isDirectory());
}

// ─────────────────────────────────────────────────────────────────────────────
// Module 'path' - manipulation des chemins
// ─────────────────────────────────────────────────────────────────────────────
const path = require('path');

// Jointure de chemins (cross-platform : / sur Linux/Mac, \ sur Windows)
const chemin = path.join(__dirname, 'sous-dossier', 'fichier.txt');
// __dirname = répertoire du fichier courant

// Résoudre un chemin absolu
const absolu = path.resolve('./relatif/chemin');

// Informations sur un chemin
const infos = path.parse('/home/user/documents/rapport.pdf');
// { root: '/', dir: '/home/user/documents', base: 'rapport.pdf',
//   ext: '.pdf', name: 'rapport' }

console.log(path.basename('/home/user/fichier.txt')); // 'fichier.txt'
console.log(path.dirname('/home/user/fichier.txt'));  // '/home/user'
console.log(path.extname('rapport.pdf'));             // '.pdf'
console.log(path.join('a', 'b', 'c'));                // 'a/b/c' (ou a\b\c)

// ─────────────────────────────────────────────────────────────────────────────
// Module 'os' - informations sur le système
// ─────────────────────────────────────────────────────────────────────────────
const os = require('os');

console.log(os.platform());   // 'linux', 'darwin', 'win32'
console.log(os.arch());       // 'x64', 'arm64'
console.log(os.cpus().length); // nombre de CPU
console.log(os.totalmem());   // mémoire totale en octets
console.log(os.freemem());    // mémoire libre en octets
console.log(os.homedir());    // '/home/user'
console.log(os.tmpdir());     // répertoire temporaire
console.log(os.hostname());   // nom de la machine

// ─────────────────────────────────────────────────────────────────────────────
// Module 'events' - EventEmitter
// ─────────────────────────────────────────────────────────────────────────────
const EventEmitter = require('events');

class Magasin extends EventEmitter {
  constructor() {
    super();
    this.produits = [];
  }

  ajouterProduit(produit) {
    this.produits.push(produit);
    this.emit('produit-ajoute', produit);
    
    if (this.produits.length > 100) {
      this.emit('stock-plein', this.produits.length);
    }
  }

  vendreArticle(nom) {
    const index = this.produits.findIndex(p => p.nom === nom);
    if (index === -1) {
      this.emit('erreur', new Error(`Produit "${nom}" non trouvé`));
      return null;
    }
    
    const produit = this.produits.splice(index, 1)[0];
    this.emit('vente', produit);
    return produit;
  }
}

// Utilisation
const magasin = new Magasin();

magasin.on('produit-ajoute', (produit) => {
  console.log(`[OK] Nouveau produit : ${produit.nom} à ${produit.prix}€`);
});

magasin.on('vente', (produit) => {
  console.log(`[ARGENT] Vendu : ${produit.nom}`);
});

magasin.on('erreur', (err) => {
  console.error(`[X] Erreur : ${err.message}`);
});

// .once() : écouter un seul événement
magasin.once('stock-plein', (count) => {
  console.log(`[ATTENTION] Stock plein : ${count} articles`);
});

magasin.ajouterProduit({ nom: 'Laptop', prix: 999 });
magasin.vendreArticle('Laptop');

// ─────────────────────────────────────────────────────────────────────────────
// 18.2 - CRÉER UN SERVEUR HTTP
// ─────────────────────────────────────────────────────────────────────────────

const http = require('http');

const serveur = http.createServer((req, res) => {
  // req : IncomingMessage (requête)
  // res : ServerResponse (réponse)
  
  const { url, method } = req;
  
  // En-têtes
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');
  
  // Routage manuel basique
  if (url === '/api/bonjour' && method === 'GET') {
    res.writeHead(200);
    res.end(JSON.stringify({ message: 'Bonjour !' }));
    
  } else if (url === '/api/utilisateurs' && method === 'POST') {
    let body = '';
    
    req.on('data', (chunk) => {
      body += chunk.toString();
    });
    
    req.on('end', () => {
      try {
        const donnees = JSON.parse(body);
        res.writeHead(201);
        res.end(JSON.stringify({ id: Date.now(), ...donnees }));
      } catch (err) {
        res.writeHead(400);
        res.end(JSON.stringify({ erreur: 'JSON invalide' }));
      }
    });
    
  } else {
    res.writeHead(404);
    res.end(JSON.stringify({ erreur: 'Route non trouvée' }));
  }
});

// serveur.listen(3000, () => {
//   console.log('Serveur démarré sur http://localhost:3000');
// });

// NOTE : En pratique, on utilise Express.js qui simplifie tout ça
// npm install express

// ─────────────────────────────────────────────────────────────────────────────
// 18.3 - NPM (Node Package Manager)
// ─────────────────────────────────────────────────────────────────────────────

/*
COMMANDES NPM ESSENTIELLES :

  # Initialiser un projet
  npm init                    # interactif
  npm init -y                 # avec valeurs par défaut

  # Installer des packages
  npm install express         # dépendance de production
  npm install --save-dev jest # dépendance de développement seulement
  npm install -g nodemon      # installation globale (outils CLI)
  npm install                 # installer depuis package.json

  # Supprimer
  npm uninstall express

  # Mettre à jour
  npm update                  # toutes les dépendances
  npm update express          # une dépendance spécifique

  # Voir les packages obsolètes
  npm outdated

  # Lancer des scripts
  npm start                   # démarre l'app
  npm test                    # lance les tests
  npm run dev                 # script personnalisé "dev"
  npm run build               # script personnalisé "build"

  # Audit de sécurité
  npm audit                   # vérifier les vulnérabilités
  npm audit fix               # corriger automatiquement

  # Infos
  npm ls                      # arbre des dépendances
  npm list --depth=0          # seulement les packages directs
*/

// package.json - le fichier de configuration du projet
/*
{
  "name": "mon-projet",
  "version": "1.0.0",
  "description": "Description du projet",
  "main": "index.js",
  "type": "module",          // pour utiliser ESM (import/export)
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js",
    "test": "jest",
    "test:watch": "jest --watch",
    "build": "webpack --mode production",
    "lint": "eslint src/",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "jest": "^29.0.0",
    "eslint": "^8.0.0",
    "nodemon": "^3.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}
*/

// Versioning sémantique (SemVer) : MAJEUR.MINEUR.PATCH
// "^4.18.2" = compatible avec 4.x.x (pas breaking changes)
// "~4.18.2" = seulement 4.18.x (patch seulement)
// "4.18.2"  = version exacte

// ─────────────────────────────────────────────────────────────────────────────
// 18.4 - VARIABLES D'ENVIRONNEMENT
// ─────────────────────────────────────────────────────────────────────────────

// Fichier .env (NE PAS committer dans Git !)
/*
PORT=3000
DB_URL=mongodb://localhost:27017/madb
JWT_SECRET=mon_secret_super_long_et_complexe
NODE_ENV=development
API_KEY=abc123xyz
*/

// Utilisation avec dotenv
// npm install dotenv
/*
require('dotenv').config(); // à mettre TOUT EN HAUT

const port = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;
const isProduction = process.env.NODE_ENV === 'production';

if (!dbUrl) {
  console.error('[X] DB_URL manquant dans .env');
  process.exit(1); // quitter avec erreur
}
*/

// Fichier .env.example (à committer - template sans vraies valeurs)
/*
PORT=3000
DB_URL=
JWT_SECRET=
NODE_ENV=development
API_KEY=
*/

// .gitignore - fichiers à exclure de Git
/*
node_modules/
.env
.env.local
dist/
build/
*.log
.DS_Store
coverage/
*/

// ─────────────────────────────────────────────────────────────────────────────
// 18.5 - ARGUMENTS EN LIGNE DE COMMANDE
// ─────────────────────────────────────────────────────────────────────────────

// process.argv[0] = 'node'
// process.argv[1] = nom du script
// process.argv[2+] = arguments passés par l'utilisateur

// node mon-script.js --nom Alice --age 25

function parseArgs() {
  const args = process.argv.slice(2);
  const params = {};
  
  for (let i = 0; i < args.length; i++) {
    if (args[i].startsWith('--')) {
      const cle = args[i].slice(2);
      const valeur = args[i + 1] && !args[i + 1].startsWith('--') 
        ? args[++i] 
        : true;
      params[cle] = valeur;
    }
  }
  
  return params;
}

// { nom: 'Alice', age: '25' }
// const { nom, age } = parseArgs();

// En pratique, utiliser le package 'commander' ou 'yargs'
// npm install commander


// ============================================================================
// CHAPITRE 19 : TESTING AVEC JEST
// ============================================================================

/*
POURQUOI tester ?
  - Éviter les régressions (quand on modifie le code)
  - Documentation vivante du comportement attendu
  - Confiance pour refactorer
  - Meilleure architecture (code testable = code modulaire)

TYPES DE TESTS :
  - Unitaires : tester une fonction isolée
  - Intégration : tester plusieurs modules ensemble
  - End-to-End (E2E) : tester comme un vrai utilisateur

QUAND utiliser Jest ?
  - Tests unitaires et d'intégration JavaScript/Node.js
  - Tests de composants React (avec React Testing Library)
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.1 - INSTALLATION ET CONFIGURATION
// ─────────────────────────────────────────────────────────────────────────────

// npm install --save-dev jest

// Dans package.json :
// "scripts": { "test": "jest", "test:watch": "jest --watch" }

// Pour TypeScript ou ESM : npm install --save-dev babel-jest @babel/preset-env
// babel.config.js :
// module.exports = { presets: [['@babel/preset-env', {targets: {node: 'current'}}]] };

// ─────────────────────────────────────────────────────────────────────────────
// 19.2 - STRUCTURE D'UN TEST
// ─────────────────────────────────────────────────────────────────────────────

// Le code à tester (calculatrice.js)
function additionner(a, b) {
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Les arguments doivent être des nombres');
  }
  return a + b;
}

function diviser(a, b) {
  if (b === 0) throw new Error('Division par zéro impossible');
  return a / b;
}

// module.exports = { additionner, diviser };

// Le fichier de test (calculatrice.test.js)
/*
const { additionner, diviser } = require('./calculatrice');

// describe() : groupe des tests liés
describe('additionner()', () => {
  
  // test() ou it() : un test individuel
  test('additionne deux entiers positifs', () => {
    expect(additionner(2, 3)).toBe(5);
  });
  
  test('additionne des nombres négatifs', () => {
    expect(additionner(-1, -2)).toBe(-3);
  });
  
  test('additionne des décimaux', () => {
    // toBe échouerait à cause de la virgule flottante !
    expect(additionner(0.1, 0.2)).toBeCloseTo(0.3);
  });
  
  test('lève une erreur si arguments invalides', () => {
    expect(() => additionner('a', 2)).toThrow(TypeError);
    expect(() => additionner('a', 2)).toThrow('doivent être des nombres');
  });
});

describe('diviser()', () => {
  test('divise correctement', () => {
    expect(diviser(10, 2)).toBe(5);
  });
  
  test('lève une erreur pour division par zéro', () => {
    expect(() => diviser(5, 0)).toThrow('Division par zéro');
  });
});
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.3 - MATCHERS JEST (assertions)
// ─────────────────────────────────────────────────────────────────────────────

/*
// Égalité
expect(valeur).toBe(5);             // ===
expect(objet).toEqual({ a: 1 });    // égalité profonde (structure)
expect(valeur).not.toBe(5);         // négation

// Vérité
expect(valeur).toBeTruthy();        // truthy (non falsy)
expect(valeur).toBeFalsy();         // falsy
expect(valeur).toBeNull();
expect(valeur).toBeUndefined();
expect(valeur).toBeDefined();

// Nombres
expect(valeur).toBeGreaterThan(3);
expect(valeur).toBeGreaterThanOrEqual(3);
expect(valeur).toBeLessThan(10);
expect(valeur).toBeCloseTo(0.3);    // pour les flottants

// Chaînes
expect(str).toMatch(/pattern/);
expect(str).toMatch('sous-chaine');
expect(str).toContain('partie');

// Arrays / Objets
expect(arr).toContain(item);
expect(arr).toHaveLength(3);
expect(obj).toHaveProperty('cle');
expect(obj).toHaveProperty('cle', valeur);
expect(arr).toEqual(expect.arrayContaining([1, 2])); // contient ces éléments

// Erreurs
expect(() => fn()).toThrow();
expect(() => fn()).toThrow(TypeError);
expect(() => fn()).toThrow('message');

// Promesses
await expect(promesse).resolves.toBe('succès');
await expect(promesse).rejects.toThrow('erreur');
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.4 - MOCKS (simuler des dépendances)
// ─────────────────────────────────────────────────────────────────────────────

/*
POURQUOI mocker ?
  - Isoler la fonction testée
  - Éviter les appels réseau/base de données dans les tests
  - Contrôler les valeurs de retour
  - Vérifier que des fonctions ont été appelées

// Service à tester (emailService.js)
const EnvoiEmail = require('./emailClient');

async function envoyerEmailBienvenue(utilisateur) {
  const sujet = `Bienvenue ${utilisateur.prenom} !`;
  const corps = `Bonjour ${utilisateur.prenom}, votre compte a été créé.`;
  
  await EnvoiEmail.envoyer({
    to: utilisateur.email,
    subject: sujet,
    text: corps
  });
  
  return { succes: true, destinataire: utilisateur.email };
}

// Test avec mock
jest.mock('./emailClient');

describe('envoyerEmailBienvenue()', () => {
  beforeEach(() => {
    jest.clearAllMocks(); // reset les mocks avant chaque test
  });
  
  test('envoie un email avec les bonnes données', async () => {
    // Simuler un retour de la fonction mockée
    EnvoiEmail.envoyer.mockResolvedValue({ messageId: '123' });
    
    const utilisateur = { prenom: 'Alice', email: 'alice@example.com' };
    const resultat = await envoyerEmailBienvenue(utilisateur);
    
    // Vérifier que la fonction a été appelée
    expect(EnvoiEmail.envoyer).toHaveBeenCalledTimes(1);
    expect(EnvoiEmail.envoyer).toHaveBeenCalledWith({
      to: 'alice@example.com',
      subject: 'Bienvenue Alice !',
      text: expect.stringContaining('votre compte a été créé')
    });
    
    expect(resultat).toEqual({ succes: true, destinataire: 'alice@example.com' });
  });
  
  test('propage les erreurs d\'envoi', async () => {
    EnvoiEmail.envoyer.mockRejectedValue(new Error('SMTP Error'));
    
    const utilisateur = { prenom: 'Bob', email: 'bob@example.com' };
    await expect(envoyerEmailBienvenue(utilisateur)).rejects.toThrow('SMTP Error');
  });
});
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.5 - HOOKS DE CYCLE DE VIE
// ─────────────────────────────────────────────────────────────────────────────

/*
describe('Suite de tests', () => {
  let db;
  let serveur;
  
  // Avant TOUS les tests de ce describe (une seule fois)
  beforeAll(async () => {
    db = await connecterBaseDeDonnees();
    serveur = await demarrerServeur();
  });
  
  // Après TOUS les tests (nettoyage global)
  afterAll(async () => {
    await db.deconnecter();
    await serveur.fermer();
  });
  
  // Avant CHAQUE test
  beforeEach(async () => {
    await db.viderTableUtilisateurs();
    await db.insererDonnéesTest();
  });
  
  // Après CHAQUE test
  afterEach(() => {
    jest.clearAllMocks();
  });
  
  test('test 1', () => { ... });
  test('test 2', () => { ... });
});
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.6 - TESTS PARAMÉTRÉS
// ─────────────────────────────────────────────────────────────────────────────

/*
// Évite la répétition quand on teste la même fonction avec beaucoup de cas
test.each([
  [1, 1, 2],
  [2, 3, 5],
  [-1, 1, 0],
  [0.1, 0.2, 0.3],
])('additionner(%i, %i) = %i', (a, b, expected) => {
  expect(additionner(a, b)).toBeCloseTo(expected);
});

// Version tableau d'objets
test.each([
  { input: 'bonjour', expected: true },
  { input: 'racecar', expected: true },
  { input: 'hello', expected: false },
])('estPalindrôme("$input") -> $expected', ({ input, expected }) => {
  expect(estPalindrôme(input)).toBe(expected);
});
*/

// ─────────────────────────────────────────────────────────────────────────────
// 19.7 - COVERAGE (couverture de code)
// ─────────────────────────────────────────────────────────────────────────────

/*
  # Lancer avec coverage
  npx jest --coverage

  Rapport généré :
  ------------------|---------|----------|---------|---------|
  File              | % Stmts | % Branch | % Funcs | % Lines |
  ------------------|---------|----------|---------|---------|
  calculatrice.js   |   95.2  |   88.5   |  100    |   95.1  |

  - Statements : toutes les instructions exécutées ?
  - Branches : tous les if/else couverts ?
  - Functions : toutes les fonctions appelées ?
  - Lines : toutes les lignes exécutées ?

  Objectif raisonnable : 80%+ (100% n'est pas toujours nécessaire)
*/


// ============================================================================
// CHAPITRE 20 : OUTILS MODERNES
// ============================================================================

// ─────────────────────────────────────────────────────────────────────────────
// 20.1 - ESLINT (qualité du code)
// ─────────────────────────────────────────────────────────────────────────────

/*
POURQUOI ESLint ?
  - Détecte les erreurs avant l'exécution
  - Enforce un style de code cohérent dans l'équipe
  - Règles configurables selon les préférences

npm install --save-dev eslint
npx eslint --init

Fichier .eslintrc.json :
{
  "env": {
    "browser": true,
    "es2022": true,
    "node": true
  },
  "extends": [
    "eslint:recommended"
    // ou "airbnb-base" (style guide populaire)
  ],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "rules": {
    "no-console": "warn",         // avertissement pour console.log
    "no-var": "error",            // interdire var
    "prefer-const": "error",      // utiliser const quand possible
    "eqeqeq": "error",           // toujours ===
    "no-unused-vars": "error",    // pas de variables non utilisées
    "no-undef": "error",          // pas de variables non déclarées
    "semi": ["error", "always"],  // point-virgule obligatoire
    "quotes": ["error", "single"] // guillemets simples
  },
  "ignorePatterns": ["node_modules/", "dist/"]
}

Commandes :
  npx eslint src/          # vérifier tout le dossier src
  npx eslint src/ --fix    # corriger automatiquement ce qui est possible

Dans VS Code : installer l'extension ESLint pour voir les erreurs en temps réel
*/

// ─────────────────────────────────────────────────────────────────────────────
// 20.2 - PRETTIER (formatage automatique)
// ─────────────────────────────────────────────────────────────────────────────

/*
npm install --save-dev prettier

Fichier .prettierrc :
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "printWidth": 80,
  "trailingComma": "es5",
  "arrowParens": "always"
}

Commandes :
  npx prettier --write src/    # formater tous les fichiers
  npx prettier --check src/    # vérifier sans modifier

Dans package.json (scripts) :
  "format": "prettier --write src/"

INTÉGRATION ESLINT + PRETTIER :
  npm install --save-dev eslint-config-prettier
  
  Dans .eslintrc.json, ajouter en DERNIER dans extends :
  "extends": ["eslint:recommended", "prettier"]
  
  Prettier gère le style, ESLint gère la qualité
*/

// ─────────────────────────────────────────────────────────────────────────────
// 20.3 - BABEL (transpilation)
// ─────────────────────────────────────────────────────────────────────────────

/*
POURQUOI Babel ?
  - Convertit JavaScript moderne (ES2022) en ES5 pour anciens navigateurs
  - Supporte les fonctionnalités expérimentales (décorateurs, etc.)
  - Transforme JSX (React) en JavaScript standard

npm install --save-dev @babel/core @babel/cli @babel/preset-env

babel.config.json :
{
  "presets": [
    ["@babel/preset-env", {
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not dead"]
      },
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}

Commandes :
  npx babel src/ --out-dir dist/    # transpiler src/ vers dist/
  npx babel src/ --watch            # mode observation

AUJOURD'HUI : Babel est moins nécessaire car les navigateurs modernes
supportent ES2020+. Pour les nouveaux projets, utiliser esbuild ou swc
(10-100x plus rapide que Babel).
*/

// ─────────────────────────────────────────────────────────────────────────────
// 20.4 - VITE (build tool moderne)
// ─────────────────────────────────────────────────────────────────────────────

/*
POURQUOI Vite plutôt que Webpack ?
  - Démarrage ultra-rapide (utilise les modules natifs du navigateur)
  - Hot Module Replacement (HMR) quasi-instantané
  - Configuration simple
  - Recommandé pour les nouveaux projets React/Vue/Svelte

Créer un projet :
  npm create vite@latest mon-projet
  # Choisir framework (React, Vue, Svelte, etc.)
  # Choisir variante (JS, TS)

  cd mon-projet
  npm install
  npm run dev      # serveur de développement (http://localhost:5173)
  npm run build    # build de production dans dist/
  npm run preview  # prévisualiser le build

vite.config.js :
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3000,
    proxy: {
      '/api': 'http://localhost:8080'  // proxy vers le backend
    }
  },
  build: {
    outDir: 'dist',
    minify: 'esbuild'
  }
});
*/

// ─────────────────────────────────────────────────────────────────────────────
// 20.5 - TYPESCRIPT (typage statique)
// ─────────────────────────────────────────────────────────────────────────────

/*
POURQUOI TypeScript ?
  - Détecte les erreurs de type à la compilation (pas à l'exécution)
  - Meilleure autocomplétion et documentation dans l'IDE
  - Refactoring plus sûr
  - Très utilisé en entreprise (Angular, de nombreux projets React)

npm install --save-dev typescript
npx tsc --init   # créer tsconfig.json

tsconfig.json (essentiel) :
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,             // toutes les vérifications strictes
    "noImplicitAny": true,      // erreur si type 'any' implicite
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

BASES TYPESCRIPT :

// Types primitifs
let nom: string = 'Alice';
let age: number = 25;
let actif: boolean = true;
let vide: null = null;
let inconnu: undefined = undefined;

// Types union
let id: string | number = '123';
id = 456; // OK

// Arrays
let nombres: number[] = [1, 2, 3];
let noms: Array<string> = ['Alice', 'Bob'];

// Tuple
let paire: [string, number] = ['Alice', 25];

// Objets avec interfaces
interface Utilisateur {
  id: number;
  nom: string;
  email: string;
  age?: number;   // propriété optionnelle
  readonly createdAt: Date; // ne peut pas être modifié
}

const user: Utilisateur = {
  id: 1,
  nom: 'Alice',
  email: 'alice@example.com',
  createdAt: new Date()
};

// Types génériques
function identite<T>(valeur: T): T {
  return valeur;
}

identite<string>('bonjour'); // type inféré automatiquement aussi

// Types utilitaires
type UtilisateurPartiel = Partial<Utilisateur>;    // tout optionnel
type UtilisateurReq = Required<Utilisateur>;       // tout requis
type SansId = Omit<Utilisateur, 'id'>;            // sans une propriété
type SeulementNom = Pick<Utilisateur, 'nom' | 'email'>; // seulement certaines

// Fonctions typées
function additionner(a: number, b: number): number {
  return a + b;
}

const multiplier = (a: number, b: number): number => a * b;

// Async
async function obtenirUtilisateur(id: number): Promise<Utilisateur> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}
*/


// ============================================================================
// CHAPITRE 21 : DESIGN PATTERNS EN JAVASCRIPT
// ============================================================================

/*
POURQUOI les patterns ?
  - Solutions éprouvées à des problèmes récurrents
  - Langage commun entre développeurs
  - Code plus maintenable et extensible
*/

// ─────────────────────────────────────────────────────────────────────────────
// 21.1 - SINGLETON
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Une seule instance d'une classe dans toute l'app
(connexion DB, configuration, logger)
*/
class BaseDeDonnees {
  static #instance = null;
  #connexion = null;

  constructor(url) {
    if (BaseDeDonnees.#instance) {
      return BaseDeDonnees.#instance;
    }
    this.url = url;
    this.#connexion = this.#connecter();
    BaseDeDonnees.#instance = this;
  }

  #connecter() {
    console.log(`Connexion à ${this.url}`);
    return { url: this.url, connecte: true };
  }

  static getInstance(url = 'mongodb://localhost:27017') {
    if (!BaseDeDonnees.#instance) {
      new BaseDeDonnees(url);
    }
    return BaseDeDonnees.#instance;
  }

  executer(query) {
    return `Résultat de: ${query}`;
  }
}

const db1 = BaseDeDonnees.getInstance();
const db2 = BaseDeDonnees.getInstance();
console.log(db1 === db2); // true - même instance !

// ─────────────────────────────────────────────────────────────────────────────
// 21.2 - OBSERVER (Publish/Subscribe)
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Notifier plusieurs objets d'un changement sans couplage fort
(événements UI, mise à jour de state, notifications)
*/
class Store {
  #state;
  #abonnes = new Map();

  constructor(etatInitial) {
    this.#state = etatInitial;
  }

  getState() {
    return { ...this.#state }; // copie pour éviter mutation directe
  }

  setState(nouvelEtat) {
    const ancienEtat = this.#state;
    this.#state = { ...this.#state, ...nouvelEtat };
    this.#notifier(ancienEtat, this.#state);
  }

  abonner(evenement, callback) {
    if (!this.#abonnes.has(evenement)) {
      this.#abonnes.set(evenement, new Set());
    }
    this.#abonnes.get(evenement).add(callback);

    // Retourner une fonction de désabonnement
    return () => {
      this.#abonnes.get(evenement)?.delete(callback);
    };
  }

  #notifier(ancienEtat, nouvelEtat) {
    // Notifier les abonnés aux propriétés modifiées
    for (const [cle, valeur] of Object.entries(nouvelEtat)) {
      if (ancienEtat[cle] !== valeur) {
        this.#abonnes.get(cle)?.forEach(cb => cb(valeur, ancienEtat[cle]));
      }
    }
    // Notifier les abonnés à tous les changements
    this.#abonnes.get('*')?.forEach(cb => cb(nouvelEtat, ancienEtat));
  }
}

// Utilisation
const store = new Store({ utilisateur: null, theme: 'clair', compteur: 0 });

const seDesabonner = store.abonner('compteur', (nouvel, ancien) => {
  console.log(`Compteur: ${ancien} -> ${nouvel}`);
});

store.abonner('*', (etat) => {
  console.log('État mis à jour:', etat);
});

store.setState({ compteur: 1 });
store.setState({ compteur: 2, theme: 'sombre' });
seDesabonner(); // se désabonner
store.setState({ compteur: 3 }); // ce changement n'est plus notifié

// ─────────────────────────────────────────────────────────────────────────────
// 21.3 - FACTORY
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Créer des objets sans spécifier la classe exacte
(différents types de notifications, connexions DB, etc.)
*/
class Notification {
  constructor(message) {
    this.message = message;
    this.date = new Date();
  }

  envoyer() {
    throw new Error('Méthode abstraite');
  }
}

class NotifEmail extends Notification {
  constructor(message, destinataire) {
    super(message);
    this.destinataire = destinataire;
  }

  envoyer() {
    console.log(`[EMAIL] Email à ${this.destinataire}: ${this.message}`);
  }
}

class NotifSMS extends Notification {
  constructor(message, telephone) {
    super(message);
    this.telephone = telephone;
  }

  envoyer() {
    console.log(`[MOBILE] SMS au ${this.telephone}: ${this.message}`);
  }
}

class NotifPush extends Notification {
  constructor(message, deviceToken) {
    super(message);
    this.deviceToken = deviceToken;
  }

  envoyer() {
    console.log(`[NOTIF] Push à ${this.deviceToken}: ${this.message}`);
  }
}

// Factory
class NotificationFactory {
  static creer(type, message, contact) {
    const types = {
      email: NotifEmail,
      sms: NotifSMS,
      push: NotifPush,
    };

    const Classe = types[type.toLowerCase()];
    if (!Classe) {
      throw new Error(`Type de notification inconnu: ${type}`);
    }

    return new Classe(message, contact);
  }
}

// Utilisation simple
const notifs = [
  NotificationFactory.creer('email', 'Bienvenue !', 'alice@example.com'),
  NotificationFactory.creer('sms', 'Code: 1234', '+33612345678'),
  NotificationFactory.creer('push', 'Nouveau message', 'token-abc'),
];

notifs.forEach(n => n.envoyer());

// ─────────────────────────────────────────────────────────────────────────────
// 21.4 - STRATEGY
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Changer l'algorithme utilisé à runtime
(tri, validation, paiement, compression)
*/
// Stratégies de tri
const strategies = {
  alphabetique: (a, b) => a.nom.localeCompare(b.nom),
  prixCroissant: (a, b) => a.prix - b.prix,
  prixDecroissant: (a, b) => b.prix - a.prix,
  popularite: (a, b) => b.ventes - a.ventes,
};

class Catalogue {
  #produits;
  #strategie;

  constructor(produits, strategie = 'alphabetique') {
    this.#produits = produits;
    this.#strategie = strategies[strategie];
  }

  changerStrategie(nomStrategie) {
    if (!strategies[nomStrategie]) {
      throw new Error(`Stratégie inconnue: ${nomStrategie}`);
    }
    this.#strategie = strategies[nomStrategie];
    return this; // chainable
  }

  getProduits() {
    return [...this.#produits].sort(this.#strategie);
  }
}

// Utilisation
const catalogue = new Catalogue([
  { nom: 'Laptop', prix: 999, ventes: 150 },
  { nom: 'Souris', prix: 29, ventes: 800 },
  { nom: 'Clavier', prix: 79, ventes: 400 },
]);

console.log(catalogue.getProduits().map(p => p.nom)); // alphabétique
catalogue.changerStrategie('prixCroissant');
console.log(catalogue.getProduits().map(p => p.nom)); // par prix

// ─────────────────────────────────────────────────────────────────────────────
// 21.5 - DECORATOR
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Ajouter des fonctionnalités à un objet sans modifier sa classe
(logging, caching, validation, authentification)
*/
// Fonction de base
async function obtenirDonnees(url) {
  const response = await fetch(url);
  return response.json();
}

// Décorateur de cache
function avecCache(fn, dureeMs = 60000) {
  const cache = new Map();

  return async function(...args) {
    const cle = JSON.stringify(args);
    const maintenant = Date.now();

    if (cache.has(cle)) {
      const { valeur, timestamp } = cache.get(cle);
      if (maintenant - timestamp < dureeMs) {
        console.log('[SAUVEGARDE] Cache hit');
        return valeur;
      }
    }

    console.log('[WEB] Fetch réel');
    const valeur = await fn(...args);
    cache.set(cle, { valeur, timestamp: maintenant });
    return valeur;
  };
}

// Décorateur de retry
function avecRetry(fn, maxEssais = 3, delaiMs = 1000) {
  return async function(...args) {
    let derniereErreur;

    for (let essai = 1; essai <= maxEssais; essai++) {
      try {
        return await fn(...args);
      } catch (err) {
        derniereErreur = err;
        if (essai < maxEssais) {
          console.log(`Essai ${essai} échoué, retry dans ${delaiMs}ms...`);
          await new Promise(r => setTimeout(r, delaiMs * essai)); // backoff exponentiel
        }
      }
    }

    throw derniereErreur;
  };
}

// Décorateur de logging
function avecLogging(fn, nom = fn.name) {
  return async function(...args) {
    const debut = Date.now();
    console.log(`[BLACK_RIGHT-POINTING_TRIANGLE] ${nom}(${args.join(', ')})`);
    try {
      const resultat = await fn(...args);
      console.log(`[OK] ${nom} terminé en ${Date.now() - debut}ms`);
      return resultat;
    } catch (err) {
      console.error(`[X] ${nom} échoué: ${err.message}`);
      throw err;
    }
  };
}

// Composition de décorateurs (de droite à gauche)
const obtenirDonneesAvecTout = avecLogging(
  avecCache(
    avecRetry(obtenirDonnees, 3),
    30000 // cache 30 secondes
  ),
  'obtenirDonnees'
);

// ─────────────────────────────────────────────────────────────────────────────
// 21.6 - COMMAND
// ─────────────────────────────────────────────────────────────────────────────

/*
QUAND : Encapsuler des actions pour les annuler/rejouer
(éditeur de texte avec undo/redo, formulaires avec historique)
*/
class Editeur {
  #texte = '';
  #historique = [];
  #futur = [];

  get texte() {
    return this.#texte;
  }

  executer(commande) {
    this.#texte = commande.executer(this.#texte);
    this.#historique.push(commande);
    this.#futur = []; // effacer le futur après une nouvelle action
  }

  annuler() {
    if (this.#historique.length === 0) return;
    const commande = this.#historique.pop();
    this.#texte = commande.annuler(this.#texte);
    this.#futur.push(commande);
  }

  retablir() {
    if (this.#futur.length === 0) return;
    const commande = this.#futur.pop();
    this.#texte = commande.executer(this.#texte);
    this.#historique.push(commande);
  }
}

class CommandeInsertion {
  constructor(position, texte) {
    this.position = position;
    this.texte = texte;
  }

  executer(contenu) {
    return contenu.slice(0, this.position) + this.texte + contenu.slice(this.position);
  }

  annuler(contenu) {
    return contenu.slice(0, this.position) + contenu.slice(this.position + this.texte.length);
  }
}

class CommandeSuppression {
  constructor(debut, fin) {
    this.debut = debut;
    this.fin = fin;
    this.texteSupprime = ''; // sauvegardé lors de l'exécution
  }

  executer(contenu) {
    this.texteSupprime = contenu.slice(this.debut, this.fin);
    return contenu.slice(0, this.debut) + contenu.slice(this.fin);
  }

  annuler(contenu) {
    return contenu.slice(0, this.debut) + this.texteSupprime + contenu.slice(this.debut);
  }
}

// Utilisation
const editeur = new Editeur();
editeur.executer(new CommandeInsertion(0, 'Bonjour'));
editeur.executer(new CommandeInsertion(7, ' monde'));
console.log(editeur.texte); // 'Bonjour monde'

editeur.annuler();
console.log(editeur.texte); // 'Bonjour'

editeur.retablir();
console.log(editeur.texte); // 'Bonjour monde'


// ============================================================================
// CHAPITRE 22 : PERFORMANCE ET SÉCURITÉ
// ============================================================================

// ─────────────────────────────────────────────────────────────────────────────
// 22.1 - OPTIMISATIONS DE PERFORMANCE
// ─────────────────────────────────────────────────────────────────────────────

// DEBOUNCE : éviter les appels trop fréquents (recherche en temps réel)
function debounce(fn, delaiMs) {
  let timer;

  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delaiMs);
  };
}

// Exemple : chercher seulement 300ms après que l'utilisateur arrête de taper
const rechercherDebounce = debounce(async (terme) => {
  console.log(`[RECHERCHE] Recherche: ${terme}`);
  // const resultats = await fetch(`/api/recherche?q=${terme}`);
}, 300);

// Sur un champ input :
// inputElement.addEventListener('input', (e) => rechercherDebounce(e.target.value));

// ─────────────────────────────────────────────────────────────────────────────
// THROTTLE : limiter la fréquence d'exécution
function throttle(fn, limiteMs) {
  let dernierAppel = 0;

  return function(...args) {
    const maintenant = Date.now();
    if (maintenant - dernierAppel >= limiteMs) {
      dernierAppel = maintenant;
      return fn.apply(this, args);
    }
  };
}

// Exemple : traiter le scroll max une fois toutes les 100ms
const gererScrollThrottle = throttle(() => {
  console.log('Position:', window.scrollY);
}, 100);
// window.addEventListener('scroll', gererScrollThrottle);

// ─────────────────────────────────────────────────────────────────────────────
// LAZY LOADING (chargement paresseux)

// 1. Import dynamique de modules
async function chargerModule() {
  // Le module n'est chargé qu'au moment où on en a besoin
  const { default: graphique } = await import('./modules/graphique.js');
  graphique.afficher();
}

// 2. Intersection Observer (lazy load d'images)
function configurerLazyImages() {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src; // charger l'image réelle
        img.removeAttribute('data-src');
        observer.unobserve(img); // ne plus observer
      }
    });
  }, { threshold: 0.1 }); // déclencher quand 10% visible

  document.querySelectorAll('img[data-src]').forEach(img => {
    observer.observe(img);
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// VIRTUALISATION (listes longues)

// Pour des listes de 1000+ éléments, n'afficher que ce qui est visible
// Libraries : react-virtual, tanstack-virtual, react-window

// Concept de base :
function renderListeVirtuelle(items, containerHeight, itemHeight) {
  let scrollTop = 0;
  const itemsVisibles = Math.ceil(containerHeight / itemHeight);
  const buffer = 5; // rendu en dehors de l'écran pour le défilement

  function getItemsARendre() {
    const premierIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
    const dernierIndex = Math.min(
      items.length,
      premierIndex + itemsVisibles + buffer * 2
    );

    return {
      items: items.slice(premierIndex, dernierIndex),
      offsetTop: premierIndex * itemHeight,
      totalHeight: items.length * itemHeight,
    };
  }

  return getItemsARendre();
}

// ─────────────────────────────────────────────────────────────────────────────
// WEB WORKERS (calculs en arrière-plan)

// Sans Worker, un calcul lourd bloquerait l'interface !

// worker.js (fichier séparé)
/*
self.onmessage = function(e) {
  const { donnees } = e.data;
  
  // Calcul lourd sans bloquer l'UI
  const resultat = donnees.reduce((acc, val) => acc + val * val, 0);
  
  self.postMessage({ resultat });
};
*/

// Dans le fichier principal
function utiliserWorker(donnees) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js');

    worker.postMessage({ donnees });

    worker.onmessage = (e) => {
      resolve(e.data.resultat);
      worker.terminate();
    };

    worker.onerror = (err) => {
      reject(err);
      worker.terminate();
    };
  });
}

// ─────────────────────────────────────────────────────────────────────────────
// 22.2 - SÉCURITÉ
// ─────────────────────────────────────────────────────────────────────────────

/*
VULNÉRABILITÉS COURANTES ET PRÉVENTION :

────────────────────────────────────────
1. XSS (Cross-Site Scripting)
────────────────────────────────────────
Problème : injecter du JS malveillant via des inputs utilisateur
*/

// [X] DANGEREUX : innerHTML avec données utilisateur
function afficherCommentaireDangereux(texte) {
  // Si texte = '<script>volerCookies()</script>' -> EXÉCUTÉ !
  document.getElementById('commentaire').innerHTML = texte;
}

// [OK] SÉCURISÉ : textContent échappe automatiquement le HTML
function afficherCommentaireSur(texte) {
  document.getElementById('commentaire').textContent = texte;
}

// [OK] Si HTML dynamique nécessaire : sanitizer
function sanitiserHTML(html) {
  // Utiliser DOMPurify (npm install dompurify)
  // return DOMPurify.sanitize(html);
  
  // Version basique (insuffisante pour la production) :
  const div = document.createElement('div');
  div.textContent = html;
  return div.innerHTML;
}

/*
────────────────────────────────────────
2. Validation des données
────────────────────────────────────────
TOUJOURS valider côté serveur (le client peut être manipulé)
La validation côté client est juste pour l'UX
*/
class ValidateurUtilisateur {
  static valider(donnees) {
    const erreurs = [];

    // Email
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!donnees.email || !emailRegex.test(donnees.email)) {
      erreurs.push('Email invalide');
    }

    // Mot de passe
    if (!donnees.motDePasse || donnees.motDePasse.length < 8) {
      erreurs.push('Mot de passe trop court (min 8 caractères)');
    }

    // Nom (éviter injection)
    if (donnees.nom && /[<>'"&]/.test(donnees.nom)) {
      erreurs.push('Nom contient des caractères invalides');
    }

    // Longueur maximale (éviter les attaques par flooding)
    if (donnees.biographie && donnees.biographie.length > 500) {
      erreurs.push('Biographie trop longue (max 500 caractères)');
    }

    return {
      valide: erreurs.length === 0,
      erreurs
    };
  }
}

/*
────────────────────────────────────────
3. Gestion sécurisée des tokens JWT
────────────────────────────────────────
*/
// [X] Ne PAS stocker les tokens dans localStorage (vulnérable au XSS)
// localStorage.setItem('token', jwt);

// [OK] Utiliser des cookies HttpOnly (inaccessibles via JavaScript)
// Le serveur doit configurer : Set-Cookie: token=jwt; HttpOnly; Secure; SameSite=Strict

// Pour les APIs, envoyer le token dans les headers :
async function requeteAuthentifiee(url, options = {}) {
  // Le cookie HttpOnly est envoyé automatiquement
  const response = await fetch(url, {
    ...options,
    credentials: 'include', // inclure les cookies
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });

  if (response.status === 401) {
    // Rediriger vers la page de connexion
    window.location.href = '/connexion';
    return null;
  }

  return response;
}

/*
────────────────────────────────────────
4. Content Security Policy (CSP)
────────────────────────────────────────
Dans les en-têtes HTTP du serveur :
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.com

Cela empêche le chargement de scripts externes non autorisés.

────────────────────────────────────────
5. Rate limiting
────────────────────────────────────────
Côté serveur (Express) :
npm install express-rate-limit

import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,                    // max 100 requêtes par IP
  message: 'Trop de requêtes, réessayez dans 15 minutes'
});

app.use('/api/', limiter);
*/


// ============================================================================
// CHAPITRE 23 : BONNES PRATIQUES ET ARCHITECTURE
// ============================================================================

// ─────────────────────────────────────────────────────────────────────────────
// 23.1 - CLEAN CODE
// ─────────────────────────────────────────────────────────────────────────────

/*
NOMMAGE EXPRESSIF
*/

// [X] Mauvais nommage
const d = new Date();
const u = users.filter(x => x.a > 18);
function calc(a, b, t) { return a + b * t; }

// [OK] Bon nommage
const maintenant = new Date();
const utilisateursMajeurs = users.filter(user => user.age > 18);
function calculerTotalAvecTaxe(prixHT, taxe, tauxTaxe) {
  return prixHT + taxe * tauxTaxe;
}

// ─────────────────────────────────────────────────────────────────────────────
// FONCTIONS COURTES (principe de responsabilité unique)

// [X] Trop de responsabilités
async function traiterCommande(commandeId) {
  const commande = await db.findCommande(commandeId);
  if (!commande) throw new Error('Commande introuvable');
  
  // Calcul prix
  let total = 0;
  for (const item of commande.items) {
    const produit = await db.findProduit(item.produitId);
    total += produit.prix * item.quantite;
  }
  total *= 1.2; // TVA
  
  // Email
  const email = await db.findUtilisateur(commande.userId);
  await emailService.envoyer(email.adresse, `Votre commande : ${total}€`);
  
  // Mise à jour
  await db.updateCommande(commandeId, { total, statut: 'traite' });
  
  return { commandeId, total };
}

// [OK] Responsabilités séparées
async function calculerTotalCommande(commande) {
  let sousTotal = 0;
  for (const item of commande.items) {
    const produit = await db.findProduit(item.produitId);
    sousTotal += produit.prix * item.quantite;
  }
  return sousTotal * 1.2;
}

async function notifierUtilisateur(userId, total) {
  const utilisateur = await db.findUtilisateur(userId);
  return emailService.envoyer(
    utilisateur.adresse,
    `Votre commande : ${total}€`
  );
}

async function finaliserCommande(commandeId, total) {
  return db.updateCommande(commandeId, { total, statut: 'traite' });
}

async function traiterCommandePropre(commandeId) {
  const commande = await db.findCommande(commandeId);
  if (!commande) throw new Error('Commande introuvable');

  const total = await calculerTotalCommande(commande);
  
  await Promise.all([
    notifierUtilisateur(commande.userId, total),
    finaliserCommande(commandeId, total),
  ]);

  return { commandeId, total };
}

// ─────────────────────────────────────────────────────────────────────────────
// ÉVITER LES MAGIC NUMBERS
// ─────────────────────────────────────────────────────────────────────────────

// [X] Mauvais
function estValide(age) {
  return age >= 18 && age <= 120;
}

setTimeout(actualiser, 86400000);

// [OK] Bon - les constantes expliquent l'intention
const AGE_MINIMUM = 18;
const AGE_MAXIMUM = 120;
const UN_JOUR_MS = 24 * 60 * 60 * 1000;

function estAgeMajeurValide(age) {
  return age >= AGE_MINIMUM && age <= AGE_MAXIMUM;
}

setTimeout(actualiser, UN_JOUR_MS);

// ─────────────────────────────────────────────────────────────────────────────
// EARLY RETURN (guard clauses)
// ─────────────────────────────────────────────────────────────────────────────

// [X] Imbrication profonde
async function traiterDemande(utilisateur, demande) {
  if (utilisateur) {
    if (utilisateur.actif) {
      if (demande) {
        if (demande.montant > 0) {
          // ... logique principale enfouie
          return await traiter(utilisateur, demande);
        } else {
          throw new Error('Montant invalide');
        }
      } else {
        throw new Error('Demande manquante');
      }
    } else {
      throw new Error('Compte inactif');
    }
  } else {
    throw new Error('Utilisateur requis');
  }
}

// [OK] Guard clauses : retour précoce si condition non remplie
async function traiterDemandeClean(utilisateur, demande) {
  if (!utilisateur) throw new Error('Utilisateur requis');
  if (!utilisateur.actif) throw new Error('Compte inactif');
  if (!demande) throw new Error('Demande manquante');
  if (demande.montant <= 0) throw new Error('Montant invalide');

  // Logique principale au même niveau, facile à lire
  return await traiter(utilisateur, demande);
}

// ─────────────────────────────────────────────────────────────────────────────
// 23.2 - ARCHITECTURE DE PROJET
// ─────────────────────────────────────────────────────────────────────────────

/*
STRUCTURE D'UN PROJET NODE.JS (API REST)

mon-api/
├── src/
│   ├── config/
│   │   ├── database.js      // connexion DB
│   │   ├── env.js           // variables d'environnement
│   │   └── logger.js        // configuration logging
│   ├── controllers/         // gestion des requêtes HTTP
│   │   ├── user.controller.js
│   │   └── auth.controller.js
│   ├── services/            // logique métier
│   │   ├── user.service.js
│   │   └── email.service.js
│   ├── repositories/        // accès aux données
│   │   └── user.repository.js
│   ├── middleware/
│   │   ├── auth.middleware.js
│   │   ├── validation.middleware.js
│   │   └── error.middleware.js
│   ├── routes/
│   │   ├── user.routes.js
│   │   └── index.js
│   ├── models/              // schémas de données
│   │   └── user.model.js
│   └── app.js               // configuration Express
├── tests/
│   ├── unit/
│   ├── integration/
│   └── fixtures/
├── .env
├── .env.example
├── .eslintrc.json
├── .prettierrc
├── package.json
└── README.md

SÉPARATION DES COUCHES :
  Route -> Controller -> Service -> Repository -> DB
  
  - Route : définit les endpoints URL
  - Controller : gère HTTP (req, res), valide les entrées
  - Service : logique métier pure (pas de HTTP, pas de SQL)
  - Repository : accès données (SQL, Mongo, etc.)
  
  Avantages :
  - Tester le Service sans HTTP ni DB (mocks)
  - Changer de DB sans toucher au Service
  - Réutiliser le Service dans différents contextes
*/

// Exemple concret - architecture en couches :

// user.repository.js
class UserRepository {
  constructor(db) {
    this.db = db;
  }

  async findById(id) {
    return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
  }

  async findByEmail(email) {
    return this.db.query('SELECT * FROM users WHERE email = ?', [email]);
  }

  async create(donnees) {
    const result = await this.db.query(
      'INSERT INTO users (nom, email, password_hash) VALUES (?, ?, ?)',
      [donnees.nom, donnees.email, donnees.passwordHash]
    );
    return this.findById(result.insertId);
  }

  async update(id, donnees) {
    const champs = Object.keys(donnees).map(k => `${k} = ?`).join(', ');
    await this.db.query(`UPDATE users SET ${champs} WHERE id = ?`, 
      [...Object.values(donnees), id]);
    return this.findById(id);
  }
}

// user.service.js
class UserService {
  constructor(userRepository, emailService, hasher) {
    this.userRepo = userRepository;
    this.emailService = emailService;
    this.hasher = hasher;
  }

  async creerUtilisateur(donnees) {
    // Validation métier
    const existant = await this.userRepo.findByEmail(donnees.email);
    if (existant) {
      throw new Error('Email déjà utilisé');
    }

    // Hash du mot de passe
    const passwordHash = await this.hasher.hash(donnees.motDePasse);

    // Création
    const utilisateur = await this.userRepo.create({
      nom: donnees.nom,
      email: donnees.email,
      passwordHash,
    });

    // Email de bienvenue (ne pas bloquer si ça échoue)
    this.emailService.envoyerBienvenue(utilisateur).catch(err => {
      console.error('Email bienvenue échoué:', err);
    });

    return this.formaterUtilisateur(utilisateur);
  }

  formaterUtilisateur(user) {
    // Ne jamais exposer le hash du mot de passe
    const { passwordHash, ...userSansMotDePasse } = user;
    return userSansMotDePasse;
  }
}

// user.controller.js
class UserController {
  constructor(userService) {
    this.userService = userService;
  }

  // Express handler
  creer = async (req, res, next) => {
    try {
      const { nom, email, motDePasse } = req.body;

      // Validation HTTP (format, présence)
      if (!nom || !email || !motDePasse) {
        return res.status(400).json({
          erreur: 'Champs requis: nom, email, motDePasse'
        });
      }

      const utilisateur = await this.userService.creerUtilisateur({
        nom, email, motDePasse
      });

      res.status(201).json({ succes: true, utilisateur });
    } catch (err) {
      next(err); // passer à l'error middleware
    }
  };
}

// ─────────────────────────────────────────────────────────────────────────────
// 23.3 - GIT ET COLLABORATION
// ─────────────────────────────────────────────────────────────────────────────

/*
CONVENTIONS DE COMMITS (Conventional Commits) :

  <type>[portée optionnelle]: <description>

  Types :
    feat     : nouvelle fonctionnalité
    fix      : correction de bug
    docs     : documentation seulement
    style    : formatage (pas de changement de logique)
    refactor : ni feat ni fix
    test     : ajout/modification de tests
    chore    : maintenance (deps, config)
    perf     : amélioration performance
    ci       : intégration continue

  Exemples :
    feat(auth): ajouter connexion OAuth Google
    fix(panier): corriger calcul total avec remises
    docs(api): documenter endpoint /utilisateurs
    test(auth): ajouter tests pour reset mot de passe
    refactor(user): séparer service et repository

WORKFLOW GIT (GitHub Flow simplifié) :

  1. main (toujours déployable)
  2. Créer une branche pour chaque feature/fix
     git checkout -b feat/connexion-oauth
  3. Commits réguliers
  4. Pull Request vers main
  5. Code review
  6. Merge + déploiement

BRANCHES :
  main          : production
  develop       : intégration (si Gitflow)
  feat/xxx      : nouvelle fonctionnalité
  fix/xxx       : correction de bug
  refactor/xxx  : refactoring
*/

// ─────────────────────────────────────────────────────────────────────────────
// 23.4 - DOCUMENTATION
// ─────────────────────────────────────────────────────────────────────────────

/**
 * Calcule le montant total d'une commande avec TVA.
 *
 * @param {Object[]} articles - Liste des articles de la commande
 * @param {number} articles[].prix - Prix unitaire HT de l'article
 * @param {number} articles[].quantite - Quantité commandée
 * @param {number} [tauxTVA=0.2] - Taux de TVA (par défaut 20%)
 * @returns {Object} Détail du calcul
 * @returns {number} returns.sousTotal - Total HT
 * @returns {number} returns.tva - Montant de la TVA
 * @returns {number} returns.total - Total TTC
 * @throws {TypeError} Si articles n'est pas un tableau
 * @throws {RangeError} Si tauxTVA est négatif
 *
 * @example
 * const commande = calculerTotaux([
 *   { prix: 100, quantite: 2 },
 *   { prix: 50, quantite: 1 }
 * ]);
 * // Returns: { sousTotal: 250, tva: 50, total: 300 }
 */
function calculerTotaux(articles, tauxTVA = 0.2) {
  if (!Array.isArray(articles)) {
    throw new TypeError('articles doit être un tableau');
  }
  if (tauxTVA < 0) {
    throw new RangeError('tauxTVA ne peut pas être négatif');
  }

  const sousTotal = articles.reduce((acc, { prix, quantite }) => {
    return acc + prix * quantite;
  }, 0);

  const tva = sousTotal * tauxTVA;
  const total = sousTotal + tva;

  return { sousTotal, tva, total };
}

// ─────────────────────────────────────────────────────────────────────────────
// 23.5 - RÉSUMÉ FINAL : LES 20 RÈGLES D'OR
// ─────────────────────────────────────────────────────────────────────────────

/*
════════════════════════════════════════════════════════════
  LES 20 RÈGLES D'OR DU DÉVELOPPEUR JAVASCRIPT
════════════════════════════════════════════════════════════

VARIABLES ET TYPES
  1. [OK] Utiliser const par défaut, let si nécessaire, JAMAIS var
  2. [OK] Toujours === (jamais ==)
  3. [OK] Nommer les variables de manière explicite (pas d, x, tmp)

FONCTIONS
  4. [OK] Une fonction = une responsabilité
  5. [OK] Arrow functions pour les callbacks (preserve 'this')
  6. [OK] Guard clauses plutôt que if imbriqués profonds
  7. [OK] Pas de magic numbers (utiliser des constantes nommées)

ASYNCHRONISME
  8. [OK] async/await + try/catch (jamais de .then sans .catch)
  9. [OK] Promise.all() pour des tâches parallèles indépendantes
  10. [OK] Vérifier response.ok après fetch()

OBJETS ET ARRAYS
  11. [OK] map/filter/reduce plutôt que boucles impératives
  12. [OK] Déstructuration pour un code plus lisible
  13. [OK] Ne pas muter les paramètres de fonction (immutabilité)

DOM ET ÉVÉNEMENTS
  14. [OK] textContent plutôt que innerHTML (sécurité XSS)
  15. [OK] Event delegation pour les éléments dynamiques
  16. [OK] Debounce/throttle pour les événements fréquents

ARCHITECTURE
  17. [OK] Séparer les préoccupations (UI / logique / données)
  18. [OK] Valider TOUJOURS côté serveur (jamais faire confiance au client)
  19. [OK] Écrire des tests pour le code critique

GÉNÉRAL
  20. [OK] Ne pas optimiser prématurément (code lisible d'abord,
         optimiser si profiling montre un problème réel)

════════════════════════════════════════════════════════════
*/


// ============================================================================
// EXERCICE FINAL : APPLICATION COMPLÈTE
// ============================================================================

/*
PROJET : Mini gestionnaire de tâches (To-Do App)

FONCTIONNALITÉS :
  - Ajouter des tâches avec priorité (haute/moyenne/basse)
  - Marquer comme terminée / non terminée
  - Filtrer par statut et priorité
  - Persister dans localStorage
  - Statistiques (total, terminées, en cours)

ARCHITECTURE à respecter :
  - TaskStorage : gestion de la persistance
  - TaskService : logique métier
  - TaskUI : manipulation du DOM

Implémentez chaque classe selon les principes du cours :
  - Responsabilité unique
  - Méthodes courtes et nommées
  - Gestion d'erreurs
  - Code immutable (ne pas muter les tableaux directement)
*/


// ────────────────────────────────────────
// CORRECTION EXERCICE FINAL
// ────────────────────────────────────────

// --- TaskStorage : persistance ---
class TaskStorage {
  static CLE_STOCKAGE = 'todo-tasks';

  static charger() {
    try {
      const donnees = localStorage.getItem(TaskStorage.CLE_STOCKAGE);
      return donnees ? JSON.parse(donnees) : [];
    } catch (err) {
      console.error('Erreur chargement tâches:', err);
      return [];
    }
  }

  static sauvegarder(taches) {
    try {
      localStorage.setItem(TaskStorage.CLE_STOCKAGE, JSON.stringify(taches));
    } catch (err) {
      console.error('Erreur sauvegarde tâches:', err);
    }
  }

  static vider() {
    localStorage.removeItem(TaskStorage.CLE_STOCKAGE);
  }
}

// --- TaskService : logique métier ---
class TaskService {
  #taches;

  constructor() {
    this.#taches = TaskStorage.charger();
  }

  // Getters
  getTaches(filtres = {}) {
    let taches = [...this.#taches];

    if (filtres.statut === 'actif') {
      taches = taches.filter(t => !t.terminee);
    } else if (filtres.statut === 'termine') {
      taches = taches.filter(t => t.terminee);
    }

    if (filtres.priorite) {
      taches = taches.filter(t => t.priorite === filtres.priorite);
    }

    if (filtres.recherche) {
      const terme = filtres.recherche.toLowerCase();
      taches = taches.filter(t => t.titre.toLowerCase().includes(terme));
    }

    return taches;
  }

  getStatistiques() {
    const total = this.#taches.length;
    const terminees = this.#taches.filter(t => t.terminee).length;
    const enCours = total - terminees;
    const pourcentage = total > 0 ? Math.round((terminees / total) * 100) : 0;

    return { total, terminees, enCours, pourcentage };
  }

  // Mutations
  ajouter({ titre, priorite = 'moyenne', description = '' }) {
    if (!titre?.trim()) {
      throw new Error('Le titre est obligatoire');
    }

    const PRIORITES_VALIDES = ['haute', 'moyenne', 'basse'];
    if (!PRIORITES_VALIDES.includes(priorite)) {
      throw new Error(`Priorité invalide. Valeurs: ${PRIORITES_VALIDES.join(', ')}`);
    }

    const nouvelleTache = {
      id: crypto.randomUUID(),
      titre: titre.trim(),
      description: description.trim(),
      priorite,
      terminee: false,
      creeLe: new Date().toISOString(),
      modifieLe: new Date().toISOString(),
    };

    this.#taches = [...this.#taches, nouvelleTache];
    this.#persister();

    return nouvelleTache;
  }

  basculerStatut(id) {
    const index = this.#taches.findIndex(t => t.id === id);
    if (index === -1) throw new Error(`Tâche introuvable: ${id}`);

    this.#taches = this.#taches.map(t =>
      t.id === id
        ? { ...t, terminee: !t.terminee, modifieLe: new Date().toISOString() }
        : t
    );

    this.#persister();
    return this.#taches.find(t => t.id === id);
  }

  supprimer(id) {
    const existe = this.#taches.some(t => t.id === id);
    if (!existe) throw new Error(`Tâche introuvable: ${id}`);

    this.#taches = this.#taches.filter(t => t.id !== id);
    this.#persister();
  }

  supprimerTerminees() {
    const avant = this.#taches.length;
    this.#taches = this.#taches.filter(t => !t.terminee);
    this.#persister();
    return avant - this.#taches.length;
  }

  #persister() {
    TaskStorage.sauvegarder(this.#taches);
  }
}

// --- TaskUI : interface utilisateur ---
class TaskUI {
  #service;
  #filtres = { statut: 'tous', priorite: '', recherche: '' };

  constructor(taskService) {
    this.#service = taskService;
  }

  initialiser() {
    this.#configurerEvenements();
    this.#rendreTaches();
    this.#rendreStatistiques();
  }

  #configurerEvenements() {
    // Formulaire d'ajout
    document.getElementById('form-tache')?.addEventListener('submit', (e) => {
      e.preventDefault();
      this.#ajouterTache(e.target);
    });

    // Filtres (délégation d'événements)
    document.getElementById('filtres')?.addEventListener('change', (e) => {
      if (e.target.name === 'statut') this.#filtres.statut = e.target.value;
      if (e.target.name === 'priorite') this.#filtres.priorite = e.target.value;
      this.#rendreTaches();
    });

    // Recherche avec debounce
    const rechercherDebounced = debounce((terme) => {
      this.#filtres.recherche = terme;
      this.#rendreTaches();
    }, 300);

    document.getElementById('recherche')?.addEventListener('input', (e) => {
      rechercherDebounced(e.target.value);
    });

    // Actions sur les tâches (délégation)
    document.getElementById('liste-taches')?.addEventListener('click', (e) => {
      const tacheEl = e.target.closest('[data-id]');
      if (!tacheEl) return;

      const id = tacheEl.dataset.id;

      if (e.target.matches('.btn-toggle')) {
        this.#basculerStatut(id);
      } else if (e.target.matches('.btn-supprimer')) {
        this.#supprimerTache(id);
      }
    });

    // Supprimer terminées
    document.getElementById('btn-vider')?.addEventListener('click', () => {
      const count = this.#service.supprimerTerminees();
      this.#afficherNotification(`${count} tâche(s) supprimée(s)`);
      this.#rendreTaches();
      this.#rendreStatistiques();
    });
  }

  #ajouterTache(form) {
    try {
      const donnees = {
        titre: form.elements.titre.value,
        priorite: form.elements.priorite.value,
        description: form.elements.description?.value || '',
      };

      this.#service.ajouter(donnees);
      form.reset();
      this.#rendreTaches();
      this.#rendreStatistiques();
      this.#afficherNotification('Tâche ajoutée !', 'succes');
    } catch (err) {
      this.#afficherNotification(err.message, 'erreur');
    }
  }

  #basculerStatut(id) {
    try {
      this.#service.basculerStatut(id);
      this.#rendreTaches();
      this.#rendreStatistiques();
    } catch (err) {
      this.#afficherNotification(err.message, 'erreur');
    }
  }

  #supprimerTache(id) {
    if (!confirm('Supprimer cette tâche ?')) return;
    try {
      this.#service.supprimer(id);
      this.#rendreTaches();
      this.#rendreStatistiques();
    } catch (err) {
      this.#afficherNotification(err.message, 'erreur');
    }
  }

  #rendreTaches() {
    const conteneur = document.getElementById('liste-taches');
    if (!conteneur) return;

    const taches = this.#service.getTaches(this.#filtres);

    if (taches.length === 0) {
      conteneur.innerHTML = '<p class="vide">Aucune tâche ici [BRAVO]</p>';
      return;
    }

    conteneur.innerHTML = taches.map(t => this.#htmlTache(t)).join('');
  }

  #htmlTache(tache) {
    const classePriorite = `priorite-${tache.priorite}`;
    const classeTerminee = tache.terminee ? 'terminee' : '';
    const dateFormatee = new Date(tache.creeLe).toLocaleDateString('fr-FR');

    return `
      <article class="tache ${classeTerminee} ${classePriorite}" data-id="${tache.id}">
        <div class="tache-contenu">
          <h3 class="tache-titre">${this.#echapper(tache.titre)}</h3>
          ${tache.description 
            ? `<p class="tache-desc">${this.#echapper(tache.description)}</p>` 
            : ''
          }
          <small class="tache-meta">
            <span class="badge-priorite">${tache.priorite}</span>
            Créée le ${dateFormatee}
          </small>
        </div>
        <div class="tache-actions">
          <button class="btn-toggle" title="${tache.terminee ? 'Rouvrir' : 'Terminer'}">
            ${tache.terminee ? '<-' : '[OK]'}
          </button>
          <button class="btn-supprimer" title="Supprimer">[SUPPRIMER]</button>
        </div>
      </article>
    `;
  }

  #rendreStatistiques() {
    const stats = this.#service.getStatistiques();
    const conteneur = document.getElementById('statistiques');
    if (!conteneur) return;

    conteneur.innerHTML = `
      <div class="stat">
        <strong>${stats.total}</strong>
        <span>Total</span>
      </div>
      <div class="stat">
        <strong>${stats.enCours}</strong>
        <span>En cours</span>
      </div>
      <div class="stat">
        <strong>${stats.terminees}</strong>
        <span>Terminées</span>
      </div>
      <div class="stat">
        <strong>${stats.pourcentage}%</strong>
        <span>Avancement</span>
      </div>
    `;
  }

  #echapper(texte) {
    // Évite XSS dans le HTML généré
    const div = document.createElement('div');
    div.textContent = texte;
    return div.innerHTML;
  }

  #afficherNotification(message, type = 'info') {
    const notif = document.createElement('div');
    notif.className = `notification notification-${type}`;
    notif.textContent = message;

    document.body.appendChild(notif);

    // Supprimer après 3 secondes
    setTimeout(() => notif.remove(), 3000);
  }
}

// Point d'entrée de l'application
// document.addEventListener('DOMContentLoaded', () => {
//   const service = new TaskService();
//   const ui = new TaskUI(service);
//   ui.initialiser();
// });


// ============================================================================
// RÉCAPITULATIF DE TOUTES LES PARTIES
// ============================================================================

/*
╔═════════════════════════════════════════════════════════════════════════════╗
║           RÉCAPITULATIF COMPLET DU GUIDE JAVASCRIPT                         ║
╠═════════════════════════════════════════════════════════════════════════════╣
║                                                                             ║
║  PARTIE 1 (js_partie1.txt) - LES FONDAMENTAUX                               ║
║  ├── Chapitre 0 : Introduction et installation                              ║
║  ├── Chapitre 1 : Variables, types et opérateurs                            ║
║  ├── Chapitre 2 : Structures de contrôle                                    ║
║  ├── Chapitre 3 : Fonctions                                                 ║
║  └── Chapitre 4 : Arrays et Objects                                         ║
║                                                                             ║
║  PARTIE 2 (js_partie2.txt) - PROGRAMMATION ORIENTÉE OBJET                   ║
║  ├── Chapitre 5 : Classes et prototypes                                     ║
║  ├── Chapitre 6 : Héritage et polymorphisme                                 ║
║  ├── Chapitre 7 : Modules ES6                                               ║
║  └── Chapitre 8 : Gestion d'erreurs                                         ║
║                                                                             ║
║  PARTIE 3 (js_partie3.txt) - ASYNC ET DOM                                   ║
║  ├── Chapitre 9  : Programmation asynchrone                                 ║
║  ├── Chapitre 10 : Manipulation du DOM                                      ║
║  ├── Chapitre 11 : Événements                                               ║
║  └── Chapitre 12 : Fetch API                                                ║
║                                                                             ║
║  PARTIE 4 (js_partie4.txt) - FONCTIONNALITÉS AVANCÉES                       ║
║  ├── Chapitre 13 : Closures et scope                                        ║
║  ├── Chapitre 14 : Programmation fonctionnelle                              ║
║  ├── Chapitre 15 : Iterators et Generators                                  ║
║  ├── Chapitre 16 : Proxy et Reflect                                         ║
║  └── Chapitre 17 : Expressions régulières                                   ║
║                                                                             ║
║  PARTIE 5 (js_partie5.txt) - ÉCOSYSTÈME ET BONNES PRATIQUES                 ║
║  ├── Chapitre 18 : Node.js et npm                                           ║
║  ├── Chapitre 19 : Testing avec Jest                                        ║
║  ├── Chapitre 20 : Outils modernes (ESLint, Prettier, Vite, TypeScript)     ║
║  ├── Chapitre 21 : Design Patterns                                          ║
║  ├── Chapitre 22 : Performance et Sécurité                                  ║
║  └── Chapitre 23 : Bonnes pratiques et Architecture                         ║
║                                                                             ║
╠═════════════════════════════════════════════════════════════════════════════╣
║  PROCHAINES ÉTAPES RECOMMANDÉES                                             ║
║                                                                             ║
║  Frontend :                                                                 ║
║    -> React (le plus utilisé en entreprise)                                  ║
║    -> Vue.js (plus simple à apprendre)                                       ║
║    -> Tailwind CSS (styles utilitaires)                                      ║
║                                                                             ║
║  Backend :                                                                  ║
║    -> Express.js (framework Node.js minimal)                                 ║
║    -> NestJS (framework Node.js structuré, comme Spring)                     ║
║    -> Prisma (ORM moderne pour Node.js)                                      ║
║                                                                             ║
║  Fullstack :                                                                ║
║    -> Next.js (React SSR/SSG)                                                ║
║    -> Nuxt.js (Vue SSR/SSG)                                                  ║
║                                                                             ║
║  Qualité :                                                                  ║
║    -> TypeScript (fortement recommandé en entreprise)                        ║
║    -> Testing Library + Cypress (tests E2E)                                  ║
║                                                                             ║
╚═════════════════════════════════════════════════════════════════════════════╝

[COURS] FÉLICITATIONS !
Vous avez parcouru l'intégralité du guide JavaScript, soit plus de 23 chapitres
couvrant le langage de ses fondamentaux jusqu'aux pratiques professionnelles.

La prochaine étape : PRATIQUER.
Construisez des projets réels. Consultez ce guide quand vous avez besoin
d'une référence. Et continuez à apprendre !

Bon code ! [RAPIDE]
*/

// ============================================================================
// FIN DU GUIDE JAVASCRIPT
// ============================================================================